> ## Documentation Index
> Fetch the complete documentation index at: https://docs.olonjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pages in OlonJS: structure, sections, and metadata

> Understand how OlonJS pages work: JSON-defined page bundles composed of typed sections, with metadata, slugs, and dynamic routes for collections.

Every OlonJS page is a JSON file. You define what lives at each URL by writing a page bundle that declares an `id`, a `slug`, an optional `meta` block for SEO, and an array of `sections`. The sections array is the engine of the page — it drives everything rendered on screen, in order, from top to bottom.

## Page JSON structure

A page bundle is a plain JSON object. Here is the home page (`src/data/pages/home.json`) simplified to its skeleton:

```json title="src/data/pages/home.json (simplified)" theme={null}
{
  "id": "home-page",
  "slug": "home",
  "meta": {
    "title": "Inkwell Journal — Notes on the craft of software",
    "description": "An editorial journal about design, engineering and process."
  },
  "sections": [
    {
      "id": "home-hero",
      "type": "hero",
      "data": { "title": "Notes on the", "titleHighlight": "craft of software" },
      "settings": { "paddingTop": "xl", "paddingBottom": "none" }
    },
    {
      "id": "home-featured-posts",
      "type": "posts-list",
      "data": {
        "title": "Latest from the desk",
        "items": { "$ref": "../collections/posts/posts.json" }
      },
      "settings": { "paddingTop": "xl" }
    }
  ]
}
```

The key top-level fields are:

<ParamField path="id" type="string" required>
  A unique identifier for this page bundle. Used internally by the engine and Studio to address the page. Convention: `<name>-page` (for example, `home-page`, `post-detail-page`).
</ParamField>

<ParamField path="slug" type="string" required>
  The URL path segment this page is served at. The special value `home` maps to `/`. Dynamic collection pages use a placeholder like `posts/[slug]` — the engine resolves the real slug at request time.
</ParamField>

<ParamField path="meta.title" type="string">
  The `<title>` tag value injected by `generateMetadata`. Used for SEO and browser tab labels.
</ParamField>

<ParamField path="meta.description" type="string">
  The `<meta name="description">` value. Keep it under 160 characters for best SEO results.
</ParamField>

<ParamField path="sections" type="Section[]" required>
  An ordered array of section objects. The visitor page renders each section in array order. See [Sections](/concepts/sections) for the full section object shape.
</ParamField>

## Static vs dynamic pages

Pages come in two flavours.

**Static pages** are one-to-one mappings between a JSON file and a URL. Each file in `src/data/pages/` becomes exactly one page:

| File | Slug | URL |
| - | - | - |
| `home.json` | `home` | `/` |
| `about.json` | `about` | `/about` |
| `contact.json` | `contact` | `/contact` |
| `posts.json` | `posts` | `/posts` |
| `tags.json` | `tags` | `/tags` |

**Dynamic collection pages** are templates that represent one page per item in a collection. They live in a subdirectory whose name matches the collection route:

| File | Slug | URL pattern |
| - | - | - |
| `posts/[slug].json` | `posts/[slug]` | `/posts/:slug` |
| `tags/[slug].json` | `tags/[slug]` | `/tags/:slug` |

A dynamic page carries an extra `collection` field that tells the engine which collection to draw from and which URL parameter holds the item key:

```json title="src/data/pages/posts/[slug].json (collection field)" theme={null}
{
  "id": "post-detail-page",
  "slug": "posts/[slug]",
  "collection": { "source": "posts", "paramKey": "slug" },
  "meta": { "title": "Post — Inkwell Journal essay detail" },
  "sections": [...]
}
```

At request time the engine reads the actual `slug` parameter, looks up the matching item in the `posts` collection, and injects it wherever sections reference `{ "$ref": "collection:current" }`.

## Sections in a page

Each entry in the `sections` array is a section object. A section object has four fields:

<ParamField path="id" type="string" required>
  A unique identifier for this section within the page. Used by Studio to target the section in the visual editor and by React as the `key` prop.
</ParamField>

<ParamField path="type" type="string" required>
  The section component to render. Must match a key registered in `ComponentRegistry` — for example `hero`, `posts-list`, or `cta-banner`. See the full list in [Sections](/concepts/sections).
</ParamField>

<ParamField path="data" type="object" required>
  The content payload passed to the section component. Shape is validated by the section's Zod schema. Values can be inline strings and objects, or `$ref` pointers that resolve to collection data.
</ParamField>

<ParamField path="settings" type="object">
  Display options such as `paddingTop`, `paddingBottom`, `sticky`, and `showLogo`. All settings are optional and section-specific. See [Section settings](/concepts/sections#section-settings).
</ParamField>

A minimal section object looks like this:

```json title="A cta-banner section" theme={null}
{
  "id": "home-cta",
  "type": "cta-banner",
  "data": {
    "label": "Start anywhere",
    "title": "Pick a thread, pull it",
    "primaryCta": {
      "id": "home-cta-1",
      "label": "Read the latest",
      "href": "/posts",
      "variant": "primary"
    }
  },
  "settings": {
    "paddingTop": "xl",
    "paddingBottom": "xl"
  }
}
```

## Page routes

The Next.js App Router catch-all route at `app/[[...slug]]/page.tsx` intercepts every incoming request. It reads the URL segments, joins them into a slug string — for example `posts/the-boring-stack` — and looks up the matching page bundle from the page registry.

Static slugs match exactly (`about`). Dynamic slugs match by template key (`posts/[slug]`) — the engine extracts the wildcard portion and uses it for section-level `$ref` resolution so the correct collection item is injected into the page.

<Note>
  Always edit your page source files in `src/data/pages/`. The framework builds the final page bundles for the visitor site as part of the standard deployment process. Changes you make to `src/data/pages/` take effect on the next deploy — you never need to edit any generated or compiled output directly.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.