Documentation

ZeroPress Preview Data Spec v0.5

Status: Active (current preview-data contract)

ZeroPress Preview Data Spec v0.5

Status: Active (current preview-data contract)

0. Core Philosophy

  • Preview-data is the canonical theme-facing content payload.
  • Preview-data is data-only and does not contain render-ready application behavior.
  • Themes consume preview-data; build tooling is responsible for rendering and file emission.
  • Preview-data must be safe to validate independently of CMS or build implementation details.

1. Scope

Preview-data v0.5 defines the public payload contract used by ZeroPress build and preview tooling.

In scope:

  • Top-level preview-data payload structure
  • Site metadata exposed to themes
  • Content collections for authors, posts, pages, categories, and tags
  • Optional enabled menus keyed by menu_id
  • Optional enabled widget areas keyed by widget_area_id
  • Optional named collections keyed by collection id
  • Optional site permalink policy
  • Optional front page and post index policy
  • Optional nested page path overrides
  • Optional trusted site customization fields
  • Contract-level safety rules for slug and route-related values

Out of scope:

  • CMS authoring workflows
  • Database schema or admin API request formats
  • Theme manifest rules (theme.json)
  • Host-specific request resolution beyond the emitted static files

2. Top-Level Contract

Preview-data v0.5 is a JSON object with the following required top-level fields:

  • version
  • generator
  • generated_at
  • site
  • content

Key points:

  • version must be "0.5".
  • generated_at is a UTC date-time string.
  • content is data-only and does not include pre-rendered archive/category/tag route arrays.
  • menus is optional and keyed by enabled menu_id values when present.
  • widgets is optional and keyed by enabled widget_area_id values when present.
  • collections is optional and keyed by collection id values when present.
  • custom_css and custom_html are optional site customization fields.

The machine-readable schema is:

3. Content Model

3.1 site

site contains theme-facing site metadata such as:

  • title
  • description
  • url
  • mediaBaseUrl
  • mediaDeliveryMode
  • favicon
  • locale
  • postsPerPage
  • dateFormat
  • timeFormat
  • timezone
  • disallowComments
  • indexing
  • permalinks
  • front_page
  • post_index
  • footer
  • meta

site is a closed object in v0.5. Generator-defined site-level extension values belong under site.meta.

site.mediaDeliveryMode is optional and defaults to "none". Supported values are:

Value Meaning
none Preserve media URLs and do not derive responsive variant URLs
media_domain Treat site.mediaBaseUrl as a ZeroPress media host and allow build tooling to derive variant URLs for managed raster media

site.favicon is optional site-level HTML head metadata. It does not replace public file passthrough. Values are emitted as favicon link tags exactly as provided, so R2/media-host favicons should use absolute URLs:

{
  "favicon": {
    "icon": "/favicon.ico",
    "svg": "/favicon.svg",
    "png": "/favicon.png",
    "apple_touch_icon": "/apple-touch-icon.png"
  }
}

Build wrappers may auto-discover root-level public files such as favicon.ico, favicon.svg, favicon.png, and apple-touch-icon.png when site.favicon is omitted. Explicit site.favicon values take priority over auto-discovered public files.

site.indexing is an optional fallback robots.txt policy. Missing or true means the generated fallback robots.txt allows indexing. false means the generated fallback robots.txt disallows all agents. This field does not stop route generation, sitemap generation, feed generation, or HTML rendering. Site-owned public/robots.txt files should be used for custom crawler rules and take priority over the fallback file. When a site-owned robots.txt exists, ZeroPress copies it as-is and does not append a Sitemap directive; add Sitemap: https://example.com/sitemap.xml manually when needed.

site.meta is optional scalar metadata for site/theme conventions:

{
  "meta": {
    "issue": "Spring 2026",
    "show_sponsor_banner": true,
    "featured_count": 4,
    "empty_value": null
  }
}

ZeroPress core does not interpret site.meta keys. Values are passed to templates as provided. Template interpolation renders scalar values, and template conditionals use native truthiness; for example, the string "0" is truthy and is not coerced to false.

site.permalinks is optional. When omitted, build tooling must use the default permalink policy.

site.front_page and site.post_index are optional. When omitted, build tooling must use the default site routing policy:

{
  "front_page": { "type": "theme_index" },
  "post_index": {
    "enabled": true,
    "path": "/",
    "paginate": true
  }
}

