---
title: 'Microformats2: Marking Up Posts for the IndieWeb'
description: How h-entry, h-feed, and the rest of the MF2 vocabulary make articles, notes, and photos machine-readable without changing how they look.
pubDate: '2026-03-23T14:28:00+01:00'
category: en/development
tags:
  - indieweb
  - astro
seriesParent: joining-the-indieweb
seriesOrder: 1
---

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:

```html
<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:

```html
<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:

```html
<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:

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

## Syndication links

**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:

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

The URLs live in frontmatter and are rendered conditionally:

```ts
syndication: z.array(z.string().url()).optional(),
```

```astro
{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](https://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](https://indiewebify.me) scan is the only visible proof that the markup is doing its job.
