Show HN: How to Automate Newsletter Based on SecondBrain and Bluesky Changes

zazuke1 pts0 comments

How to Automate Newsletter Based on SecondBrain and Bluesky Changes

All<br>Blog<br>Brain

Any time<br>This year<br>Last 2 years

How to Automate Newsletter Based on SecondBrain and Bluesky Changes

Last updatedUpdated:<br>Jun 16, 2026

by Simon Späti · CreatedCreated: Jun 1, 2026 · 4 min read

I automate the gathering of content for my Newsletter, but not the writing of it. Everything mechanical that I used to do by hand — pulling links to recently-updated Second Brain notes, finding books I&rsquo;ve read, copying top Bluesky posts — runs from a small Python script. The personal writing stays mine.

Inspired by

Simon Willison&rsquo;s Substack automation see also Simon Willison, but where Simon does it fully programmatically with SQL and Datasette into Substack, I go markdown-first into Listmonk via my

listmonk-rss repo. No notebook, no AI in the loop.

GitHub Repo

Find at

sspaeti/listmonk-rss. This repo also contains an automatic newsletter sent via listmonk-rss.py based on new blog posts (documented at Listmonk), as well as the automation newsletter.py and templating.

# Why This Process?

This is not to replace my own writing with my subscribers, but for me as I do so much work on my second brain, to surface latest changes, to give the readers a possibility to keep up with what has changes since the last email in a very simple and easy to scan way, and for me to reduce the workload to go through all my changes and posts manually.

# How it Looks

A generated email with gathered information from my second brain, my recent book notes I read or currently reading, from Bluesky post and engagment (can be pulled via DuckDB and simple query - see Querying Bluesky with DuckDB and SQL):

With make newsletter-send it will be scheduled on Listmonk automatically:

make newsletter-send<br>uv run python newsletter.py send drafts/newsletter-2026-06-02.md<br>INFO:httpx:HTTP Request: GET https://list.ssp.sh/api/lists "HTTP/1.1 200 OK"<br>INFO:httpx:HTTP Request: POST https://list.ssp.sh/api/campaigns "HTTP/1.1 200 OK"<br>Campaign draft 97 successfully created!<br>INFO:httpx:HTTP Request: PUT https://list.ssp.sh/api/campaigns/97/status "HTTP/1.1 200 OK"<br>Campaign 97 successfully scheduled with 45 mins delay!<br>INFO:httpx:HTTP Request: POST https://api.pushover.net/1/messages.json "HTTP/1.1 200 OK"<br>✓ .last_newsletter advanced

And this is how it looks:

All in beautiful Markdown - and the preview looks like this - beautiful, no? 🙂

# How it Works

The script (newsletter.py) has two commands: gather and send.

gather collects, since the last sent newsletter:

Blog posts : capped at 2 — RSS auto-announces them via the older listmonk_rss.py workflow already.

Brain notes : git log --numstat inside the second-brain-public/content/ submodule. A line-count threshold (default 20 added lines) filters out commits that are just grammar fixes. Notes are split into Major (≥100 lines added) and Smaller updates, each with a placeholder paragraph for me to write framing prose.

Books : scans my Books/ folder for any book whose Created, Started reading, or Finished reading date falls in the window, and pulls the > callout and ## Notes During Reading. The folder is copied to a local .copy/books/ snapshot first so the script never touches my live vault.

Bluesky : DuckDB query against app.bsky.feed.getAuthorFeed, top 15 by engagement. See DuckDB + Bluesky AT protocol. see ATProto queries

Output is a markdown draft with editorial slots (intro, per-section framing, closing). I edit in Neovim, then make newsletter-send pushes to Listmonk as a scheduled draft so I get a Pushover notification and a final review window.

# Design Choices

No AI in the loop . AI would flatten my voice. The script gathers; I write.

Date advances only on send . Re-running gather between sends always shows the same backlog — nothing is consumed until I actually send. If I skip a few weeks, the next draft has all of it.

Filename-as-slug for URLs . Hugo derives URLs from the filename, not the frontmatter title, so the script slugifies the file stem (e.g. zen mode for writing.md → /brain/zen-mode-for-writing/, even when the title is &ldquo;Zen Mode for Writing (Obsidian, Neovim)&rdquo;).

Wikilink handling depends on source . Brain-note intros keep wikilinks converted to ssp.sh/brain/ URLs (they&rsquo;re all published). Book notes get wikilinks stripped to plain text — they may reference notes in my private vault that don&rsquo;t exist on the web.

# Commands

make newsletter # gather a fresh draft<br>make newsletter-since SINCE=2026-04-01 # custom window<br>make newsletter-edit # open latest draft in $EDITOR<br>make newsletter-send # push latest draft to Listmonk<br>make bsky-engagement # ad-hoc: print top Bluesky posts

# Further Reads

Listmonk

Neomd

# Pollmd

I created my own poll that works with markdown and within my newsletter. See Pollmd for how to create a simple poll just by adding a couple of markdown links, e.g., to know how well-received your newsletter was.

Origin: ssp.sh Newsletter...

newsletter bluesky brain listmonk make send

Related Articles