Skip to content

Theme contract and rendering

The generated schemas in each theme repository are the exact contract for its manifest and render context. Read them before editing templates or generating theme code.

Format v1 themes have six slots and remain compatible with feed and item pages. Format v2 adds standalone Pages and public Search, for eight slots total:

Slot Purpose
webFeed Render the public feed home page.
webItem Render one item page.
webPage Render a standalone Page, including the editable default 404 Page.
webSearch Render the complete /search/ results page.
webHeader Insert markup before </head>.
webBodyStart Insert markup immediately after <body>.
webBodyEnd Insert markup immediately before </body>.
rssStylesheet Provide the complete RSS XSL stylesheet.

An active format v1 theme prevents a Page from becoming Published or Unlisted. It does not block Page drafts. Use format v2 for Pages, navigation, and public search.

microfeed-theme.json declares package identity, semantic version, compatible microfeed versions, the theme file paths, and optional assets. Its optional description is a short, public summary of what the theme is good for and is limited to 280 characters. One package ID and version identifies one immutable set of content.

A theme may also declare one previewFixture, such as fixtures/editorial.json. The value must be a safe relative path to a UTF-8 JSON object no larger than 128 KiB. Admin uses this theme-specific fixture as the default demo dataset while still allowing the owner to switch to the current site’s published content. A declared fixture is stored independently from templates and assets, participates in the immutable package checksum, and survives installation, Admin-derived drafts, publication, theme init, and theme export. Admin drafts inherit it but cannot edit it. Themes without a declared fixture continue previewing current-site data.

A format v2 manifest may also declare searchItemDestination to control where item results open in both the popup and /search/:

Value Item destination and feed field
web The local microfeed item page from JSON Feed items[]._microfeed.web_url. RSS uses it as <item><link> only when Item URL is empty.
url The custom Item URL from JSON Feed items[].url and RSS <item><link>.
attachment The media attachment from JSON Feed items[].attachments[0].url and RSS <item><enclosure url="…">.

Omission is equivalent to web. When the selected custom value is missing, microfeed falls back to the local item page; Page results always keep their local Page URL.

The render context begins with the public JSON Feed and adds:

  • current_year;
  • _theme.package_id;
  • _theme.version;
  • _theme.asset_base_url;
  • navigation_pages on public HTML views;
  • page on the Page slot; and
  • search on the Search slot.

On item pages, use items.0. The item alias remains available only for compatibility with older themes.

Mustache remains logicless: variables, escaped and unescaped values, sections, inverted sections, and iteration are supported. Owner-authored rich HTML such as item or Page content_html is intended for triple-brace rendering. Escape ordinary text and attributes by default.

navigation_pages contains Published Pages whose Show in navigation setting is enabled, in the order selected in Admin. Draft, Unlisted, and the special 404 Page are excluded. Navigation is website-only and never adds a Page to RSS or JSON Feed.

Render the same navigation in feed, item, Page, and Search templates:

<nav aria-label="Site navigation">
{{#navigation_pages}}
<a href="{{url}}">{{navigation_label}}</a>
{{/navigation_pages}}
</nav>

The webPage slot also renders the protected default 404 Page. Check page.is_not_found_page only when the theme needs distinct 404 styling; the ordinary Page structure should otherwise work unchanged.

microfeed injects one accessible search dialog into every public HTML page and owns its keyboard handling, request cancellation, safe result rendering, and typeahead behavior. Do not copy that dialog or script into the theme. Add data-microfeed-search-open to a theme control that should open it:

<button
type="button"
aria-haspopup="dialog"
aria-controls="microfeed-search-dialog"
data-microfeed-search-open
>
Search
</button>

The webSearch slot owns the surrounding layout for /search/. Use search.query and the stable input and results hooks:

<form action="/search/" method="get" role="search">
<label for="site-search">Search this site</label>
<input
id="site-search"
name="q"
type="search"
value="{{search.query}}"
data-microfeed-search-input
>
<button type="submit">Search</button>
<div
aria-live="polite"
data-microfeed-search-results
data-microfeed-search-details
></div>
</form>

The optional details hook adds a short date and excerpt to each result. The dialog and results use stable data-microfeed-search-* hooks and mf-public-search* classes for targeted styling.

Every rendered result exposes title, type, destination hostname, and optional details elements. The hostname comes from the resolved link destination and is hidden by default. Enable it for Item results and customize the two result surfaces independently in the Web header:

[data-microfeed-search-result-type="item"] {
--mf-search-result-domain-display: inline;
}
[data-microfeed-search-results-context="popup"] .mf-public-search-result {
padding: 0.6rem;
}
[data-microfeed-search-results-context="page"] .mf-public-search-result {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
}
.mf-public-search-result__domain {
color: var(--mf-muted);
}

Use .mf-public-search-result__title, __type, __domain, and __details for individual parts. The result link also has data-microfeed-search-result-type="item|page"; its container has data-microfeed-search-results-context="popup|page". Keep the supplied link and text nodes instead of reconstructing untrusted result HTML.

Define --mf-accent, --mf-background, --mf-surface, --mf-text, --mf-muted, and --mf-border color tokens so the injected dialog follows the theme. Search preview uses representative results because live search is not available inside the isolated preview.

A generated repository includes JSON Schemas under .microfeed/schemas/, a representative package fixture, and built-in test fixtures for empty, minimal, rich, paginated, media-heavy, missing-optional, multi-author, and hostile-rich- HTML content.

Package limits are:

  • 128 KiB per text slot;
  • 512 KiB total theme text;
  • 128 KiB for the optional declared preview fixture;
  • 100 declared assets;
  • 5 MiB per asset; and
  • 20 MiB total assets.

Validation rejects absolute paths, traversal, symlinks, missing or malformed declared preview fixtures, undeclared files, malformed Mustache, invalid semantic versions, incompatible microfeed ranges, and unsupported asset types. Tests check deterministic rendering, HTML structure, and valid RSS XSL; themes are trusted code, so they do not sanitize intentional HTML or JavaScript.

Continue with Bundle CSS, JavaScript, and assets or return to Build and release a theme.