历史:EdgeOne CMS 方案
历史材料: 本文记录已退役的 EdgeOne 方案与当时的约束。当前生产部署为 Cloudflare Pages + Workers KV/R2;以
/guide/deploy为准。
🔴 这份方案已作废(2026-08-22)——但结论要留着
决定:不走纯 EdgeOne 这条路,接入现有 CMS(Halo)。
作废的理由不是设计错了,是账算不过来:免费版与个人版配额相同且 CPU 时间 很紧,而触顶会殃及同域名下其他所有 EdgeOne 项目——爆炸半径超出本项目。 把可用性压在「配额够不够」上不可接受。
EdgeOne 仍然留着,但只当 CDN 与资源面,不再跑 Function。
下面这些实测结论依旧有效,只是不再约束本项目的架构。留着是因为它们是 花代价换来的,而且换任何一个「静态托管 + 边缘函数」平台都可能重演:
- 路由冲突时静态优先,函数不触发——静态产物存在时,边缘准入是死代码;
- 规则引擎与重建走同一条同步线路,生效速度一样,从来就不是快路;
- 真正的判据是「每请求现读强一致存储」而非「下发配置等它传播」;
- KV 最终一致、边缘缓存最长 60 秒;Blob 可对单次读切强一致。
铁律 11 那个「做不到」的困境随这次换宿主一起消失: 一台正常的服务器天然有请求期控制,且不按调用计费。 所以之前那三条候选(全量换 / 分级 / 改铁律 11)都不必选了。
目标:整站一个域名、功能有机结合、统一管理(装了哪些插件、什么显示什么不显示、 访问数据、SEO)。形态是 CMS + 插件,但不跑在 VPS 上,跑在 EdgeOne 边缘。
一、先说清楚缺了什么
@aio/kernel 是浏览器那一半。它管交互:点一下唤起 ADV、点一下显示精灵。
CMS 还需要边缘那一半——路由、服务端渲染、SEO 元信息。原因很硬:
2,400 多个页面(725 篇剧情 + 241 个角色 + 1,404 条卡牌)的内容如果只在 浏览器里由插件渲染,爬虫看到的是空壳。Google 勉强能跑 JS,百度基本不行—— 而中文资料站的主要流量来源恰恰是百度。
所以插件要有两半,由同一个插件 id 绑在一起:
插件 adv-player
├─ 边缘半边(@aio/site) 路由 /story/:id、SSR、meta、sitemap 条目
└─ 浏览器半边(@aio/kernel)能力 adv.play、surface 挂载、进度回流后台一个开关同时管住两边——这是「统一管理」在代码上的落点:
关掉 adv-player | 结果 |
|---|---|
| 边缘 | /story/:id 返回 404(reason: 'plugin-disabled'),且不进 sitemap |
| 浏览器 | 剧本行上的播放按钮消失,剧本照常可读 |
两半各写一套开关,迟早出现「后台显示关着,页面还在」这种查不出来的状态。 本包的 site.test.ts 里有一个测试专门钉这件事。
二、EdgeOne 上的落点
| CMS 部件 | 落在哪 | 备注 |
|---|---|---|
| 内容页 | 静态导出(现状) | 见 §二·五:静态导出读不到 KV |
| 动态行为 / 后台 / 下架兜底 | Pages Functions | KV 仅在边缘函数内可用 |
| 站点配置(插件开关、导航、SEO) | KV | 后台写、边缘读 |
| 下架清单 | Blob(强一致模式) | 见下节,不能放 KV |
| 资源字节 | Blob 或 COS + CDN | 20 GiB 量级 |
| 权威内容(交叉表、策略、契约) | git | 人工核对过的判断,需要 review 与历史 |
| 后台鉴权会话 | KV | 短 TTL |
| 访问统计 | Functions 写 KV | EdgeOne 官方有 PV 计数模板 |
| 缓存失效 | cacheTags + 主动 purge | 见第四节 |
🔴 KV 是最终一致的,最长 60 秒
官方文档写明:KV 采用中心存储 + 边缘缓存,其它边缘节点最长 60 秒才看到新值。
对导航、SEO 文案、插件开关,这没问题——晚一分钟生效无所谓。
只有一件事不能等:下架。 版权方来函时那 60 秒是实打实的暴露窗口。 所以 TakedownList 单独一份、单独一条读取路径,走 Blob 的强一致模式, 代价是需要判定下架的请求多一次强一致读。
这不是洁癖:同类项目里已经出现过因为这种压力而退役公开站的先例, 仓库里有一份 TAKEDOWN.md。下架能力是这个站的必备件,不是可选件。
其它已知限额
| 项 | 值 |
|---|---|
| KV 单值 | 25 MB |
| KV 键名 | 512 字节 |
| KV 账号总容量 | 1 GB(免费额度) |
| KV 命名空间 | 10 个/账号 |
list() 分页 | 默认 256 条 |
| KV 可用范围 | 仅边缘函数内 |
这些数字取自 EdgeOne Pages 官方文档页。开工前仍需在控制台复核一次—— 见
docs/CONSTRAINTS.md。
二·五、🔴 静态导出与 KV 不兼容——这条要先解决
apps/station 现在是 Next.js 静态导出(next build → out/,CI 已在看着)。 而官方文档写明 KV「仅支持在边缘函数内使用」。
两件事放一起就是一个硬矛盾:
静态导出的页面在构建时就定型了,请求到来时没有任何代码在跑, 因此读不到 KV。后台改一个插件开关,静态页面不会知道。
三条出路,各有代价:
| 做法 | 开关生效速度 | SEO | 代价 | |
|---|---|---|---|---|
| A | 纯静态:配置在构建期烘焙 | 一次重建+部署(分钟级) | ✅ 真 HTML | 后台变成「生成一个配置提交并触发构建」,不是即时开关 |
| B | 全部改 Functions/SSR | 秒级(KV 60 秒最终一致) | ✅ | 放弃静态导出;每请求都要算,成本与冷启动 |
| C | 混合(建议) | 见下 | ✅ | 多几个活动部件 |
建议走 C,分工如下
| 内容 | 交付方式 | 配置从哪来 |
|---|---|---|
| 2,400 个内容页(角色/剧情/卡牌) | 静态导出 | 构建期烘焙。它们的内容本来就来自交叉表与清单,本就该构建期定型 |
| 插件开关、导航、SEO 文案 | 静态页 + 构建期烘焙;后台改动触发重建 | KV 存的是「下次构建要用的值」 |
| 浏览器侧的能力门禁 | 客户端启动时向一个 Function 端点拉当前配置 | KV,秒级 |
| 下架 | 🔴 没有可用机制,见下 | Blob 强一致 |
| 后台、鉴权、统计 | Functions | KV |
为什么下架不能靠重建
静态页已经在边缘缓存里了,重建要几分钟。收到通知时的正确动作是 立刻让那条路径不可达——那只能由请求路径上的东西做。
packages/site 的 TakedownList 与 isTakenDown() 因此有两个消费者: 构建期(把下架的页排除出 sitemap 与产物)与请求期(兜住漏网的缓存副本)。 两处都要,缺一个都有窗口。
🔴 请求期那一道:判据一直写错了
原方案写「请求期由规则引擎或 Function 兜住」,把规则引擎当成快路。 两条实测事实(2026-08-22 由维护者查证)把它推翻了:
- 规则引擎与重建走同一条同步线路,生效速度一样。 它从来就不是快路—— 这条比「Pages 项目有没有规则引擎」根本得多:就算有,也不解决问题。
- 路由冲突时静态优先。 Edge Functions 与 Node.js Functions 的路由若与 静态资源冲突,请求优先被路由到静态资源,函数不会被触发。
所以真正的判据不是「规则引擎 vs 重建」,而是:
每次请求现读一个强一致存储 ——还是—— 下发一份配置等它传播。
后者不管叫重建、purge 还是规则引擎,延迟下限是同一个。 只有前者能做到「收到通知,下一个请求就不可达」。
这就把可选项收敛到了一种形状:那条路径由 Function 出内容, 且 Function 每次请求现读下架清单(本仓库早已把它设计成走 Blob 强一致读, 正是为此)。而 Function 要能触发,那条路径下就不能有静态产物。
嵌入面已经是这个形状(tools/pack-embed-pages.mjs 把页面搬进函数的包、 删掉 out/ 下的原件)。把它套到 2,400 个内容页上是一个大决定 (每请求一次函数调用的成本与延迟、边缘冷启动、SEO 影响), 不该由实现顺手定。见「待拍板事项」。
这条的落地顺序
先确认 EdgeOne 的缓存 purge 是否支持按 tag(待验证项 3)。 只支持按 URL 的话,rev: 标签方案要换成版本化路径(如 /_v7/character/1001 再由规则重写),那会改变整个 URL 设计——必须在铺开内容页之前定下来。
三、内容模型:不要做内容编辑器
这个站的内容 95% 是生成的,不是编辑出来的:
交叉表(git)+ 资源清单(Blob)
│
│ 边缘插件的 render()
▼
角色页 / 剧情页 / 卡牌页 ← 2,400+ 个,没有一个是人写的真正需要人编辑的只有四类,全都是配置而不是内容:
- 站点设置(站名、导航、SEO 模板)
- 插件开关
- 个别页面的 SEO 覆盖(
overrides['/character/1001'] = { title, description }) - 下架清单
所以 SiteConfig 里没有 posts / pages 这类表。内容是管道产物,配置才是内容管理。 这省掉了 CMS 里最重最容易做歪的一块(编辑器、草稿、版本、发布流)。
要发公告或写「关于」页怎么办?——那是一个插件(pages 插件, 内容放 git 的 Markdown,构建时进 Blob),不是往核心里加一张表。
四、缓存失效才是真正的难点
边缘 CMS 的坑不在渲染,在改了东西之后旧页面还在。
本包的做法:每个渲染结果带 cacheTags。
cacheTags: ['a:scenario/310241', 'rev:7']a:scenario/310241—— 内容标签。这篇剧情更新/下架时按此 purge。rev:7—— 配置版本号,每次后台保存自增。改了任何设置, 所有旧页面的标签集就整体失效,不必逐页记住谁受影响。
第二条是关键。没有它,「改了标题模板,为什么只有一半页面变了」会成为常驻问题。
五、SEO 的具体做法
| 要素 | 由谁产出 |
|---|---|
<title> / description | resolveMeta():页面标题套 titleTemplate,覆盖项优先 |
canonical | canonicalOrigin + 路径,防止同内容多路径 |
robots | 三层叠加,见下 |
| JSON-LD | 插件在 meta.jsonld 里给,</ 已转义防脚本提前闭合 |
sitemap.xml | site.sitemap():枚举开着的插件 × 可索引 × 未下架 |
robots.txt | site.robots():全站关索引时不再递 sitemap |
索引默认是 selective,不是 all
indexing: 'all' | 'none' | 'selective' // 默认 selective默认只索引显式登记的路径前缀。反过来(默认全收)意味着任何人加一条路由 都会顺带把它推给搜索引擎,而没人会在加路由时想到这件事。
'none' 是总闸:收到通知时一键收缩整站可见面,页面级 index 也盖不过它。
一个必须由你决定的取舍
把 725 篇剧情正文做进索引,SEO 收益很大——但那是版权方的文本, 索引得越全,被发现得越快。这类公开站退役过不止一次。
方案默认的姿态是保守的(selective + 空前缀 = 什么都不索引), 把决定权留给你。要放开哪些前缀,是你的判断,不是我的。
六、后台
后台不是「一个面板」,是三件具体的事:
| 页面 | 干什么 | 后端 |
|---|---|---|
/admin/plugins | 插件开关、看每个插件提供什么能力、哪些 ref kind 没有提供者(缺口视图) | KV |
/admin/content | 下架开关、资源线路健康、清单版本 | Blob 强一致 + KV |
/admin/seo | 索引模式、允许前缀、单页覆盖、sitemap 预览 | KV |
/admin/analytics | PV/UV、热门页面 | KV 计数 |
「缺口视图」几乎免费:内核已有 plugins / providersFor / can, 列出哪些能力没人提供就是几十行。
鉴权:EdgeOne Functions + KV 会话。robots.txt 里 Disallow: /admin/, 且后台路由一律 noindex——两道都要,因为 Disallow 只是请求,不是强制。
七、落地顺序
| 步 | 做什么 | 前提 |
|---|---|---|
| 1 | 一个 Pages Functions 入口 + Site 接上 KV 配置,跑通 /character/:id 的 SSR | 域名 |
| 2 | sitemap.xml / robots.txt / cacheTags purge | 步 1 |
| 3 | /admin/plugins + 鉴权,验证「一个开关两边都生效」 | 步 1 |
| 4 | 下架链路(Blob 强一致 + purge),在放开索引之前做完 | 步 2 |
| 5 | 资源搬迁到 Blob/COS,插件真正接上 | 独立于 1–4 |
| 6 | PV 统计与热门页 | 步 1 |
步 4 必须在放开索引之前。 先把内容推给搜索引擎、再去建下架能力, 顺序反了就是给自己挖坑。
待验证项
- Pages Functions 的 CPU 时间 / 内存 / 请求体上限(决定 SSR 能做多重)。
- Blob 强一致读的延迟与配额(下架链路的关键路径)。
- EdgeOne 的缓存 purge API 是否支持按 tag,还是只能按 URL / 前缀。 只能按 URL 的话,第四节的
rev:标签方案要改成版本化路径。 - ICP 备案(国内加速必须)。