Logo TodTom休闲时光

技术复盘:通过优化 Sitemap 真实性提升 Google 爬虫抓取频率

背景#

近期发现两个站点的 Google 抓取频率差异巨大。A 站(Typecho 驱动)基本每 2-3 天抓取一次,而 B 站(Trilium 笔记经 Cloudflare Tunnel 穿透,详情可参考 TriliumNext 博客自动生成 Sitemap 站点地图及在搜索引擎添加站点收录)的上次读取时间竟然停留在两周前。

经过分析发现,B 站原有的自动脚本存在一个致命逻辑:每天定时任务运行时,会将所有页面的 <lastmod>(最后修改时间)强制更新为当天。这种“伪更新”被 Google 算法识别后,导致站点地图的信用分下降,抓取预算(Crawl Budget)被回收。

优化思路#

  1. 还原真实修改时间:从笔记元数据中提取真正的 dateModified
  2. 引入阶梯权重:给首页设置最高权重(1.0),普通笔记设置次高权重(0.8)。
  3. 优化抓取建议:首页设置为 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 后,状态已转为“成功”,预计抓取频率将在未来一周内回归正常。