GeoJSONcn
GeoJSONcn / 技术专栏 / Serverless 也能做"相…

Serverless 也能做"相关阅读"?KV + Worker 自动内链方案

发布于 2026-07-15 · GeoJSONcn 技术团队 · 阅读约 6 分钟

上篇讲了 4.6 万静态页怎么靠边缘缓存秒开。但有个矛盾:页面是死的,内容却想活一点,比如文章之间自动加"相关阅读"内链,加一篇新文,旧文自动带上它。

按本能你可能会想:"那别用静态页了,上 SSR 吧。" 但我前面说了,4.6 万页 SSR 现算不现实。这篇分享我在 GeoJSONcn 文章子项目里用的解法:静态页加 Serverless 轻加工,靠 Cloudflare KV 加 Worker 把"相关阅读"这种动态部分单独处理。

为什么是 KV 而不是数据库

"相关阅读"要的数据很简单:每篇文章的标题、标签、URL。这种小、读多写少、要毫秒级读取的数据,放 KV(键值存储)正合适:

架构长这样:

请求 /articles/article-1.html
  → Worker 拦截
    → 从 KV 读「目录索引 index」(含所有文章标签)
    → 按当前文章标签算「相关」的几篇
    → 把 <!--RELATED--> 占位替换成内链 HTML
    → 返回

页面本体还是静态生成的,只有"相关阅读"这块在边缘现场算。

数据怎么存:一篇文章一个 KV 对

我后来把设计改了改,这点挺关键:

好处是改一篇只动一个 key,不用重写整个索引。几百篇也不慌。

核心代码(Worker 侧)

async function getArticleIndex(env) {
  if (!env.ARTICLES) return [];
  const raw = await env.ARTICLES.get('index');
  try { return JSON.parse(raw || '[]'); } catch { return []; }
}

function renderRelated(slug, list) {
  const cur = list.find(a => a.slug === slug);
  if (!cur) return '';
  const related = list
    .filter(a => a.slug !== slug && a.tags.some(t => cur.tags.includes(t)))
    .slice(0, 3);
  return `<div class="related"><div class="rl-title">相关阅读</div>`
    + related.map(a => `<a href="/articles/${a.slug}.html">${a.title} →</a>`).join('')
    + `</div>`;
}

就这么点代码,文章页底部就自动长出"相关阅读",而且新发一篇,所有旧文按标签自动带上它,因为每次渲染都实时算。

发布一篇只要一条命令

因为数据都在 KV,发新文不用动静态生成流水线:

node publish_article.mjs --file articles/article-4.html \
  --meta '{"slug":"article-4","title":"...","tags":["..."]}'

脚本干三件事:写 article:article-4、更新轻量 index、刷一下边缘缓存。4.6 万地图页和旧文纹丝不动。

避坑提醒

最后

静态页负责快,Serverless(KV + Worker)负责活。把读多写少的小数据丢给 KV,在边缘做轻加工,既保住 4.6 万页的秒开,又有了动态内链能力。这个模式对博客、文档站、商品页都通用。

下一篇聊点更"中国特色"的:中国地图的"完整"到底怎么在数据里表达,尤其是南海诸岛和十段线。