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 |
|---|---|
page | One published page, resolved by slug or id. The page's title, meta, host options, and its block tree. |
pages | The published index — what you build sitemaps and link pickers from. |
menus | Header, footer, and account navigation in one call. |
Only the published revision is served there. Drafts stay in the editorial surface, which is not part of the delivery contract. 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.
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, which produces 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.
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.