---
title: Variables and context
description: Every object Quilium injects into a Liquid template at render time, what each one holds, and how to read it.
updated: 2026-06-22
prev: page-types
next: liquid-filters
---

Quilium renders [Liquid](/en/docs/templating-philosophy) server-side. At render time the engine injects a fixed set of context objects. A template reads them; it never fetches data itself. Dynamic data comes from [queries](/en/docs/content-model) declared on the page-type (route-aware, exposed as `page.custom.<key>`) and from [content-block fields](/en/docs/custom-fields) (`content.custom.<field>`).

## Context objects

All field, section, and query keys are **camelCase**. Query results are **always arrays**, even at `limit: 1`; guard with `and <expr>.size > 0` before indexing.

| Object | Access | Holds |
|---|---|---|
| `page` | `page.contents['<section>']`, `page.custom.<queryKey>`, `page.metas.{title,description,robots}`, `page.title`, `page.id` | The current page. `page.contents['main']` is the array of blocks dropped in the `main` section. |
| `page.custom` | `page.custom.<queryKey>` (array) | Route-aware [query](/en/docs/content-model) results declared on the page-type. Each key is one `query` field. Always an array, so guard before `[0]`. |
| `content` | `content.custom.<field>`, `content.custom.<query>`, `content.id`, `content.template`, `content.form.<field>.errors` | Inside an element template: the current content block. `content.id` is the block id (form target value); `content.template` is its element path. |
| `content.custom` | `content.custom.<field>` | The block's own fields. Idiom: `{% assign c = content.custom %}` then `c.title`. A content-type may carry its own query, read at `content.custom.<query>`. |
| `content.form` | `content.form.<field>.errors` | Per-field validation errors after a POST, as an object `{rule: message}`. Empty before submission. |
| `globals` | `globals.<fieldKey>` | [Global fields](/en/docs/site-settings), **flat**: the tabset key is dropped from the path. Schema `tabsets.infos.fields.copyright` reads as `globals.copyright`, never `globals.infos.copyright`. Field keys must be unique across all tabsets. |
| `request` | `request.body.<field>`, `request.query`, `request.path`, `request.cookies` | The incoming request. `request.body.<field>` holds submitted form values, used to prefill on re-render. |
| `request.body` | `request.body.<field>` | Posted form values, keyed by field name. |
| `route` | `route.<param>` | **QUERY-ONLY.** Valid **only** inside a page-type `query` config (e.g. `where: { '&=': { slug: '{{route.article.slug}}' } }`). It does **not** exist in a template. Templates read the resolved result at `page.custom.<key>`. See [Routing and URLs](/en/docs/routing-and-urls). |
| `query` | `query.page` | **QUERY-ONLY.** The URL query string inside a query config. `{{query.page}}` substitutes (pagination); `{{query.limit}}` does **not** substitute. |
| `now` | `now`, `now.year`, `now.month`, `now.day`, `now.hour`, `now.datetime`, `now.timestamp`, `now.timezone` | Server time. Format with `{{ now | date: 'YYYY' }}`. Sub-properties expose individual parts. |
| `locale` | `locale.slug` | The current locale. Use for `<html lang="{{ locale.slug }}">`. See the [multilingual model](/en/docs/multilingual-model). |
| `locales` | `loc.slug`, `loc.url` (per item) | Every locale. `loc.url` is **this** page in that locale. Iterate for a language switcher. |
| `sitemap` | `sitemap.<nav>.children` → `item.url`, `item.navTitle`, `item.active` | Navigation trees. `item.url` is already locale-aware; do not re-prefix it. |

<div class="q-callout q-callout-warn"><i class="fa-solid fa-triangle-exclamation lead"></i>
  <div><b>route and query are query-only.</b> Both exist solely inside a page-type's <code>query</code> configuration. A template that reads <code>route.*</code> or <code>query.*</code> directly gets nothing. The page-type resolves <code>{{route.article.slug}}</code> into <code>page.custom.article</code>; the template reads <code>page.custom.article[0]</code>.</div>
</div>

## Reading a query result

Queries are arrays. Always test `size` before indexing.

```liquid
{% assign source = page.custom.latest %}
{% if source and source.size > 0 %}
  {% for a in source %}{% include '_articleCard', art: a %}{% endfor %}
{% else %}
  <p>{{ 'Aucun article pour le moment.' | _ }}</p>
{% endif %}
```

A current-item join (limit 1) reads index `[0]` behind the same guard:

```liquid
{% if page.custom.author and page.custom.author.size > 0 %}
  {% assign au = page.custom.author[0] %}
  <h2>{{ au.nom }}</h2>
{% endif %}
```

## Function-form helpers are inert in Liquid

The render payload also carries function-form helpers, `url`, `redirect`, `responseCode`, `setCookie`, and `_`. They are leftovers from the engine that preceded Liquid and are **inert in Liquid**. In a Liquid template you use the **filter** forms instead (`'article' | url: …`, `'msgid' | _`). See [Liquid filters](/en/docs/liquid-filters) for every filter signature.

<div class="q-callout q-callout-note"><i class="fa-solid fa-circle-info lead"></i>
  <div><b>Globals are flat.</b> The tabset key never appears in the path. A field <code>copyright</code> under tabset <code>infos</code> is <code>globals.copyright</code>. Because of this, every global field key must be unique across all tabsets, or one shadows the other.</div>
</div>
