Google Search Console 上周突然开始报警,说我们站点的索引率从 92% 掉到了 65%,一排查才发现,前阵子重构路由时把 URL 末尾的斜杠搞得一团糟。SEO 最忌讳的事情之一就是爬虫访问同一个资源可以通过 /about 和 /about/ 两个地址,而这两个地址没有做 301 重定向,甚至在 sitemap.xml 里的路径跟页面中的 Canonical 标签指向不一致。结果就是搜索引擎认为你存在大量重复内容,直接惩罚性地把页面从索引库里干掉了。
大部分人写 Sitemap 都是顺手用一个第三方框架自带的插件。对于小型静态网站来说,插件往往只会机械地获取当前的系统时间作为 lastmod(最后修改时间)。一旦你在 CI/CD 流程里进行构建,所有的文件创建时间都会被重置为拉取代码的那一刻,这就导致每次部署都会向爬虫宣告:“我整个站的几千个页面全部更新了!”爬虫疯狂爬了一圈发现内容根本没变,久而久之它就不信任你的 lastmod,进而大幅度降低抓取频率。
为了彻底摆脱这个大坑,我直接写了一个 Node.js 脚本,在构建生成静态页面时运行。它做了两件事:第一,强制统一 URL 的斜杠后缀,避免 canonical URL 不一致;第二,通过 git log 直接读取每个 markdown 文件的真实提交记录,提取真正的最后修改日期作为 lastmod。
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
const SITE_URL = 'https://example.com';
const CONTENT_DIR = path.resolve('./src/content/articles');
const OUTPUT_FILE = path.resolve('./public/sitemap.xml');
// 统一 URL 规范:强制添加末尾斜杠
function formatUrl(subPath) {
const cleanPath = subPath.replace(/\/+$/, '').replace(/^\/+/, '');
return cleanPath ? `${SITE_URL}/${cleanPath}/` : `${SITE_URL}/`;
}
// 获取 Git 文件最后一次提交的 ISO 时间戳
function getGitLastMod(filePath) {
try {
const gitDate = execSync(`git log -1 --format=%cI -- "${filePath}"`, { encoding: 'utf-8' }).trim();
return gitDate ? gitDate.split('T')[0] : new Date().toISOString().split('T')[0];
} catch (err) {
// 降级处理,获取系统文件修改时间
const stat = fs.statSync(filePath);
return stat.mtime.toISOString().split('T')[0];
}
}
function generateSitemap() {
const files = fs.readdirSync(CONTENT_DIR).filter(file => file.endsWith('.md') || file.endsWith('.mdx'));
let xmlItems = [];
// 1. 添加首页
xmlItems.push(` <url>
<loc>${formatUrl('')}</loc>
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>`);
// 2. 遍历内容目录,动态生成 URL 节点
files.forEach(file => {
const filePath = path.join(CONTENT_DIR, file);
const slug = file.replace(/\.mdx?$/, '');
const lastMod = getGitLastMod(filePath);
const pageUrl = formatUrl(`articles/${slug}`);
xmlItems.push(` <url>
<loc>${pageUrl}</loc>
<lastmod>${lastMod}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>`);
});
const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${xmlItems.join('\n')}
</urlset>`;
fs.writeFileSync(OUTPUT_FILE, sitemapXml, 'utf-8');
console.log(`[Success] Sitemap generated at ${OUTPUT_FILE}`);
}
generateSitemap();
如果你是在传统的 CMS 系统(比如基于 MySQL 的 WordPress 或 Strapi)里运行,那就更不能信任框架默认的 Sitemap 生成逻辑了。它们的做法通常是实时查一遍数据库把数据吐出来,每次爬虫请求都伴随着沉重的数据库查询,在高并发扫描下直接把数据库 CPU 拖垮到 100%。更好的做法是利用 Webhook 机制:只有在后台触发内容更新时,才异步构建一个静态的 sitemap.xml 到内存或者公共静态目录中,直接通过 Web 服务器(如 Nginx)直出。
把这套基于 Git 提交历史生成 lastmod 且统一后缀斜杠的方案上线两周后,Google 蜘蛛的抓取频率明显稳定了下来,Search Console 中的“已编入索引,未在站点地图中提交”的报错数量也成功清零。
你们的站点在配置 Sitemap 时是选择无脑信任框架插件,还是用过别的自定义生成脚本?有没有踩过 canonical tag 和 sitemap 冲突的坑?