Troubleshooting
The failures that waste an afternoon, what actually causes them, and the check that tells them apart.
Most Quilium problems are one of a dozen, and almost none of them say what they are. This page maps the symptom to the cause.
The page returns 404
| Check | If it is that |
|---|---|
Is the page state: inactive? |
An inactive page 404s everywhere, locally included. "Draft" never means inactive. Set it back to active. |
Does the route exist, and does its path start with a slash? |
A path without it glues to the page URL and matches nothing: {{article.slug}} on a page at /article gives /article<slug>. |
| Is it a detail page? | Its page must exist, be hidden rather than deleted, and use the detail page-type. |
| Is it the 404 template itself failing? | miscellaneous.rendering.404 must point at a template that exists, extension included. |
The page renders empty, with no error
The template ran and produced nothing, which almost always means the data was not where you looked.
- A collection item's fields are under
data, a content block's undercustom. Reading the wrong one returns blank, not an error. - A query's results live on the object that declares it:
page.custom.<queryKey>for a page-type query,content.custom.<queryKey>for a block query. route.*only exists inside a query config. In a template it silently resolves to nothing.
When it stays mysterious, probe instead of theorising:
<!--DBG articles=[{{ articles.size }}] first=[{{ articles[0].data.title }}]-->
Then curl <url> | grep DBG and delete the line. Some filters return empty without erroring, and guessing which one is slower than looking.
A write returns 422
| Cause | Fix |
|---|---|
| Wrong wrapper | update-content writes under custom:, update-itemset-item under data:. |
| A reference sent as a string | Images and files are arrays of objects, [{id: '<uuid>'}]; items and page references are flat arrays of ids, ['<uuid>']. Never a bare string. |
| A translation-only payload | Send the main data plus one locale per call, never a translation alone. |
UniqueUrl on publish |
Two pages resolve to the same URL, typically a detail child with an empty slug. Give it a real slug, or uncheck the parent prefix on the child. |
A settings write silently does nothing
update-settings-partial requires target to be the top-level key of the YAML you send. A mismatch fails without a warning. And a replace sends the whole block: read it, merge, write it back entire, or you delete the fields you left out.
Translations do not show up
- The interface string must be a literal:
{{ 'Read more' | _ }}is extracted,{{ label | _ }}is not. - Extraction is a step, not magic. Run
quilium dictionary parse(orquilium push, which runs it), then fill the translations. - A field that is not
translatable: truein its type will never carry a second locale.
The live site does not match my machine
Push moves templates, publishing moves content. Doing one and expecting the other is the single most common surprise.
quilium push
Then publish the pages. See Publish and go live.
The CLI refuses to work
| Symptom | Cause and fix |
|---|---|
quilium run says local files are missing |
You are in a folder with no theme. Run quilium pull. |
pull refuses to run |
public/ is not empty. Back it up, or use --force, which replaces everything. |
push refuses |
public/ is empty. Pushing an empty theme would blank the site. |
| A 403 mid-command | Wrong account or expired session. quilium whoami answers it in one call. |
| Endpoints behave oddly | Check quilium.json for a local override that travelled up on a push. |
A block renders as a 500, or not at all
| Symptom | Cause and fix |
|---|---|
ENOENT: Failed to lookup "elements/<key>.html" |
The content-type has no template: key, so the engine looked for <key>.html. Only .liquid loads. Add template: 'elements/<key>.liquid'. |
The template: key is there and the error still names .html |
A content block freezes its template when it is created, exactly as a page freezes its page-type. Fixing the YAML does not reach blocks that already exist — delete the block and add it again. |
| A block silently loses its data halfway through a loop | An {% include %} shares the caller's scope. A partial that does {% assign c = … %} overwrites the caller's c — and almost every element file opens on {% assign c = content.custom %}. Name partial-local variables after the partial. See Reuse blocks. |
| An external feed's block is empty, page returns 200 | A failed api fetch yields an empty object, silently. See Consume an external feed. |
The agent did something unexpected
- It deactivated pages because it was told to keep them "in draft". Say "leave them unpublished", never "deactivate".
- It filled a collection and the entries went live immediately. Entries have no draft state; only pages do. See Publish and go live.
- It invented a structure because it never read the site. Make
get-skillsthenget-site-settingsthe first two calls of the session, see Prompting well for Quilium. - It reported "done" without checking. Ask for the verification output, not the summary.
Related
Last updated September 7, 2026.