How I Built a Decentralized CMS with Nostr + GitHub Actions — Part 2

Refinements, resilience, and real-world usage. In this follow-up to Part 1, I document how I styled and streamlined my Nostr-based CMS — including relay deduplication, HTML fixes, GitHub Actions automation, and thoughtful publishing defaults — all running on free infrastructure I control.
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_at if 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:30023 long-form articles daily from Nostr relays

  • Deduplicates and filters by tag

  • Generates one .html file per article and updates the index.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 npub so 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