site.footer is optional theme-facing footer display data:

{
  "footer": {
    "copyright_text": "Copyright 2026 Example Corp.",
    "attribution": {
      "enabled": false
    }
  }
}

copyright_text is plain footer text. ZeroPress does not add a copyright symbol automatically.

attribution.enabled controls theme support for Published with ZeroPress. style attribution. Missing or true means a supporting theme may show attribution. false means a supporting theme should hide it.

3.2 content

content contains these collections:

  • authors
  • posts
  • pages
  • categories
  • tags
  • media

content.media is optional managed media metadata. It is intended for generators that know image dimensions, such as admin/import pipelines:

{
  "media": [
    {
      "src": "/originals/2026/05/concrete.jpg",
      "width": 1600,
      "height": 900,
      "alt": "A concrete structure in afternoon light"
    }
  ]
}

Each item uses:

Field Required Meaning
src Yes URL-like media source matching a post/page featured image or author avatar
width Yes Positive integer source image width in pixels
height Yes Positive integer source image height in pixels
alt No Plain alternate text hint

Exact duplicate src values are invalid.

Important v0.5 notes:

  • Posts keep both id and public_id.
  • Post public_id values are positive unique integers.
  • Pages, categories, and tags do not carry internal ids in the public contract.
  • Pages may carry optional path for nested page URLs.
  • Post and page bodies use raw content plus document_type.
  • Taxonomy membership on posts is represented by category_slugs[] and tag_slugs[].

3.3 menus

menus is an optional object map keyed by menu_id.

Each menu contains:

  • name
  • items

Each menu item contains:

  • title
  • url
  • type
  • target
  • children

When menus is omitted, build tooling provides an empty menu map to theme render contexts.

3.4 widgets

widgets is an optional object map keyed by widget_area_id.

When widgets is omitted, build tooling provides an empty widget map to theme render contexts.

3.5 collections

collections is an optional object map keyed by collection id. A collection is a curated list of page and post references for theme-specific layouts such as cover stories, issue sections, portfolio highlights, or landing page groups.

{
  "collections": {
    "cover-story": {
      "title": "Cover Story",
      "description": "Primary feature",
      "items": [
        { "type": "post", "slug": "honest-weight-of-concrete" },
        { "type": "page", "slug": "about" }
      ]
    }
  }
}

Collection ids use the same id style as menu and widget maps. Collection items support type: "post" and type: "page". Build tooling resolves each item to summary data before rendering. Missing referenced slugs are build errors.

3.6 Site Customization Fields

custom_css is optional site-level stylesheet input:

{
  "custom_css": {
    "content": "body { color: rebeccapurple; }"
  }
}

Build tooling emits this as a generated CSS asset and links it before </head>.

custom_html is optional trusted site-level HTML input:

{
  "custom_html": {
    "head_end": {
      "content": "<meta name=\"site-verification\" content=\"...\">\n<script async src=\"https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX\"></script>"
    },
    "body_end": {
      "content": "<script defer src=\"/vendor/app.js\"></script>"
    }
  }
}

Injection points:

Field Insertion point
custom_html.head_end.content Immediately before </head>
custom_html.body_end.content Immediately before </body>

custom_html is trusted raw HTML. ZeroPress does not sanitize, escape, validate tag safety, or block closing tags inside content. It is intended for admin-authorized or trusted generator input such as analytics snippets, verification tags, external scripts, and site-owned public/ vendor scripts.

Themes may also expose partial-based integration points such as {{partial:tracker}}. The distinction is:

  • partials are theme/site template integration points
  • custom_html is preview-data driven site/admin customization

4. Slug Contract

In preview-data v0.5, every content slug is defined as a safe single URL path segment.

This applies to:

  • content.posts[].slug
  • content.pages[].slug
  • content.categories[].slug
  • content.tags[].slug
  • content.posts[].category_slugs[]
  • content.posts[].tag_slugs[]

4.1 Allowed

  • Unicode characters, including Hangul
  • Letters and digits from any supported script
  • Internal punctuation that does not create path ambiguity and is accepted by the schema/runtime validators

4.2 Forbidden

  • Empty or whitespace-only values
  • Path separators: / and \
  • Reserved dot segments: . and ..
  • Percent-encoded slug segments
  • ASCII control characters, including NUL

