Skip to main content
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:
src/data/pages/home.json (simplified)
The key top-level fields are:
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).
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.
string
The <title> tag value injected by generateMetadata. Used for SEO and browser tab labels.
string
The <meta name="description"> value. Keep it under 160 characters for best SEO results.
Section[]
required
An ordered array of section objects. The visitor page renders each section in array order. See Sections for the full section object shape.

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: 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: 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:
src/data/pages/posts/[slug].json (collection field)
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:
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.
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.
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.
object
Display options such as paddingTop, paddingBottom, sticky, and showLogo. All settings are optional and section-specific. See Section settings.
A minimal section object looks like this:
A cta-banner section

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.
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.