Syndication Links Without Frontmatter Editing
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 automatically. Editing frontmatter by hand after every deploy is not a workflow that scales.
The problem with manual syndication
The obvious approach is to store the Bluesky URL in the article’s frontmatter:
This works, but requires a second commit after every syndication run: one to publish the article, one to add the Bluesky URL back into its source file. It also means frontmatter and syndication state can drift out of sync if the file is edited later.
The state file as the source of truth
After the syndication script runs, .bluesky-posted.json holds a mapping from each canonical URL to its Bluesky post URL:
{
"https://adrian-altner.de/articles/2026/03/23/joining-the-indieweb/": "https://bsky.app/profile/adrian-altner.de/post/3mhqb4t6ceu2w"
}
This file is committed to the repository and available at build time. Astro can import it directly:
import bskyPosted from "../../../.bluesky-posted.json";
Deriving syndication URLs at build time
In the article page, the canonical URL is constructed first, then used to 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 covers any 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 with no duplication.
Notes use the same pattern with noteUrl in place of articleUrl.
Rendering the links
The syndicationUrls array 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 becomes the label for Bluesky posts. Adding another syndication target would show its hostname automatically.
Trailing slashes matter
webmention.io stores URLs exactly as submitted. If Bridgy sends a webmention for https://adrian-altner.de/articles/.../hello-world/ (with a trailing slash), the webmention component must query with the same URL. Astro’s URL construction with a trailing slash in the template literal ensures this matches:
const articleUrl = new URL(`/articles/${post.id}/`, Astro.site).toString();
// → "https://adrian-altner.de/articles/2025/12/01/hello-world/"
The same applies to notes. Without the trailing slash, a lookup for /hello-world returns zero results even when mentions exist.
No frontmatter edits required
The full flow is: deploy → syndication script posts to Bluesky → state file updated → state file committed and pushed → next build reads state file → syndication links appear on every page that has a Bluesky post.
No article frontmatter is touched after the initial publish.