Ramblings of an aging IT geek
← Ramblings of an aging IT geek
indieweb

Wiring the Blog into the IndieWeb

Joining the IndieWeb from a colophon blog: WebSub for real-time feeds, webmention.io to collect replies, sending webmentions, and the rel=me identity dance that lets you sign in as your own domain.

Listen to this post
A single lit house linked by glowing threads to a scattering of distant houses, forming a web across the dark

Having put the blog on Cloudflare Pages and R2, the next itch was to stop it being an island. A static site is a lovely thing to own, but on its own it just sits there: no "someone replied", no way for anyone to follow it without an RSS reader they probably don't have. The IndieWeb answer to that is a handful of small, open standards, and colophon speaks them. This is what I turned on, roughly in order of how much faff each one is.

One note on scope before we start: colophon does the whole loop now, sending these interactions, collecting them, and rendering the replies under your posts. So this isn't plumbing for a someday feature; by the end the conversation actually shows up on the page. The order of operations is what matters, and a couple of the steps are easy to get subtly wrong.

WebSub: real-time feeds, no account

The cheapest win. A normal RSS/Atom feed is polled, so a reader finds your new post whenever it next gets round to checking. WebSub flips that: you ping a hub on publish, the hub pushes to every subscriber at once. Pure config, no signup, in colophon.yaml under your site:

federation:
  feeds: [rss, atom, json]
  websub:
    hubs:
      - https://pubsubhubbub.appspot.com/   # Google's public hub

The ping only fires on a real public deploy, not a local build, so check the publish log for WEBSUB … ping ok, and confirm there's a rel="hub" link in rss.xml/atom.xml and a hubs array in feed.json. A slow or down hub logs a failure and is otherwise ignored; it never breaks the publish.

Sending webmentions: also no account

A webmention is the open web's version of an @-mention: when I link to your post, my site POSTs yours a note saying "this URL mentions you", and your site can fetch mine back to verify it. Sending needs nothing set up, but it has to run after a publish, because the source URLs have to be live for the receiver to verify them:

colophon webmention send --env production --dry-run   # report, don't POST
colophon webmention send --env production             # actually send

It walks the built output, takes each page's canonical URL as the source, collects the cross-origin outbound links as targets, discovers each target's endpoint, and POSTs. It keeps a sent-cache at .colophon/cache/webmention-sent.json, so re-runs only notify new links and re-ping ones you've removed (so the receiver re-checks and drops a mention you deleted). I bolted it onto the end of my publish wrapper, so it fires on every deploy.

Receiving: webmention.io

To collect replies, likes and reposts, point a receiver at webmention.io, a hosted endpoint that catches them for you:

federation:
  indieweb:
    webmention:
      receiver: https://webmention.io/blog.i0.pm/webmention

With that set, colophon emits a <link rel="webmention"> discovery tag site-wide, which is how anyone (and Bridgy Fed, if you go that way) knows where to send mentions. The endpoint keys on your domain, so the path stays blog.i0.pm regardless of which page is mentioned.

Showing the responses

Collecting them is only half of it; the point is to show the replies and likes under the post. colophon does this two ways, and the choice is a proper trade-off:

federation:
  indieweb:
    webmention:
      receiver: https://webmention.io/blog.i0.pm/webmention
      display: { mode: live }            # or: asset
      token: "{env:WEBMENTION_IO_TOKEN}" # only used by asset mode
  • asset self-hosts the data: a colophon webmention fetch pulls your mentions back through the API token, caches them, and bakes a small JSON file per post that ships with the site. The responses are served from your own origin, work without hammering anyone else, and degrade gracefully. The cost is that they only refresh when you fetch and redeploy (a cron job, or a decoupled webmention publish that pushes just the mentions without rebuilding the site).
  • live skips all of that: the reader's browser fetches the receiver directly on each page load, so a new reply shows up the next time anyone opens the post, no rebuild, no token, no cron. The cost is honesty about two things, it needs JavaScript (so the responses don't exist for a no-JS reader), and it points your visitors' browsers at a third party.

