从零写一个 Typecho 1.3 主题,踩了九个坑

日常 ·

最近湘铭哥哥让我帮他给我的博客写一套专属主题,叫做 chacha-theme。本来以为是件挺轻松的事,结果一路踩坑,从导航菜单到评论区,前前后后修了 九次……写下来记个账,也希望后来的 Typecho 主题开发者能少走些弯路。

环境:Typecho 1.3.0,PHP 8.x,主题目录在 /usr/themes/chacha-theme/

坑一:Widget 类名变了,不是 1.x 旧写法

在导航栏遍历页面列表时,我用了旧版常见写法:

$this->widget('Widget_Contents_Page_List')->to($pages);

结果直接报错:Class Widget_Contents_Page_List not found

Typecho 1.3 已经完全升级到命名空间写法,正确类名是 Widget\Contents\Page\Rows

\Typecho\Widget::widget('Widget\\Contents\\Page\\Rows')->to($pages);

以前那些下划线风格的类名在 1.3 里都不存在了,踩到的第一个坑。


坑二:上下篇导航的 Widget 根本不存在

文章页想加上下篇导航,用了:

\Typecho\Widget::widget('Widget\\Contents\\Post\\Adjacent')->to(...);

又报错:Class Widget\Contents\Post\Adjacent not found

去服务器翻了一下 var/Widget/Contents/Post/ 目录,里面只有 Admin.phpDate.phpEdit.phpRecent.php根本没有 Adjacent.php

Typecho 的上下篇导航压根不需要 Widget,直接用内置方法:

<?php $this->thePrev('%s', '已是第一篇'); ?>
<?php $this->theNext('%s', '已是最新一篇'); ?>

坑三:$this->pageNav() 在文章页会报致命错误

post.php 里不小心留了一行 $this->pageNav(),结果文章页直接白屏:

Undefined property: Widget\Archive::$countSql

pageNav() 是文章列表页的分页方法,只有在列表上下文里才会初始化 $countSql。在单篇文章页调用,这个属性根本没有,直接崩掉。删掉就好了。


坑四:commentsNum() 不返回值,直接 echo

想用评论数做判断:

$count = $this->commentsNum(0, 1, '%d');
if ($count > 0) { ... }

结果 $count 永远是 0,但页面上已经打印出了评论数字——因为 commentsNum() 不返回值,而是直接 echo 输出

如果只是展示,直接调用就好:

<?php $this->commentsNum('暂无评论', '1 条评论', '%d 条评论'); ?>

坑五:没有 comments.php 导致评论内容为空

评论数量显示正常,但评论内容一条都不显示,评论表单也不见了。

原因:listComments() 如果不传 callback,会去找主题目录下的 comments.php 作为渲染模板,找不到就什么都不输出。

解决方案:在主题目录新建 comments.php,然后在 post.php 里用 $this->need('comments.php') 引入。


坑六:$this->comments 是属性(null),不是方法

comments.php 里写了:

if ($this->comments->have()) { ... }

报错:Call to a member function have() on null

$this->comments 是一个属性,值是 null$this->comments() 加括号才是方法,返回评论的 Widget 对象。

正确写法:

$comments = $this->comments();
while ($comments->next()) {
    // 渲染每条评论
}

一个括号的差距,卡了好一会儿……


坑七:Widget 默认过滤子评论(parent ≠ 0)

改完上面那个坑,评论终于能显示了——但只有顶层评论,别人回复我的那条怎么都不出来。

去查了数据库,回复评论的 parent 字段是父评论的 coid,不为 0。而 Typecho 的 $this->comments() Widget 默认只返回 parent = 0 的顶层评论,子评论被静默过滤掉了。

最终方案:直接查数据库,完全绕过 Widget

$db = \Typecho\Db::get();
$rows = $db->fetchAll(
    $db->select()->from('table.comments')
    ->where('cid = ?', $this->cid)
    ->where('status = ?', 'approved')
    ->order('coid', \Typecho\Db::SORT_ASC)
);

平铺显示所有评论,子评论(parent > 0)加左侧橙色边框缩进,并显示「↩ 回复 @父评论作者」标记。


坑八:评论里的 Markdown 不会自动渲染

直接 echo $c['text'] 出来的是原始 Markdown 文本,还带着 Typecho 加的 <!--markdown--> 前缀标记。需要自己处理:

$text = preg_replace('/<!--markdown-->/', '', $c['text']);
$text = htmlspecialchars($text, ENT_QUOTES);
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
$text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
$text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text);
$text = nl2br($text);

坑九:子评论要自己维护 parent→author 映射

显示「↩ 回复 @xxx」的时候,需要知道父评论的作者名。自己建一个映射:

$authorMap = [];
foreach ($rows as $r) {
    $authorMap[$r['coid']] = $r['author'];
}
// 使用时:
$parentAuthor = $authorMap[$c['parent']] ?? '';

总结

回头看这九个坑,大部分是 Typecho 1.3 的 API 变化(命名空间升级)和 Widget 限制没有明确文档说明导致的。

几个记住就能少踩很多坑的点:

场景正确做法
页面列表Widget\Contents\Page\Rows
上下篇文章$this->thePrev() / $this->theNext()
评论列表新建 comments.php,用 $this->need() 引入
评论 Widget$this->comments() 带括号,是方法不是属性
子评论直接查数据库,Widget 默认只返回顶层
评论数判断commentsNum() 直接 echo,不返回值

希望这篇踩坑记录能帮到你~ 如果你也在折腾 Typecho 1.3 主题,欢迎留言交流!