本文约 7,198 字,预计阅读 18 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:想给”客户案例”做个单独的列表页,改了半天没反应
网站有个”客户案例”分类,希望它的列表页显示大图网格,其他分类保持普通列表。照着教程新建 category-cases.php,刷新后台、清缓存、改文件名大小写,折腾一小时毫无变化。
最后在数据库里一查:这个分类的别名是 cases-2。因为早期建过一个同名分类删掉了,WordPress 为了防止 URL 冲突,自动给新分类的别名加了数字后缀。
模板层级的规则不复杂,真正的坑在于”你以为的标识”和”WordPress 实际用的标识”不一致。
原理分析:分类模板的四级查找链
访问 /category/news/ 时,核心调用 get_category_template(),它交给 get_query_template() 按这个候选顺序找:
// wp-includes/template.php(简化)
function get_category_template() {
$category = get_queried_object();
$templates = array();
if ( ! empty( $category->slug ) ) {
$templates[] = "category-{$category->slug}.php"; // ① 按别名
$templates[] = "category-{$category->term_id}.php"; // ② 按 ID
}
$templates[] = 'category.php'; // ③ 所有分类通用
$templates[] = 'archive.php'; // ④ 所有归档通用
return get_query_template( 'category', $templates );
}
四个要点:
| 要点 | 说明 |
|---|---|
| 标识是 slug 或 term_id | 用分类名称(如”客户案例”)命名无效 |
| 顺序固定 | 别名 → ID → category.php → archive.php → index.php |
| 子主题优先 | 每个候选都先在子主题目录找,再找父主题 |
| 最终兜底 index.php | 找不到就走兜底,不会报错 |
get_queried_object() 返回当前查询的 WP_Term 对象,是分类模板里获取分类信息最可靠的方式:
$term = get_queried_object();
// $term->term_id / ->name / ->slug / ->description / ->count / ->parent / ->taxonomy
另一个高频混淆点:category.php 与 archive.php 的关系。archive.php 是”所有归档的兜底”,包括分类、标签、作者、日期、自定义类型归档;category.php 只管分类,且优先级更高。很多主题只有 archive.php,那分类页就会走它。
保姆级实操
第 1 步:确认分类的真实标识
# 列出所有分类的 ID 与别名(这才是文件名的依据)
wp term list category --fields=term_id,name,slug,count
后台也能看:「文章 → 分类目录 → 编辑」页面上的「别名(slug)」字段。注意:编辑框里如果显示的是老的别名,修改后要点”更新”才会生效,且改别名会让老 URL 失效(需要设置 301)。
确认当前页面真实使用的模板,加一段调试代码:
add_filter( 'template_include', function ( $template ) {
if ( current_user_can( 'manage_options' ) && ! is_admin() ) {
printf( '<!-- 模板:%s -->', esc_html( basename( $template ) ) );
}
return $template;
}, 99 );
第 2 步:创建分类专属模板
<?php
/** category-cases.php —— 客户案例分类:大图网格布局 */
get_header();
$term = get_queried_object();
?>
<main id="primary" class="site-main archive-cases">
<header class="page-header page-header--cases">
<h1 class="page-title"><?php echo esc_html( $term->name ); ?></h1>
<?php if ( $term->description ) : ?>
<div class="archive-description"><?php echo wp_kses_post( $term->description ); ?></div>
<?php endif; ?>
<p class="term-count">
<?php
printf(
esc_html__( '共 %d 个案例', 'mimangfei' ),
(int) $term->count
);
?>
</p>
</header>
<?php if ( have_posts() ) : ?>
<div class="cases-grid">
<?php
while ( have_posts() ) :
the_post();
get_template_part( 'template-parts/content/content', 'case' );
endwhile;
?>
</div>
<?php the_posts_pagination(); ?>
<?php else : ?>
<p><?php esc_html_e( '该分类下暂无内容。', 'mimangfei' ); ?></p>
<?php endif; ?>
</main>
<?php
get_footer();
第 3 步:给分类加专属字段
分类默认只有名称、别名、描述三个字段。需要”分类封面图””SEO 标题”这类额外字段时,用 term meta:
// 保存 term meta(挂在插件或主题里均可,建议插件)
add_action( 'created_category', 'mmf_save_cat_meta' );
add_action( 'edited_category', 'mmf_save_cat_meta' );
function mmf_save_cat_meta( $term_id ) {
if ( ! isset( $_POST['mmf_cat_image_nonce'] )
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['mmf_cat_image_nonce'] ) ), 'mmf_cat_image' ) ) {
return;
}
if ( isset( $_POST['mmf_cat_image'] ) ) {
update_term_meta( $term_id, 'mmf_cat_image', absint( $_POST['mmf_cat_image'] ) );
}
}
前台读取:
$image_id = get_term_meta( $term->term_id, 'mmf_cat_image', true );
if ( $image_id ) {
echo wp_get_attachment_image( (int) $image_id, 'hero', false, array( 'class' => 'archive-cover' ) );
}
update_term_meta/get_term_meta 自 WordPress 4.4 起可用,替代了旧的”把数据塞进 option”的做法。ACF(Advanced Custom Fields)等插件提供的分类字段,底层也是 term meta,读取方式一致。
第 4 步:不用堆文件——用过滤器按条件选模板
分类很多、但只有几种布局时,不要建十几个文件:
add_filter( 'category_template', function ( $template ) {
$term = get_queried_object();
if ( ! $term instanceof WP_Term ) {
return $template;
}
// 这几个分类用大图网格
$grid = array( 'cases', 'products', 'gallery' );
if ( in_array( $term->slug, $grid, true ) ) {
$alt = get_theme_file_path( 'template-parts/archive-grid.php' );
if ( file_exists( $alt ) ) {
return $alt;
}
}
// 顶级分类用带子栏目导航的版本
if ( 0 === (int) $term->parent ) {
$alt = get_theme_file_path( 'template-parts/archive-with-children.php' );
if ( file_exists( $alt ) ) {
return $alt;
}
}
return $template;
} );
get_theme_file_path() 会自动先查子主题、再查父主题,是 WordPress 4.7 起推荐的路径函数,比手写 get_stylesheet_directory() 更省心。
第 5 步:按分类调整每页数量
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_category( 'cases' ) ) {
$query->set( 'posts_per_page', 9 ); // 网格布局每行 3 个,取 9 的倍数
}
if ( $query->is_category( 'news' ) ) {
$query->set( 'posts_per_page', 20 );
}
} );
is_category() 的参数可以是别名、ID、名称或数组,判断多个分类时传数组:is_category( array( 'cases', 'products' ) )。
第 6 步:输出子分类导航
分类模板里常需要列出当前分类的子分类:
$children = get_terms( array(
'taxonomy' => 'category',
'parent' => $term->term_id,
'hide_empty' => true,
) );
if ( ! is_wp_error( $children ) && $children ) {
echo '<ul class="subcat-nav">';
foreach ( $children as $child ) {
printf(
'<li><a href="%1$s">%2$s <span class="count">(%3$d)</span></a></li>',
esc_url( get_term_link( $child ) ),
esc_html( $child->name ),
(int) $child->count
);
}
echo '</ul>';
}
get_terms() 返回 WP_Error 的可能不能忽略, taxonomy 不存在或参数错误时都会返回错误对象。
常见踩坑
- 用分类名称当文件名。 必须是别名(slug),中文名分类的别名往往是拼音或英文。
- 别名带
-2后缀。 删过同名分类就会出现,用wp term list category --fields=term_id,slug确认。 - 改了别名没做 301。 老 URL 直接 404,需要在
.htaccess或重定向插件里加规则。 - 文件名大小写不符。 Linux 下
Category-Cases.php与category-cases.php是两个文件,查找会静默失败。页面模板上也有同样的大小写坑,参见自定义页面模板 Page Template。 - 子主题放了文件但父主题的
category.php优先级更高。 不会——子主题的任何模板都优先于父主题同名文件;若发现相反,检查子主题的style.css有没有Template头。 - 案例: 某站给”行业动态”分类做了
category-news.php,本地测试正常,上线后无效。最后发现生产环境数据库里该分类的别名是news(正确),但生产环境启用的是另一个子主题目录——文件被上传到了wp-content/themes/parent/而非wp-content/themes/child/。父主题更新时文件还被覆盖丢失了一次。教训是:模板文件一律放子主题,并用wp option get stylesheet确认当前启用的主题目录名。模板层级的完整规则见WordPress 模板层级图解。 is_category()在archive.php里判断失效。 在归档页里is_category()是可用的,但必须在主查询建立之后(模板阶段没问题,挂在pre_get_posts里要用$query->is_category())。
总结
分类模板的核心就三句话:文件名用别名或 ID(category-{slug}.php / category-{id}.php),别名必须用 wp term list 核实过;模板里用 get_queried_object() 拿分类信息,用 get_term_meta() 读扩展字段;分类数量多时用 category_template 过滤器按条件选模板,别堆文件。每页数量用 pre_get_posts + is_category() 控制。排查”不生效”的固定顺序是:查别名 → 查当前真实模板 → 查文件是否在子主题目录。
