Page JSON
The JSON shape of the page detail response and its nested component tree.
This page covers the data field of GET /api/v1/page/detail/{language}?path= and how to read the component tree.
Page
{
"id": 123,
"title": "About us",
"path": "about",
"menuDescription": "Get to know our company.",
"icon": "https://cdn.example.com/Photos/og.jpg",
"templateId": 2,
"publishStartDate": "2026-09-01T00:00:00Z",
"redirectTo": null,
"seo": {
"metaTitle": "About us",
"metaDescription": "…",
"canonicalUrl": "…",
"ogImage": "…",
"noIndex": false,
"noFollow": false
},
"schemas": [
{ "schemaType": "Organization", "schemaJsonLd": "{…}", "isActive": true }
],
"entities": [
{ "entityType": "Product", "name": "…", "metadataJson": "{…}" }
],
"components": []
}| Field | Description |
|---|---|
id | The page ID. |
title | Page title. Metadata falls back to it when there is no SEO title. |
path | The page's CMS path. Don't build links from it; use the href the API returns. |
menuDescription | Short description. Metadata falls back to it when there is no SEO description. |
templateId | The template the page belongs to. |
publishStartDate | Publish start. Used as the publish date in JSON-LD. |
redirectTo | When set, the page is not rendered and redirects to this target instead. |
seo | Meta title, description, canonical, Open Graph, Twitter, and noIndex/noFollow. |
schemas | JSON-LD schemas authored in the panel. schemaJsonLd is a complete JSON-LD string. Only isActive: true schemas are printed. |
entities | GEO entities. Printed as JSON-LD nodes attached to the main node. |
components | The page's component tree. |
Component node
Components can nest to any depth. Every node has the same shape.
{
"id": 10, // ID of the component definition in the catalog
"uniqueId": 4711, // ID of this placement: use for React keys and the inspector
"slug": "hero-banner", // registry key, matched first
"componentType": "Custom", // fallback key
"title": "Hero",
"visibility": "Both",
"data": null, // set on leaves only
"children": [
{ "slug": "title", "componentType": "SingleLine", "data": "Hello" },
{ "slug": "description", "componentType": "Markdown", "data": "A **short** intro." },
{ "slug": "image", "componentType": "Photo", "data": "https://cdn.example.com/Photos/hero.webp" }
]
}Visibility values
| In the admin panel | API value | Meaning |
|---|---|---|
| Public | Both | Shown on both the preview and live sites. |
| Preview only | PreviewOnly | Shown on the preview site only. |
| Live only | ProdOnly | Shown on the live site only. |
| Hidden | NotShow | Shown on neither site. |
id versus uniqueId
id points to the component's definition in the catalog. If the same component appears twice on a page, both nodes share the same id. uniqueId belongs to that one placement. Always use uniqueId for React keys and the panel inspector.
Containers and leaves
The rule is simple: containers carry children, leaves carry data.
| Role | componentType values | Value |
|---|---|---|
| Container | Custom, Form | children[] |
| Leaf | SingleLine, Markdown, Photo, Document, Dropdown, FormItem | data |
data is a value such as text, a URL, or a selection. On FormItem leaves, data is JSON describing the field. See Forms.
Reading the tree
A block finds its children by slug. Don't rely on order; always look up by slug.
import type { ApiComponent } from "@/types/cms";
export const pick = (children: ApiComponent[] | undefined, slug: string) =>
children?.find((c) => c.slug === slug)?.data as string | undefined;For repeating items, filter the children and read each one through its own children:
import type { ApiComponent } from "@/types/cms";
import { pick } from "@/lib/pick";
export default function FeatureList({ children }: { children?: ApiComponent[] }) {
const items = children?.filter((c) => c.slug === "item") ?? [];
if (items.length === 0) return null;
return (
<ul className="grid gap-4 md:grid-cols-3">
{items.map((item) => (
<li key={item.uniqueId}>
<h3 className="font-semibold">{pick(item.children, "title")}</h3>
</li>
))}
</ul>
);
}Any field can be missing
An editor can leave a field empty, or the component definition can change later. Your component must tolerate every missing field and carry its own defaults. If a required field is missing, rendering nothing beats rendering a broken section.
Only the top level goes through the registry
The site template passes only the top-level items of components[] through the registry. Blocks render their nested children themselves, so leaves like title have no registry entry. Components explains how this works.