Skip to content

边缘半边:路由、SEO、下架

@magireco/kernel 只有浏览器那一半,它管交互。CMS 还需要边缘那一半, 理由很硬:

2400 多个页面(725 篇剧情 + 241 个角色 + 1404 条记忆结晶)的内容若只在浏览器里 由插件渲染,爬虫看到的是空壳——Google 勉强能跑 JS,百度基本不行, 而中文资料站的主要流量恰恰来自百度。

@magireco/site 是这一半。设计推理见 CMS-ON-EDGEONE.md

一个插件,两半,一个开关

铁律 10

边缘半边与浏览器半边由同一个插件 id 绑定,后台一个开关同时管住两边。 各写一套开关,迟早出现「后台显示关着、页面还在」这种查不出来的状态。

关掉 adv-player  →  边缘:/story/:id 返回 404(reason: 'plugin-disabled'),
                          且不进 sitemap
                →  浏览器:kernel.can('adv.play', ref) 为假,播放按钮消失

这条有专门的测试钉着。

装一个站点

ts
import { Site, loadConfig, DEFAULT_CONFIG } from '@magireco/site';

const { config, problems } = loadConfig(await kv.get('site-config'));
if (problems.length > 0) console.warn('站点配置有问题,已回退:', problems);

const site = new Site({
  config,
  takedown: await readTakedownList(),          // 强一致读,见下
  capabilityProviders: { 'adv.play': ['adv-play'] },
});

site.register({
  id: 'adv-play',
  routes: [{ pattern: '/story/:id', enumerate: () => listScenarioPaths() }],
  async render(match) {
    const doc = await loadScenario(match.params['id']!);
    if (doc === null) return null;             // → 404 'not-found'
    return {
      meta: { title: doc.title, description: doc.summary },
      html: renderScenarioHtml(doc),           // 爬虫看到的就是这一段
      needs: ['adv.play'],                     // 决定下发哪些插件 chunk
      cacheTags: [`mr:scenario/${doc.id}`],
    };
  },
});

const res = await site.handle('/story/310241');
// { status: 200, meta, html, needs, cacheTags, pluginId } | { status: 404, reason }

404 分四种,不压成一种

no-route / plugin-disabled / not-found / taken-down。 后台要看得出是哪一种——压成一个 404 会让排查无从下手。

配置:坏了就回退,绝不抛

ts
const { config, problems } = loadConfig(raw);

loadConfig 永远不抛,坏字段一律回退默认值并把问题列进 problems。 理由:这东西每次请求都要读,一个手滑写坏的 JSON 不该让整站 500。

indexing 缺省是 selective 而不是 all默认全收意味着任何人加一条路由 都会顺带把它推给搜索引擎,而没人会在加路由时想到这件事。

路由:只有静态段与 :param

没有正则。路由表是 sitemap 的来源,越花哨越难枚举,枚举不出来的路由等于没有 SEO。静态段赢过参数段,与注册顺序无关(/story/latest 不会被 /story/:id 抢走)。

ts
site.unenumerableRoutes();   // 带参数却没法枚举的路由——CI 里报出来

SEO

ts
await site.sitemap();   // 只收:插件开着 + 可索引 + 未下架
site.robots();

这三道过滤必须合并在一处做:分散在别处的话,后台关了一个插件而 sitemap 还在递它的 URL,搜索引擎会一直来撞 404。

robots 三层叠加:全站闸 > 页面声明 > 单页覆盖。全站关索引时不再递 sitemap。 JSON-LD 里的 </ 被转义,防止脚本提前闭合。

🔴 下架:两处都要做

铁律 11

EdgeOne KV 是最终一致的(边缘缓存最长 60 秒),而静态导出的页面读不到 KV、 重建要几分钟。下架必须同时做两处:

  1. 构建期排除出产物与 sitemap;
  2. 请求期由规则引擎 / Function 兜住缓存副本。

缺一个都有暴露窗口。收到下架通知时,「重建要几分钟」那几分钟是实打实的暴露。

所以下架清单单独一份、单独一条读取路径:站点配置放 KV(最终一致), 下架清单走强一致读,代价是每次请求多一次强一致读,只在需要判定的路径上做。

支持两种口径:

ts
const takedown = {
  pathPrefixes: ['/story/310241'],       // 路径前缀
  refPrefixes: ['mr:sprite/'],           // ref 前缀(页面 cacheTags 里带 ref)
};

页面自己声明的 ref 也要过一遍判定——角色页引用了被下架的精灵,那个角色页 也得挡住。

缓存标签带配置版本号

每个页面的 cacheTags 自动带上 rev:N,配置每次保存自增。 没有它,「改了标题模板为什么只有一半页面变了」会成为常驻问题。

静态导出与 KV 的矛盾

apps/stationoutput: 'export' 的静态导出,而 KV 仅在边缘函数内可用 ——静态页读不到 KV,后台改开关静态页不会知道。三条出路写在 CMS-ON-EDGEONE.md 里,当前建议是:

  • 内容页保持静态(爬虫要的就是它,而且最快);
  • 动态行为(开关、下架判定、鉴权)走 Functions;
  • 下架靠规则引擎 + purge 兜住缓存副本,不等重建

GPLv3。素材版权归各自的版权方所有,本站与本仓库不含任何素材。