4.3 Security Intent

These rules exist to ensure that a preview-data slug cannot be misinterpreted as:

  • a multi-segment route
  • a parent-directory traversal sequence
  • an encoded path-escape sequence
  • an ambiguous filesystem output path

Preview-data must remain safe even when produced or consumed by tooling outside the main CMS.

Preview-data v0.5 may include site.permalinks to define build-time public URLs and static output paths.

Default policy:

{
  "output_style": "directory",
  "posts": "/posts/:slug/",
  "pages": "/:slug/",
  "categories": "/categories/:slug/",
  "tags": "/tags/:slug/"
}

Supported output_style values:

Value Public URL Output path
directory /path/foo/ path/foo/index.html
html-extension /path/foo path/foo.html

The site root always outputs index.html.

Supported tokens:

Collection Tokens
posts :slug, :public_id, :year, :month, :day
pages :slug
categories :slug
tags :slug

Token rules:

  • Tokens must occupy a full path segment, such as /posts/:public_id/.
  • Unknown tokens are contract-invalid.
  • Post patterns must include :slug or :public_id.
  • Page, category, and tag patterns must include :slug.
  • Post date tokens are derived from published_at_iso using site.timezone.
  • :month and :day are zero-padded.
  • Literal .html permalink patterns are not part of v0.5.

Examples:

{
  "permalinks": {
    "output_style": "html-extension",
    "posts": "/posts/:public_id",
    "pages": "/:slug/",
    "categories": "/categories/:slug/",
    "tags": "/tags/:slug/"
  }
}

This creates public post URLs such as /posts/123 and output files such as posts/123.html.

Pages may override the page permalink pattern with path:

{
  "title": "Preview Data v0.5",
  "slug": "preview-data-v0.5",
  "path": "spec/preview-data-v0.5"
}

With html-extension, this page has public URL /spec/preview-data-v0.5 and output file spec/preview-data-v0.5.html.

For source-tree style docs, index can be used in the page path:

{
  "title": "CLI Tools",
  "slug": "cli",
  "path": "cli/index"
}

With html-extension, this page has public URL /cli/ and output file cli/index.html. A sibling page such as path: "cli/zeropress-theme" has public URL /cli/zeropress-theme and output file cli/zeropress-theme.html.

path is relative, has no leading or trailing slash, has no empty segment, and each segment follows the slug segment safety policy.

ZeroPress does not emit extensionless files. For html-extension, static hosts may resolve /path/foo to path/foo.html without changing the URL.

6. Front Page And Post Index Contract

Preview-data v0.5 may define which content owns the site root and whether a post index is emitted.

Default policy:

{
  "front_page": { "type": "theme_index" },
  "post_index": {
    "enabled": true,
    "path": "/",
    "paginate": true
  }
}

Supported front_page.type values:

Value Behavior
theme_index Render theme/index.html at /
page Render the page identified by page_slug at /
standalone_html Write trusted full HTML from html directly to /index.html

For front_page.type: "page", page_slug is required. The selected page is rendered at /, and its normal page route is not emitted. For example, if the selected page would normally render at /home/, only / is generated for that page. Sitemap, canonical, and OpenGraph URL use /.

For front_page.type: "standalone_html", html must be a non-empty string. The value is trusted raw full HTML. Theme layout, theme asset rewriting, custom_css, and custom_html injection are not applied to this root file.

post_index controls the post list route rendered with theme/index.html.

Field Default Meaning
enabled true Whether to emit the post index route
path / Absolute public route for the post index
paginate true Whether to emit page 2+ routes

post_index.path must be / or a safe absolute route path such as /blog/. It cannot include .html, query strings, hash fragments, empty segments, or unsafe path segments.

Post index behavior:

  • enabled: false means page 1 and page 2+ routes are not emitted. If path or paginate are present, validators still check their type and format.
  • enabled: true with paginate: false emits only page 1. posts.items[] contains at most site.postsPerPage posts and pagination.enabled is false.
  • enabled: true with paginate: true emits page 1 and page 2+ routes when needed.

Theme capability can disable the post index. If theme.json sets features.postIndex: false, build treats the post index as effectively disabled even when preview-data requests it. This is a theme capability hint, not a preview-data validation error.

If front_page.type is not theme_index, an enabled post index cannot also use /; configure a separate post_index.path, such as /blog/, or disable the post index.

