Documentation Menu

Inline and referenced components

8 min read

A component reaches a page in one of two ways: inline, as a block whose fields are copied into the page, or by reference, as a pointer to a separate document that delivery puts in place when the page is read.

Why a section is either copied or shared

Some sections belong to one page, such as a hero written for this page alone. Others appear in many places and must change everywhere at once: a sale banner, an author card, a footer. Copy the shared kind, and you edit every page that holds a copy. Reference the single kind, and you end up with a document per section that nothing else uses. Quillet offers both, and the body shows which one you chose: a block carries the component's fields, while a reference carries only a key.

Sharing has consequences, and Quillet guards them. A referenced document cannot be deleted from under the pages that use it, and publishing it tells you which live pages change with it.

How a reference is checked, resolved and guarded

Inline and referenced sections differ in where they live, how they are translated and when a change reaches readers:

Inline Referenced
In the component a Component field (a bare $ref to a component), or a Block list a Reference field: x-quillet.ref naming a component, or "*" for any structured document
In the body the component's fields, with _component in a block list { "$ref": "<key>" } and nothing else
Where it lives inside the page in its own document, with its own key, versions and publishing
A change reaches readers when the page is published when the referenced document is published, on every page that points at it
Translated with the page; a block is translated whole in its own locales, resolved into the reader's locale

A reference is checked where the component marks the slot. On save, the target must exist, be a structured document and be bound to the named component; otherwise the save is refused with "pages/about" is not a "hero" document. or No document at "components/heroes/nope". On publish, the target must be published too: Publish "components/heroes/spring-sale" first. A $ref may not point at an _index, at a component definition or at the document itself. A { "$ref" } in a slot the component does not mark, or in a document with no component, is still recorded and resolved. It is just not checked.

Delivery resolves references. It replaces each { "$ref" } with the published body of the document it names, in the same locale and under the same fallback rules, to a depth of 3 by default. ?refs= asks for 0 to 5, and refs=0 leaves every reference as written. _meta.refs lists what was reached, with the path in the page, the key, the locale that answered and the version. A target that is missing or not published becomes null, and its entry says "missing": true. One answer resolves at most 200 references. Past that, the rest stay as written and _meta.refsTruncated is true.

The page's ETag is taken over the resolved body. Republishing the referenced document therefore changes the page's ETag and body, even though nobody saved or published the page. _meta.version stays the page's own; the referenced document's version is in _meta.refs.

A Link field is not a reference. A link's { "ref": "<key>" } names a page to link to, so delivery leaves it alone, and it does not appear in _meta.refs.

You can ask what uses a document with GET …/references/<key>, in the editor's Used by panel, or with quillet content refs. While anything uses it, delete and rename are refused with 409 in_use and the list of documents. Before a publish, the dashboard's Publish dialog, GET …/publish-impact/<key> and quillet publish --impact list the live documents the publish changes: those that reference it, and those that reference them.

How to

Dashboard

  1. In the builder, add a field of type Reference and choose its Target: a component, or Any component. For an inline section, use Component or Block list instead.
  2. In a page bound to that component, the Reference field is a picker: a searchable list of the documents bound to the target, title over key, with Clear beside it. A block list has Add block under it.
  3. In the JSON view of any structured document, Insert component… offers both kinds, as New block (the component's fields at the cursor, with _component) and as Reference (a pointer to an existing document).
  4. Open the shared document, such as the hero instance. Its Used by panel lists every document that points at it, and the field each one points from. While the list is not empty, rename and delete are unavailable, and the button gives the reason.
  5. Edit it, press Save, then Publish. Before you confirm, the dialog lists the live pages that change with it.

API

This example uses the hero component and its instance components/heroes/spring-sale from Components and the builder, published. Set the variables, with a delivery key for the reads:

export QUILLET_API_URL=<cms-url> QUILLET_PROJECT=<project> QUILLET_ENV=<environment>
export QUILLET_MANAGEMENT_KEY=<keyId>:<secret>
export QUILLET_KEY_ID=<delivery-keyId> QUILLET_KEY_SECRET=<delivery-secret>
E="$QUILLET_API_URL/api/v1/projects/$QUILLET_PROJECT/envs/$QUILLET_ENV"
D="$QUILLET_API_URL/d/v1/$QUILLET_PROJECT/$QUILLET_ENV"

Write a page component with a reference slot, and two pages that point at the same hero:

curl -s -X PUT "$E/documents/_schemas/campaign" -u "$QUILLET_MANAGEMENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"type":"json","title":"Campaign","content":{
       "type":"object","title":"Campaign","required":["title"],"additionalProperties":false,
       "properties":{
         "title":{"type":"string"},
         "hero":{"type":"object","x-quillet":{"ref":"hero","label":"Hero"}}},
       "x-quillet":{"page":true,"preview":"title","order":["title","hero"]}}}'

