在 EdgeOne 上做 CMS
目标:整站一个域名、功能有机结合、统一管理(装了哪些插件、什么显示什么不显示、 访问数据、SEO)。形态是 CMS + 插件,但不跑在 VPS 上,跑在 EdgeOne 边缘。
一、先说清楚缺了什么
@magireco/kernel 是浏览器那一半。它管交互:点一下唤起 ADV、点一下显示精灵。
CMS 还需要边缘那一半——路由、服务端渲染、SEO 元信息。原因很硬:
2,400 多个页面(725 篇剧情 + 241 个角色 + 1,404 条记忆结晶)的内容如果只在 浏览器里由插件渲染,爬虫看到的是空壳。Google 勉强能跑 JS,百度基本不行—— 而中文资料站的主要流量来源恰恰是百度。
所以插件要有两半,由同一个插件 id 绑在一起:
插件 adv-player
├─ 边缘半边(@magireco/site) 路由 /story/:id、SSR、meta、sitemap 条目
└─ 浏览器半边(@magireco/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,秒级 |
| 下架 | EdgeOne 规则引擎按路径 404/跳转 + 缓存 purge | Blob 强一致 |
| 后台、鉴权、统计 | Functions | KV |
为什么下架不能靠重建
静态页已经在边缘缓存里了,重建要几分钟。收到通知时的正确动作是 立刻让那条路径不可达——那只能由请求路径上的东西做,也就是 EdgeOne 的规则引擎或一个前置 Function,配合缓存 purge。
packages/site 的 TakedownList 与 isTakenDown() 因此有两个消费者: 构建期(把下架的页排除出 sitemap 与产物)与请求期(规则/Function 兜住漏网的缓存副本)。 两处都要,缺一个都有窗口。
这条的落地顺序
先确认 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: ['mr:scenario/310241', 'rev:7']mr: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 备案(国内加速必须)。