Skip to main content
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 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: The supporting maps — nodeKinds, sectionTypes, settingsSchema, pageTypes, nestingRules, and the consumer profiles — are generated into the catalog reference on every catalog update.
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.

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: 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:
  • 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); it is not stored on the created hub.
  • visibilitypublic, 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:
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:
  • 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.
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.

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):
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 — the generated reference: current version and digest, all templates, node kinds, and the full settings vocabulary.
  • Pages and page builder — the node envelope, draft/publish contract, and the silent-drop render gotchas.
  • Create a hub — the scaffold command this vocabulary feeds, plus the step-by-step manual recipe.