If you already manage your posts and notes in Mix Space but want Hexo to serve a lightweight, fast site, you can have your Hexo repo pull content from the Mix Space API on a schedule, then build and deploy to GitHub Pages automatically.
The overall flow
The workflow runs every 8 hours, and you can also trigger it with a push or by hand. Every run re-reads the public content from the live site, generates a complete, current set of articles, and then runs the Hexo build.
That "assign or reuse a number" step in the diagram isn't arbitrary numbering — it's driven by a mapping table. Keep it in the back of your mind; I'll explain the details later.
Why sync everything from the API
Appending only new posts looks simpler, but it can't correctly handle deletions, hidden content, or edits. The current implementation reads the full list of posts and notes every time:
GET /api/v3/posts?page=1&size=50&truncate=1
GET /api/v3/notes?page=1&size=50&withSummary=1
Once the list endpoint returns pagination info, the script keeps requesting pages until page === total_pages. The list is only used to get each item's identity and summary; after that it requests the details of every item:
GET /api/v3/posts/{id}
GET /api/v3/notes/nid/{nid}?single=1
That gives you fields like title, body, tags, category, creation time, modification time, and the original path. Unauthenticated requests only return what Mix Space exposes publicly, so drafts, scheduled-but-unpublished posts, and password-protected content won't be synced.
How posts and notes coexist
Posts and notes use different API identities:
| Type | Stable identity | Original site path |
|---|---|---|
| Post | post:{id} | /posts/{categorySlug}/{slug} |
| Note | note:{id} | /notes/{nid} |
The sync script doesn't use titles as identities, since titles can change. It uses the immutable id that Mix Space returns, tagging posts as post:id and notes as note:id.
Numeric IDs and the mapping table
By now you might be asking: if Mix Space's id already tells posts and notes apart, why bother with numeric IDs at all?
The reason is that post paths on the Hexo site use numeric IDs, like this:
/archives/1/
/archives/2/
/archives/3/
Those numbers have nothing directly to do with Mix Space post IDs. They're just path numbers Hexo uses internally.
Think of it this way:
- Mix Space's
post:idornote:idis like an ID number — it says which piece of content this is; - Hexo's
/archives/1/,/archives/2/are like street addresses — they say where this content lives on the Hexo site; - The
.content-sync-map.jsonin the root directory is the address book that remembers how ID numbers map to street addresses.
The mapping table records roughly this:
| What's recorded | What it does |
|---|---|
post:{id} / note:{id} | The content's stable identity in Mix Space |
A number, such as 1, 2, 3 | The content's fixed number on the Hexo site |
active / inactive | Whether the content is currently still within the sync scope |
How it works isn't complicated:
- On the first sync there's nothing in the table yet, so the script assigns the next number to each piece of content, working through them by creation time.
- On later syncs the script takes the
post:idornote:idand looks up the old number in the table. If it finds one, it keeps using it; only if it doesn't does it assign a new number. - When you change a title, category, or slug in Mix Space, the identity stays the same, so the Hexo number stays the same too.
- When content is deleted or hidden in Mix Space, the corresponding file is removed from the current site and the table marks it
inactive. - A number that's already been used is never reassigned to another piece of content, even if the original post has since been deleted.
There are two benefits to this:
- Changing a title, category, or Mix Space slug won't change the Hexo permalink.
- Numbers that have already been used are never reassigned to another piece of content.
So don't manually rename files like source/_posts/1.md, and don't delete the map file. They're what the sync script relies on to keep paths permanent.
How original links are generated automatically
Every generated Hexo file gets an original_url written into it:
original_url: https://moitr.ren/posts/categories/example
A post's original path is its category slug combined with its post slug, while a note uses its nid:
Post: /posts/{category.slug}/{slug}
Note: /notes/{nid}
The domain is hard-coded to https://moitr.ren. That way, even though Hexo uses numeric archive paths, readers can still get from the post page back to the original on Mix Space. This requires your theme to support it.
Body format and Core containers
When content_format is markdown, the script uses the text returned by the API. When the format is lexical, the current implementation likewise prefers the public text that Mix Space provides, which avoids re-implementing all of the editor's JSON rendering rules on the sync side.
The sync step also handles some of Mix Space's custom Markdown containers. For example:
- A
masonrycontainer is converted into the image gallery structure already built into the Hexo theme. - A
successcontainer is converted into a Markdown blockquote. - Unknown containers are left as they are, so auto-sync doesn't accidentally strip out original content.
Line endings are normalized to LF, the body keeps exactly one trailing newline, and timestamps are written in UTC ISO format. That way you get identical file contents whether you run it locally on Windows or on Linux in Actions.
The build order in GitHub Actions
The workflow's core steps are:
The current cron expression is:
- cron: "20 */8 * * *"
GitHub Actions uses UTC, so that works out to 00:20, 08:20, and 16:20 Beijing time every day. When you need an immediate update, push to main or run the workflow manually from the Actions page.
Files you need to keep
The sync feature consists mainly of these files:
tools/sync-content.js # Fetches from the API, normalizes data, generates posts
.content-sync-map.json # Permanent map from Mix Space identities to numeric IDs
.github/workflows/deploy-pages.yml
source/_posts/ # Auto-generated Hexo post directory
The numbered Markdown files in source/_posts are auto-generated, so it's best not to edit them directly. The next sync will overwrite any local changes with content from the live API.
Checking it locally
Before committing the workflow, you can run:
pnpm install --frozen-lockfile
pnpm content:sync
pnpm test
pnpm clean
pnpm build
If the API is temporarily unavailable, pnpm content:sync should fail without touching your existing posts. After a build, check public/archives/ to see whether the archive page and numeric post paths were generated.
Common pitfalls
1. Don't delete the map file
Deleting .content-sync-map.json wipes out the script's record of which old posts correspond to which numbers. On the next sync, content may be treated as brand new and assigned new paths.
2. Don't fetch only the first page
Once you have more than 50 posts, the first page isn't the full dataset. You have to request every page based on meta.pagination.total_pages, or your site will quietly end up missing posts.
3. Don't put API keys in the frontend
The current public content endpoints don't require authentication, so Actions can just call them directly. If you later switch to endpoints that need auth, put the credentials in GitHub Actions Secrets and use them only in the workflow — never in your theme's JavaScript or the generated HTML.
Summary
Mix Space handles content management, GitHub Actions fetches and verifies it on a schedule, Hexo generates the static pages, and GitHub Pages distributes them. With stable remote identities, numbers that are never reused, and failure safeguards, you can keep your original blog and your personal site reliably in sync without ever maintaining Hexo post files by hand.