本文记录了在一次 Typecho 建站维护中的完整排查与实施过程:让两个独立页面分别只显示指定标签下的文章,过程中顺带修掉了主题在 Typecho 新版本下的一个归档页 Bug。全程仅通过 FTP 操作,未改动数据库、未改动 Typecho 核心。
一、需求与环境
- 站点由 Typecho 1.3.0 搭建,主题为 Matcha 1.2.0(BigCoke233/matcha)
- 站点上已有两个空白页面:
diary.html(日记)和ms.html(迷思) - 需求:两个页面分别只显示标签为「日记」和「迷思」的文章
手里只有一个 FTP 账号,没有数据库权限,也没有后台管理权限(但不影响本文方案,因为最终方案根本不需要动后台)。
二、关键发现:Typecho 支持按 slug 匹配页面模板
很多人以为自定义页面模板必须在后台"自定义模板"下拉框里选(那需要写数据库),其实翻一下 var/Widget/Archive.php 的 render() 方法就会发现,Typecho 1.2+ 的模板解析顺序是:
1 | // 1. 后台指定的自定义模板 ($this->themeFile) |
也就是说:只要在主题目录下新建 page/diary.php 和 page/ms.php,对应 slug 的页面就会自动使用它们,完全不用碰数据库。
三、主题目录 FTP 不可写
动手时才发现:usr/themes/matcha/ 整个目录属主是 root:root、权限 755,FTP 账号写入直接被拒(553 Permission denied):
1 | > STOR _test.php |
但排查各级目录权限后发现,上级目录 usr/themes/ 是 777。于是有了迁移方案:
- 通过 FTP 重命名(RNFR/RNTO 只需要父目录写权限):
matcha→matcha.bak(完整备份) - 新建
matcha/目录,把主题文件完整迁移回去 - 在新目录里追加
page/diary.php、page/ms.php
由于新目录由 FTP 账号属主创建,后续修改主题文件(比如下文的 Bug 修复)也畅通无阻。
四、按标签调用文章的正确姿势
4.1 Widget 参数如何传给 tagHandle
Widget_Archive 的 tagHandle() 是从 request 对象 读取标签标识的:
1 | if ($this->request->is('mid')) { ... } // 按 mid 查 |
而 Typecho\Widget::widget() 的第三个参数会作为 request 参数注入(工厂方法内部会构造 WidgetRequest(Request::getInstance(), new Config($request)))。所以正确写法是第三个参数传 slug:
1 | $tagPosts = $this->widget( |
4.2 容错处理
如果标签还不存在,tagHandle 会抛出 WidgetException('标签不存在', 404)。站点初期可能还没给文章打标签,所以模板里用 try/catch 兜底:
1 | $tagPosts = null; |
4.3 模板核心结构(page/diary.php)
保留页面本身的标题和内容,下方按主题原有的文章列表样式渲染:
1 | <?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; |
page/ms.php 完全同理,把别名换成 tag_ms、slug 换成 迷思 即可。
4.4 验证结果
- 两个页面返回 200,正常渲染;标签不存在时显示空状态,不报错
- 给文章打上对应标签后,列表自动出现(中文标签的 slug 默认就是中文本身,如「日记」)
- 首页、归档页等其余页面不受影响
五、修复归档页的 permalink Warning
验证时发现主题的"往鉴"(归档)页面一直有个 Warning:
1 | Warning: Undefined array key "permalink" in .../themes/matcha/libs/Matcha.php on line 402 |
5.1 根因
主题的 Matcha::archives() 按旧版 Typecho 的习惯写法取链接:
1 | $row = $widget->filter($row); |
翻核心源码确认:新版 Widget\Base\Contents::filter() 只处理 title/text/slug/password/date,不再把 permalink 写进返回数组;permalink 改成了魔术属性,由路由动态生成:
1 | protected function ___path(): string |
所以这不仅是 Warning,还意味着归档页所有文章链接都是空的(<a href="">)——一个实打实的功能 Bug。
5.2 修复
查 Router::url() 的签名:第二个参数既支持数组,也支持 ParamsDelegateInterface(内部按路由 pattern 的参数名取值)。核心的 Contents::getRouterParam() 覆盖了 cid/slug/directory/category/year/month/day。照此逻辑在主题里复刻一份即可:
1 | // Matcha.php |
1 | /** |
上传后验证:
- Warning 消失
- 归档条目链接正确生成(
/index.php/archives/10/,与首页一致) - 首页等其余页面无报错
六、踩坑小结
- Typecho 1.2+ 的
page/{slug}.php精确模板机制鲜为人知,却是最省事的按页面定制方案——不碰数据库、不改核心,主题目录下加文件即可。 Widget::widget()的第三个参数是 request 参数。想在外部(模板/插件)按标签、分类调用文章,slug/mid 要从第三个参数传,不是写在第二个参数里。- 给别名 Widget 传参:
Widget_Archive@别名的调用方式可以安全地与当前页面 Widget 并存。 - FTP 权限问题的绕行方案:目标目录不可写但父目录可写时,"重命名走人 → 重建目录 → 迁移回来"是只靠 FTP 就能完成的标准操作(RNFR/RNTO 只需要父目录写权限)。
- 升级 Typecho 后主题报警,先看核心 API 是否变了:
filter()返回的数组里少掉的键,很可能已经变成了魔术属性(___xxx()),照抄核心的生成逻辑最稳。 - 迁移/修改前先留好完整备份(本次是
matcha.bak/),随时可回滚。
参考文献
- SegmentFault:《Typecho 如何在页面中显示指定标签的文章》问答(本文方案的出处思路)
https://segmentfault.com/q/1010000047290321/a-1020000047291068 Typecho 1.3.0 核心源码(本文分析所依据的服务器上实际代码)
var/Widget/Archive.php——render()模板解析顺序(page/{slug}.php精确匹配)、tagHandle()标签查询逻辑var/Typecho/Widget.php——widget()工厂方法,第三参数注入 request 参数var/Typecho/Widget/Request.php——WidgetRequest参数代理与is()/get()实现var/Widget/Base/Contents.php——filter()过滤器、___permalink()魔术属性、getRouterParam()路由参数var/Typecho/Router.php——Router::url()对数组 /ParamsDelegateInterface的支持
- Matcha 主题仓库
https://github.com/BigCoke233/matcha - Typecho 官网
http://www.typecho.org/