3 min read

One Post, Two Blogs

I keep two blogs. One lives on prose.sh β€” terminal-native, plain text, no dashboard. The other runs on Ghost (this one here), which is prettier and friendlier for longer pieces. I like both for different reasons, and for a while I told myself I'd just keep them in sync by hand.

You already know how that ended.

Maintaining the same post in two places, by hand, is exactly the kind of chore I quietly abandon around week two. So instead of pretending I'm more disciplined than I am, I automated it. This is the story of how that automation grew β€” from a single scp to a little bridge that keeps both blogs in sync on its own, and even lets me publish by sending an email.

This is the part I love, so let me start here. prose.sh has no admin panel. You write a markdown file and copy it to the server over SSH:

scp my-first-post.md me@prose.sh:/

The file is just text with a tiny bit of frontmatter on top:

---
title: My First Post
date: 2026-06-27
---

Hello from a plain text file.

That's the whole thing. Your editor, your files, zero lock-in.

Doing this once is delightful. Doing it for every post β€” remembering the filename, the frontmatter, the exact scp line β€” while also keeping a Ghost blog alive, on a week where work ate every evening... the friction adds up. And friction is where my good intentions go to die.

I wanted to write once and have it show up in both places. That's it.

First I did the obvious thing and wrapped the boring parts in a small CLI. List my posts, create a new one, publish β€” all over the same SSH the manual way used, just without me typing the incantation every time.

blog publish my-first-post.md

There was one genuinely annoying detail worth mentioning: my SSH key lived on a network share, and OpenSSH flat-out refuses keys with "loose" permissions. So the script learned to copy the key to a safe local spot and lock it down before connecting. Small thing, but it's the difference between "works on my machine" and "works on every machine."

This already made my life better. But it still meant I had to run something, and it still treated the two blogs as two separate chores.

The real shift was a decision, not code: Ghost is canonical, prose is a mirror. I write in Ghost, and prose should just... follow.

This is an old idea with an ugly acronym β€” POSSE: Publish on your Own Site, Syndicate Elsewhere. You keep one home for your writing and let copies flow outward automatically.

The two halves were already there, waiting to be introduced:

  • Ghost can fire a webhook the moment you publish.
  • prose accepts markdown over SSH.

All that was missing was a small translator in the middle.

So I wrote one. A tiny service in a container. When I publish in Ghost, it:

  1. catches the webhook,
  2. converts the post's HTML into clean Markdown,
  3. writes the frontmatter for me (title and date β€” I don't even bother with excerpts),
  4. and scps the file to prose.
   Write in Ghost
        β”‚  webhook: "post published"  (HTML)
        β–Ό
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚   bridge service  β”‚   HTML ──► Markdown ──► add frontmatter
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚  scp
        β–Ό
    prose.sh  ──►  notes.example.com

I publish in one place. A few seconds later the same post is sitting on prose, formatted correctly, no copy-paste, no second chore.

Plot twist: posting by email

Then I got greedy. Some of my best ideas show up when I'm nowhere near a laptop β€” in line somewhere, on the couch, on my phone. I wanted to fire off a quick post the laziest way imaginable: send an email.

So I added one more door. I email a plain message β€” the subject becomes the title, the body becomes the post β€” and it lands on both blogs.

The trick that made it clean: the email doesn't talk to prose at all. It just creates a Ghost post through Ghost's API. Then Ghost's normal webhook does the rest, exactly like a post I'd written by hand. One road to prose, not two.

   Write in Ghost ───────────────┐
                                 β”‚ webhook
   Send an email                 β”‚
        β”‚                        β–Ό
        β–Ό                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  inbound email  ──POST─►│ bridge service │──API──► create post in Ghost
   (mail provider)       β”‚                │◄─webhookβ”€β”˜
                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚ scp
                                  β–Ό
                              prose.sh

One more wrinkle: my little box sits at home behind a connection with no public IP, so a stranger on the internet (Ghost's webhook, the mail provider) can't reach it directly. I tunnel those requests in through a cheap VPS, which terminates HTTPS and forwards them down to the container. The box never has to be "on the internet" β€” it just keeps a quiet outbound tunnel open.

  internet ──► tiny VPS (HTTPS) ──tunnel──► home box ──► bridge service

Few things that only became obvious after building it:

  • One source of truth saves you from sync hell. The moment you have two "originals," you have a merge conflict waiting to happen.
  • Make every feature funnel through the same path. Even the email trick ends as a normal Ghost publish, so there's exactly one place that can break β€” and exactly one place to fix.
  • Verify signatures. The open internet will happily POST garbage at any URL it can find. Both the webhook and the inbound mail are checked before I trust a byte.
  • Only mirror what's public. Members-only drafts stay where they belong.
  • Convert, don't paste. Turning the rendered HTML back into Markdown keeps prose clean instead of full of editor cruft.

It's the kind of boring magic I like: invisible when it works, and it mostly just works.

And yes. It's working.

See you around.

Reply

Got a thought? Reply by email β€” or publish a response on your own site and it'll show up above via Webmention.

Reply by email