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

# Hub templates and the page catalog

> What the page catalog publishes — hub templates, page templates, playlist and document seeds — and how the artifact is versioned.

The page catalog is one versioned JSON artifact that every Membership.io surface — backend, hub, admin, and CLI — consumes instead of hand-copying constants. `GET /api/v1/page-builder/catalog` returns it raw, and the [catalog reference](/reference/page-builder-catalog) tracks its node-level vocabulary: templates, node kinds, and settings.

This guide covers the catalog's higher-level registries, which the reference page does not: what a **hub template** is, how templates seed playlists and placeholder documents, and how the artifact is versioned and pinned. If you consume the catalog directly — to scaffold hubs, mirror the vocabulary into your own tooling, or diff releases — this is the contract you are consuming.

## One artifact, three registries

The catalog carries three registries of increasing scope, plus the supporting vocabulary the reference page documents in full:

| Registry          | One entry is                                                                                                  | Scope                 |
| ----------------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
| `templates[]`     | A **section recipe** — the starter subtree for one section (`hero`, `row`, `grid`, …), with optional variants | One section of a page |
| `pageTemplates[]` | A **page outline** — a complete starter tree for one page type (`page-homepage-starter`, `page-login`, …)     | One page              |
| `hubTemplates[]`  | A **whole-hub manifest** — branding, navigation, spaces, policies, playlists, and page references             | An entire hub         |

The supporting maps — `nodeKinds`, `sectionTypes`, `settingsSchema`, `pageTypes`, `nestingRules`, and the consumer `profiles` — are generated into the [catalog reference](/reference/page-builder-catalog) on every catalog update.

<Note>
  **Parse the artifact tolerantly.** The response is the raw catalog JSON, not a JSON:API envelope, and most objects in it are deliberately open — new keys, new enum members, and new templates appear over time as additive changes. Ignore keys you don't recognize rather than rejecting them; treat any enum list you mirror as the *currently known* set, not a closed one.
</Note>

## What a hub template is

A hub template is a **thin manifest, not a snapshot of a finished hub**. It references page templates by id and carries preset values for everything else; applying one — `mio hubs scaffold --template starter --name "Acme" --slug acme` — creates the real resources through the same validated write paths as the individual commands. The current catalog ships one hub template, `starter`, and `mio hubs templates` lists exactly what the backend you are pointed at offers.

Each entry has an `id`, `label`, and `lifecycle` (`active` or `deprecated`), plus the blocks below:

| Block                           | What it seeds                                                                                                                                                                                                                          |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `branding`                      | The hub palette and chrome colors — plain hex strings (`"#151516"`), not page-node color tokens                                                                                                                                        |
| `navigation.header` / `.footer` | Menu entries: `{type, label, href, position, icon}`                                                                                                                                                                                    |
| `settings`                      | The hub settings blob — registration, header visibility, menu layout, policies display                                                                                                                                                 |
| `spaces[]`                      | Discussion spaces: `{name, slug, description, access_level, posting_permission, icon}` — `icon` is a hub sprite name, not an emoji                                                                                                     |
| `onboarding[]`                  | Contact-attribute definitions to create (may be empty)                                                                                                                                                                                 |
| `policies`                      | Terms / privacy-policy blocks. A per-policy `enabled: true` declares intent **and** makes the applier flip the hub's enforcement gate — writing policy text without the gate would leave a hub where nobody is ever asked to accept it |
| `playlists[]`                   | Playlists to create, including placeholder documents — see [below](#playlists-and-placeholder-documents)                                                                                                                               |
| `welcomePost`                   | The one welcome discussion post: `{space, title, body, is_published}`. `space` names one of the template's own `spaces[].slug`                                                                                                         |
| `pages[]`                       | Pages to instantiate from page templates                                                                                                                                                                                               |

A `pages[]` entry is `{role, pageTemplate, slug, title, privacy, isHomepage}`. `pageTemplate` must name an existing `pageTemplates[].id` — the hub template couples to the page-tree vocabulary only through these references, which is what keeps the manifest thin.

### Name interpolation

Two tokens, `{{hub_name}}` and `{{hub_slug}}`, are substituted when a template is applied — a closed vocabulary, not general templating. They are honored in exactly three places: the string `value` of `headline`, `text`, and `button` leaves inside recipes; `pages[].title`; and navigation labels. Substituted values are capped (5,000 code points for leaf values, 200 for page titles, 80 for nav labels), and any other `{{…}}` sequence in those locations is a validation error rather than being passed through.

`welcomePost.title` and `.body` are **literal** — a token written there is stored verbatim, not substituted.

## Playlists and placeholder documents

A `playlists[]` entry describes one playlist the template creates:

```json theme={null}
{
  "title": "Getting Started",
  "key": "getting-started",
  "visibility": "public",
  "file_ids": [],
  "documents": [
    {
      "title": "Add your first lesson",
      "description": "This is a placeholder lesson — open it in the editor and swap it for your first real one.\n\n## Three ways to add content\n\n- **Upload a video or audio file** …"
    }
  ]
}
```

* **`title`** — the playlist's display title.
* **`key`** — a template-scoped handle, unique within the template. It exists so other parts of the same template can reference the playlist before it has an id (see [binding](#binding-pages-to-seeded-playlists)); it is not stored on the created hub.
* **`visibility`** — `public`, `unlisted`, or `private`. This flows to playlist creation; note that a `private` playlist's detail page is unreachable for members, so shipped templates use `public`.
* **`file_ids`** — ids of existing media files to attach. Usually empty: a template cannot reference media that doesn't exist until the hub does.
* **`documents[]`** — placeholder text documents, described next.

The base playlist object is open (new keys may appear), but each `documents[]` entry is **strictly typed**: exactly `{title, description?}`, with `title` required and no other keys allowed. Catalogs from `0.22.0` onward enforce this shape in the schema itself; the shape has been stable since the field first shipped. A document seed deliberately has no body or media fields — it describes a file that has no bytes.

### How documents materialise

Each `documents[]` entry becomes a real, ready **document file with no uploaded media**. Applying the template registers a synthetic document file carrying the entry's title and description, publishes that file to the hub (an attached-but-unpublished file would be invisible to visitors), and adds it as a playlist item. Items land in declared order — `file_ids` first, then `documents`, positions assigned explicitly.

The result is a playlist of text lessons a new hub owner is meant to open in the editor and replace. Nothing about them is special after creation — they are ordinary document files.

### The description does triple duty

For a document seed, `description` is not just a blurb. It serves as:

1. **The lesson body** — a document file with no media renders its description as the lesson text, markdown included.
2. **The read-time estimate** — the "N min read" chip on the card is computed from it.
3. **The card blurb** — the starter homepage's Getting Started band renders each document as a card, and that card binds the document's description into a dedicated field (node id `gs-card-description` in the catalog artifact), clamped to two lines. The field is optional: a document with no description renders no blurb rather than an error.

That third role imposes an authoring rule on the shipped seeds, worth following in your own content too: **the first line is one plain-prose sentence with no markdown syntax** — no heading marker, list dash, bold, or link — because the clamped blurb renders the raw first line. Headings and lists start after the first blank line.

### Binding pages to seeded playlists

A hub template's pages can data-bind sections to the playlists the same template creates, before either exists. The page recipe ships a playlist `dataSource` with an empty id and a `key`:

```json theme={null}
{ "dataSource": { "type": "playlist", "id": "", "key": "getting-started" } }
```

`key` names the same template's `playlists[].key`. When the template is applied, the created playlist's real id is written into `id` before the page tree is stored — the hub renderer ignores a data source whose `id` is empty, so an unresolved binding renders as a blank band rather than an error. A `key` that names no playlist in the template fails preflight, before anything is created.

## Anatomy of a recipe

`templates[]` and `pageTemplates[]` entries wrap **recipe nodes** — the declarative form of the [node envelope](/guides/pages-page-builder#node-shape):

* A recipe node has a `kind`, `settings`, and either a `children` array **or** a literal `value` — never both.
* Recipe ids are placeholders. Instantiating a recipe deep-clones it and mints a fresh UUIDv7 for every node, so ids in the catalog (`root`, `hero`, `gs-card`) are symbolic and never survive into a real page.
* Structural settings keys — `slot`, `name`, `role` — survive cloning, which is how a template can address its own nodes after ids are re-minted.
* Data-bound nodes additionally carry `dataSource`, `repeat`, `collapseWhenEmpty`, and binding settings such as `valueFrom` and `actionFromScope`.
* Templates can declare `variants` keyed by variant id (`hero.playlist`, `row.3eq`); instantiation picks the requested variant and falls back to `starter`.
* **Recipes omit defaults rather than restating them.** A setting a recipe leaves out defers to the renderer's built-in default for that key — so don't read the absence of a key as the absence of behavior.

Instantiation does exactly one imperative thing: clone and re-id. The settings cascade, data-source resolution, and access-gate pruning all stay runtime concerns of the render path.

<Note>
  A `pageTemplates[]` entry is the **seeding contract** for its page type — the starter tree a scaffolded hub gets — not a statement about how the hub currently renders that page. Several system pages (login, account, members, …) still render via dedicated routes rather than by walking a stored page tree; don't infer the render path from a template's existence.
</Note>

## Versioning and pinning

The artifact's `meta` block is the version contract (values here are illustrative — read the live ones from the artifact you fetch, or the [catalog reference](/reference/page-builder-catalog)):

```json theme={null}
{
  "meta": {
    "schemaVersion": "2.1.0",
    "catalogVersion": "0.23.0",
    "revision": 40,
    "digest": "sha256:76ba947342356c25873d8852264fa0014340157080bdd21ef1e793732736a87b",
    "createdAt": "2026-07-14T00:00:00Z"
  }
}
```

| Field            | Meaning                                                                                                                                                 |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schemaVersion`  | The **shape** of the artifact. Bumps only on a breaking schema change — a parser written against `2.x` keeps working until this moves                   |
| `catalogVersion` | The **content** version. Bumps on any recipe, template, or vocabulary change: patch for content-only edits, minor for new templates or tightened shapes |
| `revision`       | A monotonic integer that increments with every published change. Two artifacts can always be ordered by it, even across content-version rewrites        |
| `digest`         | `sha256` over the canonical artifact (excluding `meta.digest` itself). Identifies the exact content, and doubles as the endpoint's `ETag`               |

Three practical consequences:

* **The version you fetch is a pin, not the newest catalog.** The platform vendors a specific catalog release and bumps it deliberately, so the served artifact moves in steps. Trust the `meta` of the artifact you actually fetched; never assume it matches the catalog's latest release.
* **Cache by digest.** Send `If-None-Match: "<digest>"` and handle `304 Not Modified`; the response is cacheable for five minutes. When the digest changes, everything derived from the catalog — mirrored enums, scaffolded trees, generated docs — should be re-derived.
* **The CLI carries its own pin.** The offline verbs (`mio pages catalog templates` / `section-types` / `scaffold`) work against the CLI's embedded, digest-pinned copy and need no network; `mio hubs templates` and `mio hubs scaffold` read the backend's live catalog, so hub scaffolding always follows the backend's pin. The two pins can differ between releases — when exactness matters, compare digests.

Changes are additive by default: new keys on open objects, new enum members, new templates and variants. A removal or shape break is rare, is called out in the content version, and — if it breaks parsing — moves `schemaVersion`.

## Where to go next

* [Page-builder catalog](/reference/page-builder-catalog) — the generated reference: current version and digest, all templates, node kinds, and the full settings vocabulary.
* [Pages and page builder](/guides/pages-page-builder) — the node envelope, draft/publish contract, and the silent-drop render gotchas.
* [Create a hub](/guides/creating-a-hub) — the scaffold command this vocabulary feeds, plus the step-by-step manual recipe.
