Documentation

ZeroPress Theme Authoring

ZeroPress themes are static template packages for the current runtime: "0.5" contract.

ZeroPress Theme Authoring

ZeroPress themes are static template packages for the current runtime: "0.5" contract.

A theme decides how ZeroPress content looks and behaves in the browser. The build pipeline prepares structured data, renders Markdown content, resolves widgets, copies theme assets, and emits static output. The theme owns the HTML templates, CSS, and optional client-side enhancement.

What A Theme Contains

A minimal theme directory contains:

my-theme/
  theme.json
  layout.html
  index.html
  post.html
  page.html
  assets/
    style.css

Common optional files:

my-theme/
  archive.html
  category.html
  tag.html
  404.html
  partials/
    *.html
  assets/
    theme.js

Required files for a usable v0.5 theme:

  • theme.json
  • layout.html
  • index.html
  • post.html
  • page.html
  • assets/style.css

ZeroPress Site Shape

When creating a complete ZeroPress site, the practical authoring unit is:

my-site/
  preview-data.json
  theme/
    theme.json
    layout.html
    index.html
    post.html
    page.html
    partials/
      tracker.html
      content-enhancements.html
    assets/
      style.css
      theme.js
  public/
    favicon.ico
    vendor/
  • preview-data.json defines site data, content, menus, widgets, and permalink policy.
  • theme/ defines deterministic rendering through the v0.5 theme runtime.
  • theme/assets/ contains theme-owned CSS and JavaScript referenced by the theme.
  • public/ contains site-owned passthrough files such as favicons, PDFs, source files, and third-party vendor assets.

Reusable themes should avoid hard-coding site-specific analytics tokens, vendor URLs, or product copy. Prefer a documented partial or public/ integration point that a site owner can fill.

theme.json

The theme manifest identifies the package and declares the v0.5 runtime.

{
  "$schema": "./theme.v0.5.runtime.schema.json",
  "name": "My Theme",
  "namespace": "your-namespace",
  "slug": "my-theme",
  "version": "0.5.0",
  "license": "MIT",
  "runtime": "0.5",
  "description": "Short summary of the theme.",
  "features": {
    "comments": true,
    "newsletter": true,
    "postIndex": true
  },
  "menuSlots": {
    "primary": {
      "title": "Primary Menu",
      "description": "Main navigation menu"
    }
  },
  "widgetAreas": {
    "sidebar": {
      "title": "Sidebar Widgets",
      "description": "Sidebar widget area"
    }
  }
}

The current runtime accepts only runtime: "0.5". There is no fallback to older theme runtimes.

Use the Theme Manifest Runtime v0.5 schema as the source of truth for manifest fields.

Templates And Partials