curl -s -X PUT "$E/documents/pages/spring" -u "$QUILLET_MANAGEMENT_KEY" -H 'Content-Type: application/json' \
  -d '{"type":"json","schema":"campaign","title":"Spring","content":{"title":"Spring at the shop","hero":{"$ref":"components/heroes/spring-sale"}}}'

curl -s -X PUT "$E/documents/pages/autumn" -u "$QUILLET_MANAGEMENT_KEY" -H 'Content-Type: application/json' \
  -d '{"type":"json","schema":"campaign","title":"Autumn","content":{"title":"Autumn at the shop","hero":{"$ref":"components/heroes/spring-sale"}}}'

curl -s -X POST "$E/publish" -u "$QUILLET_MANAGEMENT_KEY" -H 'Content-Type: application/json' \
  -d '{"documents":[{"key":"_schemas/campaign"},{"key":"pages/spring"},{"key":"pages/autumn"}]}'

Read one page with the reference resolved, and once more as written:

curl -s -u "$QUILLET_KEY_ID:$QUILLET_KEY_SECRET" "$D/content/pages/spring"
curl -s -u "$QUILLET_KEY_ID:$QUILLET_KEY_SECRET" "$D/content/pages/spring?refs=0"
{"key":"pages/spring",…,"content":{"title":"Spring at the shop","hero":{"headline":"Spring sale",…}},
 "_meta":{…,"version":1,…,"refs":[{"path":"hero","key":"components/heroes/spring-sale","resolvedLocale":"en-US","version":1}],"assets":[]}}

Ask what uses the hero and what publishing it would change, then try to delete it:

curl -s "$E/references/components/heroes/spring-sale" -u "$QUILLET_MANAGEMENT_KEY"
curl -s "$E/publish-impact/components/heroes/spring-sale" -u "$QUILLET_MANAGEMENT_KEY"
curl -s -X DELETE "$E/documents/components/heroes/spring-sale" -u "$QUILLET_MANAGEMENT_KEY"
{"error":"in_use","message":"This document is referenced by other documents; remove those references first.",
 "usedBy":[{"key":"pages/autumn","locale":"en-US"},{"key":"pages/spring","locale":"en-US"}]}

CLI

With QUILLET_API_URL, QUILLET_PROJECT, QUILLET_ENV and QUILLET_MANAGEMENT_KEY set as in the API tab, run:

npx quillet content refs components/heroes/spring-sale
npx quillet publish components/heroes/spring-sale --impact
KEY           LOCALE  PATH
pages/autumn  en-US   hero
pages/spring  en-US   hero
pages/autumn (en-US): inlines components/heroes/spring-sale
pages/spring (en-US): inlines components/heroes/spring-sale

--impact prints what the publish would change and publishes nothing. Without it, publish prints the same lines and, at a terminal, asks before it publishes; --yes skips the question.

SDK

With the delivery key and the other settings in the environment, as in the quick start, save this as refs.mjs and run node refs.mjs:

// refs.mjs: read a page with its reference resolved, then as written.
import { quillet } from '@quilletcms/sdk/node'

const q = quillet()

const page = await q.get('pages/spring')
console.log(page.content.hero.headline)
for (const ref of page._meta.refs) {
  console.log(ref.path, ref.key, ref.missing ? 'missing' : `v${ref.version}`)
}

const asWritten = await q.get('pages/spring', { refs: 0 })
console.log(asWritten.content.hero)
Spring sale
hero components/heroes/spring-sale v1
{ '$ref': 'components/heroes/spring-sale' }

refs can also be set once for the client, in its configuration. The generated Ref<T> type describes the field both ways; see Generated types.

Limits

  • The dashboard's insertion dialog does not yet explain the consequences of a copy versus a reference. Choose with the table above: a block belongs to the page, and a reference is shared.
  • References stay inside one environment of one project. Nothing can reference another environment or another project.
  • Resolution goes at most 5 levels deep, and it resolves at most 200 references per answer.
  • Publishing a referenced document clears the cached answers of the documents that reference it and of those that reference them: two hops, and no further. A longer chain shows the change once its cache entry expires.
  • The guard against deleting a used document reads each document's current references. Two changes that land at the same moment, one adding a reference and one deleting its target, can each pass. Delivery then answers null, and _meta.refs says missing.
  • The publish impact list leaves out a document that only links to the published key.

Last updated