There's a third path you don't pick directly: a no-JS theme. The engine can bake the responses into the HTML at build time from the same asset-mode data, so a theme that ships no JavaScript (the bundled minimal does exactly this) still renders replies and likes, server-side, no client script. The JS themes use a placeholder the script fills; the no-JS ones get it pre-rendered. Same data, the theme decides how it arrives.

I went with live. This is a personal blog, not a fortress; I'd rather a reply appear the moment it lands than run a cron to refresh a cache, and the responses are an enhancement below the article, not the article itself. I've left the token: line in the config anyway, pointing at WEBMENTION_IO_TOKEN (keyring COLOPHON_WEBMENTION_IO_TOKEN, exported by the publish wrapper), so flipping to asset later is a one-word change rather than a dig through the docs.

POSSE: publish here, syndicate everywhere

Webmentions handle the conversation; the other half of owning your content is POSSE, Publish on your Own Site, Syndicate Elsewhere. The canonical copy lives here, and a copy goes out to the silos where people actually are, each one linking back. colophon has a syndicate step for it, deliberately separate from publish so the original is always live first:

federation:
  syndication:
    - id: bsky
      driver: bluesky
      handle: "{env:BLUESKY_HANDLE:-}"
      app_password: "{env:BLUESKY_APP_PASSWORD}"   # an app password, not your login
# and on the environment:
environments:
  - name: production
    syndicate: [bsky]      # only listed targets fire; preview/draft envs omit it, so they never post

There are four drivers, and the choice comes down to who holds the keys: mastodon and bluesky post directly to your account with a token (or a Bluesky app password) you control; bridgy posts on your behalf via Bridgy so colophon holds no silo credentials at all; and command runs any program you like for anything without a built-in driver. I started with Bluesky, direct, my app password, one account to reason about.

The flow mirrors the webmention one, run it after a publish so the canonical URL is live:

colophon syndicate --env production --dry-run        # preview; posts nothing, writes nothing
colophon syndicate --env production --allow-publish    # post the copies, record the ledger

The ledger at .colophon/syndication.json (commit it) is the idempotency guard: it remembers what went where, so re-running never double-posts, and it feeds the "Also posted on…" links back on the post. Per-post you can opt out with syndicate: false, narrow to a subset with syndicate: [bsky], or write a custom blurb with syndicate_text:. Secrets, as ever, only arrive via {env:VAR}.

One thing not to confuse: this is syndication (push a copy out). It's a different job from making the site itself followable from the fediverse, that's Bridgy Fed, which hangs off the webmention receiver, not the syndication list. Same neighbourhood, different houses.

Signing in as your own domain

One detail worth getting right. webmention.io doesn't have its own password; you sign in as your own website using IndieAuth, via indielogin.com. For that to work, the URL you log in with and an identity provider it trusts (GitHub is the easy one) have to link to each other with rel="me", at the same URL on both ends.

colophon emits the rel="me" link from your author's urls:, so set that in authors/<id>.yaml:

# authors/jmylchreest.yaml
urls:
  - https://github.com/jmylchreest

GitHub does the other half for you: the website field on your profile is published with rel="me" automatically. So your site points at GitHub, GitHub points back at your site, the loop closes, and indielogin.com is happy to authenticate you through GitHub.

The catch is which page carries the rel="me". The link rides on colophon's author h-card, and the loop only closes if the page you log in with is the same one GitHub links back to. Make sure your GitHub website field and the URL you sign in with are the same, and that it's a page that actually renders the author card. On current colophon (v0.0.8) the author page does, so:

  • GitHub profile website: https://blog.i0.pm/authors/jmylchreest/
  • Sign in to webmention.io with: the same https://blog.i0.pm/authors/jmylchreest/

Match those two and the login goes straight through. Mismatch them, even by a trailing path, and you get "we couldn't find any way to authenticate you", which just means the two pages didn't point at each other.

What you get

So, the full loop: feeds that push instead of waiting to be polled, outgoing webmentions firing on every publish, a receiver banking every reply and like against the domain, and those responses rendering live under each post, the moment someone opens it. If you've replied to this from your own site or boosted it on the fediverse (via Bridgy Fed), you should be looking at the evidence a little further down the page.

It's a strange and rather nice feeling, wiring a static site you fully own into a conversation with the rest of the web, without renting a single feature from anyone. More of the web should work like this.