ZeroPress renders one route template inside layout.html.

  • layout.html defines the shared document shell.
  • index.html renders the front page, post index, or combined default root route.
  • post.html renders an individual post.
  • page.html renders an individual page.
  • archive.html, category.html, tag.html, and 404.html are optional route templates.
  • partials/*.html are reusable template fragments.

The layout must include the content slot:

<main>
  {{slot:content}}
</main>

Common helpers:

{{menu:primary}}
{{partial:header}}
{{partial:post-card variant="compact" show_excerpt=true}}

Template syntax supports variables, if, if_eq, else_if, else_if_eq, for, loop metadata, partial arguments, and template comments. See Theme Runtime v0.5 for the full contract.

If a theme needs to iterate a menu manually, use menus.<slot>.items with the same slot id declared in theme.json. Both plain ids such as primary and hyphenated ids such as docs-sidebar are valid:

{{#for section in menus.docs-sidebar.items}}
  <section>
    <h2>{{section.title}}</h2>
  </section>
{{/for}}

For example, menus.primary.items and menus.docs-sidebar.items are both valid. Hyphens must stay inside a path segment, so menus.-docs.items, menus.docs-.items, and menus.docs--sidebar.items are invalid.

Common Render Context

Every rendered template receives common build data:

  • site
  • currentUrl
  • language
  • route
  • menus
  • widgets
  • meta
  • taxonomies.categories[]
  • taxonomies.tags[]

Global taxonomy items are generated by build for theme rendering. They are useful for home filters, sidebars, tag clouds, and navigation chips without scanning post card attributes in client JavaScript.

Each taxonomy item provides:

  • name
  • slug
  • url
  • count
  • description

The url field follows the active permalink policy. Declared categories and tags remain present even when count is 0, so themes can decide whether to show or hide empty taxonomy links.

<nav class="taxonomy-filter" aria-label="Topics">
  {{#for category in taxonomies.categories}}
    {{#if category.count}}
      <a href="{{category.url}}">{{category.name}}</a>
    {{/if}}
  {{/for}}
</nav>

Route Data

Build output exposes structured route data to templates.

Post lists should render from:

  • posts.items[]
  • pagination

The route object identifies the current render target:

  • route.type
  • route.is_front_page
  • route.is_post_index
  • route.path
  • route.url

Use route.is_post_index before rendering post-index-only UI. Use pagination.enabled before rendering page navigation. A site may request a non-paginated post index, and a theme may declare "postIndex": false to opt out of post index rendering entirely.

When a site uses a page as the front page, the selected page is rendered at / through page.html, and its normal page route is not emitted. The root render has route.type: "front_page" and route.is_front_page: true. Use that flag when front-page markup should differ from normal document pages.

Category and tag routes also receive:

  • taxonomy.kind
  • taxonomy.slug
  • taxonomy.name
  • taxonomy.count

Posts should render from fields such as:

  • post.title
  • post.url
  • post.excerpt
  • post.featured_image
  • post.html
  • post.published_at
  • post.updated_at
  • post.reading_time
  • post.author
  • post.categories[]
  • post.tags[]
  • post.prev
  • post.next
  • post.comments_enabled
  • post.toc[]

Pages should render from fields such as:

  • page.title
  • page.html
  • page.toc[]

For Markdown-first document pages, remember that page.html is the rendered Markdown body. If the source Markdown starts with # Title, then page.html already contains that H1. In that case, do not render a second <h1>{{page.title}}</h1> in page.html; use the body HTML as the document heading source.

Front pages often use Markdown that already starts with an H1. A simple pattern is:

{{#if route.is_front_page}}
  <div class="prose">{{page.html}}</div>
{{#else}}
  <header>
    <h1>{{page.title}}</h1>
  </header>
  <div class="prose">{{page.html}}</div>
{{/if}}

Menus are declared by preview data and rendered by theme templates through menu helpers:

<nav aria-label="Primary">
  {{menu:primary}}
</nav>

Widget areas are resolved before template rendering. Themes should consume the resolved widget data, not raw widget settings.

Example:

{{#if widgets.sidebar.items}}
<aside>
  {{#for widget in widgets.sidebar.items}}
    <section>
      <h2>{{widget.title}}</h2>
      {{widget.html}}
    </section>
  {{/for}}
</aside>
{{/if}}

Markdown Content And TOC

For document_type: "markdown", ZeroPress renders Markdown to HTML and assigns stable id attributes to headings.

Markdown pages and posts also receive generated TOC data:

  • level
  • id
  • title
  • href

Example:

{{#if page.toc}}
<aside aria-label="Table of contents">
  <ol>
    {{#for item in page.toc}}
      <li class="toc-level-{{item.level}}">
        <a href="{{item.href}}">{{item.title}}</a>
      </li>
    {{/for}}
  </ol>
</aside>
{{/if}}

Heading anchor UI is optional theme UI. Build output provides heading ids and TOC data, but it does not wrap heading text in permalink anchors.

Markdown rendering also includes common authoring conventions that themes may style:

  • GFM tables render as <table> markup.
  • ~~deleted~~ renders as <s>deleted</s>.
  • Task lists render disabled checkbox inputs and use contains-task-list, task-list-item, and task-list-item-checkbox classes.
  • GitHub alert blockquotes such as > [!NOTE] render as <aside class="zp-alert zp-alert--note" role="note"> with a zp-alert__title title paragraph.
  • Fenced code language info is preserved as language-* classes, including language-mermaid.

Supported alert markers are NOTE, TIP, IMPORTANT, WARNING, and CAUTION. Mermaid remains a code block at build time; themes can progressively enhance pre code.language-mermaid on the client.

For document_type: "html" and document_type: "plaintext", build-generated TOC data is empty. Themes may add client-side progressive enhancement if they want TOC behavior for non-Markdown content.

Progressive Enhancement

The initial static document should be useful without JavaScript.

Theme-owned progressive enhancement is appropriate for:

  • search UI
  • comment islands
  • newsletter feedback
  • active TOC states
  • non-Markdown TOC behavior

Site Integrations With Partials And Public Files

layout.html should remain a strict document shell and should not contain direct <script> tags. When a site needs shared analytics, third-party loaders, or content enhancement code, include a named partial from the layout instead:

<head>
  {{partial:tracker}}
</head>
<body>
  {{slot:content}}
  {{partial:content-enhancements}}
</body>

This keeps the layout source strict while giving the theme or site a clear integration point. A site-specific tracker partial can contain the provider snippet.

Google Analytics example:

<!-- partials/tracker.html -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
</script>

Cloudflare Web Analytics example:

<!-- partials/tracker.html -->
<script
  defer
  src="https://static.cloudflareinsights.com/beacon.min.js/..."
  integrity="..."
  data-cf-beacon='{"token":"..."}'
  crossorigin="anonymous"
></script>

For Markdown body enhancement, load the integration only on post and page routes. The v0.5 template syntax does not support or, so use else_if:

{{#if post}}
  {{partial:mermaid-loader}}
{{#else_if page}}
  {{partial:mermaid-loader}}
{{/if}}

cdnjs Mermaid example:

<!-- partials/mermaid-loader.html -->
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.12.0/mermaid.min.js"></script>
<script defer src="/assets/mermaid-renderer.js"></script>

/assets/mermaid-renderer.js can scan rendered code blocks such as pre code.language-mermaid, replace them with Mermaid containers, and call Mermaid after load. Without JavaScript, the original code block remains readable.

Third-party files that belong to a site rather than a reusable theme should live in public/:

public/
  vendor/
    highlight.js-11.11.1/
      highlight.min.js

They are referenced from the output root:

<!-- partials/content-enhancements.html -->
<script defer src="/vendor/highlight.js-11.11.1/highlight.min.js"></script>

Use this pattern for analytics, Mermaid, highlight.js, code-copy buttons, heading UI, and other optional integrations. Core document content should still render meaningfully before these scripts run.

Preview data may also provide custom_html for trusted site/admin customization. Use partials when the theme wants a named template integration point; use custom_html when trusted preview-data generation should inject final site snippets before </head> or </body> without editing the theme.

For reusable themes, footer branding should be driven by preview-data rather than hard-coded site names. site.footer.copyright_text is optional footer text, and supporting themes should hide Published with ZeroPress. style attribution when site.footer.attribution.enabled is false.

Comments are gated by both features.comments in theme.json and post.comments_enabled in the render context.

{{#if post.comments_enabled}}
  {{partial:comments-island}}
{{/if}}

Newsletter support currently means the theme may expose static newsletter UI. Storage and third-party integrations are not part of the v0.5 runtime contract.

Start with the ZeroPress CLI tools:

  1. Create a starter theme with create-zeropress-theme.
  2. Preview, validate, and package the theme with @zeropress/theme.
  3. Build static output with @zeropress/build.

See CLI Tools for package roles and npm references.

Checklist

  • Use runtime: "0.5".
  • Keep theme.json valid against the v0.5 theme schema.
  • Render post lists from posts.items[].
  • Render pagination from structured pagination data when pagination.enabled is true.
  • Render taxonomy from post.categories[], post.tags[], and route taxonomy.
  • Render global taxonomy filters from taxonomies.categories[] and taxonomies.tags[].
  • Render Markdown TOC from page.toc[] or post.toc[] when the theme includes TOC UI.
  • Avoid duplicate page headings when page.html already contains a Markdown H1.
  • Keep class names, data attributes, CSS selectors, and JS selectors aligned across templates and assets.
  • Keep common analytics and content enhancement scripts in named partials instead of writing them directly in layout.html.
  • Put site-owned third-party assets in public/ and reference them from root paths such as /vendor/....
  • Keep comments, search, newsletter behavior, and non-Markdown TOC behavior as progressive enhancement.
  • Document public CSS variables if the theme exposes customization hooks.