内容来源
Realm 项目把「内容」明确分成三层,任何一层都不是完整的 source of truth——它们互相补全、互相回退。
1. 仓库内类型化数据(data/)
所有结构化内容(作品、经历、生活、友链、技能、站点元信息)都在 data/ 下,每个 topic 按 locale 切分:
静态注册
lib/i18n-data.ts 是 locale → 模块的显式静态注册表,提供:
resolveWebProject(id, locale) 与 resolveLifeItem(slug, locale) 提供单条解析的 fallback 链:
返回的 TranslationStatus('source' | 'translated' | 'fallback')让详情页能展示 LocaleFallbackBanner。
为什么用静态注册表
不要把显式注册替换成动态 require。Pages Router 在 Node 侧做数据加载,动态 require 会触发 webpack 的 Critical dependency 警告,并让打包结果变得不可预测。GOTCHAS.md 与 CLAUDE.md 都显式标注了这条。
2. 仓库内兜底(content/)
只有 content/blog/init/ 一项:六个 locale 的内置兜底文章(zh-CN.mdx zh-TW.mdx en.mdx ja.mdx ru.mdx fr.mdx),在外部内容仓库未配置时充当博客的占位内容。
3. 外部 GitHub 内容仓库
当以下环境变量齐全时,运行时从私有 GitHub 仓库拉取:
期望仓库结构:
UI locales 与内容 locales 是两套。UI locales 只有 zh-CN / zh-TW / en;内容 locales 在此基础上可叠加 ja / ru / fr。多出来的内容 locale 走博文详情页内的语言切换器,不改变 UI 语言。
缺译时仍走 lib/i18n-data.ts 的 fallback 链,并在详情页渲染 LocaleFallbackBanner。
4. Tweet 压测模式
开发期可以开 TWEETS_STRESS_TEST=1,让 tweets 页面渲染合成数据(不再依赖外部仓库),可用 knob:
TWEETS_STRESS_YEARSTWEETS_STRESS_MONTHS_PER_YEARTWEETS_STRESS_TWEETS_PER_MONTH
生产环境绝不要开。
安全约束
lib/content/github.ts 在拼 GitHub Contents API 路径前会严格 normalize:
- 拒绝绝对 URL、协议相对 URL
- 拒绝前缀
/、反斜杠、query、hash、traversal、编码后的 traversal - 按段切分后逐段编码
- URL 由固定可信的 GitHub API base 拼装
GOTCHAS.md 第 22 条专门强调这点。任何把用户输入直接喂进 lib/content/github.ts 的改动都要先重新阅读。