Sakurairo 说说模块的一个坑:/shuoshuo/ 和首页内容完全重复

Zou, Ning 发布于 17 小时前 技术实践 2931 字 29 次阅读


我的博客是 WordPress 搭的,跑 Sakurairo 主题(iro)。前阵子做 SEO 体检,翻到 /shuoshuo/ 这一页的时候愣了一下——它的标题显示的是「文章 – XiaoZou123」。

这就有点奇怪了。shuoshuo 是我用来发短动态的一个自定义内容类型,注册的时候名字就叫「说说」。一个说说的归档页,标题怎么会是「文章」?

本来以为是个小问题,改个标题模板就完了。结果顺着往下查,发现问题比标题严重得多:这个页面和首页的内容,一模一样。

记录一下排查和处理过程。

第一反应:是不是模板配错了

先确认最表层的东西。

shuoshuo 这个内容类型的注册信息是正常的——我用接口把它拉出来看,label 字段明明白白写着「说说」,has_archivetrue,也关联了分类。单篇说说的页面更是完全正常,标题、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/ 的时候:

  1. WordPress 先按 URL 解析,得出 post_type = shuoshuo
  2. 主题的这个钩子跑起来,命中 is_archive(),把 post_type 覆写成 ['post', 'shuoshuo']
  3. 查询结果变成「文章 + 说说」的混合列表

而我的文章有 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/
内容链接数1919
交集19
各自独有0 / 0

19 条对 19 条,完全重合,两边都没有独一份的内容。

再补两个佐证:这个页面的 RSS(/shuoshuo/feed/)频道标题是「文章 – XiaoZou123」,条目全都是根级文章 URL,一条说说都没有/shuoshuo/page/2/ 也返回 200——这是因为我设置了每页 10 篇,60 多篇文章才有第二页,1 篇说说不会有。

也就是说,/shuoshuo/ 实质上就是首页的另一个 URL。

这在 SEO 上是个挺典型的问题:

  • 两个 URL 渲染几乎相同的内容,构成重复内容
  • 这个页面当时是可索引的(robots 里没有 noindex)
  • 它还被收录进了 sitemapsitemap-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 里消失了。

小结

几点经验:

  1. 标题不对,先查 body class。 页面标题取错类型标签,根因往往在更上游——查询本身被改了。body 上的 post-type-archive-xxx 是最快的切入点。
  2. 主题的 pre_get_posts 影响面比看上去大。 is_archive() 这种宽条件会把自定义内容类型的归档页一并卷进来。判断"这个归档页是否正常",光看注册信息(labelhas_archive)是不够的,那些字段全对也不代表渲染出来的对。
  3. 判断归档类型时记得处理数组。 post_type 被覆写成数组后,'shuoshuo' === get_query_var('post_type') 这种写法会静默失效——插件看起来装上了,其实一点作用没有。
  4. noindex 之后就不用管标题了。 这是我一贯的做法:页面既然不进索引,花力气去改它的 <title> 没有收益。真正该做的是把页面从 sitemap 里摘干净。
  5. 别假设 SEO 插件会"顺带"帮你处理。 我一开始以为 Slim SEO 会把 noindex 的页面自动踢出 sitemap,实测才发现它对 post type 归档页根本没有这项检查。这类假设一定要抓一次实际输出验证。

目前 /shuoshuo/ 已经 noindex 并从 sitemap 移除,插件本身不写数据库、不留残留,停用即完全还原。这轮算是收拾干净了。

欢迎来到XiaoZou123,这里是一个电脑极客、数码爱好者网站。我平时喜欢关注数码新闻,研究计算机技术。如果你看我头像觉得我是二次元,那我其实还算不上!
最后更新于 2026-09-14