Syndication Links Without Frontmatter Editing
How .bluesky-posted.json auto-populates u-syndication links on every article and note page — no manual frontmatter required after each post.
Category: Development
The Microformats2 spec defines u-syndication — a link that marks where a post has been copied to. When a Bluesky post exists for an article, that link should appear on the article automatically. Editing frontmatter by hand after every deploy is not a workflow that scales past the second post. This one has a clean answer: the syndication state file is already the source of truth — let the build read it.
The setup
- Site: Astro 6 static build, deployed via shell script.
- Syndication state:
.bluesky-posted.jsoncommitted to the repo root — a map of canonical URL → Bluesky post URL. - Frontmatter: a
syndicationarray for manually added targets (e.g. a newsletter), nothing else.
The problem with manual syndication
The obvious approach is to write the Bluesky URL into the article’s frontmatter after it’s been posted:
syndication:
- https://bsky.app/profile/adrian-altner.de/post/3mhqb4t6ceu2w
This works, but it requires a second commit after every syndication run: one to publish the article, one to add the Bluesky URL back into its source. The two sources of truth also drift — edit the article later and it’s easy to forget the syndication line exists.
The state file as the source of truth
Implementation: .bluesky-posted.json already has the information, and it’s already committed after each run. Astro can import it directly at build time:
import bskyPosted from "../../../.bluesky-posted.json";
The file is a map of canonical URL → Bluesky post URL:
{
"https://adrian-altner.de/articles/2026/03/23/joining-the-indieweb/": "https://bsky.app/profile/adrian-altner.de/post/3mhqb4t6ceu2w"
}
Deriving syndication URLs at build time
On the article page, build the canonical URL first, then look up the Bluesky post URL from the imported JSON:
const articleUrl = new URL(`/articles/${post.id}/`, Astro.site).toString();
const bskyPostUrl =
(bskyPosted as Record<string, string | null>)[articleUrl] ?? null;
const syndicationUrls: string[] = [
...(post.data.syndication ?? []),
...(bskyPostUrl ? [bskyPostUrl] : []),
];
post.data.syndication stays for manually added URLs — Mastodon, a newsletter, anything else. The Bluesky URL is appended only if it exists in the state file. The result is a unified list, no duplication.
Notes use the same pattern with noteUrl in place of articleUrl.
Rendering the links
syndicationUrls drives the “Also on:” line below each post:
{syndicationUrls.length > 0 && (
<p class="syndication-links">
Also on:{" "}
{syndicationUrls.map((url, i) => {
const host = new URL(url).hostname.replace("www.", "");
return (
<>
{i > 0 && ", "}
<a class="u-syndication" href={url} rel="syndication noopener" target="_blank">
{host}
</a>
</>
);
})}
</p>
)}
The hostname is extracted from the URL for the link text — bsky.app labels Bluesky posts automatically. Adding another syndication target later would show its hostname without any template changes.
Trailing slashes matter
Problem: webmention.io stores URLs exactly as submitted. If Bridgy sends a webmention for https://adrian-altner.de/articles/.../hello-world/ (with trailing slash), the webmention component has to query with the same URL.
Implementation: Astro’s URL construction with a trailing slash in the template literal keeps this consistent:
const articleUrl = new URL(`/articles/${post.id}/`, Astro.site).toString();
// → "https://adrian-altner.de/articles/2025/12/01/hello-world/"
Solution: lookup keys in .bluesky-posted.json and query strings sent to webmention.io are byte-for-byte identical. Without the trailing slash, a lookup for /hello-world returns zero results even when mentions exist — and you spend half an hour debugging “webmentions broken”.
No frontmatter edits required
The full flow: deploy → syndication script posts to Bluesky → state file updated → state file committed and pushed → next build reads state → syndication links appear on every page that has a Bluesky post.
Article frontmatter is touched exactly once: at initial publish.
What to take away
- Let the syndication state file be the source of truth. Frontmatter editing doesn’t scale, and it desyncs silently.
- Import JSON directly into Astro components. Build-time imports are type-safe and don’t cost anything at runtime.
- Merge with
post.data.syndication. Manual entries and auto-derived URLs should flow through the same array. - Normalise on trailing slashes everywhere. Canonical URL in the lookup, in webmention.io, in the sitemap — one format, no exceptions.
- Render hostnames, not URLs. The list stays readable and new targets slot in without template changes.