7. URL-Like Fields vs Slugs

Slug fields and URL-like fields have different roles.

  • Slugs are safe single path segments.
  • URL-like fields such as featured_image, avatar, or menu item url may represent either absolute URLs or safe relative paths, depending on the field contract.

Media fields such as featured_image and author avatar are normalized by the renderer:

  • absolute URLs are preserved after protocol validation
  • relative or root-relative paths are resolved against site.mediaBaseUrl when mediaBaseUrl is non-empty
  • relative or root-relative paths are preserved as written when site.mediaBaseUrl is empty

Generated SEO fields such as og:image are emitted only when the resolved media value is absolute. Set site.mediaBaseUrl when relative media should also appear in social preview metadata.

Managed media registry matching is exact after renderer media normalization. content.media[] does not replace existing media string fields. The original fields remain available as-is. When ZeroPress can match a normalized media string to a registry entry, build tooling exposes a derived companion object:

  • posts receive post.featured_media
  • pages receive page.featured_media
  • post authors receive post.author.avatar_media

The derived object has:

{ src, width, height, alt, srcset }

srcset is generated only when all of these are true:

  • site.mediaDeliveryMode is "media_domain"
  • site.mediaBaseUrl is non-empty
  • the matched media URL is under site.mediaBaseUrl
  • the media source is a raster image path such as .jpg, .jpeg, .png, .webp, or .avif

Responsive candidates are clipped to the original image width. Variant URLs use w=<width>&fit=scale-down&format=auto. Body Markdown or HTML <img> tags are not rewritten by this contract.

A value that is valid for a URL-like field is not automatically valid for a slug field.

8. Validation and Enforcement Layers

Preview-data security is intentionally enforced in multiple layers.

8.1 Contract Validation

The schema and preview-data validator reject contract-invalid slug values before build rendering begins.

This is the layer that communicates:

  • what a valid preview-data slug is
  • which fields the rule applies to
  • why a payload is contract-invalid

8.2 Build Enforcement

Build tooling must independently enforce output path safety even when preview-data has already been validated.

This is required because:

  • build is an independent process
  • preview-data may be produced by external tooling
  • final filesystem writes must not rely on upstream validation alone

Schema validation does not replace final path-safety checks in the build engine.

9. Validation Profile

Errors include:

  • missing required top-level fields
  • invalid site field types
  • missing required content fields
  • invalid document_type
  • invalid menu item structure
  • invalid slug values that violate the safe single-segment contract
  • invalid permalink or page path values
  • invalid front page or post index values
  • duplicate post public_id values
  • invalid custom_css or custom_html object shape

Notes:

  • slug validation is intentionally stricter than a plain non-empty string check
  • build implementations should still reject any computed output path that attempts to escape the build root

10. Compatibility Notes

  • v0.5 is the current preview-data contract.
  • Earlier preview-data versions are historical and may differ in content shape and route-related fields.
  • Tooling may evolve, but public v0.5 payloads must continue to satisfy the published schema and slug safety contract.

11. Normative vs Informative Summary

Item Classification Notes
top-level version, generator, generated_at, site, content Normative (Required) Missing fields are contract-invalid
top-level menus, widgets, collections Normative (Optional) Missing fields are treated as empty maps by build tooling
content.posts[].slug, content.pages[].slug, content.categories[].slug, content.tags[].slug Normative (Required) Must be safe single path segments
content.posts[].public_id Normative (Required) Must be a positive unique integer
site.permalinks Normative (Optional) Defines build-time URL/output policy when present
site.front_page Normative (Optional) Defines which content owns /
site.post_index Normative (Optional) Defines whether and where the post index is emitted
content.pages[].path Normative (Optional) Overrides the page permalink pattern when present
custom_css Normative (Optional) Site-level stylesheet input emitted as a generated CSS asset
custom_html Normative (Optional) Trusted raw HTML inserted before </head> and/or </body>
content.posts[].category_slugs[], content.posts[].tag_slugs[] Normative (Required) Each referenced slug must also be a safe single path segment
Unicode slug content including Hangul Informative (Allowed) Allowed when all path-safety rules are still satisfied
/, \, ., .., percent-encoded slug segments, control characters Normative (Forbidden) Rejected for security and path clarity
final output path enforcement in build tooling Normative (Required) Must be enforced independently of schema validation