本文约 6,199 字,预计阅读 16 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:文章类型建好了,归档页一直是 404
用插件建了个”产品”类型,后台能看到、能发布、详情页也能打开,唯独 /product/ 这个列表页 404。反复检查固定链接设置,点了好几次保存,偶尔好一次,过两天又 404。
这是自定义文章类型最经典的三个坑叠加:has_archive 没开、归档模板文件名不对、重写规则在错误的时机被刷新。
原理分析:归档页存在需要两个条件同时满足
条件一:文章类型声明了归档(has_archive)。
register_post_type( 'product', array(
'public' => true,
'has_archive' => true, // 决定有没有 /product/ 这个归档页
'rewrite' => array( 'slug' => 'product', 'with_front' => false ),
'show_in_rest' => true,
) );
has_archive 可以是 true(用 rewrite['slug'])也可以直接是字符串(指定归档别名)。它是重写规则生成的开关——没有它,核心根本不会为这个类型生成归档规则,/product/ 在 parse_request 阶段就匹配失败,直接抛 404,模板文件做得再对也没用。
条件二:重写规则里有对应条目。规则存在 wp_options 的 rewrite_rules 这个 option 里,是一张巨大的 正则 => query_vars 映射表:
product/?$ → post_type=product
product/page/([0-9]{1,})/?$ → post_type=product&paged=$matches[1]
product/([^/]+)/?$ → post_type=product&name=$matches[1]
这张表在 register_post_type() 时不会自动写入数据库,只在刷新时重建。所以”注册完之后必须刷新一次”是硬要求。
刷新规则的三种方式:
| 方式 | 时机 | 代价 |
|---|---|---|
| 后台「设置 → 固定链接」点保存 | 手动 | 无,最安全 |
wp rewrite flush |
命令行 | 无 |
flush_rewrite_rules() |
代码 | 昂贵,重建整张表并写库 |
flush_rewrite_rules() 绝不能挂在 init 上——那等于每次请求都重建整张规则表,站点流量一上来数据库就崩。这是 Codex 反复强调的红线。
条件三(模板层):文件名必须为 archive-{post_type}.php,其中 {post_type} 是 register_post_type() 的第一个参数,且必须全小写。
保姆级实操
第 1 步:正确注册文章类型
add_action( 'init', function () {
$labels = array(
'name' => '产品',
'singular_name' => '产品',
'add_new_item' => '添加产品',
'edit_item' => '编辑产品',
'all_items' => '所有产品',
'search_items' => '搜索产品',
);
register_post_type( 'product', array(
'labels' => $labels,
'public' => true,
'has_archive' => 'products', // 归档别名用复数,与详情页 slug 区分
'rewrite' => array( 'slug' => 'product', 'with_front' => false ),
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'show_in_rest' => true, // 古腾堡 + REST API 支持
'menu_icon' => 'dashicons-cart',
'taxonomies' => array( 'product_cat' ),
) );
} );
小贴士:has_archive 用复数 products、rewrite['slug'] 用单数 product,可以让列表页 /products/ 与详情页 /product/xxx/ 一眼可辨,避免与同名的 Page 撞车。
第 2 步:在正确的时机刷新规则
三种安全写法:
# 方式一:命令行(推荐,最可控)
wp rewrite flush
// 方式二:插件激活时刷新
register_activation_hook( __FILE__, function () {
myplugin_register_post_types(); // 先注册
flush_rewrite_rules(); // 再刷新
} );
register_deactivation_hook( __FILE__, function () {
flush_rewrite_rules(); // 卸载插件后清掉规则
} );
// 方式三:主题启用时刷新(类型注册写在主题里时用这个)
add_action( 'after_switch_theme', function () {
flush_rewrite_rules();
} );
wp rewrite flush 之后用 wp rewrite list --match="/products" 抽查规则是否生成。
第 3 步:创建归档模板
<?php
/** archive-product.php —— 产品归档页 */
get_header();
?>
<main id="primary" class="site-main archive-product">
<header class="page-header">
<?php
the_archive_title( '<h1 class="page-title">', '</h1>' );
the_archive_description( '<div class="archive-description">', '</div>' );
?>
</header>
<?php if ( have_posts() ) : ?>
<div class="product-grid">
<?php
while ( have_posts() ) :
the_post();
get_template_part( 'template-parts/content/content', 'product' );
endwhile;
?>
</div>
<?php the_posts_pagination( array(
'mid_size' => 2,
'prev_text' => __( '上一页', 'mimangfei' ),
'next_text' => __( '下一页', 'mimangfei' ),
) ); ?>
<?php else : ?>
<?php get_template_part( 'template-parts/content/content', 'none' ); ?>
<?php endif; ?>
</main>
<?php
get_footer();
the_archive_title() 会输出 post_type_archive_title() 的结果,前置文案可用 post_type_archive_title 相关过滤器调整:
add_filter( 'post_type_archive_title', function ( $title ) {
if ( 'product' === get_query_var( 'post_type' ) ) {
return '产品中心';
}
return $title;
} );
第 4 步:控制归档页的每页数量与排序
归档页数量必须在主查询层面改,不能在模板里 query_posts:
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_post_type_archive( 'product' ) ) {
$query->set( 'posts_per_page', 12 );
$query->set( 'orderby', 'menu_order title' );
$query->set( 'order', 'ASC' );
}
} );
第 5 步:按分类法再细分模板
给产品分类单独做页时,模板层级是:
taxonomy-product_cat-phone.php → taxonomy-product_cat.php → taxonomy.php → archive.php → index.php
归档页里常用的查询对象获取方式:
$term = get_queried_object(); // 归档页上是 WP_Term 或 post type 对象
if ( is_post_type_archive() ) {
$post_type = get_query_var( 'post_type' );
$obj = get_post_type_object( $post_type );
echo esc_html( $obj->labels->name );
}
分类法归档模板的命名与优先级规则见分类模板 category-slug.php 用法。
第 6 步:验证清单
wp post-type list --fields=name,public,has_archive,rewrite
wp rewrite list --match="/products"
wp eval 'var_dump( is_post_type_archive( "product" ) );'
常见踩坑
- 漏了
has_archive。 详情页正常,归档页 404 且没有任何提示。 register_post_type挂在init之后。 规则生成早于类型注册,刷新也没用。archive-posttype.php用了注册时的标签名。 必须用第一个参数($post_type),如product而非产品。- 把
flush_rewrite_rules()挂在init上。 每次请求重建规则表,高并发下数据库压力剧增,是官方明令禁止的写法。 - 归档别名与某个页面别名相同。
/products/既有 Page 又有归档规则时,规则表里排在前面的胜出,表现随机。改名或改with_front规避。 - 案例: 某站用代码注册了
case类型却在调试时把flush_rewrite_rules()写进了init钩子里,本地一切正常。上线后日均 PV 三万,wp_options里rewrite_rules每次访问都被重写,数据库写入量飙升,站点在流量高峰直接 502。排查靠 Query Monitor 发现每次请求都有一次UPDATE wp_options。改成部署脚本里执行wp rewrite flush后恢复正常。教训是:刷新规则是一次性动作,永远不要放进常驻钩子。 show_in_rest没开导致区块编辑器不可用。 文章类型在前台正常,但后台编辑时回退到经典编辑器,REST 接口返回 404。- 换了主题后归档页 404。 类型注册被写在了旧主题的
functions.php里,换主题即失效,应按functions.php 与插件职责划分迁到插件里。
总结
归档页出问题按这个顺序查:wp post-type list 确认 has_archive 是否为真 → wp rewrite list 确认规则是否已生成(没有就 wp rewrite flush)→ 确认模板文件名是 archive-{post_type}.php 且 post_type 全小写 → 用 is_post_type_archive() 确认查询条件。三条红线记住:flush_rewrite_rules() 只放在激活钩子或命令行里,绝不放 init;文章类型注册代码放插件不放主题;归档页数量用 pre_get_posts 改,不在模板里另起查询。
