我的博客是 WordPress 搭的,跑 Sakurairo 主题(iro)。前阵子做 SEO 体检,翻到 /shuoshuo/ 这一页的时候愣了一下——它的标题显示的是「文章 – XiaoZou123」。
这就有点奇怪了。shuoshuo 是我用来发短动态的一个自定义内容类型,注册的时候名字就叫「说说」。一个说说的归档页,标题怎么会是「文章」?
本来以为是个小问题,改个标题模板就完了。结果顺着往下查,发现问题比标题严重得多:这个页面和首页的内容,一模一样。
记录一下排查和处理过程。
第一反应:是不是模板配错了
先确认最表层的东西。
shuoshuo 这个内容类型的注册信息是正常的——我用接口把它拉出来看,label 字段明明白白写着「说说」,has_archive 是 true,也关联了分类。单篇说说的页面更是完全正常,标题、H1 都对。
那问题只可能出在归档页。于是我把 /shuoshuo/ 的 HTML 抓下来,看 body 上的 class:
archive post-type-archive post-type-archive-post wp-theme-Sakurairo hfeed
注意最后那个 post-type-archive-post。
WordPress 给归档页加 body class 的时候,格式是 post-type-archive-{类型名}。这里是 -post,也就是它认为自己在渲染的是「文章(post)」的归档,而不是「说说(shuoshuo)」的归档。
这就不是模板的问题了,是查询本身被改了。
顺手做了个对照实验,访问 /?post_type=shuoshuo——渲染结果和 /shuoshuo/ 一模一样,同样是 post-type-archive-post。而访问 /?post_type=post 反而回到了首页。两条线索指向同一个结论:这个页面的查询被改写成了 post 类型。
根因在主题的 pre_get_posts
去翻 Sakurairo 的源码。在 functions.php 里找到了这么一段:
add_action( 'pre_get_posts', 'customize_query_functions' );
function customize_query_functions( $query ) {
// 只影响前端
if ( $query->is_main_query() && ! is_admin() ) {
// 主页可以显示文章和说说
if ( is_home() ) {
$post_types = array( 'post', 'shuoshuo' );
$query->set( 'post_type', $post_types );
} elseif ( is_archive() || is_category() || is_author() ) {
// 保持其他页面的原有逻辑
$query->set( 'post_type', array( 'post', 'shuoshuo' ) );
}
}
}
问题就出在 is_archive() 这个分支上。
主题的本意是让归档页同时显示文章和说说——想法挺好,说说也是一种内容。但 is_archive() 是个很宽的条件,说说的归档页本身也满足它。于是访问 /shuoshuo/ 的时候:
- WordPress 先按 URL 解析,得出
post_type = shuoshuo - 主题的这个钩子跑起来,命中
is_archive(),把post_type覆写成['post', 'shuoshuo'] - 查询结果变成「文章 + 说说」的混合列表
而我的文章有 60 多篇,说说只有 1 篇。混在一起,说说直接被淹没了。
后面两个现象也都能解释通了:
- body class 变成
post-type-archive-post:WordPress 输出这个 class 时,如果 post_type 是数组会取第一个元素,['post','shuoshuo']的首个就是post。 - 标题变成「文章」:Slim SEO 生成归档页标题时要取 post_type 的标签,拿到的既然是
post,取出来的自然就是「文章」。
说白了,标题只是症状,查询被改写才是病。
影响其实比标题严重得多
标题难看是小事,接下来这个才是真正要处理的。
既然查询变成了「文章 + 说说」,而文章占绝大多数,那 /shuoshuo/ 渲染出来的列表,会不会就是首页那个列表?
做了个验证:用 Googlebot 的 UA 分别抓首页和 /shuoshuo/,把两页正文里的内容链接都提取出来做集合对比。结果是:
| 首页 | /shuoshuo/ | |
|---|---|---|
| 内容链接数 | 19 | 19 |
| 交集 | 19 | |
| 各自独有 | 0 / 0 |
19 条对 19 条,完全重合,两边都没有独一份的内容。
再补两个佐证:这个页面的 RSS(/shuoshuo/feed/)频道标题是「文章 – XiaoZou123」,条目全都是根级文章 URL,一条说说都没有;/shuoshuo/page/2/ 也返回 200——这是因为我设置了每页 10 篇,60 多篇文章才有第二页,1 篇说说不会有。
也就是说,/shuoshuo/ 实质上就是首页的另一个 URL。
这在 SEO 上是个挺典型的问题:
- 两个 URL 渲染几乎相同的内容,构成重复内容
- 这个页面当时是可索引的(robots 里没有 noindex)
- 它还被收录进了 sitemap(
sitemap-post-type-shuoshuo.xml)
等于主动告诉搜索引擎"我这里有两页一样的东西,都来抓吧"。权重被分散,抓取预算也被浪费。
怎么处理
摆在面前的路有几条,我一条条想过:
改主题的那段代码? 不太行。customize_query_functions() 同时管着首页、分类、作者页,为了修一个归档页去动它,影响面太大,而且主题一升级就白改了。
改标题模板? 治标。就算把标题改成「说说」,页面内容还是和首页重复的那一堆文章,问题没解决。
干脆让它不索引? 这看起来最合适。理由有两条:
一是这个页面无论怎么修都没有独立价值。保持现状,它和首页重复;就算把根因修好让它只显示说说,我这儿也就 1 篇说说(而且那篇本身已经是 noindex 了),照样是个薄页面。按我一贯的做法,薄内容页和重复内容页都不进索引。
二是一旦 noindex,标题就不再是问题了。不进索引的页面不会出现在搜索结果里,标题写给谁看呢。
所以方案定为:让 /shuoshuo/ 输出 noindex, follow,并把它从 sitemap 里摘出去。 保留 URL 可访问,也不影响内链权重传递。
写成一个输出层的小插件
不动主题、不动数据库,用一个插件在输出层解决,随时停用随时还原。
核心就两个过滤钩子。第一个负责输出 noindex:
add_filter( 'wp_robots', function ( $robots ) {
if ( xzsh_is_target_archive() ) {
unset( $robots['index'] );
$robots['noindex'] = true;
$robots['follow'] = true;
}
return $robots;
}, 99 );
这里用 wp_robots 而不是直接 echo 一个 meta 标签,是因为 WordPress 5.7 之后 robots meta 是统一从这个过滤器出来的,走这条路可以和核心、以及 Slim SEO 已有的 robots 设置合并进同一个标签,不会出现两个 robots 打架的情况。我站点的标签归档页也是走这条链路,等于机制已经被验证过了。
判断条件这里有个容易踩的坑。因为主题把 post_type 改成了数组,所以不能直接比字符串:
function xzsh_is_target_archive() {
if ( ! is_post_type_archive() ) {
return false;
}
$post_type = get_query_var( 'post_type' );
if ( is_array( $post_type ) ) {
return in_array( 'shuoshuo', $post_type, true ); // 主题改写后是数组
}
return 'shuoshuo' === $post_type;
}
写成 'shuoshuo' === get_query_var('post_type') 的话,因为拿到的是 ['post','shuoshuo'],这个条件永远不会成立,插件就等于没装。
第二个钩子负责把它从 sitemap 里摘掉。这一条是装了第一版之后才发现要补的——
我原本以为页面 noindex 之后,Slim SEO 会自动把 sitemap 里那一条去掉。结果实测发现 /shuoshuo/ 还好好地待在 sitemap-post-type-shuoshuo.xml 里。去翻 Slim SEO 的源码才明白:
它输出单篇文章时,会检查
slim_seo['noindex']跳过已 noindex 的条目;但输出 post type 归档 URL 的那段代码,完全不检查 noindex。
所以这事儿得自己来。好在 Slim SEO 留了钩子——Manager::get_post_types() 里有 slim_seo_sitemap_post_types 过滤器,而且 index sitemap 和各个子 sitemap 共用同一份列表,在这里剔除一次,两处都会生效:
add_filter( 'slim_seo_sitemap_post_types', function ( $post_types ) {
if ( ! is_array( $post_types ) ) {
return $post_types;
}
return array_values( array_diff( $post_types, [ 'shuoshuo' ] ) );
} );
完整源码
一个文件,存成 xz-shuoshuo-archive-noindex.php,放进 wp-content/plugins/xz-shuoshuo-archive-noindex/ 目录再启用就行:
<?php
/**
* Plugin Name: XZ 说说归档 Noindex
* Description: 让 /shuoshuo/ 归档页输出 noindex, follow,并从 sitemap 中移除。
* 该页被 Sakurairo 主题的 pre_get_posts 改成「文章 + 说说」混合查询,
* 渲染结果与首页完全重复。
* Version: 1.1.0
* Requires at least: 6.0
* Requires PHP: 7.4
* License: GPL-2.0-or-later
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'XZSH_TARGET_POST_TYPE', 'shuoshuo' );
/**
* 判断当前请求是否为 shuoshuo 归档页。
* 注意:Sakurairo 会把 post_type 覆写成 array( 'post', 'shuoshuo' ),
* 该查询变量此时是数组,必须展开判断。
*/
function xzsh_is_target_archive() {
if ( ! is_post_type_archive() ) {
return false;
}
$post_type = get_query_var( 'post_type' );
if ( is_array( $post_type ) ) {
return in_array( XZSH_TARGET_POST_TYPE, $post_type, true );
}
return XZSH_TARGET_POST_TYPE === $post_type;
}
/**
* 1) 输出 noindex, follow
*/
add_filter(
'wp_robots',
function ( $robots ) {
if ( xzsh_is_target_archive() ) {
unset( $robots['index'] );
$robots['noindex'] = true;
$robots['follow'] = true;
}
return $robots;
},
99
);
/**
* 2) 从 Slim SEO 的 sitemap 中移除 shuoshuo
* Slim SEO 输出 post type 归档 URL 时不检查 noindex,需要在这里剔除。
*/
add_filter(
'slim_seo_sitemap_post_types',
function ( $post_types ) {
if ( ! is_array( $post_types ) ) {
return $post_types;
}
return array_values( array_diff( $post_types, [ XZSH_TARGET_POST_TYPE ] ) );
}
);
如果你也遇到同样的问题但不想装插件,还有个更省事的做法:在 Slim SEO 的 Meta Tags 设置里,给「说说」这个内容类型勾上 No index。 效果和上面第二段代码等价,因为 Slim SEO 的 sitemap 逻辑读的就是这个开关。我用插件只是因为它的作用范围更明确,也不会顺带影响说说单篇的 robots 输出。
实测结果
启用之后逐项验证了一遍。目标页:
<meta name='robots' content='max-image-preview:large, ..., noindex, follow' />
/shuoshuo/ 和 /shuoshuo/page/2/ 都已经带上 noindex,而且 robots 标签仍然只有一个——说明走 wp_robots 这条路是对的,没有和 Slim SEO 打架。
再确认没有误伤别的页面:
| 页面 | robots |
|---|---|
| 首页 | 可索引 ✅ |
分类页 /category/technical-practice/ | 可索引 ✅ |
| 普通文章 | 可索引 ✅ |
| 标签页 | noindex(本来就是)✅ |
| 日期归档 | noindex(本来就是)✅ |
/shuoshuo/ 也确实从 sitemap 里消失了。
小结
几点经验:
- 标题不对,先查 body class。 页面标题取错类型标签,根因往往在更上游——查询本身被改了。
body上的post-type-archive-xxx是最快的切入点。 - 主题的
pre_get_posts影响面比看上去大。is_archive()这种宽条件会把自定义内容类型的归档页一并卷进来。判断"这个归档页是否正常",光看注册信息(label、has_archive)是不够的,那些字段全对也不代表渲染出来的对。 - 判断归档类型时记得处理数组。 post_type 被覆写成数组后,
'shuoshuo' === get_query_var('post_type')这种写法会静默失效——插件看起来装上了,其实一点作用没有。 - noindex 之后就不用管标题了。 这是我一贯的做法:页面既然不进索引,花力气去改它的
<title>没有收益。真正该做的是把页面从 sitemap 里摘干净。 - 别假设 SEO 插件会"顺带"帮你处理。 我一开始以为 Slim SEO 会把 noindex 的页面自动踢出 sitemap,实测才发现它对 post type 归档页根本没有这项检查。这类假设一定要抓一次实际输出验证。
目前 /shuoshuo/ 已经 noindex 并从 sitemap 移除,插件本身不写数据库、不留残留,停用即完全还原。这轮算是收拾干净了。
- 给博客做了一轮系统性的 SEO 优化,记录一下全过程Sakurairo主题
- Slim SEO与Sakurairo主题使用造成SEO冲突Sakurairo主题
- 给 Sakurairo 的 H2 高亮条BUG做减法,踩了三个 CSS 的坑Sakurairo主题

Comments NOTHING