TriliumNext 博客自动生成 Sitemap 站点地图及在搜索引擎添加站点收录
摘要: 将 TriliumNext 笔记作为个人博客后,面临的最大问题是搜索引擎不收录。为了实现全自动 Sitemap 更新,并搞定百度死活连不上服务器的问题,我花了几个小时打通了这套 Cloudflare + Trilium 的联动方案。
前置环境※#
- Trilium服务部署在nas上
- 拥有一个Cloudflare管理的域名
- Cloudflare Tunnel连接nas,并将Trilium服务映射到该域名的子域名下
- 博客主题使用的是Ankia-Theme
一、 核心流程※#
- Trilium 脚本:定期扫描笔记树,过滤掉不想公开的“未发表”笔记,将 URL 列表打包推送。
- Cloudflare Worker:作为接收端,把链接存进 KV 数据库,并伪装成网站原生的
/sitemap.xml路径吐出数据。 - 路由映射:将主域名下的
/sitemap.xml及相关搜索引擎的验证文件流量引导至 Worker。
二、 Cloudflare 侧:从零搭建云端存储※#
- 创建 KV 命名空间:
- 进入
存储和数据库-> Workers KV。 - 创建一个名为
SITEMAP_DATA的空间。
- 进入
- 配置 Worker 绑定:
- 进入
Compute→Workers和Pages,创建新的worker,如sitemap-worker - 绑定→添加 KV 命名空间绑定:变量名设为
SITEMAP_KV,值为SITEMAP_DATA。 - 设置→添加 变量和机密:设置
AUTH_TOKEN作为一个简单的推送暗号,值自己设定随机字符串。
- 进入
- 映射域名:
- 域→你的域名→Workers路由,添加路由将<你的域名>/sitemap.xml设置为对应的worker
sitemap-worker,同样对应的百度验证文件设置方法也是类似 - 关键点:确保路由覆盖了验证文件和 XML 路径。
- 域→你的域名→Workers路由,添加路由将<你的域名>/sitemap.xml设置为对应的worker
三、 关键代码※#
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或百度填入你的站点地图※#
- Google
- https://search.google.com/search-console 填写站点,并在站点地图中填入
https://<你的域名>/sitemap.xml
- https://search.google.com/search-console 填写站点,并在站点地图中填入
- 百度
- https://ziyuan.baidu.com/site
- 如果你发现百度验证一直提示“无法连接到服务器”,大概率是 Cloudflare 的安全机制把它当成恶意攻击拦住了。
- 免费放行方案:
- 进入域名的安全性 -> 安全规则(别去点账户级那个 WAF,那个要钱!)。
- 创建规则:设置 ASN 等于 38365。
- 操作选择 “跳过”,并勾选所有安全组件。
- 部署后,百度就能秒连你的站点了。
- 如果HTML验证始终无法通过,使用文件验证,下载百度的验证html文件后,按上述步骤操作
- Bing
- https://www.bing.com/webmasters/填写站点,并在站点地图中填入
https://<你的域名>/sitemap.xml
- https://www.bing.com/webmasters/填写站点,并在站点地图中填入
四、 重点避雷(这都是几个小时换来的教训)※#
- 路由伪装:直接给 Worker 设置域名路由(如
<你的域名>/sitemap.xml),比直接推送到workers.dev子域名要稳得多,且对搜索引擎更友好。 - Content-Type 必填:Worker 返回 XML 时,必须带上
headers: { "Content-Type": "application/xml" }。否则浏览器会把它当成纯文本,导致搜索引擎无法识别结构。 - 反馈机制:trilium中的js脚本不要只写
console.log,那是看不见的(也可能是学艺不精)。一定要写一个try...catch把错误信息直接生成笔记,否则你根本不知道脚本死在哪里了。
博文结语: 自动化虽然前期调试痛苦,但一旦跑通,以后每一篇笔记更新都能自动收录,这几个小时的汗水绝对值了。