Pages
A page is a document bound to a component marked page: true, whose field roles tell the dashboard how to preview it and whose block list holds its sections.
Why a page is an ordinary document
Content managers think in pages, and a page has needs of its own: search-engine fields, a stack of sections chosen page by page, and a way to see the result before it goes live. A page is also content. It needs everything content gets: versions, publishing, translations, references and types. Quillet therefore keeps a page an ordinary document rather than a second kind of record, so all of that applies to it with nothing extra to learn, in the dashboard, the API, the CLI and delivery alike.
The one new thing is a flag on the component. It tells the dashboard to list these documents as pages, to offer New › Page, and to draw a Content preview from what each field is for.
What the page flag, roles and blocks add
x-quillet.page: true goes on a component's root, and only there. Below the root it is refused with "page is a setting of the component's root." The flag changes no validation of any body and appears in no generated type. The library shows such a component as name · makes pages, GET …/schemas reports "page":true, and every document bound to it is a page.
A role is x-quillet.role on a field. It says what the field is for on the finished page, as distinct from the widget that edits it. The preview is the only thing that reads a role: it constrains no data and generates no type. A role that a field cannot carry is refused on save at that field's path, with a message such as "A media field must be an image, video or file field."
| Role | Legal on | Drawn in the preview as |
|---|---|---|
heading |
any text field, Rich text and Markdown included | a heading |
eyebrow |
any text field | small capitals above the heading |
body |
any text field | prose |
media |
an image, video or file field | the picture, a poster, or a file chip |
action |
a link, or a text field with format: uri |
a button |
meta |
any field | small muted text |
hidden |
any field | nothing |
seo |
a group | a search result and a social card |
A field with no role is drawn by inference. The first short plain text field of an object is the heading. Later ones, and every Long text, Markdown or Rich text field, are body; a file field is media, a link or URL is an action, and numbers, booleans, dates and choices are meta. Write a role only where that guess is wrong. hidden is the one role that is never guessed. Several headings in one object are legal, and they are drawn in order.
SEO is a group with the role seo. Nothing enforces its fields, so a project adds or drops them as it likes. New › Page in the Components tab seeds the usual five:
| Field | Label | Type |
|---|---|---|
title |
Title | Text |
description |
Description | Long text |
ogImage |
Social image | Image |
canonical |
Canonical URL | URL |
noIndex |
Hide from search engines | Boolean |
The preview draws the group as a search result and an Open Graph card. It warns past about 60 characters of title and about 155 of description, and it greys the result when noIndex is on, but it never refuses a save. The address it shows is canonical, or example.com/<key> when there is none.
A block list is an array whose items is a oneOf of bare component names, and the builder allows up to 20 components in it. Each block holds the chosen component's fields inline, marked with _component:
"blocks": [
{ "_component": "hero", "headline": "Who we are" },
{ "_component": "cta", "label": "Write to us", "link": { "href": "mailto:hello@example.com" } }
]
Where a list allows more than one component, every block must name its own. A block without _component is refused with "Say which component this block is." A block is not a document: it has no key, it is not counted as an instance of its component, and it is translated whole.
This site is built this way. Its page component has a title with the role heading, an seo group, and blocks, a block list of seven components. The home page is the document pages/home, and the site draws each block by its _component.
How to
Dashboard
- In Components, press New › Page. The builder opens on a page component that already holds the SEO group.
- Name it, such as
landing-page. Press Add field, choose the type Block list, and pick its Allowed components ("Up to 20. A block is one of these, and says which in_component."). Add a Text field for the title if you want one. - Press Save, then Publish. Until the component has a block list and an SEO group, a notice above the form says which one is missing.
- In Content, press New › Page. Choose the Component, a Folder and a Key (the key is the page's address), give the page a Title and press Create instance.
- The page opens as a form. Under the block list, Add block offers the allowed components.
- Open Preview › Content preview to see the page drawn beside the form and redrawn as you type. Press a block in the preview to jump to it in the form. Once the environment has a Website preview URL, Preview › On website opens the draft on your own site instead.
- Press Save, then Publish.
API
This example uses the hero and cta components from Components and the builder, published. Set the variables once:
export QUILLET_API_URL=<cms-url> QUILLET_PROJECT=<project> QUILLET_ENV=<environment>
export QUILLET_MANAGEMENT_KEY=<keyId>:<secret>
E="$QUILLET_API_URL/api/v1/projects/$QUILLET_PROJECT/envs/$QUILLET_ENV"Write the page component: a heading, an SEO group, and a block list of the two components.
curl -s -X PUT "$E/documents/_schemas/landing-page" -u "$QUILLET_MANAGEMENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"type":"json","title":"Landing page","content":{
"type":"object","title":"Landing page","required":["title","blocks"],"additionalProperties":false,
"properties":{
"title":{"type":"string","maxLength":80,"x-quillet":{"role":"heading"}},
"seo":{"type":"object","additionalProperties":false,
"x-quillet":{"role":"seo","order":["title","description","noIndex"]},
"properties":{
"title":{"type":"string","maxLength":70},
"description":{"type":"string","maxLength":160,"x-quillet":{"widget":"textarea"}},
"noIndex":{"type":"boolean"}}},
"blocks":{"type":"array","maxItems":20,"items":{"oneOf":[{"$ref":"hero"},{"$ref":"cta"}]}}},
"x-quillet":{"page":true,"preview":"title","order":["title","seo","blocks"]}}}'A block that does not say what it is gets refused:
curl -s -X PUT "$E/documents/pages/about" -u "$QUILLET_MANAGEMENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"type":"json","schema":"landing-page","title":"About",
"content":{"title":"About us","blocks":[{"headline":"Who we are"}]}}'{"error":"validation_failed","message":"Could not save the document.",
"fields":{"content.blocks.0":"Say which component this block is.",…}}With _component on each block, the page saves. Publish it together with its component:
curl -s -X PUT "$E/documents/pages/about" -u "$QUILLET_MANAGEMENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"type":"json","schema":"landing-page","title":"About","content":{"title":"About us",
"seo":{"title":"About us","description":"Who we are and what we make."},
"blocks":[{"_component":"hero","headline":"Who we are"},
{"_component":"cta","label":"Write to us","link":{"href":"mailto:hello@example.com"}}]}}'
curl -s -X POST "$E/publish" -u "$QUILLET_MANAGEMENT_KEY" -H 'Content-Type: application/json' \
-d '{"documents":[{"key":"_schemas/landing-page"},{"key":"pages/about"}]}'A page published before its component is refused with Publish the schema "landing-page" first.; one request that carries both is one decision, and it goes through. Delivery serves pages/about exactly as it serves any document, with each block carrying its _component.
CLI
With the same variables set, a page is a body file plus a component name:
cat > about.json <<'EOF'
{
"title": "About us",
"seo": { "title": "About us", "description": "Who we are and what we make." },
"blocks": [
{ "_component": "hero", "headline": "Who we are" },
{ "_component": "cta", "label": "Write to us", "link": { "href": "mailto:hello@example.com" } }
]
}
EOF
npx quillet content put pages/about --file about.json --schema landing-page --title "About"
npx quillet publish pages/about --yesIn a synced repository, the body is pages/about.en-US.json, and the binding lives in its sidecar, pages/about.en-US.meta.json: { "title": "About", "schema": "landing-page" }.
Limits
- Nothing refuses a page component without SEO or without blocks. The builder shows a notice; a page of pure SEO, or one whose SEO a framework supplies, is legitimate.
- The SEO length counters warn and never refuse. A field's own
maxLengthis the only hard limit. - There is no page tree and no URL routing. A page's address is its key, and your site decides which URL serves which key.
- Content preview draws the page from its roles in a neutral style, inside a frame that runs no scripts. It is not your site's design; On website is.
- On website opens one document in one locale, with a token that lasts fifteen minutes and cannot be revoked on its own. With the delivery key, the same token lists every draft in the environment, as keys, titles and tags without bodies. It shows the current draft only, against the published component.
- The builder has no control for a role. Write it in the JSON tab, and the builder keeps it when it saves.