本文约 6,485 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:新建的 category-news.php 为什么不生效
想给”新闻”分类做一个单独的列表页,照着教程在主题根目录新建 category-news.php,刷新前台毫无变化。检查文件名没错、文件权限没错、缓存也清了。最后发现:这个分类的别名其实是 news-2——早期建过一个同名分类删除后,别名被 WordPress 自动加了后缀。
模板层级(Template Hierarchy)的规则并不复杂,难的是”你以为的查询条件”和”WordPress 实际判定的查询条件”不一致。搞清楚判定逻辑,比背文件名有用得多。
原理分析:从 URL 到模板的三步
访问任意地址时,WordPress 依次做三件事:
① 解析 URL → WP::parse_request(),根据重写规则得到 query_vars
② 执行查询 → WP_Query,根据 query_vars 决定 is_category / is_single / is_page 等条件标签
③ 挑选模板 → wp-includes/template-loader.php,按条件标签顺序找文件
第③步的核心是一串 if / elseif:
// wp-includes/template-loader.php(简化示意)
if ( is_404() && $template = get_404_template() ) :
elseif ( is_search() && $template = get_search_template() ) :
elseif ( is_front_page() && $template = get_front_page_template() ) :
elseif ( is_home() && $template = get_home_template() ) :
elseif ( is_post_type_archive() && $template = get_post_type_archive_template() ) :
elseif ( is_tax() && $template = get_taxonomy_template() ) :
elseif ( is_attachment() && $template = get_attachment_template() ) :
elseif ( is_single() && $template = get_single_template() ) :
elseif ( is_page() && $template = get_page_template() ) :
elseif ( is_category() && $template = get_category_template() ) :
elseif ( is_tag() && $template = get_tag_template() ) :
elseif ( is_author() && $template = get_author_template() ) :
elseif ( is_date() && $template = get_date_template() ) :
elseif ( is_archive() && $template = get_archive_template() ) :
else :
$template = get_index_template(); // 最终兜底
endif;
if ( $template = apply_filters( 'template_include', $template ) ) {
include $template;
}
每个 get_xxx_template() 内部又是一份候选清单,交给 get_query_template() 按从特殊到一般的顺序在子主题目录、父主题目录里逐个 locate_template(),找到第一个存在的就返回:
分类页: category-{slug}.php → category-{id}.php → category.php → archive.php → index.php
标签页: tag-{slug}.php → tag-{id}.php → tag.php → archive.php → index.php
自定义分类:taxonomy-{taxonomy}-{term}.php → taxonomy-{taxonomy}.php → taxonomy.php → archive.php → index.php
文章详情:single-{post_type}-{slug}.php → single-{post_type}.php → single.php → singular.php → index.php
页面: 自定义页面模板 → page-{slug}.php → page-{id}.php → page.php → singular.php → index.php
文章类型归档:archive-{post_type}.php → archive.php → index.php
作者页: author-{nicename}.php → author-{id}.php → author.php → archive.php → index.php
首页: front-page.php →(设置为静态首页时)page 规则 → home.php → index.php
查找顺序里有一条容易忽略的规则:先查子主题目录,再查父主题目录。所以子主题放一个同名文件就能覆盖父主题的任意模板,这条规则也是子主题机制的基础。
保姆级实操
第 1 步:先看当前页面到底用了哪个模板
装 Show Current Template 插件(作者 Andrey Savchenko)会在管理工具条上显示当前模板文件名与所属主题;也可以自己写一小段:
add_filter( 'template_include', function ( $template ) {
if ( current_user_can( 'manage_options' ) && ! is_admin() ) {
printf(
'<!-- 当前模板:%s -->',
esc_html( str_replace( WP_CONTENT_DIR, '', $template ) )
);
}
return $template;
}, 99 );
刷新页面后查看源代码,注释里就是真实生效的模板路径。这是排查一切的起点。
第 2 步:按”先通用后特殊”的顺序建模板
直接从 index.php 起步,缺哪个补哪个:先有 archive.php 覆盖所有归档,再按需加 category-news.php 这类特例。反过来从特例开始建,会导致维护时不知道通用逻辑在哪。
一个典型的分类特例页:
<?php
/** 新闻分类列表页:category-news.php */
get_header();
$term = get_queried_object(); // 当前分类的 WP_Term 对象
?>
<main class="site-main">
<header class="page-header">
<h1><?php echo esc_html( $term->name ); ?></h1>
<?php if ( $desc = category_description() ) : ?>
<div class="archive-description"><?php echo wp_kses_post( $desc ); ?></div>
<?php endif; ?>
</header>
<?php if ( have_posts() ) : ?>
<?php while ( have_posts() ) : the_post(); ?>
<?php get_template_part( 'template-parts/content', 'card' ); ?>
<?php endwhile; ?>
<?php the_posts_pagination(); ?>
<?php else : ?>
<p><?php esc_html_e( '该分类下暂无文章。', 'mimangfei' ); ?></p>
<?php endif; ?>
</main>
<?php
get_footer();
第 3 步:用 {$type}_template 过滤器做条件覆盖
不想新建一堆文件(比如要给几十个分类做不同布局)时,用动态钩子 {$type}_template 在代码里挑:
add_filter( 'category_template', function ( $template ) {
$term = get_queried_object();
// 顶级分类用带子栏目导航的版本,子分类用紧凑版
if ( $term && 0 === (int) $term->parent ) {
$alt = get_stylesheet_directory() . '/template-parts/archive-top.php';
if ( file_exists( $alt ) ) {
return $alt;
}
}
return $template;
} );
可用的钩子名与模板类型一一对应:index_template、category_template、tag_template、single_template、page_template、archive_template、attachment_template、frontpage_template、home_template。
第 4 步:用 template_include 做全局兜底干预
template_include 是最后一个过滤器,适合做”整站级别”的模板替换,比如落地页系统、A/B 测试:
add_filter( 'template_include', function ( $template ) {
if ( is_page() && get_post_meta( get_queried_object_id(), '_use_landing', true ) ) {
$landing = get_theme_file_path( 'page-templates/landing.php' );
if ( file_exists( $landing ) ) {
return $landing;
}
}
return $template;
} );
第 5 步:新建模板不生效时按这个顺序查
- 用第 1 步的注释确认当前真实模板;
- 核对标识:分类看别名(slug)不是名称,页面看别名或 ID;
- 清
wp_cache_flush()与页面缓存插件; - 确认文件在子主题或当前启用主题目录下,且大小写与 Linux 文件系统一致;若是页面类模板,还要确认后台是否另外选中了某个模板,参见自定义页面模板 Page Template;
- 用
wp option get stylesheet确认启用的到底是不是你正在改的那个主题。
常见踩坑
- 把分类名称当别名用。 分类名叫”行业新闻”但别名是
news,文件名必须是category-news.php。 home.php与front-page.php搞混。 后台「设置 → 阅读」选择静态首页时,front-page.php优先级最高,home.php只在”博客文章页”生效;设为”最新文章”时front-page.php依然优先。is_home()不是”首页”。 它指的是”文章列表页”,静态首页上is_home()为 false,这个命名坑了很多人。singular.php被忽略。 它在single.php/page.php之后、index.php之前,是两者共用的兜底,很多主题没建这个文件。- 案例: 某站给自定义文章类型建了
archive-product.php,前台却一直走archive.php。原因是register_post_type()里漏了'has_archive' => true,归档页根本没有被识别为文章类型归档,is_post_type_archive()为 false,模板层级自然匹配不到。补上参数后还要去「设置 → 固定链接」点一次保存刷新重写规则,详见自定义文章类型归档模板。 - 子主题只放了文件忘了
style.css的Template头。 子主题没被正确识别时,其模板文件不会进入查找链。 - 在
template_include里include之后没有exit。 有些老教程直接include再return,若同时挂了其他钩子会出现内容重复输出。正确做法是只返回路径,让 WordPress 自己去 include。
总结
模板层级只有一句话:WordPress 先由 URL 判定查询条件,再按”从特殊到一般”的顺序找文件,最后一定回落到 index.php。排查任何”模板不生效”的问题,第一步永远是确认当前真实生效的模板路径,第二步是确认查询条件的标识(slug / ID / post_type)与文件名是否一致,第三步才轮到缓存和权限。掌握 {$type}_template 和 template_include 这两个过滤器后,绝大多数定制需求不必靠堆文件实现。
