Serverless 也能做"相关阅读"?KV + Worker 自动内链方案
上篇讲了 4.6 万静态页怎么靠边缘缓存秒开。但有个矛盾:页面是死的,内容却想活一点,比如文章之间自动加"相关阅读"内链,加一篇新文,旧文自动带上它。
按本能你可能会想:"那别用静态页了,上 SSR 吧。" 但我前面说了,4.6 万页 SSR 现算不现实。这篇分享我在 GeoJSONcn 文章子项目里用的解法:静态页加 Serverless 轻加工,靠 Cloudflare KV 加 Worker 把"相关阅读"这种动态部分单独处理。
为什么是 KV 而不是数据库
"相关阅读"要的数据很简单:每篇文章的标题、标签、URL。这种小、读多写少、要毫秒级读取的数据,放 KV(键值存储)正合适:
- 不需要连数据库那套重架构;
- 边缘节点就近读,延迟极低;
- Worker 里几行代码就能取。
架构长这样:
请求 /articles/article-1.html
→ Worker 拦截
→ 从 KV 读「目录索引 index」(含所有文章标签)
→ 按当前文章标签算「相关」的几篇
→ 把 <!--RELATED--> 占位替换成内链 HTML
→ 返回
页面本体还是静态生成的,只有"相关阅读"这块在边缘现场算。
数据怎么存:一篇文章一个 KV 对
我后来把设计改了改,这点挺关键:
- index:一个轻量数组,只放每篇的 {slug, title, summary, tags},专门给算相关和列卡片用;
- article:
:每篇正文加元数据一体存一个 key。
好处是改一篇只动一个 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 万地图页和旧文纹丝不动。
避坑提醒
- KV 的 list 别放热路径。想靠列出所有 key 拼 hub?那是慢查询加分页,塞首屏会拖垮性能。聚合视图(index)老老实实单独维护。
- 绑定名要对。Worker 读的是 env.ARTICLES,你在控制台加变量时名字得填 ARTICLES,别填成命名空间标题,不然 env.ARTICLES 是 undefined,页面全空。
- 静态资源别被路由吞了。/articles/style.css 也要走正常静态分支出正确的 text/css,否则样式全失效。
最后
静态页负责快,Serverless(KV + Worker)负责活。把读多写少的小数据丢给 KV,在边缘做轻加工,既保住 4.6 万页的秒开,又有了动态内链能力。这个模式对博客、文档站、商品页都通用。
下一篇聊点更"中国特色"的:中国地图的"完整"到底怎么在数据里表达,尤其是南海诸岛和十段线。