一、主题从一台机器搬到另一台,为什么总是「差不多能用」
这两年我做的站里,有一半以上是 Typecho。原因很实际:它轻、依赖少、PHP 5.6 和 PHP 8 都能跑,塞进一个 1G 内存的小机器毫无压力。但每次给客户或者自己的新站写主题,同样的坑总要重踩一遍:明明后台选项都对,页面上就是少一块;明明本地看着正常,上线就变样。
这些坑很少是语法错误——语法错误 PHP 会直接报出来,反而是最好解决的。真正耗时间的是那些「不报错但不生效」的问题:配置读了没读到、模板找没找到、缓存有没有更新、权限够不够。它们不会在日志里留下明显痕迹,只能靠一套固定的排查顺序找出来。
这篇把写 Typecho 主题时必须过的几道关按顺序写清楚,从目录结构、模板钩子、配置表单,到缓存、路由、多站点与上线前的自检。目标很明确:让一个主题从开发机搬到活站上,一次就能对。
二、目录结构与必须存在的文件
Typecho 主题放在 usr/themes/你的主题名/,一个能用的主题至少需要三个东西:
usr/themes/mytheme/
├── index.php # 必须有,Typecho 靠它识别主题
├── style.css # 不是必须,但惯例放这里
├── functions.php # 主题钩子、配置表单、自定义函数
├── header.php # 不是必须,但几乎人人用
├── footer.php
├── post.php # 文章详情
├── page.php # 独立页面
├── archive.php # 归档/分类/搜索列表
├── comments.php
└── screenshot.png # 后台主题列表里的缩略图,400x300index.php 是识别标志,缺了它后台根本看不到这个主题。另外几个模板文件不是硬性要求——Typecho 有模板继承:找不到 post.php 就会用 index.php 里的循环输出。所以一个只有 index.php 的主题也能跑起来,这就是为什么很多「能显示但样式不对」的问题,其实是模板文件没写全。
模板继承的查找顺序值得记一下:文章页 → post.php → single.php → index.php;页面 → page.php → index.php;分类/归档/搜索 → archive.php → index.php。写自定义模板时用 custom-xxx.php 命名,并在文章编辑页的「自定义模板」里选择,这个机制在 themeConfig 之外非常有用。
三、header.php 里的三个必备钩子
header.php 是最容易出错的文件,因为它承担了输出 head 的全部内容。有三个函数必须调用,缺任何一个都会出问题:
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php $this->header(); ?>
<title><?php $this->archiveTitle(); ?></title>
...
<?php $this->header('description') ?>
</head>$this->header():输出主题在后台配置里填的「自定义头部代码」,以及 Typecho 自己注入的插件钩子。缺了它,后台的统计代码、SEO 插件全部失效,而且不会报错,只会「代码怎么没生效」。
$this->header('description'):输出 SEO 插件生成的 meta description 和 keyword。这行必须在 header() 之后、且在 head 内,位置错了就会跑到 body 里变成可见文本。
$this->archiveTitle():生成符合站点名称规则的标题,会自动区分首页、分类、搜索、文章。自己手写 <title> 的常见后果是分类页的标题全都一样,或者首页标题里少了站点名。
还有一个隐形坑:<title> 与 archiveTitle() 之间不能有空格或换行,否则输出的标题前面会多一个空白字符,某些浏览器会把它吃掉、某些不会,排查起来很麻烦。我一般写成同一行。
四、functions.php:hooks 与配置表单
functions.php 是主题的大脑。Typecho 用的是插件式的 hooks 机制,主题里最常用的是这几个:
<?php
// 注册主题钩子
function themeInit($archive) {
// 在文章查询之前干预,常用于改 postsPerPage
// $archive->response->setPageSize(10);
}
// 在 head 之前注入
function themeHead($header) {
// 输出额外资源
}
// 统计阅读次数(经典示例)
function themeFields($layout) {
$layout->miniPagenav->setValue(5);
}注意 hook 函数名不是随便起的:Typecho 会扫描 functions.php 里以 theme 开头的函数,并把它挂到同名的插件点(Plugin Point)上。所以 themeInit 挂的是 init 点,themeFields 挂的是 fields 点。如果名字写错,函数永远不会被调用,而且没有任何报错——这是最折磨人的一类问题。
配置表单用 ThemeConfig 类,配置项的值在模板里用 $this->options->字段名() 取:
function themeConfig($form) {
$logoUrl = new Typecho_Widget_Helper_Form_Element_Text(
'logoUrl', NULL, 'https://example.com/logo.png',
_t('站点 LOGO 地址'), _t('留空则不显示'));
$form->addInput($logoUrl);
$showThumb = new Typecho_Widget_Helper_Form_Element_Radio(
'showThumb', array('1' => _t('显示'), '0' => _t('不显示')),
'1', _t('列表是否显示缩略图'));
$form->addInput($showThumb);
}模板里这样取:<?php echo $this->options->logoUrl(); ?>。注意 options 后面跟的是方法调用(带括号),不是属性访问。写成 $this->options->logoUrl 会得到 null,页面上什么都不显示,也不报错。
第二个常见问题是配置项改了不生效。这通常不是代码问题,而是 Typecho 的配置缓存:主题选项保存在 options 表里,后台保存后会更新,但如果浏览器缓存了页面,或者开了 CDN,看到的就是旧的。排查顺序:后台保存 → 看数据库 options 表对应的 theme:mytheme 行 → 再刷页面。
五、模板里的常用循环与判据
写循环时最需要注意的是「判断在哪一层」。Typecho 的 widget 是拉式的,循环的每次迭代会改变 $this 的上下文:
<?php while ($this->next()): ?>
<article class="post">
<div class="post-title"><a href=<?php $this->permalink(); ?>><?php $this->title(); ?></a></div>
<?php if ($this->fields->thumb): ?>
<img src="<?php echo $this->fields->thumb; ?>" alt="<?php $this->title(); ?>">
<?php endif; ?>
<div class="excerpt"><?php $this->excerpt(120); ?></div>
</article>
<?php endwhile; ?>几个容易踩的点:
$this->excerpt(120) 的长度单位是字符(中英文都算一个字符?实际上是按字符数截断),但它的截断逻辑会先剥离 HTML 标签再生成本文,所以如果原文有大量短标签,实际长度会比预期短。需要完整控制摘要时,用 $this->content 自己处理更可靠。
自定义字段用 $this->fields->xxx 访问,判断是否存在用 if ($this->fields->thumb)。注意如果字段存在但值为空字符串,这个判断也是 false;如果需要区分「没有这个字段」和「字段是空的」,要用 $this->fields->has('thumb')。
分页不要手写,用 $this->pageNav() 或 $this->pageLink。手写分页最常见的错误是生成的链接丢掉了搜索参数或者分类前缀,导致翻到第二页就跳回首页。
六、改完不生效?先查这四处
这是主题开发里最高频的问题。按下面的顺序查,能省掉一大半时间。
1. 模板文件真的被加载了吗。在模板顶部加一行 <?php echo __FILE__; ?>,刷新页面看到的是哪个路径,就知道实际加载了哪个文件。我遇到过好几次是改错了文件:改的是 post.php,实际走的是 single.php。
2. 缓存有没有清。Typecho 本身对模板不做编译缓存,但有三层外部缓存会挡住你的修改:浏览器缓存、CDN 缓存、以及如果装了 OPcache 的话——OPcache 会缓存 PHP 文件,改动后需要 reload 或清空。查法:
# 看 OPcache 是否开启以及是否验证文件时间戳
php -i | grep -E 'opcache.enable|validate_timestamps|revalidate_freq'
# 生产环境常见配置:validate_timestamps=0
# 这意味着改了 PHP 文件不会自动生效,必须 reload php-fpm
systemctl reload php8.1-fpm3. 文件权限与属主。Typecho 以 php-fpm 的运行用户(通常 www-data)读文件。如果新上传的模板属主是 root 且权限是 600,php-fpm 读不到,表现是「主题列表里能看到但应用后白屏」。查法:
ls -l usr/themes/mytheme/
# 正确:drwxr-xr-x www-data www-data
chown -R www-data:www-data usr/themes/mytheme/
find usr/themes/mytheme -type f -exec chmod 644 {} \;
find usr/themes/mytheme -type d -exec chmod 755 {} \;4. 后台是否真的保存了。主题配置表单有一个很容易忽略的细节:ThemeConfig 里注册的字段如果类型是 Text,后台默认会做 HTML 转义处理,所以想在配置里填一段 HTML 代码,必须用 Textarea 或者 Radio 这类不做转义的类型。填进去的代码显示成了源码,就是这个原因。
七、多站点与子主题的取舍
手上有多个站时,最省事的做法是复制主题改个名字。短期省事,长期是灾难:三个站的主题各自演化半年之后,修一个 bug 要改三处,改完还得一个个上传。
Typecho 没有官方的子主题机制,但有两套替代方案:
方案一,共用主题 + 配置差异。所有站用同一个主题目录,靠 themeConfig 的选项区分外观(logo、配色、是否显示侧栏)。适合外观差异不大的站。好处是升级只改一处,坏处是配置项会越堆越多。
方案二,公共片段 + 各站模板。把 header.php、footer.php、comments.php 这些高度重复的部分抽出来,用 include 引用同一个文件;各站的 index.php 和 post.php 保留差异。适合外观差异大但结构相同的站。
// 公共片段放在主题目录里的 common/ 下
<?php include __DIR__ . '/common/head-meta.php'; ?>还有一种做法是用软链接:把同一个主题目录软链到多个站的 usr/themes/ 下。我试过,能用,但升级时要小心——软链接指向的目录如果被覆盖,所有站同时变样。如果要做,建议在更新前先做个快照。
八、上线前的自检清单
主题写完,别急着应用。我固定跑一遍下面这几项,能挡掉九成事故:
语法检查:每个 PHP 文件都过一遍 php -l。批量跑:
find usr/themes/mytheme -name '*.php' -exec php -l {} \;错误日志检查:把 error_reporting 临时开到 E_ALL,把 display_errors 开成 On,逐页点一遍,看有没有未定义变量、未定义索引的警告。这些警告在生产环境被藏起来,但它们是真 bug。
页面结构检查:首页、文章页、分类页、独立页面、搜索页、404 页、标签页,七个页面逐个打开。搜索结果为空时是否崩、分类下没有文章时是否崩、文章没有缩略图时是否崩——这三个空态是最容易漏掉的。
HTML 合法性:把首页源码丢进 W3C 校验器,容错模式即可。常见问题是 block 元素套在 p 里面、表格缺 tbody、img 缺 alt。这些不会让页面崩,但会影响浏览器渲染的一致性。
移动端与窄屏:用浏览器 DevTools 把宽度调到 320px。主题里最常出问题的是顶部的导航和代码块的横向溢出——代码块没有 overflow-x: auto 时,整页会被撑出横向滚动条。
性能自检:看文章页的 SQL 查询数。Typecho 自带 $this->db->queryCount(开发模式下),或者在 config.inc.php 里开 __TYPECHO_DEBUG__,页面底部会输出查询数与耗时。超过 30 条查询的页面,通常是相关文章或者评论的循环写重复了。
九、小结
写 Typecho 主题的难点不在 PHP 语法,而在理解它的三套机制:模板继承的查找顺序、theme 前缀函数的自动挂载、以及主题选项的存取方式。这三套机制都在「不生效时不报错」的地方,所以必须靠固定的排查顺序去撞。
我给自己的规矩是:新模板先只写 index.php 跑通,确认数据都出得来;再拆出 header.php 和 footer.php,确认钩子都在;最后才加配置表单和自定义字段。这样每一步都能单独验证,出问题时知道是这一步引入的。比一次写完再排查快得多。