Content model
Every piece of content in Quillet is a document with a key, a type, a body and five metadata fields, and folders hand their settings down through a document called _index.
Why one record holds content and its settings
A content model has to say what a piece of content is called, what shape its body has, and which rules apply to a whole group of pieces at once. Quillet answers all of that with one record. A document's key is its address, and the key implies the folder it sits in. Its type says whether the body is structured JSON, Markdown or plain text. Its metadata carries what a listing needs, so a listing never has to read the body.
A folder's settings are themselves a document, _index, so they are versioned, published, pulled into a repository and pushed back exactly like the content they describe. There is no second store of configuration to keep in step. When something moves, no tree of records needs rebuilding.
What a document, its metadata and a folder carry
A document is a key, with one body per locale. The key is a path such as guides/internal/onboarding, made of lowercase letters, numbers, dots, dashes and underscores, in which each segment starts with a letter or a number; the whole key is at most 512 characters and 16 folders deep. The folders guides/ and guides/internal/ exist because the key says so; nothing stores them separately. Documents, keys and folders gives the reason for that design.
A document's type is chosen when the document is created, and it never changes:
| Type | In the dashboard | Body | Can have a component |
|---|---|---|---|
json |
Structured | a JSON object | yes |
md |
Markdown | a GitHub-Flavored Markdown string | no |
text |
Plain text | a string | no |
A save that names another type is refused with "This document is json. Delete it and create a new one to change its type."
The metadata sits beside the body, not inside it. A listing can show it without reading the body, and a Markdown file stays plain Markdown:
| Field | What it holds | Rules |
|---|---|---|
title |
what screens and listings call the document | up to 200 characters, or null |
order |
its place among its siblings, lower first | a whole number, 0 by default |
tags |
labels a delivery listing can filter by | up to 32, each up to 64 characters of lowercase letters, numbers and hyphens; repeats are dropped |
schema |
the component the body must fit | a component's name, json documents only; null goes back to the folder's default |
searchable |
whether delivery search may return it | true, false, or null to follow the folder |
A save can also carry a message of up to 500 characters, which is stored with the version rather than on the document.
Folder settings live in <folder>/_index, a json document whose body may hold any of these keys:
| Key | What it decides |
|---|---|
title, description, order |
how the folder itself is named and sorted in listings |
defaultType |
the type the dashboard's New document offers first in this folder |
defaultSchema |
the component every json document below is bound to, unless it names its own |
searchable |
whether documents below are searchable, unless they say otherwise |
syncSource |
who may change the folder: cli, dashboard or any (the default) |
A document inherits each setting field by field, from the deepest _index above it that sets that field. In the example below, guides/_index sets defaultType and guides/internal/_index sets searchable: false, so guides/internal/ has both. A key the CMS does not recognise, such as a note to the next editor, stays in the body and is ignored as a setting, while a recognised key with the wrong kind of value is treated as if it were absent.
syncSource is enforced on every save, delete and publish. A folder marked dashboard refuses an API key with 403 "Folder cli-only/ accepts changes only from the dashboard.", and a folder marked cli refuses changes made as a person. A project owner, acting as a person rather than through a key, can still change the _index itself, so a mistyped policy is never permanent.
Two folders are reserved. _schemas/ holds components, and _singletons/ holds the one instance of each singleton component. Delivery serves _singletons/ and never serves _schemas/.
How to
Dashboard
- In the Content tab, press New and choose Document.
- Fill in Key (for example
guides/internal/onboarding), choose a Type (Structured, Markdown or Plain text) and pick the Locale. Press Create document, and the editor opens on the new document. - The column beside the editor holds the metadata in three cards. Details has Title, Order ("Lower comes first, within its folder.") and Tags, comma-separated. Publishing has Searchable (inherit from the folder, On or Off) and, for a structured document, Schema. Change note has Message, for the next save.
- To set a folder's settings, create a Structured document whose key ends in
/_index, such asguides/_index, and write the settings as its JSON body. Above the text, the editor says thatdefaultSchemais a key in this body; an_indexhas no Schema select. - Press Save, then Publish.
defaultSchemaandsyncSourceapply from the moment the_indexis saved. The folder's title, description, order andsearchablereach delivery once it is published.
API
Once per terminal, set the CMS address, the project, the environment and a CLI key with content:write and content:publish:
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 two folder settings, then a document under both. A request with no ?locale= addresses the project's default locale.
curl -s -X PUT "$E/documents/guides/_index" -u "$QUILLET_MANAGEMENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"type":"json","content":{"title":"Guides","description":"How-to guides.","order":2,"defaultType":"md"}}'
curl -s -X PUT "$E/documents/guides/internal/_index" -u "$QUILLET_MANAGEMENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"type":"json","content":{"title":"Internal","searchable":false}}'
curl -s -X PUT "$E/documents/guides/internal/onboarding" -u "$QUILLET_MANAGEMENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"type":"md","title":"Onboarding","order":1,"tags":["staff","how-to"],"message":"First draft",
"content":"# Onboarding\n\nWhere things are, and who to ask."}'The last answer shows the metadata and what the document inherited:
{"key":"guides/internal/onboarding","locale":"en-US","type":"md","title":"Onboarding","order":1,
"tags":["staff","how-to"],"searchable":false,"searchableSource":"folder","schema":null,…,"version":1,…}"searchableSource":"folder" says that the false came from guides/internal/_index, not from the document. Publish all three. A delivery listing of guides/ then carries the folder's title, description and order beside its items.
curl -s -X POST "$E/publish" -u "$QUILLET_MANAGEMENT_KEY" -H 'Content-Type: application/json' \
-d '{"documents":[{"key":"guides/_index"},{"key":"guides/internal/_index"},{"key":"guides/internal/onboarding"}]}'CLI
The CLI reads the same variables, or a .quilletrc:
export QUILLET_API_URL=<cms-url> QUILLET_PROJECT=<project> QUILLET_ENV=<environment>
export QUILLET_MANAGEMENT_KEY=<keyId>:<secret>In a synced repository, the body and the metadata are two files: guides/internal/onboarding.en-US.md holds the Markdown, and guides/internal/onboarding.en-US.meta.json beside it holds title, order, tags, schema and searchable. You can also write one document on its own:
printf '# Onboarding\n\nWhere things are, and who to ask.\n' > onboarding.md
npx quillet content put guides/internal/onboarding --file onboarding.md --title "Onboarding" --tags staff,how-to --message "First draft"
npx quillet content get guides/internal/onboarding --meta
npx quillet content list guides/ --include-indexcontent put takes the type from the file's extension. content list hides folder settings the way the dashboard's tree does, and --include-index lists them too.
Limits
- A document's type cannot change. To change it, delete the document and create it again under the new type.
- Only
jsondocuments can have a component; aschemaon Markdown or text is refused with "Only json documents can have a schema." - The five metadata fields are the whole list. Anything else a document needs to say belongs in its body, and for a structured document that means a field in its component.
- A key is lowercase only and cannot be renamed in place: a rename in the dashboard is a copy and a delete, and it is refused while other documents use the old key.
defaultTypeis only a suggestion to the dashboard. Every API and CLI write still states its type, and a write without one is refused.- An
_indexthat binds documents to a component cannot drop itsdefaultSchemaor be deleted while those documents exist; the API answers 409 "This folder binds documents to a component." with the documents it means. - The content index has no view of documents whose body no longer fits their component; a component's Instances tab shows them one component at a time. Components and the builder says what is checked and when.