Logo TodTom休闲时光

TriliumNext 博客自动生成 Sitemap 站点地图及在搜索引擎添加站点收录

摘要: 将 TriliumNext 笔记作为个人博客后,面临的最大问题是搜索引擎不收录。为了实现全自动 Sitemap 更新,并搞定百度死活连不上服务器的问题,我花了几个小时打通了这套 Cloudflare + Trilium 的联动方案。

前置环境#

  1. Trilium服务部署在nas上
  2. 拥有一个Cloudflare管理的域名
  3. Cloudflare Tunnel连接nas,并将Trilium服务映射到该域名的子域名下
  4. 博客主题使用的是Ankia-Theme

一、 核心流程#

  1. Trilium 脚本:定期扫描笔记树,过滤掉不想公开的“未发表”笔记,将 URL 列表打包推送。
  2. Cloudflare Worker:作为接收端,把链接存进 KV 数据库,并伪装成网站原生的 /sitemap.xml 路径吐出数据。
  3. 路由映射:将主域名下的 /sitemap.xml 及相关搜索引擎的验证文件流量引导至 Worker。

二、 Cloudflare 侧:从零搭建云端存储#

  1. 创建 KV 命名空间
    • 进入 存储和数据库 -> Workers KV。
    • 创建一个名为 SITEMAP_DATA 的空间。
  2. 配置 Worker 绑定
    • 进入 ComputeWorkers和Pages ,创建新的worker,如sitemap-worker
    • 绑定→添加 KV 命名空间绑定:变量名设为 SITEMAP_KV,值为SITEMAP_DATA
    • 设置→添加 变量和机密:设置 AUTH_TOKEN 作为一个简单的推送暗号,值自己设定随机字符串。
  3. 映射域名
    • →你的域名→Workers路由,添加路由将<你的域名>/sitemap.xml设置为对应的workersitemap-worker,同样对应的百度验证文件设置方法也是类似
    • 关键点:确保路由覆盖了验证文件和 XML 路径。

三、 关键代码#

1. Cloudflare Worker 端 负责接收推送并渲染成标准 XML 格式。

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // --- 1. 百度验证文件 (直接返回,不经过 KV) ---
    // 百度爬虫访问 todtom.org/baidu_verify_codeva-UfR46D0VWT.html 时触发
    if (url.pathname === "/baidu_verify_<百度给的内容>.html") {
      return new Response("<下载百度验证文件里面的字符串>", { 
        headers: { "Content-Type": "text/html; charset=utf-8" } 
      });
    }
    
    // --- 2. Bing验证文件 (直接返回,不经过 KV) ---
	if (url.pathname === "/BingSiteAuth.xml") {
      	const xmlContent = `<?xml version="1.0"?>
<users>
	<user>FB87BF58E6F5B88E1DB611258058FACE</user>
</users>`;

      	return new Response(xmlContent, {
        	headers: {
          		"Content-Type": "text/xml; charset=utf-8",
          		"Cache-Control": "public, max-age=86400"
        	}
      	});

    }
    // --- 3. Sitemap XML 展现 (GET) ---
    // 搜索引擎或用户访问 todtom.org/sitemap.xml 时触发
    if (url.pathname === "/sitemap.xml") {
      const storedUrls = await env.SITEMAP_KV.get("URL_LIST");
      if (!storedUrls) return new Response("Sitemap data not found. Please push from Trilium.", { status: 404 });

      const urls = JSON.parse(storedUrls);
      const date = new Date().toISOString().split('T')[0];
      const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map(u => `  <url><loc>${u}</loc><lastmod>${date}</lastmod></url>`).join('\n')}
</urlset>`;

      return new Response(xml, { 
        headers: { "Content-Type": "application/xml; charset=utf-8" } 
      });
    }

    // --- 4. 接收 Trilium 推送 (POST) ---
    if (request.method === "POST") {
      const auth = request.headers.get("Authorization");
      if (auth !== env.AUTH_TOKEN) return new Response("Unauthorized", { status: 401 });
      try {
        const data = await request.json();
        await env.SITEMAP_KV.put("URL_LIST", JSON.stringify(data.urls));
        return new Response("OK");
      } catch (err) {
        return new Response("Error: " + err.message, { status: 500 });
      }
    }

    return new Response("Sitemap Worker is running. Created by Gemini for TodTom.");
  }
};
JavaScript
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (request.method === "POST") {
      const auth = request.headers.get("Authorization");
      if (auth !== env.AUTH_TOKEN) return new Response("Unauthorized", { status: 401 });
      const data = await request.json();
      await env.SITEMAP_KV.put("URL_LIST", JSON.stringify(data.urls));
      return new Response("OK");
    }
    if (url.pathname === "/sitemap.xml") {
      const storedUrls = await env.SITEMAP_KV.get("URL_LIST");
      if (!storedUrls) return new Response("Not Found", { status: 404 });
      const urls = JSON.parse(storedUrls);
      const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map(u => `  <url><loc>${u}</loc><lastmod>${new Date().toISOString().split('T')[0]}</lastmod></url>`).join('\n')}
</urlset>`;
      return new Response(xml, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
    }
    return new Response("Running");
  }
};

