搜索全站
主题 17 分钟阅读

WordPress 模板层级图解:一个 URL 怎么找到模板文件

用一张完整的模板层级图和源码级调用链,讲清访问任意 URL 时 WordPress 如何挑选模板文件,附三个常用过滤器与"新建模板不生效"的排查步骤。

U
UFO_PF 2017-05-15 · 阅读 17 分钟
Wo 主题
阅读提示
本文约 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. 用第 1 步的注释确认当前真实模板;
  2. 核对标识:分类看别名(slug)不是名称,页面看别名或 ID;
  3. 清 wp_cache_flush() 与页面缓存插件;
  4. 确认文件在子主题或当前启用主题目录下,且大小写与 Linux 文件系统一致;若是页面类模板,还要确认后台是否另外选中了某个模板,参见自定义页面模板 Page Template;
  5. 用 wp option get stylesheet 确认启用的到底是不是你正在改的那个主题。

常见踩坑

  1. 把分类名称当别名用。 分类名叫”行业新闻”但别名是 news,文件名必须是 category-news.php。
  2. home.php 与 front-page.php 搞混。 后台「设置 → 阅读」选择静态首页时,front-page.php 优先级最高,home.php 只在”博客文章页”生效;设为”最新文章”时 front-page.php 依然优先。
  3. is_home() 不是”首页”。 它指的是”文章列表页”,静态首页上 is_home() 为 false,这个命名坑了很多人。
  4. singular.php 被忽略。 它在 single.php / page.php 之后、index.php 之前,是两者共用的兜底,很多主题没建这个文件。
  5. 案例: 某站给自定义文章类型建了 archive-product.php,前台却一直走 archive.php。原因是 register_post_type() 里漏了 'has_archive' => true,归档页根本没有被识别为文章类型归档,is_post_type_archive() 为 false,模板层级自然匹配不到。补上参数后还要去「设置 → 固定链接」点一次保存刷新重写规则,详见自定义文章类型归档模板。
  6. 子主题只放了文件忘了 style.css 的 Template 头。 子主题没被正确识别时,其模板文件不会进入查找链。
  7. 在 template_include 里 include 之后没有 exit。 有些老教程直接 include 再 return,若同时挂了其他钩子会出现内容重复输出。正确做法是只返回路径,让 WordPress 自己去 include。

总结

模板层级只有一句话:WordPress 先由 URL 判定查询条件,再按”从特殊到一般”的顺序找文件,最后一定回落到 index.php。排查任何”模板不生效”的问题,第一步永远是确认当前真实生效的模板路径,第二步是确认查询条件的标识(slug / ID / post_type)与文件名是否一致,第三步才轮到缓存和权限。掌握 {$type}_template 和 template_include 这两个过滤器后,绝大多数定制需求不必靠堆文件实现。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。