Back to Blogs

如何将你的 MixSpace 文章自动同步到 Hexo

Archived on Polygon with a publicly verifiable record.
Block Height 95634190 96486016
Transaction Hash 0x65c8f438fa53cdd6bffc007c04327671fa921e18fa2cda4af2ef2d8067c663ba
Contract Address 0xDad794c60d7cAD5E85769D29c4F96C7ca0F3760b

如果你已经用 Mix Space 管理文章和笔记,又希望用 Hexo 提供一个轻量、快速的站点,可以让 Hexo 仓库定期从 Mix Space API 拉取内容,再自动构建并部署到 GitHub Pages。

整体流程

工作流每 8 小时运行一次,也可以由 push 或手动操作触发。每次运行都会重新读取线上公开内容,生成当前完整的文章目录,然后执行 Hexo 构建。

图中的“分配或复用数字编号”并不是随便编号,而是靠一张映射表来完成。先有个印象,后面会具体讲。

为什么从 API 全量同步

只追加新文章看起来更简单,但无法正确处理删除、隐藏和修改。当前实现每次都读取文章和笔记的完整列表:

GET /api/v3/posts?page=1&size=50&truncate=1
GET /api/v3/notes?page=1&size=50&withSummary=1

列表接口返回分页信息后,程序会一直请求到 page === total_pages。列表只用于获得文章身份和摘要,随后会为每一项请求详情:

GET /api/v3/posts/{id}
GET /api/v3/notes/nid/{nid}?single=1

这样可以拿到标题、正文、标签、分类、创建时间、修改时间和原始路由等字段。未登录请求只能获得 Mix Space 允许公开的内容,未发布、定时未公开、密码内容不会被同步。

文章和笔记如何共存

文章和笔记使用不同的 API 身份:

类型 稳定身份 原站路径
文章 post:{id} /posts/{categorySlug}/{slug}
笔记 note:{id} /notes/{nid}

同步程序不会使用标题作为身份,因为标题可以修改。它使用 Mix Space 返回的不可变 id,并把文章和笔记分别标记为 post:id 或 note:id。

数字编号和映射表

看到这里,可能有人会问:既然 Mix Space 的 id 已经能区分文章和笔记,为什么还要数字编号?

原因是,Hexo 站点的文章路径用的是数字编号,例如:

/archives/1/
/archives/2/
/archives/3/

这个数字和 Mix Space 的文章 ID 没有直接关系。它只是 Hexo 内部使用的路径编号。

你可以这样理解:

  • Mix Space 的 post:id 或 note:id 像身份证号,代表“这篇内容是谁”;
  • Hexo 的 /archives/1/、/archives/2/ 像门牌号,代表“这篇内容在 Hexo 里住哪里”;
  • 根目录的 .content-sync-map.json 就是一张通讯录,负责记住“身份证号”和“门牌号”的对应关系。

这张映射表大致会记录:

记录内容 作用
post:{id} / note:{id} 这篇内容在 Mix Space 里的稳定身份
数字编号,如 1、2、3 这篇内容在 Hexo 里的固定编号
active / inactive 这篇内容当前是否还在同步范围内

它的工作方式并不复杂:

  1. 第一次同步时,映射表里还没有记录,程序会按照创建时间给内容分配下一个数字。
  2. 后续同步时,程序先拿到 post:id 或 note:id,再去映射表里查旧编号。查到了就继续用,查不到才分配新编号。
  3. Mix Space 修改标题、分类或 slug 时,身份不变,所以 Hexo 数字编号也不变。
  4. Mix Space 删除或隐藏内容时,对应文件会从当前站点移除,映射表里把它标记为 inactive。
  5. 已经用过的数字不会重新分配给另一篇内容,哪怕原来的文章已经被删除。

这样做的好处有两个:

  1. 修改标题、分类或 Mix Space slug 不会改变 Hexo 永久链接。
  2. 已经使用过的编号不会重新分配给另一篇内容。

所以不要手动重命名 source/_posts/1.md 这类文件,也不要删除映射文件。它们是同步程序保持永久路径的依据。

原文链接如何自动生成

每篇生成的 Hexo 文件都会写入 original_url:

original_url: https://moitr.ren/posts/categories/example

文章的原文路径由分类 slug 和文章 slug 组合而成,笔记则使用它的 nid:

文章:/posts/{category.slug}/{slug}
笔记:/notes/{nid}

域名是固定的 https://moitr.ren。这样即使 Hexo 使用数字归档路径,读者仍然可以从文章页回到 Mix Space 原文。这需要你的主题支持。

正文格式和 Core 容器

当 content_format 为 markdown 时,程序使用 API 返回的 text。当格式为 lexical 时,当前实现同样优先使用 Mix Space 提供的公开 text,避免在同步端重复实现编辑器 JSON 的全部渲染规则。

同步阶段还会处理 Mix Space 的部分自定义 Markdown 容器。例如:

  • masonry 容器会转换成 Hexo 主题已有的图片画廊结构。
  • success 容器会转换成 Markdown 引用块。
  • 未知容器保持原样,避免自动同步误删原文。

换行会统一为 LF,正文尾部只保留一个换行,时间统一写成 UTC ISO 格式。这样 Windows 本地运行和 Linux Actions 运行也能得到稳定的文件内容。

GitHub Actions 的构建顺序

工作流的核心步骤如下:

当前定时表达式是:

- cron: "20 */8 * * *"

GitHub Actions 使用 UTC 时间,因此对应北京时间每天 00:20、08:20 和 16:20。需要立即更新时,可以推送到 main,或在 Actions 页面手动运行工作流。

需要保留的文件

同步功能主要由以下文件组成:

tools/sync-content.js       # 拉取 API、规范化数据、生成文章
.content-sync-map.json      # Mix Space 身份到数字编号的永久映射
.github/workflows/deploy-pages.yml
source/_posts/              # 自动生成的 Hexo 文章目录

source/_posts 中的数字 Markdown 属于自动生成内容,不建议直接编辑。下一次同步时,线上 API 内容会覆盖本地手动修改。

本地验证

在提交工作流之前,可以先运行:

pnpm install --frozen-lockfile
pnpm content:sync
pnpm test
pnpm clean
pnpm build

如果 API 暂时不可用,pnpm content:sync 应该失败且不改变现有文章。构建完成后,可以检查 public/archives/ 是否生成了归档页和数字文章路径。

常见注意事项

1. 不要删除映射文件

删除 .content-sync-map.json 会让程序失去旧文章与数字编号的对应关系。重新同步时,内容可能被当作全新文章分配新的路径。

2. 不要只请求第一页

文章数量超过 50 后,第一页并不代表完整数据。必须根据 meta.pagination.total_pages 请求所有页面,否则站点会悄悄缺文章。

3. 不要把 API 密钥放进前端

当前公开内容接口不需要登录,Actions 直接请求即可。如果以后改成需要认证的接口,应把凭据放在 GitHub Actions Secrets,并只在工作流中使用,不能写入主题 JavaScript 或生成的 HTML。

总结

Mix Space 负责内容管理,GitHub Actions 负责定期获取和验证,Hexo 负责生成静态页面,GitHub Pages 负责分发。通过稳定的远程身份、不可复用的数字编号和失败保护机制,可以在不手动维护 Hexo 文章文件的情况下,保持原博客与个人主页之间可靠同步。

Last updated Oct 16, 2026