一个人的站群怎么做 SEO——同源 sitemap、域名资源与一条 308 陷阱
主站加六个产品子域名,sitemap 只覆盖主站。为什么一份 sitemap 管不了整个站群、Search Console 该建域名资源还是逐站验证、以及 Cloudflare Pages 上一条把每个条目都变成重定向的 .html 陷阱。
建站那篇里我写过一句话:「RSS 和 sitemap 由官方集成自动生成,不需要手工维护。」今天给整个站群补 SEO 时,这句话被现实修正了——它只对主站成立。
我的站不是一个站,是一串:主站 mahui.me(Astro / Cloudflare Pages),外加六个产品子域名——pier.app.mahui.me、diskly.app.mahui.me、mtinker.app.mahui.me、typing.app.mahui.me、shellby.app.mahui.me,以及自托管在 Docker + nginx 上的 www.yuwei.mahui.me。每个子域名是独立部署、独立仓库。主站的 sitemap 自动生成了 89 个页面,但一个子域名的页面都不在里面。
这篇讲清楚三件事:为什么一份 sitemap 管不了站群、该怎么在 Search Console 里组织、以及一条今天踩到的具体陷阱。
为什么一份 sitemap 覆盖不了子域名
这不是配置没做对,是 sitemap 协议本身的红线:一份 sitemap 只能列出与它同源(同一 host)的 URL。mahui.me/sitemap-index.xml 里塞 pier.app.mahui.me 的地址,Google 会直接忽略那些跨域条目——它们既不该在里面,塞进去也没用。
协议留了一个「跨站提交」的例外:如果两个域名在同一个 Search Console 属性下、或通过 robots.txt 交叉声明,可以在 A 的 sitemap 里引用 B。但那是给大型多站点场景设计的复杂机制,对一个人的站群是过度工程。正解简单得多:每个源各自一份 sitemap,各自在 robots.txt 里声明。
https://mahui.me/sitemap-index.xml ← 主站,89 页,自动生成
https://pier.app.mahui.me/sitemap.xml ← 各产品站自己一份
https://diskly.app.mahui.me/sitemap.xml
...
主站这份是 @astrojs/sitemap 构建时生成的;产品站大多是几页的手写静态站,sitemap 就是手写几行——一个几页的官网,维护成本约等于零。
Search Console:验证一次,覆盖全部
真正的「一起还是分开」的问题不在 sitemap,在 Search Console 怎么组织。这里有两种属性类型,选错了会给未来的自己埋活:
- URL 前缀属性(URL-prefix):按「协议 + 域名」精确匹配,
https://pier.app.mahui.me/是一个、https://diskly.app.mahui.me/是另一个。每建一个都要单独验证一次。站群有七个源,就是验证七次;以后每上一个新产品,再来一次。 - 域名资源(Domain property):按裸域名匹配,自动覆盖该域名下所有子域名和所有协议。验证方式是加一条 DNS TXT 记录。
对站群,答案明确:建一个 mahui.me 的域名资源,一条 DNS TXT 记录验证一次,mahui.me 和它未来所有的 *.mahui.me 子域名全部纳入。我的 DNS 本来就托管在 Cloudflare,加一条 TXT 是一分钟的事。
所以「一起还是分开」的准确回答是分两层的:验证一起做(一个域名资源盖全部),sitemap 各自提(协议要求同源),但所有 sitemap 都在这同一个域名资源的后台里提交和管理,入口还是一个。
Cloudflare 会往你的 robots.txt 前面塞东西
部署完 curl 线上的 robots.txt,开头多出一大段我没写的内容:
# BEGIN Cloudflare Managed content
User-agent: *
Content-Signal: search=yes,ai-train=no,use=reference
Allow: /
User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: Google-Extended
Disallow: /
...
这是 Cloudflare 的「屏蔽 AI 爬虫」功能——我在 dashboard 上开过,它会自动把这段托管内容前置到每个站的 robots.txt。要点是分清两类爬虫:GPTBot / ClaudeBot / CCBot / Google-Extended 是训练用爬虫,屏蔽它们不影响 Googlebot / Bingbot 这些搜索索引爬虫。搜索抓取照常,我自己写的 User-agent: * Allow: / 和 Sitemap: 声明也照常生效。看着吓人,实际不影响收录。
顺带一提,那个 Content-Signal: search=yes,ai-train=no 是一种较新的声明式表态:允许被搜索索引、不允许拿去训练模型。它不是强制手段,但把版权保留意图写在了机器可读的地方。
那条 308 陷阱
给 Pier 站补 sitemap 时,发现仓库里已经有一份 sitemap.xml(应该是之前某次没部署的残留)。内容看着没问题,但每个 URL 都带 .html 后缀:
<loc>https://pier.app.mahui.me/mac-port-monitor.html</loc>
<loc>https://pier.app.mahui.me/changelog.html</loc>
问题在于 Cloudflare Pages 默认做干净 URL:访问 /changelog.html 会 308 永久重定向到无扩展名的 /changelog。也就是说,sitemap 里列的每一个 URL,都是一个会立刻跳走的地址。
这对 SEO 是实打实的损耗。Search Console 抓到 sitemap 里的 /changelog.html,发现它 308 到别处,会把它标成「Page with redirect(有重定向的网页)」并拒绝索引这个 URL——它要的是重定向的终点。整份 sitemap 会变成一列「请不要索引我」的地址,白白浪费抓取预算,还制造 canonical 归属的混乱。
修复是把 sitemap 里的地址直接写成规范形态(无 .html),和 <link rel="canonical">、和用户地址栏里最终看到的地址三者对齐:
# 顺手一条 sed 把 9 个 URL 的 .html 削掉
sed -i '' 's/\.html<\/loc>/<\/loc>/' website/sitemap.xml
教训很小但很典型:sitemap 里该放的是「你希望被索引的最终 URL」,不是「能访问到内容的 URL」。两者在有重定向规则的托管平台上经常不是同一个。列进去之前,先 curl -I 看一眼有没有 3xx。
复盘
- 「自动生成、不用维护」是有边界的。框架的自动化只覆盖它自己那个源;站群的每一个源都得各自补齐,自动化不会跨过部署边界;
- sitemap 的同源红线决定了架构形态:文件按源拆分,管理靠 Search Console 的域名资源收拢。别想用一份文件覆盖全部,也别为此上跨站提交那套重机制;
- 域名资源是站群的默认选择。DNS TXT 验证一次,盖住所有现存和未来的子域名;逐个建 URL 前缀属性是在给自己攒重复劳动;
- 托管平台的重定向规则会污染 sitemap。Pages 的干净 URL、尾斜杠归一、www 跳转,任何一条都可能让你 sitemap 里的地址变成「重定向源」。列进 sitemap 的每类 URL,都值得先验一次它自己返不返回 200。
主站的 sitemap 今天已经随构建更新,六个产品站也各自补齐部署。剩下的只有一步 DNS 验证,那是只有我能做的事。
留言