2. Trilium 推送脚本

  • 路径递归:利用 rootNote.getDescendantNoteIds() 抓取所有笔记。
  • 异常捕获:必须写 try...catch 并将错误输出到新笔记,否则后台报错你根本看不见。
  • 避坑提示:某些环境下 api.fetch 会报错 is not a function,请改用全局 fetch。如果遇到 fetch failed,请检查网络或 User-Agent 伪装。
async function runSitemapTask() {
    // --- 配置区 ---
    const BASE_URL = 'https://<你的域名>/share/'; 
    const ROOT_ID = '<你的根笔记ID>'; 
    const EXCLUDE_FOLDER = '未发表';
    const REPORT_TITLE = 'Sitemap_最新抓取结果';
    const ERR_TITLE = 'Sitemap_运行报错';
    const WORKER_URL = 'https://<你的域名>/sitemap.xml'; 
    const TOKEN = '<你的TOKEN>'; 

    try {
        const rootNote = await api.getNote(ROOT_ID);
        const ids = await rootNote.getDescendantNoteIds();
        let urls = [BASE_URL];

        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)) {
                    urls.push(BASE_URL + id);
                }
            }
        }

        // --- 推送 ---
        const resp = await fetch(WORKER_URL, {
            method: 'POST',
            headers: { 
                'Content-Type': 'application/json', 
                'Authorization': TOKEN,
                'User-Agent': 'TriliumNext-Sitemap-Bot' 
            },
            body: JSON.stringify({ urls: urls })
        });

        const statusText = resp.ok ? "推送成功 ✅" : `推送失败 ❌ 代码: ${resp.status}`;
        const me = await api.getNote(api.currentNote.noteId);
        const children = await me.getChildNotes();
        
        // 加入了更新时间
        const now = new Date().toLocaleString();
        const html = `<h3>${statusText}</h3>` +
                     `<p>链接数: ${urls.length}</p>` +
                     `<p>更新时间: ${now}</p>` + 
                     `<h4>URL 列表:</h4>` + 
                     urls.join('<br>');

        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) {
        const me = await api.getNote(api.currentNote.noteId);
        const children = await me.getChildNotes();
        const errMsg = `错误详情: ${e.message}<br>时间: ${new Date().toLocaleString()}`;
        const errNote = children.find(n => n.title === ERR_TITLE);
        if (errNote) {
            await errNote.setContent(errMsg);
        } else {
            await api.createNewNote({ parentNoteId: me.noteId, title: ERR_TITLE, content: errMsg, type: 'text' });
        }
    }
}
runSitemapTask();

三、Google或百度填入你的站点地图#

  1. Google
  2. 百度
    • https://ziyuan.baidu.com/site
    • 如果你发现百度验证一直提示“无法连接到服务器”,大概率是 Cloudflare 的安全机制把它当成恶意攻击拦住了。
    • 免费放行方案:
      • 进入域名的安全性 -> 安全规则(别去点账户级那个 WAF,那个要钱!)。
      • 创建规则:设置 ASN 等于 38365
      • 操作选择 “跳过”,并勾选所有安全组件。
      • 部署后,百度就能秒连你的站点了。
    • 如果HTML验证始终无法通过,使用文件验证,下载百度的验证html文件后,按上述步骤操作
  3. Bing
    1. https://www.bing.com/webmasters/填写站点,并在站点地图中填入https://<你的域名>/sitemap.xml

四、 重点避雷(这都是几个小时换来的教训)#

  1. 路由伪装:直接给 Worker 设置域名路由(如 <你的域名>/sitemap.xml),比直接推送到 workers.dev 子域名要稳得多,且对搜索引擎更友好。
  2. Content-Type 必填:Worker 返回 XML 时,必须带上 headers: { "Content-Type": "application/xml" }。否则浏览器会把它当成纯文本,导致搜索引擎无法识别结构。
  3. 反馈机制:trilium中的js脚本不要只写 console.log,那是看不见的(也可能是学艺不精)。一定要写一个 try...catch 把错误信息直接生成笔记,否则你根本不知道脚本死在哪里了。

博文结语: 自动化虽然前期调试痛苦,但一旦跑通,以后每一篇笔记更新都能自动收录,这几个小时的汗水绝对值了。