How I Built a Decentralized CMS with Nostr + GitHub Actions — Part 2
Andrew G .Stanton - June 9, 2025
How I Built a Decentralized CMS with Nostr + GitHub Actions – Part 2
Refinements, resilience, and real-world publishing
In Part 1, I shared the first steps of building a publishing stack that puts authors in control — using Nostr for content, GitHub Actions for automation, and GitHub Pages for free, fast hosting.
But that was just the foundation.
In this second installment, I focused on improving the system with better data hygiene, styling consistency, and end-to-end automation — all while keeping things minimal and open-source.
Here’s what changed — and what’s coming next.
✅ Key Improvements (Week of June 8)
🔁 1. Deduplication across relays
Nostr relays often echo the same events. In the original version, my script could pull the same article multiple times.
To fix that, I updated the fetch_articles.py script to:
-
Gather events from 8–10 relays concurrently using
asyncio -
Flatten the results
-
Deduplicate based on
event.id
This brought the number of unique articles down from 90+ to around 20 — a huge cleanup win.
🏷 2. Tag styling and wrapping
Previously, the tags were unstyled or ran off the edge of the container. Now:
-
Tags wrap cleanly and align left
-
Each tag has a soft background and spacing (
#tag) -
Tags are parsed directly from Nostr
["t", ...]tags
It looks better. It feels more coherent. And it reflects the decentralized ethos — clean, not corporate.
📅 3. Proper published_at parsing
Nostr events can include a ["published_at", timestamp] tag. Originally, my site sorted and displayed articles by their created_at time — which was sometimes misleading.
Now, the script:
-
Extracts
published_at(if present) -
Falls back to
created_atif missing -
Uses this value for both sorting and display in
index.html
This way, articles appear in the correct order — based on when I intended to publish them.
🎨 4. Markdown cleanup and layout polish
Markdown content from Nostr sometimes included extra backslashes (\\) or unparsed characters. I updated the rendering logic to:
-
Remove unnecessary escape characters
-
Use consistent fonts, spacing, and margins
-
Inherit the overall styling from stantonweb.com
Now the articles don’t just work — they feel native to the rest of my site.
🤖 5. GitHub Actions automation
This was a big one: enabling daily auto-publishing.
I created a GitHub Actions workflow that:
-
Pulls all my
kind:30023long-form articles daily from Nostr relays -
Deduplicates and filters by tag
-
Generates one
.htmlfile per article and updates theindex.json -
Commits the changes back to GitHub Pages — no manual pushing needed
After some token setup troubleshooting (and a wrong known_hosts key 😅), it works beautifully.
You can still run the script manually via:
bash
CopyEdit
./fetch_articles.sh
But now, it’s fire-and-forget. Just post on Nostr — the rest happens automatically.
⏱ Total Time Spent (est. 15 hours)
-
Saturday (~5h): initial build, tried sync-based fetch logic
-
Sunday (~5h): rewrote with
asyncio, deduplication, cleaned up article objects -
Monday (~5h): full styling pass, timestamp fixes, token creation, and automation pipeline setup
All running with zero paid infrastructure.
🔜 Coming Soon
-
🗂 Pagination: View 10–50 articles per page
-
🗓 Monthly Archives: Top-level by Year → then Month → then article list
-
🧠 RSS Feed: Auto-generate RSS from my
npubso any Nostr long-form post becomes a blog feed
(Bonus: use it to follow others via Primal or standard readers)
🙌 Built With
-
Nostr (Primal, Damus, etc.)
-
GitHub Pages + Actions
-
Python (asyncio, websockets)
-
A little stubbornness
-
A lot of help from ChatGPT (“Dr. C”) 🧠
💡 Why This Matters
Most people don’t own their publishing tools.
Medium can unlist you. Substack can shadowban. Even Ghost requires trust in a hosted instance.
But with Nostr + GitHub, I control the data, the visibility, and the publishing logic.
That’s the future I want — for myself, for others, for truth.
One article at a time.
🟧 View live:
https://andrewgstanton.github.io/blog-stantonweb-site
📬 Zap: https://tinyurl.com/yuyu2b9t
Write a comment