做Typecho主题开发这段时间,我遇到最多的需求就是三个:首页不想用默认文章流、某个独立页面要单独画一套版式、不同分类的文章列表要长得不一样。前两个是给站点加“门面”,第三个是给内容做“分区”,看起来是三个问题,实际上都围绕同一个核心——Typecho的模板加载机制。搞清楚它,你就能自由控制任意页面的输出结构。
这篇文章我按自己的实践路径来写:先讲Typecho到底按照什么规则选模板,再分别拆解自定义首页、自定义单页、分类差异化三种场景的具体写法,最后汇总我踩过的坑和排查办法。内容不依赖插件,纯主题层实现,适合主题开发者、想要深度定制自己站点的Typecho用户参考。
1. 先理解Typecho的模板加载机制
1.1 一个请求进来后,Typecho如何选择模板
Typecho的渲染规则比某些建站系统简单得多,但也因为简单,很多人一开始会迷茫。它的核心逻辑是:根据当前访问的URL和请求类型,找到对应的主题模板文件并执行。
默认情况下,请求首页会执行主题目录下的index.php,请求归档页(分类、标签、日期存档)会执行archive.php,请求文章详情页执行post.php,请求独立页面执行page.php。如果某个模板文件不存在,Typecho会尝试往下找替代文件。例如请求独立页面时,如果主题里没有page.php,它会回退到index.php来渲染。
这带来一个关键认知:你看到的页面结果,不完全由URL决定,而是由“入口模板文件 + 模板内的判断逻辑”共同决定。所以当你想要自定义首页时,不需要改任何内核,只需要控制index.php的输出,或者让Typecho在首页请求时加载你指定的另一个模板文件。
1.2 为什么说模板选择是“入口决定,逻辑微调”
这里要注意一个容易混淆的点:index.php既负责首页,也持有独立页面和文章详情的兜底渲染能力。很多主题开发者会把所有逻辑都堆在index.php里,用$this->is('post')、$this->is('page')判断当前是文章还是页面,再输出不同结构。这种做法可行,但维护起来非常痛苦。
我更推荐的做法是:入口模板只做“路由分发”,具体渲染交给独立的模板文件。比如index.php只判断当前是不是首页,如果是首页就引入home.php,如果不是则走默认文章列表逻辑。这样首页、归档、详情的代码彻底分离,改动互不干扰。
理解了这个机制,后面的三种自定义方式就顺理成章了。它们本质上都是在做同一件事:在正确的入口位置,加载正确的模板文件。
2. 自定义首页模板:三种思路与取舍
2.1 方案A:改造 index.php,直接在首页输出定制结构
最直接的做法,是打开index.php,把首页要展示的内容写进去。比如做企业站或落地页时,首页不需要文章列表,而需要轮播图、产品展示、公司介绍等模块,那就在index.php里写静态HTML结构,只在底部放几篇最新文章作为资讯区块。
代码结构大概是这样的:
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?> <?php $this->need('header.php'); ?> <!-- 首页定制内容 --> <section class="hero">...</section> <section class="featured"> <h2>最新动态</h2> <?php while ($this->next()): ?> <a href="<?php $this->permalink(); ?>"><?php $this->title(); ?></a> <?php endwhile; ?> </section> <?php $this->need('footer.php'); ?>这样的好处是简单,文件少,适合首页结构相对固定、后续不太需要切换的站点。但缺点是首页和文章列表逻辑耦合在同一个文件里,如果你将来想临时把首页切换回默认文章流,就得改代码,不够灵活。
2.2 方案B:新建 home.php,在 index.php 中按条件加载
这是我最常用也最推荐的方式。主题目录下单独建一个home.php,专门负责首页展示,然后在index.php开头判断当前是否为首页,是则加载home.php,否则输出文章列表。
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?> <?php if ($this->is('index')): ?> <?php $this->need('home.php'); ?> <?php return; ?> <?php endif; ?> <!-- 默认文章列表逻辑,保留给子目录或兜底场景 -->为什么这样设计?因为$this->is('index')判断的是“当前请求是否是站点首页”,无论你的首页最终渲染的是文章流还是静态模块,这个判断都成立。当你把首页逻辑收拢到home.php后,想要切换首页风格,只需要改home.php一个文件,想要回退到默认列表,注释掉判断即可。
注意:在
index.php中使用return终止后续输出,是为了避免加载完home.php后又执行下面的文章循环,导致页面出现两段内容。这是一个很容易被忽略的坑。
2.3 方案C:通过后台“固定页面”作为首页
Typecho后台的“设置 -> 阅读”里,可以把首页显示方式改为“显示固定页面”,然后选择一个已经创建的独立页面作为网站首页。配合单页模板,可以实现“首页是一张独立页面”的效果。
这个方式的优点是完全不用动index.php,首页内容和发布文章彻底分离。你可以像写一个普通页面一样更新首页内容,甚至让非技术人员通过后台直接编辑首页模块。
但它的限制也很明显:
- 首页变成了一个独立页面,不再自动拥有文章列表和分页参数;
- 如果希望首页底部显示最新文章,需要在该页面模板内手动调用文章查询;
- 一些伪静态规则和分页逻辑在固定页面下可能表现异常。
所以我的建议是:除非首页确实是一张“静态展示页”,而且内容需要经常后台编辑,否则优先用方案B。方案A适合极简站点,方案C适合内容更新频繁的展示型站点。
2.4 自定义首页模板中必须包含的核心元素
无论选择哪种方案,首页模板都要保证几个基本元素不缺失:
- head和头部导航:通过
$this->need('header.php')引入,保证全站风格统一。 - 底部版权:通过
$this->need('footer.php')引入。 - 文章循环(如果首页放最新文章):使用
while ($this->next())遍历。 - SEO基础标签:首页的标题、描述建议在
header.php中统一处理,模板中不要遗漏。
经常有人问我,为什么自己写的首页模板看起来没有样式。大多数情况是忘记引入header和footer,或者只写了内容区,导致侧边栏、导航、版权信息全部丢失。页面不是孤立的,它必须嵌入到主题的整体结构中。
3. 自定义单页模板完整实操
3.1 单页模板文件的正确写法
Typecho的单页模板识别非常特殊,它靠的是模板文件头部的一段注释。文件不一定要叫page-xxx.php,任何放在主题目录下的PHP文件,只要头部注释符合规范,后台就能识别它。
一个标准的单页模板头:
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?> <?php /** * 全宽展示页 * @package Page */ ?>关键在注释的第一行“全宽展示页”,这行文字会显示在后台“页面 -> 新建/编辑页面”右侧的“模板”下拉列表中。@package字段是模板分组,可以省略,但我习惯写上,方便在文件较多时区分。
要注意的坑是:注释必须放在文件最顶部,并且在此之前不能有任何输出,包括空格和换行。如果你在<?php前多了一个空行,页面可能会报错“headers already sent”,后台也识别不到模板。
3.2 后台调用单页模板的完整路径
- 在主题目录下新建文件,比如
page-about.php,写入上述模板头,然后编写页面展示逻辑。 - 进入Typecho后台,“管理 -> 页面 -> 新建页面”。
- 在编辑页右侧找到“模板”下拉框,选择刚创建的模板名。
- 填写页面标题、内容、缩略名,发布。
- 前台访问该页面的URL,Typecho会自动使用该模板渲染。
有一点值得说明:Typecho的页面模板机制会自动匹配。如果你在后台没有手动选择模板,访问页面时,Typecho会优先查找page-缩略名.php,找不到再使用page.php。利用这个规则,你可以通过文件名直接绑定页面,无需每次去后台选择模板。
例如,给“关于我们”页面创建page-about.php,那么访问/about.html时,即使后台没选模板,Typecho也会加载这个文件。这个约定在快速搭建多个风格不同的页面时非常实用。
3.3 单页模板内输出页面内容的标准代码
在单页模板中,页面内容同样通过$this对象输出:
<?php while ($this->next()): ?> <h1><?php $this->title(); ?></h1> <div class="page-content"> <?php $this->content(); ?> </div> <?php endwhile; ?>这里很多人会忘记while循环。Typecho的页面和文章共用一套数据输出方式,必须通过next()移动数据指针才能拿到当前页面的标题、内容和元信息。我见过不少新手写单页模板时直接写<?php $this->title(); ?>,结果页面空白,就是因为少了while包裹。
3.4 在单页模板中展示动态内容
单页模板的价值不仅在于“换皮的页面”,还能做成各种动态聚合页。比如我想建一个“归档页”,按时间列出所有文章,可以这样写:
<?php $this->widget('Widget_Archive@archive', 'pageSize=30&type=post')->to($archives); ?> <ul> <?php while ($archives->next()): ?> <li> <a href="<?php $archives->permalink(); ?>"><?php $archives->title(); ?></a> <time><?php $archives->date('Y-m-d'); ?></time> </li> <?php endwhile; ?> </ul>用widget方式可以手动拉取文章、指定分类文章、甚至拉取某个标签下内容。这意味着单页模板完全可以充当一个自定义查询页面。如果你需要做一个“产品列表页”或“资源导航页”,这种方式比后台一篇篇写文章再排版要高效得多。
提醒一下:
Widget_Archive@xxx中的@xxx是别名,用来区分同一个Widget的多个实例。如果你在一个页面里同时查询两批不同条件的文章,必须使用不同别名,否则数据会互相覆盖。
4. 不同分类使用不同模板
4.1 所有分类页共用 archive.php
这是Typecho一个明显的特性:所有归档页(分类、标签、日期、作者)都默认使用同一个archive.php渲染。如果你不做任何处理,所有分类页长得完全一样,只能靠内容区分彼此。
要做不同分类不同模板,核心思路是在archive.php中判断当前请求的分类,然后加载对应的子模板。Typecho在归档页上下文里提供了几个有用的属性:
$this->is('category'):判断当前是否为分类页;$this->category:当前分类的缩略名(slug);$this->getArchiveSlug():当前归档的缩略名,分类页和标签页都能用。
4.2 archive.php 中的分发代码
以默认主题为基底,archive.php可以写成如下结构:
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?> <?php $this->need('header.php'); ?> <?php if ($this->is('category')): ?> <?php $slug = $this->category; $categoryTemplate = __DIR__ . '/cat-' . $slug . '.php'; if (file_exists($categoryTemplate)) { include $categoryTemplate; } else { $this->need('archive-default.php'); } ?> <?php else: ?> <?php $this->need('archive-default.php'); ?> <?php endif; ?> <?php $this->need('footer.php'); ?>这样,每个分类都可以有一个独立的模板文件。比如我建了一个“视频”分类,缩略名设置为video,那么在主题目录建cat-video.php,访问视频分类时就会自动加载它。而其他所有分类共用archive-default.php。
4.3 子模板文件内部的循环结构
分类模板文件的内容,本质上是archive.php的拆分版。以cat-video.php为例:
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?> <section class="video-list"> <?php if ($this->have()): ?> <?php while ($this->next()): ?> <article class="video-card"> <a href="<?php $this->permalink(); ?>"> <?php if ($this->fields->thumb): ?> <img src="<?php $this->fields->thumb(); ?>" alt="<?php $this->title(); ?>"> <?php endif; ?> <h2><?php $this->title(); ?></h2> </a> <p><?php $this->excerpt(80); ?></p> </article> <?php endwhile; ?> <?php else: ?> <p>该分类下暂无内容</p> <?php endif; ?> </section> <?php $this->pageNav('上一页', '下一页'); ?>注意,子模板里不能再调用$this->need('header.php')和$this->need('footer.php'),因为archive.php已经在外面引入了。子模板只负责列表中间部分,避免页面结构重复。
这个设计的好处是:公共头部、底部、侧边栏只写一次,各分类模板只关注列表区差异。后续要调整全站导航,只改header.php,所有分类页同时生效。
4.4 分类模板的目录组织策略
如果站点分类很多,主题根目录会堆满cat-xxx.php文件。我建议在主题下建一个子目录统一存放,比如template/category/。分发改动如下:
$slug = $this->category; $categoryTemplate = __DIR__ . '/template/category/cat-' . $slug . '.php'; if (file_exists($categoryTemplate)) { include $categoryTemplate; } else { $this->need('archive-default.php'); }目录化之后,主题根目录干净很多。同样的思路也可以用到单页模板上:所有自定义单页模板放在template/page/下,后台依然能正常识别。
4.5 不同分类走不同侧边栏或样式细节
除了整个列表区换模板,有时只需要某个分类多一个侧边栏模块。这种情况下,不必把整个分类模板文件复制一遍,可以在archive.php里用一个变量控制侧边栏内容。
例如在archive.php中先获取分类,设置一个$sideBarType变量,然后在sidebar.php中根据这个变量输出不同的模块。这种方式比整模板切换更轻量,适合分类很多但差异不大的站点。
我个人的经验是:先把分类模板按“差异大小”分级。差异只在小部件层面,就用变量控制;差异在整体布局层面,才拆独立文件。一上来就给所有分类都建独立模板,往往会造成大量重复代码,后期改一个公共样式要同步十几个文件。
4.6 分类缩略名的匹配细节
使用$this->category时要注意:如果分类没有设置缩略名,Typecho生成的缩略名可能是中文字符或路径编码后的字符串。直接用来拼文件名,轻则匹配不上,重则产生非法文件名。
稳妥一点的做法,是在后台给需要的分类都设置好英文缩略名,比如news、video、life。如果某个分类忘设置缩略名,分发代码会自动让该分类走默认模板,不会报错,只是无法使用独立模板。
另外一个隐藏细节:Typecho的分类支持多级嵌套。如果文章同时属于父分类和子分类,$this->category返回的是主分类还是当前访问分类取决于具体上下文。为了让判断更精准,建议在分发时同时校验$this->archiveSlug或直接使用$this->getArchiveSlug()。后者返回的是当前存档页实际访问的分类缩略名,更可靠。
5. 常见问题与排查技巧实录
5.1 模板没有出现在后台下拉列表
这是单页模板最常遇到的问题。新建的page-xxx.php文件在后台“页面编辑”界面看不到,通常有三个原因:
- 模板头注释格式不正确,Typecho无法解析;
- 文件被放在子目录而没有通过后台“外观设置”重新检查,虽然子目录模板也能识别,但部分环境有缓存;
- 文件编码问题,包含BOM头导致输出干扰。
排查方法是:直接查看文件源码,确认<?php是文件第一个字符,注释块完整,且第一行文字就是你想要的模板名。必要时在控制台查看页面编辑接口返回的模板列表,能直观看到Typecho扫描到了哪些模板。
5.2 页面模板设置了,但前台页面空白
空白页在Typecho页面模板里最常见的原因是缺少while ($this->next())循环。页面、文章和归档数据输出的机制一致,都是迭代式获取。忘写循环,数据指针不会移动,所有输出函数都拿不到当前数据。
另一个常见原因是模板里声明的函数或变量与主题其他文件冲突。如果你在单页模板中定义了一个函数,而header.php里也定义了同名函数,直接报致命错误,页面自然空白。解决方式是给模板内函数加唯一前缀。
5.3 分类模板加载后内容重复出现
我在分发代码中第一次include分类模板时,就遇到过内容重复的诡异现象。后来发现是archive.php的分发代码没有return,include完cat-video.php后,程序继续执行了后面默认的列表输出逻辑,导致同一个文章列表渲染了两次。
正确做法是include后立即终止后续输出:
if (file_exists($categoryTemplate)) { include $categoryTemplate; return; }或者把分发逻辑写成else分支,确保每次只执行一条输出路径。
5.4 伪静态下分类页404
Typecho开启伪静态后,分类页URL通常形如/category/video/。如果你自定义分类模板后访问地址一直404,多半不是模板问题,而是伪静态规则没匹配上。
检查服务器规则是否包含Category路径的转发,比如Nginx下需要把/category相关的请求 rewrite 到index.php。这类问题和模板关系不大,但容易让人误判为模板缺陷,所以我把它列出来。
5.5 分类模板中分页不生效
如果你在cat-video.php里手动写查询,而不是复用当前$this对象,分页很可能丢失。原因是你用widget()建立了一个全新查询,它并不知道当前分类上下文,自然无法生成正确的分页链接。
正确做法是:分类子模板直接复用$this对象的循环,不要重新查询。只有单页模板需要聚合内容时才手动widget()查询。
5.6 自定义首页后文章列表失联
用方案C把首页设置为固定页面后,很多人发现站点访问首页时不再显示最新文章列表了,觉得是模板坏了。其实这是设置生效的正常表现,因为首页已经被指定为那个固定页面。
如果你希望首页保留文章流,同时又有静态展示效果,建议回到方案B:把静态首页代码写到home.php,由index.php分发。这样才能让首页既拥有自由度,又不丢失Typecho默认的文章流能力。
6. 最终落地建议
做这套自定义模板时,我踩过最大的坑是“一次性铺开”。比如一开始就想让十个分类都有自己的模板,结果模板没写完,主题文件倒是堆了一堆,最后维护成本高于收益。
更务实的推进路径是:先在archive.php里搭好分发骨架,然后只给流量最大、最重要的两三个分类做独立模板,其他分类走默认。等骨架稳定了,再逐步添加新分类模板,每次只动一个文件,风险可控,回退也容易。
另外,建议在主题的functions.php里预留一段模板优先级函数。比如允许通过后台选项指定某个分类使用哪个模板,而不是硬编码文件名。这样后续非技术人员也能在后台自由配置分类样式,扩展性更好。
Typecho模板系统的灵活性其实不低,只是它的“灵活性”更多体现在PHP代码组织层面,而不是像某些建站系统那样提供可视化的模板选择器。你只要理解了入口模板、分发逻辑、头部脚引用这三件事,就能把首页、单页、分类页都拿捏住。做完这套改造之后,站点不再是一套模板打天下,而是每个栏目都有自己该有的样子。