The content model
Understand the handful of building blocks a Quilium site is made of, and learn a reliable rule for picking the right one each time, so you model content once and never have to unpick it later.
Why this matters
Almost every modelling mistake in Quilium comes from one confusion: reaching for the wrong building block. You put a list of features inside a collection when a single rich-text field would have done, or you bury a real catalogue of articles inside one page where it can never be reused. Both work for a while, then they don't, and unpicking them means migrating content by hand.
This page exists to prevent that. It defines each building block, gives you a decision table that maps a need to a block, and names the traps that catch people most. Read it once and the rest of the docs, the templates, the CLI, the agents, will make sense, because they all speak in these same terms.
The payoff of getting this right is real: content you model cleanly stays cheap to edit, reuse, query, and translate. Content modelled in the wrong block fights you on every one of those.
The shape of a site
A Quilium site is structured, not a pile of pages with free-text bodies. Three objects carry that structure, and they nest in a predictable way.
A page-type is the schema for a page: it declares the layout zones (its sections) and any route-aware queries the page needs. A page is one instance of a page-type, the same way a row is an instance of a table.
Into a page's zones you drop content blocks. A content block is an instance of a content-type, a reusable, typed block like a hero, a gallery, or a call to action. The content-type is the schema; the content block is what an editor actually places and fills on a given page.
Standing apart from both is the collection: a managed set of structured entries, each editable on its own, listable, searchable, and optionally addressable by its own URL. Articles, members, events, products. A page-type or a content-type consumes a collection through a query or a relation field; it never copies the data in.
Tell the three building blocks apart
These three are the load-bearing objects. Keeping them distinct is what prevents most modelling mistakes.
| Object | What it is | Lives | Reusable across pages | Has its own URL | Example |
|---|---|---|---|---|---|
page-type |
The schema for one page: its layout zones and route-aware queries. | One file per page-type. | A page-type is reused by every page built from it. |
A page does, via its route. | Home, Article detail |
content-type |
The schema for a reusable block dropped into a page zone. An instance is a content block. | One file per content-type. | Yes, the same content-type is placed on many pages. | No, it renders inside a page. | Hero, Gallery, Quote |
collection |
A managed set of structured entries, each independently editable. | Declared once under item-set-types. |
Yes, consumed by queries from many places. | Optionally, per entry, via a route. | Articles, Members, Events |
Two distinctions inside that table are worth stating plainly:
pageis notpage-type. Apage-typeis the template and schema; apageis an instance filled with content. Five "About", "Contact", and "Pricing" pages can all share onedefaultpage-type, because they share a structure, even though their content differs. Page-types describe layout, not purpose.- A content block is not a collection entry. A content block belongs to the single page it sits on. A collection entry belongs to the collection and can surface anywhere a query reaches for it. This difference drives the wrapper rule below, and it is the single most common source of errors.
custom: {}; a collection entry's data goes in data: {}. Never mix the two. Reading an entry as content.custom.<field> when it lives in data: {}, or writing a block under data:, is the error you will hit most often.The smaller pieces: how a block holds a list
The three objects above answer "where does this content live". A second, finer question comes up constantly inside a single content-type: how should this block hold a list, or a value, or a body of text? Quilium gives you a few field-level primitives, and choosing among them is where most day-to-day modelling decisions actually happen.
- A
wysiwygfield is one rich-text body. It handles bullet and numbered lists, links, headings, bold and italic, plus site-wide custom styles (buttons, callouts) through thewysiwyg.stylespalette. It is the right answer for any list of styled text, which is far more often than people expect. - A
repeatfield holds N items of one fixed shape, inline and bounded. Use it only when each item is a typed mini-record whose shape cannot be expressed in HTML an editor can type: icon plus title plus image, avatar plus name plus role. - A
choicefield lets an editor compose an open, ordered flow of typed blocks, where each block picks one of several declared variants, each with its own field shape. An article body of paragraph, image, quote, embed. - A
selectfield is one value picked from a fixed, closed list. A size, a theme, an alignment. - A
collection(consumed by a query or relation, not embedded) is a listable, queryable set of entries that lives outside the block.
The golden rule that resolves most of these at a glance: repeat is for non-standard HTML shapes; wysiwyg is for standard HTML. If a candidate repeat's fields would only contain text, wysiwyg, or link, it is a wysiwyg in disguise. Collapse it.
I need this → I use X
This is the table to keep. Read the left column as the need you actually have, the right column as the block to reach for.
| I need… | I use | Why, and not the others |
|---|---|---|
| A single body of rich text: a bullet list, a link list, a paragraph followed by a CTA button, "why us" prose with a checklist. | wysiwyg |
One field. The editor types <ul><li>…</li></ul> natively and styles the CTA with a site-wide palette class. A repeat here just turns prose into a stack of mini-forms and loses copy-paste, sub-bullets, and inline links. |
| A fixed, bounded set of same-shaped mini-records, always shown together, each carrying more than plain text: feature cards (icon + title + description), team avatars (image + name + role), gallery items (image + caption + link), pricing tiers. | repeat |
A typed shape HTML can't express, shown as a group, never browsed or filtered on its own. Not a wysiwyg (shape is non-standard), not a collection (not reused, not listable, bounded and inline). Fields live under config.fields. |
| Mutually-exclusive block variants the editor composes into an open flow: an article body that mixes paragraphs, images, quotes, and embeds, each with its own fields and template. | choice |
Several different shapes in one ordered sequence, picked per block. A repeat can't, it is one shape only. A select can't, it carries no nested fields. Variants live under config.choices, each with name and fields; templates switch on _choice. |
| One value from a closed list: a size, a theme, an opacity, an alignment, an icon name. | select |
A single enum value, no list, no nested fields. Not a choice (that's a flow of typed blocks, not a value picker). Options are a key: Label map under config.options. |
| A real, listable, queryable collection: items editors manage on their own, that appear in more than one place, may have their own URL, their own draft/published state, and grow without bound. Articles, members, events, products. | collection |
The only block that is reusable across pages, searchable, route-addressable, and stateful per entry. A repeat traps the data inside one content block where nothing else can reach it; a wysiwyg can't be queried at all. Consumed via a query or relation, never copied in. |
The two boundaries people cross most are the last two rows of this table, in both directions. The next section names them directly.
The common pitfalls
Wrapping a collection entry like a content block
The wrapper rule again, because it is the error you will meet most. A content block's fields are read and written under custom: {}; a collection entry's under data: {}. Mixing them, reading content.custom.title when the value lives in an entry's data, or writing an entry under custom:, fails quietly or returns nothing. Decide which object you are touching first, then use its wrapper.
Choosing a collection when a repeat suffices
A static set of three social links, or four pricing tiers shown only on one page, is not a collection. Modelled as a collection it bloats the backoffice with a near-empty managed list that no one browses. If the items are bounded, always shown together, and never reused or searched, a repeat inside the content-type (or a small select/global field for site-wide singletons) is the lighter, correct choice.
Choosing a repeat when you actually need a collection
The mirror mistake, and the more painful one to undo. You model team members as a repeat on the About page. Months later: "can we show the team on the home page too?" You can't, the data is locked inside one content block. Or the list grows past a dozen articles and the inline form becomes unmanageable. The test is a single question: might the editor ever want to reuse, search, or individually publish these? If yes, it is a collection, not a repeat. Migrating later means moving every record by hand.
Forking a content-type because a style is missing
When a visual variant (a button, a styled list, a callout) isn't in the editor's palette, the temptation is to model it as a typed repeat or a new content-type. Don't. Add the variant once to the site-wide wysiwyg.styles palette and every editor on the site gains it, with no new schema. A missing style is a palette gap, not a modelling problem.
wysiwyg field and you can stop. Only if the answer is no do the other blocks come into play.How the pieces relate at render time
Modelling decisions show up again when a template renders. A page-type declares its queries; the engine resolves them (route-aware) and lands the results on page.custom.<key> as arrays. Content blocks expose their own fields on content.custom.<field> inside the block's element template. A collection never renders itself, it is pulled in by a query or relation and rendered through whichever template consumes it. The wrapper rule you follow when writing data, custom for blocks, data for entries, is the same boundary you read across in templates. See Templating philosophy for how that reading works, and How Quilium renders the site for the full request-to-HTML path.
Where this fits
- Architecture, the system around these objects, the CMS, the CLI, the MCP server.
- Routing and URLs, how a collection entry earns its own page and URL.
- The multilingual model, how fields and entries carry their translations.
- Templating philosophy, why templates only ever read the model, never reshape it.
Where to go next
Last updated September 6, 2026.