Webmentions: Receiving and Sending Cross-Site Reactions
How webmention.io handles incoming mentions and likes, how a build-time component displays them, and how webmention.app sends outgoing notifications after each deploy.
Category: Development
With MF2 markup in place, the next piece was cross-site notifications. Webmentions are the IndieWeb’s answer: when someone links to one of my posts from their own site, they can send a webmention — a simple HTTP POST to my endpoint — and I can display those mentions alongside the post.
The protocol is symmetric: you send webmentions out when you link to others, and you receive them when others link to you. This post walks through both sides — receiving through webmention.io, displaying them at build time, and sending outgoing notifications via webmention.app after each deploy.
The setup
- Static Astro 6 site deployed on every content update.
- webmention.io as the hosted receiving endpoint.
- webmention.app as the outgoing notifier, triggered by the deploy script.
- Goal: incoming mentions baked into the static HTML at build time; outgoing mentions sent automatically after each successful deploy.
Receiving webmentions with webmention.io
Problem: Implementing a webmention receiver means accepting arbitrary POSTs, validating source URLs, storing mentions, and exposing them. Not something I wanted to run myself on a static site.
Implementation: webmention.io is a hosted endpoint that handles all of that. After logging in — more on that below — it gives you two link tags to add to <head>:
<link rel="webmention" href="https://webmention.io/adrian-altner.de/webmention" />
<link rel="pingback" href="https://webmention.io/xmlrpc" />
Solution: Any page on the site that includes these tags can now receive webmentions. Incoming mentions are validated and stored by webmention.io.
Logging in without a password
webmention.io uses IndieLogin, which authenticates by domain. It follows the rel=me links from the homepage, finds one pointing to a provider that supports OAuth — GitHub, for example — and sends you through that OAuth flow. The result: I log in to IndieWeb services as adrian-altner.de, not as a username or email.
Bluesky with a custom domain handle works as a login provider too, but through the AT Protocol verification mechanism rather than rel=me HTML.
Displaying incoming webmentions at build time
Problem: Fetching mentions at runtime on a static site would require either client-side JavaScript — with all its CSP and privacy implications — or an edge function. Neither fits.
Implementation: webmention.io exposes a REST API. A build-time Astro component fetches mentions for each page URL and renders them statically:
const WEBMENTION_IO = "https://webmention.io/api/mentions.jf2";
const url = `${WEBMENTION_IO}?target=${encodeURIComponent(pageUrl)}&per-page=100`;
const res = await fetch(url);
const data = await res.json();
const mentions = data.children ?? [];
Mentions are grouped by type — likes (like-of), reposts (repost-of), and replies (in-reply-to, mention-of) — and rendered in separate sections. If there are zero mentions, the component renders nothing.
An optional WEBMENTION_TOKEN environment variable allows authenticated requests, which raises the per-page limit. Without it, the public API works fine for most pages.
Solution: The component runs at build time, so mentions are baked into the static HTML. A redeploy is required to pick up new mentions — acceptable for a site that deploys on every content update anyway.
Sending outgoing webmentions with webmention.app
Problem: The receiving side is passive. The sending side requires action after each deploy — discovering which new outgoing links need notifying, finding their endpoints, and firing the POSTs.
Implementation: webmention.app scans an RSS feed, finds all outgoing links in recent posts, checks whether those pages have webmention endpoints, and sends notifications to them. Authentication is via a token stored in .env.production.
The deploy script triggers this automatically after each successful deploy, once per RSS feed:
for feed in rss.xml rss/articles.xml rss/notes.xml rss/links.xml rss/photos.xml; do
curl -s -X POST \
"https://webmention.app/check?url=https://adrian-altner.de/${feed}&token=${TOKEN}"
done
Solution: Five feeds, five passes — articles, notes, links, photos, and the combined feed. Any outgoing link in any recent post gets a webmention if the target supports it.
What triggers a webmention in practice
Typical scenario: I write an article that links to another IndieWeb site. On the next deploy, webmention.app scans the RSS feed, finds the link, discovers the target’s webmention endpoint, and sends the notification. If the target site displays webmentions, my link shows up there.
Currently, most sites don’t support webmentions. But the infrastructure is in place — when a post links to a site that does, the notification goes out automatically.
What to take away
- Hosting your own webmention receiver is not worth it. webmention.io handles validation, storage, and the API for free; two
<link>tags are the entire integration. - IndieLogin means no password, no account. The domain is the identity — the same one already used by Bridgy and other IndieWeb services.
- Rendering mentions at build time avoids client-side JavaScript entirely — important for CSP, privacy, and performance on a static site.
- The send side is a shell loop around curl. webmention.app plus five RSS feeds covers everything the site publishes.
- Most targets don’t support webmentions yet. That’s fine — the mechanism is cheap to run, and when the target does support it, the notification goes out without any extra work.