技术复盘:通过优化 Sitemap 真实性提升 Google 爬虫抓取频率
背景※#
近期发现两个站点的 Google 抓取频率差异巨大。A 站(Typecho 驱动)基本每 2-3 天抓取一次,而 B 站(Trilium 笔记经 Cloudflare Tunnel 穿透,详情可参考 TriliumNext 博客自动生成 Sitemap 站点地图及在搜索引擎添加站点收录)的上次读取时间竟然停留在两周前。
经过分析发现,B 站原有的自动脚本存在一个致命逻辑:每天定时任务运行时,会将所有页面的 <lastmod>(最后修改时间)强制更新为当天。这种“伪更新”被 Google 算法识别后,导致站点地图的信用分下降,抓取预算(Crawl Budget)被回收。
优化思路※#
- 还原真实修改时间:从笔记元数据中提取真正的
dateModified。 - 引入阶梯权重:给首页设置最高权重(1.0),普通笔记设置次高权重(0.8)。
- 优化抓取建议:首页设置为
daily(每日检查),笔记页设置为weekly(每周检查)。
核心方案实现※#
1. Trilium 自动发送脚本 (客户端)※#
该脚本运行在 Trilium 内部,负责遍历笔记并提取真实的修改日期,最后通过 fetch 推送至远端。
async function runSitemapTask() {
// --- 配置区 ---
const BASE_URL = 'https://xxx.xxx/'; // 你的站点地址
const ROOT_ID = 'xxxxxxxxxxxx'; // 笔记根目录 ID
const EXCLUDE_FOLDER = '未发表';
const REPORT_TITLE = 'Sitemap_最新抓取结果';
const ERR_TITLE = 'Sitemap_运行报错';
const WORKER_URL = 'https://xxx.xxx/sitemap.xml';
const TOKEN = 'your_secure_token'; // 验证令牌
try {
const rootNote = await api.getNote(ROOT_ID);
const ids = await rootNote.getDescendantNoteIds();
// 1. 初始化数据,首页日期设为运行当天
let sitemapData = [{
url: BASE_URL,
lastmod: new Date().toISOString().split('T')[0]
}];
// 2. 遍历并获取真实修改日期
for (const id of ids) {
const note = await api.getNote(id);
// 过滤:文本笔记、未删除、且不在排除目录
if (note && !note.isDeleted && note.type === 'text') {
const parents = await note.getParentNotes();
if (!parents.some(p => p.title === EXCLUDE_FOLDER)) {
// Trilium 的 dateModified 格式通常为 "YYYY-MM-DD HH:mm:ss"
// 截取前 10 位得到 W3C 标准的 "YYYY-MM-DD"
const realDate = note.dateModified.substring(0, 10);
sitemapData.push({
url: BASE_URL + id,
lastmod: realDate
});
}
}
}
// 3. 推送数据到 Worker
const resp = await fetch(WORKER_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': TOKEN,
'User-Agent': 'TriliumNext-Sitemap-Bot'
},
// 推送包含 url 和 lastmod 的对象数组
body: JSON.stringify({ data: sitemapData })
});
// 4. 更新 Trilium 内部运行报告
const statusText = resp.ok ? "推送成功 ✅" : `推送失败 ❌ 代码: ${resp.status}`;
const now = new Date().toLocaleString();
const html = `<h3>${statusText}</h3>
<p>有效链接数: ${sitemapData.length}</p>
<p>更新时间: ${now}</p>
<h4>数据预览 (前 5 条):</h4>
${sitemapData.slice(0, 5).map(item => `<code>${item.url}</code> (修改于: ${item.lastmod})`).join('<br>')}`;
const me = await api.getNote(api.currentNote.noteId);
const children = await me.getChildNotes();
const target = children.find(n => n.title === REPORT_TITLE);
if (target) {
await target.setContent(html);
} else {
await api.createNewNote({ parentNoteId: me.noteId, title: REPORT_TITLE, content: html, type: 'text' });
}
} catch (e) {
console.error("Sitemap Task Error: ", e);
// 错误报告逻辑可在此扩展
}
}
runSitemapTask();2. Cloudflare Worker 渲染引擎 (服务端)※#
Worker 负责持久化存储数据(KV 存储),并在爬虫访问时动态渲染出标准的 XML 格式。
export default {
async fetch(request, env) {
const url = new URL(request.url);
// 1. 处理 POST 请求 (接收来自客户端的数据并写入 KV)
if (request.method === "POST") {
const auth = request.headers.get("Authorization");
// 验证由环境变量定义的 AUTH_TOKEN
if (auth !== env.AUTH_TOKEN) return new Response("Unauthorized", { status: 401 });
try {
const payload = await request.json();
// 兼容旧版 urls 数组或新版 data 对象数组
const dataToStore = payload.data || payload.urls;
if (dataToStore && Array.isArray(dataToStore)) {
console.log("【写入 KV】记录总数:", dataToStore.length);
// 存入配置好的 KV 命名空间
await env.SITEMAP_KV.put("URL_DATA_LIST", JSON.stringify(dataToStore));
return new Response("OK - KV Updated");
}
return new Response("Invalid Format", { status: 400 });
} catch (err) {
return new Response("Error: " + err.message, { status: 500 });
}
}
// 2. 验证文件处理 (用于搜索引擎所有权验证)
if (url.pathname === "/baidu_verify_xxxx.html") {
return new Response("xxx_your_verify_code_xxx", { headers: { "Content-Type": "text/html" } });
}
if (url.pathname === "/BingSiteAuth.xml") {
return new Response(`<?xml version="1.0"?><users><user>xxx_bing_auth_id_xxx</user></users>`, { headers: { "Content-Type": "text/xml" } });
}
// 3. 处理 GET /sitemap.xml 展现
if (url.pathname === "/sitemap.xml" && request.method === "GET") {
const storedData = await env.SITEMAP_KV.get("URL_DATA_LIST");
if (!storedData) return new Response("Sitemap data not found.", { status: 404 });
const items = JSON.parse(storedData);
const xmlEntries = items.map(item => {
// 兼容性处理:区分 item 是字符串还是对象
const loc = typeof item === 'string' ? item : item.url;
const lastmod = typeof item === 'object' && item.lastmod ? item.lastmod : new Date().toISOString().split('T')[0];
// 逻辑优化:首页权重 1.0/daily,其他内页 0.8/weekly
const isHome = loc === "https://your-domain.xxx/" || loc === "https://your-domain.xxx";
const priority = isHome ? "1.0" : "0.8";
const freq = isHome ? "daily" : "weekly";
return ` <url>
<loc>${loc}</loc>
<lastmod>${lastmod}</lastmod>
<changefreq>${freq}</changefreq>
<priority>${priority}</priority>
</url>`;
}).join('\n');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${xmlEntries}
</urlset>`;
return new Response(xml, {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"X-Content-Type-Options": "nosniff"
}
});
}
return new Response("Sitemap Worker is running. Version: 2.0 (Dynamic LastMod)");
}
};总结※#
在 SEO 优化中,诚实比勤奋更重要。 过去我以为每天更新 Sitemap 日期能吸引爬虫,结果适得其反。通过将 lastmod 还原为真实的修改时间,Google 能够精准识别哪些页面是真正需要更新索引的。
后续: 手动在 Google Search Console 重新提交 sitemap.xml 后,状态已转为“成功”,预计抓取频率将在未来一周内回归正常。