前言
Fork 一个博客主题用了几年,文章越写越多,相册、番剧数据也越堆越厚。这时候再去合并上游更新,git merge upstream/master 一回车,满屏的 src/content/posts/、public/images/、src/data/ 都在打架——明明不是上游改的,只是因为我把这些东西也放进了代码仓库。
主题作者早就料到了这种场景,所以 Mizuki 自带一套「内容分离」机制:把文章、数据、图片、个人配置都搬进一个独立的内容仓库,代码仓库只保留主题本身。同步由 scripts/sync-content.js 完成,构建时通过 prebuild 钩子自动拉取。
这一篇就记录我自己的分离过程,从本地准备到推送远程、再到 Cloudflare Pages 部署,一气走通。
整体结构
分离之后,两个仓库各管各的:
| 仓库 | 负责什么 | 怎么更新 |
|---|---|---|
代码仓库 Mizuki | 主题、组件、构建、部署 | 推 master 触发 Cloudflare 构建 |
内容仓库 Mizuki-Content | 文章、关于/友链、数据、图片、overrides | 推 main,再回代码仓触发一次部署 |
代码仓库里的 src/content/posts、src/data、public/images 不再是真实文件,而是指向 content/ 目录的链接(Windows 上是 junction)。日常改文章,改的是 content/,提交也是 content/。
两个仓库和同步方向大致是这样:
准备 .env
在代码仓库根目录新建一个 .env(已经在 .gitignore 里,不会提交),写三行:
ENABLE_CONTENT_SYNC=trueCONTENT_REPO_URL=git@github.com:your-username/Mizuki-Content.gitCONTENT_DIR=./contentENABLE_CONTENT_SYNC 是总开关,true 才会从远程仓库同步;CONTENT_REPO_URL 是内容仓库的 Git 地址,支持 HTTPS 和 SSH,私有仓推荐 SSH;CONTENT_DIR 是同步到本地的目录,默认 ./content,一般不用动。
把内容搬进 content/
主题自带的脚本只负责「同步」,不负责「搬家」。搬家要自己来,把 src/content/posts、src/content/spec、src/data、public/images 整个复制到 content/ 下:
mkdir contentxcopy /E /I src\content\posts content\postsxcopy /E /I src\content\spec content\specxcopy /E /I src\data content\dataxcopy /E /I public\images content\images复制完,content/ 里就是一份完整的内容镜像。这时候还不要急着删原目录——备份比后悔药便宜。
备份比后悔药便宜在确认站点正常工作之前,不要删除代码仓库里的原始内容目录。Mizuki 的
sync-content脚本会自动把它们备份为*.backup,留着以防万一。
导出 overrides(可选但推荐)
fork 主题的人大多会改 src/config/ 下的配置:站点名、导航、侧栏、樱花特效……这些个人配置也是和上游冲突的高发区。Mizuki 提供了一个实验性的 export-config 脚本,自动把你和上游不一样的字段抽出来,写成覆盖文件:
实验性功能配置覆盖(overrides)目前仍处于早期阶段,测试覆盖的场景有限。迁移前请先提交或备份当前配置,迁移后务必完整校验一遍;重要站点建议先观察一段时间再决定是否长期使用。
pnpm export-config --out=content/overrides跑之前先确认代码仓库里有一个指向上游的 remote,并把它 fetch 下来作为基准:
git remote -v # 确认有 upstreamgit fetch upstream # 更新基准脚本会逐字段比对,自检「覆盖合并回去是否等于你当前的配置」,无法还原时会报出具体原因,不会生成不一致的覆盖文件。生成的文件名和被覆盖的导出常量名一致,比如:
content/overrides/├── siteConfig.ts├── profileConfig.ts├── navBarConfig.ts└── ...可覆盖的配置有 18 个,名单见官方文档。文件名不在名单内会在构建期报错并列出合法名单,不会静默失效,这点设计得很踏实。
初始化内容仓库
content/ 准备好之后,把它当作一个独立仓库初始化:
cd contentgit initgit add .git commit -m "chore: initialize content repository"git branch -M maingit remote add origin git@github.com:your-username/Mizuki-Content.gitgit push -u origin main推送之前,GitHub 上要先建好对应的空仓库。如果远程已经有自动生成的初始提交(比如 README),用 rebase 合一下就行:
git pull origin main --rebasegit push私有仓库的话,本机的 SSH 公钥要先加到 GitHub 账号,这一步只能手动,没人能替你登录。
跑一次同步
回到代码仓库根目录,运行:
pnpm run sync-content脚本会做这几件事:
git stash保存content/里的本地改动;git fetch --all更新远程引用;git reset --hard origin/main把内容仓拉到最新;- 把
content/posts、content/spec、content/data、content/images链接到代码仓里对应位置(原目录会被备份成*.backup); - 把
content/overrides复制到src/config/overrides/(不是链接,因为覆盖文件带相对 import,链接会让 Vite 解析到内容仓的真实路径)。
这五步的时序大致如下:
输出大致是这样的:
正在同步远程内容(强制模式)...内容同步成功(分支:main)正在建立内容链接...已创建符号链接:src/content/posts -> posts正在备份已有内容:src/data -> src/data.backup已创建符号链接:src/data -> data已复制配置覆盖:overrides -> src/config/overrides看到 内容同步完成 就稳了。
原目录已被备份
sync-content把src/content/posts、src/content/spec、src/data、public/images链接到content/后,原目录会被自动重命名为*.backup(如src/data.backup)。这些备份在.gitignore里,不会进入提交历史,确认站点正常后可手动删除。
一个坑:MDX 的相对导入
分离之后,本地 pnpm dev 一启动,Vite 立刻报了一连串错误:
Failed to load url ../../components/features/markdown/ContentPipelineFixture.astroin D:/BLOG/Mizuki/content/posts/content-pipeline-fixture.mdx. Does the file exist?文件明明在 src/components/features/markdown/ 下,怎么找不到?原因和 overrides 必须复制不能链接是同一个:Vite 会穿透 junction,按真实路径解析相对 import。
文章逻辑位置是 src/content/posts/xxx.mdx,相对路径 ../../components/... 算出来是 src/components/...,没问题。但 src/content/posts 是 junction,真实路径是 D:/BLOG/Mizuki/content/posts/,Vite 从这里出发算 ../../components/...,就跑到 content/components/ 去了,那地方当然什么都没有。
两条路径解析的对比如下,左侧能找到组件,右侧找不到:
修法很简单:MDX 里别用相对路径,改用代码仓配好的别名 @/:
// 改前import ContentPipelineFixture from "../../components/features/markdown/ContentPipelineFixture.astro";
// 改后import ContentPipelineFixture from "@/components/features/markdown/ContentPipelineFixture.astro";@/ 指向 src/,和物理路径无关,Vite 能正确解析。改完提交到内容仓即可。
部署到 Cloudflare Pages
内容分离在本地跑通之后,还要让线上构建也能拉到内容仓。这里以 Cloudflare Pages 为例。
配置环境变量
在 Pages 项目的 Settings → Variables and Secrets 里加两个变量(Production 和 Preview 都加):
ENABLE_CONTENT_SYNC=trueCONTENT_REPO_URL=https://<GITHUB_TOKEN>@github.com/your-username/Mizuki-Content.git注意两点:
- 不要用本机的 SSH URL,构建机里没有你的私钥;
CONTENT_REPO_URL用 HTTPS + Token 的形式,Token 单独建一个,权限只给内容仓的Contents: Read。
Token 安全GitHub Token 一旦泄露需立即在 https://github.com/settings/tokens 撤销并重新生成。
CONTENT_REPO_URL里嵌入的 Token 会被存进 Cloudflare Pages 的构建日志和变量历史,请使用最小权限(仅Contents: Read)的 fine-grained Token,并定期轮换。
CONTENT_REPO_URL 那个变量要标记为 secret_text,这样在 Dashboard 里不会再明文显示。
触发部署
Cloudflare Pages 监听的是代码仓库的 master。所以日常发版流程是:
- 改文章:在
content/里改,git push到内容仓; - 触发构建:再回到代码仓,做一个空提交或随便改一下,
git push origin master; - 等 Cloudflare 构建完成,
blog.aegisr.xyz就更新了。
只推内容仓不会自动发版——这是内容分离的副作用,需要在内容仓里再配一个 trigger-build.yml,用 repository_dispatch 反向触发代码仓构建,这一步留到下一篇文章再写。
只推内容仓不会发版Cloudflare Pages 监听的是代码仓库的
master分支。内容仓库的git push不会触发线上构建。临时方案是回代码仓做一个空提交再推master;长期方案见下一篇文章的repository_dispatch自动触发。
收尾
至此内容分离基本走通:
- 代码仓只管主题,合并上游不再和文章打架;
- 内容仓私有,草稿、图片、个人配置都和主题解耦;
- overrides 让跟进上游时配置冲突大幅减少;
- Cloudflare Pages 用 Token 拉私有内容仓,构建和本地一致。
剩下的可选收尾:
- 还原
src/config/为上游原版:让 overrides 真正生效,跟上游时配置文件零冲突; - 清理
*.backup:确认站点正常后,删掉src/content/posts.backup等备份目录; - 配置内容仓自动触发构建:写一个
repository_dispatch,让内容更新也能自动发版。
这些不影响线上,可以慢慢来。希望这篇能帮到同样在 fork 主题、被合并冲突折磨的朋友 o( ̄▽ ̄)d
If this article helped you, please share it with others!
Some information may be outdated