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:
versiongeneratorgenerated_atsitecontent
Key points:
versionmust be"0.5".generated_atis a UTC date-time string.contentis data-only and does not include pre-rendered archive/category/tag route arrays.menusis optional and keyed by enabledmenu_idvalues when present.widgetsis optional and keyed by enabledwidget_area_idvalues when present.collectionsis optional and keyed by collection id values when present.custom_cssandcustom_htmlare optional site customization fields.
The machine-readable schema is:
3. Content Model
3.1 site
site contains theme-facing site metadata such as:
titledescriptionurlmediaBaseUrlmediaDeliveryModefaviconlocalepostsPerPagedateFormattimeFormattimezonedisallowCommentsindexingpermalinksfront_pagepost_indexfootermeta
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:
authorspostspagescategoriestagsmedia
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
idandpublic_id. - Post
public_idvalues are positive unique integers. - Pages, categories, and tags do not carry internal ids in the public contract.
- Pages may carry optional
pathfor nested page URLs. - Post and page bodies use raw
contentplusdocument_type. - Taxonomy membership on posts is represented by
category_slugs[]andtag_slugs[].
3.3 menus
menus is an optional object map keyed by menu_id.
Each menu contains:
nameitems
Each menu item contains:
titleurltypetargetchildren
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_htmlis 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[].slugcontent.pages[].slugcontent.categories[].slugcontent.tags[].slugcontent.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.
5. Permalink Contract
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
:slugor:public_id. - Page, category, and tag patterns must include
:slug. - Post date tokens are derived from
published_at_isousingsite.timezone. :monthand:dayare zero-padded.- Literal
.htmlpermalink 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: falsemeans page 1 and page 2+ routes are not emitted. Ifpathorpaginateare present, validators still check their type and format.enabled: truewithpaginate: falseemits only page 1.posts.items[]contains at mostsite.postsPerPageposts andpagination.enabledisfalse.enabled: truewithpaginate: trueemits 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 itemurlmay 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.mediaBaseUrlwhenmediaBaseUrlis non-empty - relative or root-relative paths are preserved as written when
site.mediaBaseUrlis 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.mediaDeliveryModeis"media_domain"site.mediaBaseUrlis 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_idvalues - invalid
custom_cssorcustom_htmlobject 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.5is 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.5payloads 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 |