JJourney AI

Page JSON

The JSON shape of the page detail response and its nested component tree.

Updated 27 October 20264 min read

This page covers the data field of GET /api/v1/page/detail/{language}?path= and how to read the component tree.

Page

data
{
  "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": []
}
FieldDescription
idThe page ID.
titlePage title. Metadata falls back to it when there is no SEO title.
pathThe page's CMS path. Don't build links from it; use the href the API returns.
menuDescriptionShort description. Metadata falls back to it when there is no SEO description.
templateIdThe template the page belongs to.
publishStartDatePublish start. Used as the publish date in JSON-LD.
redirectToWhen set, the page is not rendered and redirects to this target instead.
seoMeta title, description, canonical, Open Graph, Twitter, and noIndex/noFollow.
schemasJSON-LD schemas authored in the panel. schemaJsonLd is a complete JSON-LD string. Only isActive: true schemas are printed.
entitiesGEO entities. Printed as JSON-LD nodes attached to the main node.
componentsThe page's component tree.

Component node

Components can nest to any depth. Every node has the same shape.

components[0]
{
  "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 panelAPI valueMeaning
PublicBothShown on both the preview and live sites.
Preview onlyPreviewOnlyShown on the preview site only.
Live onlyProdOnlyShown on the live site only.
HiddenNotShowShown 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.

RolecomponentType valuesValue
ContainerCustom, Formchildren[]
LeafSingleLine, Markdown, Photo, Document, Dropdown, FormItemdata

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.

src/lib/pick.ts
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:

src/components/cms/FeatureList.tsx
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.

Related pages

On this page