Over the past few days, I’ve been exploring how to sync articles from Notion to my Hexo static blog. The main reason is that editing posts directly in Hexo can get a bit annoying—especially when dealing with lots of images, URLs, and formatting tags.
I’ve always used Notion as a kind of “thinking space” where I keep fragments of ideas and drafts. Over time, those pieces start to feel like something more valuable than leaving things unwritten. Even if they’re incomplete, they still represent progress.
At the same time, there’s an obvious trend: people are writing less themselves in the age of AI-generated content. That makes me feel like it’s even more important to keep writing—however rough or imperfect it may be.
Notion edit, Hexo publish
One simple idea came to mind: since editing in Notion feels so much easier, why not sync those articles directly to my Hexo blog?
So I set up a small workflow. Using GitHub Actions, I run a script that calls the Notion API and syncs content between the two sides. Along the way, it also converts everything into proper Markdown format. Once that’s done, the usual Hexo CI/CD flow takes over and publishes the site.
It’s nothing too complicated—but it works. A simple and effective workflow, just the way I like it.
Friction
While building this workflow, a few issues quickly came up:
- Hexo supports many custom blocks, but Notion’s block types are more limited — they don’t map cleanly one-to-one.
- How should I determine the state of an article? Has it actually been updated, or not?
- What about images? Should I download them into the local folder (and include them in Git), or keep them in external storage?
None of these problems are particularly hard on their own, but together they create a kind of friction in the workflow. It’s not just about syncing content—it’s about making sure the process stays consistent and predictable over time.
Format Inconsistency
I tried using notion2md to handle most of the conversion, but many inconsistencies still come from user-defined blocks.
- A
calloutblock in Notion gets converted into a Markdownquote(>). - A
quoteblock in Notion also becomes a Markdownquote, which makes them indistinguishable after conversion. - Inline equations in Notion use
$...$, while block equations use$$...$$. However, block equations don’t always include proper line breaks, which can cause parsing issues when unexpected newlines are involved. - Some JavaScript string operations—like
replace()—treat$as a special character. That means you often need to escape it as$$just to represent a single$, which can easily break the pipeline if not handled carefully.
These small inconsistencies add up. It’s not just about converting formats—it’s about preserving meaning across systems that were never designed to fully align.
Article Status
In Notion, an article can exist in several different states:
- It may be
in_trash - It may be
archived - Its content may be
updatedornot updated - Sometimes, it’s simply an unpublished draft
In the worst case, that gives up to 16 possible combinations. To keep things manageable, I defined a simple priority system:
- If an article is
in_trash, remove it. - If an article is
archived, move it to_archives/. - Since any change in Notion updates the
last_edited_time, I treat all changes asupdated. So: - If the
publishedattribute is not checked, treat it as a draft and place it under_draft/.
This way, instead of handling every possible combination, the logic becomes a simple, predictable flow.
Trade-offs for Image Storage
In my previous blog setup, I used external image hosting (e.g. Imgur). Naturally, I first considered continuing with that approach—but in the context of syncing with Notion, things become a bit more complicated.
Here’s how I broke it down:
| Method | Pros | Cons |
|---|---|---|
| Download images from Notion, then upload to external hosting | Follows the legacy approach and reduces Git repository size | More complex workflow, and images are not fully under my control |
| Download images from Notion and store them locally (tracked by Git) | Simple workflow, full control over assets | Increases repository size |
| Directly use Notion image URLs | Simplest approach, no file handling required | Notion URLs are not stable—they may expire or change, and assets are not under your control |
After weighing these trade-offs, I decided to go with the second option. It’s simpler, more predictable, and gives me full ownership of the content—even if it comes at the cost of a larger Git repository.