Microformats2: Marking Up Posts for the IndieWeb

Category: Development

Tags: indieweb, astro


With identity verified via rel=me and h-card, the next job was making individual posts — articles, notes, photos — machine-readable. I wanted parsers and IndieWeb services to extract title, date, author, and body without guessing, and I didn’t want to ship a separate metadata file alongside every page.

Microformats2 (MF2) is the answer: a vocabulary of CSS class names that embeds semantic meaning into existing HTML. No new elements, no JSON-LD in the head, no extra metadata files — the structure lives in the markup itself.

The setup

  • Astro 6 static site with per-collection layouts for articles, notes, and photo sets.
  • Goal: add MF2 classes (h-entry, p-name, dt-published, e-content, h-card, h-feed) to existing templates so posts are machine-readable without any visible change.

The anatomy of an h-entry

The key unit is h-entry — the class that marks a post. Everything else hangs off it. A minimal article looks like this:

<article class="h-entry">
  <a href="https://adrian-altner.de/articles/..." class="u-url" hidden>Permalink</a>
  <h1 class="p-name">Article Title</h1>
  <time class="dt-published" datetime="2026-03-23T00:00:00.000Z">March 23, 2026</time>
  <div class="e-content">
    <!-- article body -->
  </div>
</article>

Each class has a specific meaning:

  • h-entry — the root; marks this element as a post
  • u-url — the canonical URL of the post
  • p-name — the post title (plain text)
  • dt-published — the publication date, read from datetime
  • e-content — the full post body, parsed as HTML

Authorship

Problem: A post on its own doesn’t tell a parser who wrote it.

Implementation: The author is embedded as a nested h-card inside the h-entry. Since author information is the same on every page, it’s hidden from visual display but present for parsers:

<span class="p-author h-card" style="display:none">
  <img class="u-photo" src="https://adrian-altner.de/avatar.jpg" alt="Adrian Altner" />
  <span class="p-name">Adrian Altner</span>
  <a class="u-url" href="https://adrian-altner.de">adrian-altner.de</a>
</span>

Solution: Parsers read this as “the author of this entry is the person described by the nested h-card” — zero visual cost, full semantic payload.

Notes and photos follow the same pattern

Notes use the same h-entry structure. Because notes don’t have long titles, p-name is the note’s first sentence and e-content contains the full body.

Photos wrap each image in an h-entry with u-photo on the <img> element itself:

<article class="h-entry">
  <img class="u-photo" src="..." alt="..." />
  <h1 class="p-name">Photo Title</h1>
  <time class="dt-published" datetime="2025-10-06T00:00:00.000Z">October 6, 2025</time>
</article>

A grid of photos becomes an h-feed — a collection of h-entries — by adding the class to the container:

<div class="h-feed">
  <div class="h-entry">...</div>
  <div class="h-entry">...</div>
</div>

Problem: When a post has been copied to another platform — Bluesky, for example — parsers need a way to find the syndicated copy.

Implementation: u-syndication marks the external copy:

<a class="u-syndication" href="https://bsky.app/profile/...">Also on Bluesky</a>

The URLs live in frontmatter and are rendered conditionally:

syndication: z.array(z.string().url()).optional(),
{post.data.syndication?.map((url) => (
  <a class="u-syndication" href={url}>{new URL(url).hostname}</a>
))}

Solution: A parser following the u-syndication link can reconcile the original and the copy — the basis of how Bridgy backfeeds reactions later.

No visible change

None of this markup changes how the page looks. The classes are invisible to CSS unless you target them explicitly. The only additions are the hidden u-url permalink anchor and the hidden p-author h-card — both carry zero visual weight.

indiewebify.me validated the h-entry on the first pass. The parser found the author, title, date, and content without any issues.

What to take away

  • MF2 is markup, not metadata. The same HTML that renders the page carries the semantics — no JSON-LD, no sidecar files.
  • h-entry is the unit. Once articles, notes, and photos all wear that class, every IndieWeb service that cares can find them.
  • Hidden h-card for authorship is a feature, not a hack. The author is the same on every page — there’s no reason it should be visible.
  • u-syndication is what makes backfeed possible. Declaring the external copy is how Bridgy and similar services stitch interactions back to the canonical URL.
  • None of this changes how the site looks. A passing indiewebify.me scan is the only visible proof that the markup is doing its job.