← Home

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.

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.

← Home