Reading published pages
Page content does not come from Cockpit at request time. Cockpit is where an editor composes a page; the storefront reads the published result from the public pages delivery surface through the gateway.
Getting that direction right matters, because it explains the performance profile: a page render is a read of published content, not a call into an editing system.
The delivery surface
The published side of pages is exposed on the public gateway under /v1/pages/delivery/*:
| Route | What it returns |
|---|---|
GET /v1/pages/delivery/page | One published page, resolved by slug or id (send one: with both, id is used; with neither, 400), for an optional langcode. The page's title, meta, host options, and its block tree. |
GET /v1/pages/delivery/pages | The published index — what you build sitemaps and link pickers from. |
GET /v1/pages/delivery/menus | Header, footer, and account navigation. The route takes no filter, but it is paged like a list (limit up to 200, default 50, and offset): page through it once and index the menus by id. |
GET /v1/pages/delivery/popups | The live pop-ups for the visitor, with their trigger and delay. Pop-ups are not part of the page tree. |
GET /v1/pages/delivery/preview/{token} | One page as its unpublished working copy, resolved from a preview token. The page shape with the unpublished content. See Previewing unpublished edits. |
GET /v1/pages/delivery/site-settings | The tenant's site settings that a theme styles the whole storefront with, in one object keyed by setting name. appearance (colour scheme, corner style, spacing density, font family), design (design tokens such as the primary colour, neutral palette, radius and font) and customCss (CSS the theme emits after its own) are always present, and are null when the tenant has not set them, which is the theme's cue to use its own default. Any other key the tenant set comes back next to them. |
GET /v1/pages/delivery/template | Which page a record renders with. Takes type (the kind of record, such as product or category) and id (the record's id in the app that owns it), both needed, and answers {"slug": "product-detail"}; render that slug through GET /v1/pages/delivery/page. 404 means use your default template: no page is assigned, or the assigned page is hidden or not published. 400 when type or id is missing, type is not lower-case letters, digits and underscores starting with a letter (up to 64), or id is longer than 255 characters. |
The page and popups routes take two optional query parameters that decide which gated blocks and pop-ups come back:
audiences: comma-separated audience codes the visitor belongs to, as the storefront resolved them for a signed-in contact (seePOST /v1/audiences/memberships). Left out, the visitor is anonymous: blocks that require an audience are left out, and blocks that only exclude audiences are returned.consents: comma-separated consent purposes the consent manager holds for the visitor. A block that names aconsent_purposeis returned only when that purpose is in this list, and the same goes for a pop-up.
The delivery trusts both values as sent and checks nothing itself, so your server must resolve them. Never take them from the visitor's browser.
Only the published revision is served there (except preview/{token}, which serves a draft and must not be cached). Drafts stay in the editorial surface, which is not part of the delivery contract (see Managing pages over the API). That's the property that makes the delivery surface cacheable.
Resolution is per language: the delivery response applies i18n fallback and the block publish windows server-side, so your theme receives the tree that should render for that locale right now and doesn't have to filter anything itself.
Like every platform call, this goes out from your server routes, never from a component — the API key stays server-side. See The BFF pattern.
Pop-ups
A pop-up is a library item of the bundle popup: its content is a block tree (tree), its appearance is a popup object, and campaign_ref names the campaign its impressions and clicks count for. It is not part of any page's tree.
The popup object (unknown keys are refused with 400 invalid_popup):
trigger:exit_intent(default),delay_secondsorscroll_percent.delay_seconds(0 to 3600) andscroll_percent(1 to 100): each is required with its own trigger.frequency:once_per_session(default),once_per_dayorevery_page. The storefront applies it and keeps the count.form_id: the id of the Forms form the pop-up shows, ornull. Only its UUID shape is checked; the form itself is not looked up.visibility:{ audiences, exclude_audiences, consent_purpose }, ornullfor everybody.starts_atandends_at: the window it may appear in.nullmeans no limit on that side.ends_atmust be afterstarts_at.active: switched on, defaulttrue.
| Route | What it does |
|---|---|
GET /v1/pages/popups | Lists the pop-ups, never soft-deleted. ?campaign_ref= lists one campaign's. |
POST /v1/pages/popups | Creates one. label is required; tree, popup, campaign_ref and metadata are optional. 201 on success, 400 when a field is invalid. |
PUT /v1/pages/popups/{id} | Partial update, at least one field. popup is merged over the stored one and then checked whole. campaign_ref: null releases it from its campaign. 404 when no pop-up has that id. |
GET /v1/pages/delivery/popups | The pop-ups a visitor may see now: switched on, inside their window, and visible to them. Takes langcode, audiences and consents, as a page does. Each item has id, label, trigger, delay_seconds, scroll_percent, frequency, form_id, campaign_ref and content. |
The delivery answer is an object: items holds the pop-ups, and personalization lists the audiences and consents that were applied. As on pages, audiences and consents are supplied by your server and are trusted as sent (see the delivery surface above).
The storefront applies the trigger and the frequency itself, and posts impressions and clicks to POST /v1/campaigns/touches.
The catch-all route
A theme serves editor-composed pages from a single Nuxt catch-all route. One file handles every page an editor will ever create:
app/pages/[...slug].vue
Its job is small and always the same:
- Join the route segments into a slug.
- Fetch the published page for that slug.
- Apply what the page declares about itself — its Nuxt layout, and its per-page appearance.
- Set the SEO meta from the page's own title and description.
- Hand the block tree to the renderer.
<script setup lang="ts">
const route = useRoute()
const slug = computed(() => {
const segments = route.params.slug
return Array.isArray(segments) ? segments.join('/') : segments
})
// Reads the published page through the theme's own server route,
// which calls the delivery surface on the gateway.
const { data: page, status, error, refresh } = await useFetch(() => `/api/pages/${slug.value}`)
const blocks = computed(() => page.value?.blocks ?? [])
const layout = computed(() => page.value?.layout ?? 'default')
// The page declares its own Nuxt layout — 'default' for the shop shell,
// a minimal one for focused flows like checkout and login.
setPageLayout(layout.value)
watch(layout, l => setPageLayout(l))
useSeoMeta({
title: () => page.value?.title,
description: () => page.value?.description,
})
</script>
<template>
<UiFeedbackErrorBoundary v-if="error" @retry="refresh" />
<PageSkeleton v-else-if="status !== 'success'" />
<BlokkliRenderer v-else :blocks="blocks" />
</template>
Three things this shape gets you for free:
- New pages need no deploy. An editor publishes a page at a new slug and it's live, because the route already matches everything.
- The page controls its own chrome. Layout, colour scheme, corner style, spacing density and font family come from the page's host options, so an editor can make a landing page look different from a category page without a developer.
- A missing page is a 404, not an error. The delivery surface answering
404means the slug isn't published; render your not-found page rather than an error boundary.
You don't write the tree-walking code. The renderer resolves each node's bundle to the component registered for it — see Registering blocks.
Previewing unpublished edits
An editor working on a page needs to show someone the draft before it goes live, and that person shouldn't need Cockpit access or the editor bundle.
The editor issues a preview grant for the page in Cockpit. The grant gives a token and a shareable link on the theme's own domain:
https://shop.example.com/preview/<token>
That route renders the page's current, unpublished edit state, read-only, through the same lightweight renderer live pages use — no editor code involved, so the preview looks and performs like the real thing.
Your server resolves the token with GET /v1/pages/delivery/preview/{token}. It takes an optional langcode and returns the page and its fields in the same shape as the page route, built from the unpublished working copy instead of the published revision. Unlike page, the answer has no personalization object and no audience or consent gating: every block is shown unless it is outside its publish window. It answers 404 when no grant has that token, or when the page has been deleted, and 410 when the grant has expired. A 410 means the sharer needs a new link from the editor.
A grant lasts 72 hours by default. The editor can set another lifetime when it issues the grant.
Two properties matter for how you use it:
- Tokens are short-lived. Expiry is enforced server-side. A preview link is for a review conversation, not a permanent staging URL. For a durable environment, use a branch preview host instead.
- Preview pages must not be indexed. A theme's preview route sets
robots: noindex,nofollowand suppresses the page's canonical SEO meta. If you write your own preview surface, do the same — an indexed draft is the thing you can't take back.
This is distinct from a deployment preview. A preview grant shows unpublished content on the live build; a deployment preview host shows a different build. See Previews.
Next steps
- The BFF pattern — why the delivery call goes through your server routes.
- Registering blocks — how a bundle in the tree becomes a component.
- Shells and fields — the preview provider your containers must handle.
- Previews — deployment preview hosts and how to protect them.
- Multi-tenant themes — partitioning a page cache by tenant.