Automating Substack Sync from a Static Blog
How I built a system to automatically sync markdown posts to Substack as drafts
I want the best of both worlds: own my content in plain markdown files, but use Substack's distribution and monetization. The problem? Substack doesn't offer a plugin or API for external content. You have to put your content on their platform.
So I built a bridge.
The Hybrid Approach
Instead of choosing between my blog and Substack, I automated the sync:
- Write posts in markdown on my static blog
- Add
substack: trueto frontmatter for posts I want on Substack - Push to main
- A GitHub Action syncs tagged posts to Substack as drafts
- I review and publish from Substack's dashboard
This gives me ownership (source lives in my repo) plus distribution (Substack's ecosystem).
How It Works
The Trigger
Any post with substack: true in its frontmatter gets synced:
---
title: "My Post"
description: "A great read"
datePublished: "2025-01-15"
tags:
- writing
substack: true
---
The Script
A Python script (scripts/substack-sync.py) does the heavy lifting:
- Scans all markdown files in
content/ - Filters for
substack: true - Converts markdown to Substack's format
- Creates new drafts or updates existing ones
- Writes the Substack ID back to frontmatter
After the first sync, the frontmatter gets updated:
substack: true
substackId: "123456" # Added automatically
The substackId lets the script know this post already exists on Substack, so it updates instead of creating a duplicate.
The GitHub Action
A separate workflow (.github/workflows/substack-sync.yml) runs on every push to main:
on:
push:
branches: ["main"]
paths:
- 'content/**/*.md'
It only triggers when content files change, keeping things efficient. After syncing, it commits any frontmatter updates back to the repo.
The Unofficial API
Here's the catch: Substack has no official API.
The script uses Substack's internal endpoints, authenticated with a session cookie from your browser. It's the same API their web app uses, just called programmatically.
To set it up, you need three GitHub secrets:
SUBSTACK_PUBLICATION_URL- Your Substack subdomain (e.g.,yourname.substack.com)SUBSTACK_SID- Session cookie from browser (DevTools > Application > Cookies >substack.sid)SUBSTACK_USER_ID- Your user ID (visithttps://substack.com/api/v1/user/profile/selfwhile logged in and copy theidfield)
The session cookie typically stays valid for months, so you don't need to refresh it often.
Limitations
This approach has some constraints:
Drafts only - Posts sync as drafts. You manually publish from Substack's dashboard. This is intentional; I want a human review step before anything goes live.
No images - The script syncs text content only. Images would need to be uploaded to Substack's CDN separately.
Unofficial API - If Substack changes their internal API, the script might break. But it's simple enough to fix.
One-way sync - Changes flow from my blog to Substack, not the reverse. If I edit on Substack, those changes don't come back.
Why Drafts?
I deliberately chose to sync as drafts rather than auto-publish. Reasons:
- Quality control - I can review formatting before publishing
- Timing - Substack has optimal send times; I want to control when posts go out
- Paid content - Some posts might need paywall settings configured
- Safety net - If something breaks, it's just a draft
What's Next
This is version 1. Future improvements might include:
substack: publishto auto-publish certain posts- Image syncing via Substack's CDN
- Two-way sync (probably not worth the complexity)
- Better markdown-to-Substack formatting
For now, this simple bridge lets me write where I want while tapping into Substack's ecosystem. The best of both worlds.