---
title: Page-type schema
description: The YAML keys that define a page-type, the template and content zones for a single page, including the query-plus-route pattern for detail pages.
updated: 2026-06-22
prev: content-types
next: itemset-types
---

A page-type defines the template and the named content zones for a single page. Editors drop [content blocks](/en/docs/content-types) into its `sections`; page-level fields live in `tabsets`. A page is an instance of a page-type.

## Top-level keys

| Key | Type | Notes |
|---|---|---|
| `name` | string | Backoffice label. |
| `template` | string | Full template path. Live: `'layouts/accueil.liquid'`. |
| `icon` | string | Font Awesome Pro id (`home`, `folder`, `file`). |
| `group` | string | Backoffice grouping. |
| `headers` | object | HTTP response-header override. Setting `content-type: application/json` is what makes a page answer with something other than HTML — the template then writes the body with no `{% layout %}`. Live on Le Cinquième; see [Events agenda](/en/docs/events-agenda). |
| `sections` | object | Named drop zones for content blocks. |
| `fieldsets` | object | Visual field grouping. `fieldsets.<k>.{name, width}` with `width` up to 100. |
| `tabsets` | object | Editing tabs holding page-level fields. `tabsets.<k>.{name, fields}`. |

## Sections and section kinds

`sections` declares the named drop zones a page exposes. A bare key is a single drop zone; a `type` changes its behaviour.

| `type` | Behaviour |
|---|---|
| `section` (default, bare key) | single drop zone. |
| `rows` | vertical stack; editors add rows. |
| `cols` | horizontal columns; layout fixed in YAML via nested `sections`. |
| `shared: true` | content identical across every page site-wide (footer, top-bar). Use sparingly. |

Templates read a section's blocks as `page.contents['<section>']`. See [Template variables](/en/docs/template-variables).

<div class="q-callout q-callout-note"><i class="fa-solid fa-circle-info lead"></i>
  <div><b>Sections are not type-gated.</b> There is no <code>allowedContents</code> key. Any section accepts any content-type; the usable content-type list is global to the site.</div></div>

## Page-level fields

`tabsets` holds fields scoped to the page itself rather than to a content block, same shape as a content-type tabset (`tabsets.<key>.{name, fields}`). `fieldsets` groups fields visually within a tab. The detail-page `query` fields below are page-level fields declared in a tabset.

## YAML anchors

Page-types use `_`-prefixed anchors to share repeated structure (`_footer: &footer`, then `footer: *footer`). An anchor must be **defined before it is used**.

<div class="q-callout q-callout-warn"><i class="fa-solid fa-triangle-exclamation lead"></i>
  <div><b>Never reference an undefined anchor.</b> Pointing at an anchor that does not exist (for example <code>*_robots</code> or <code>*custom-fields</code>) breaks the push. Reference only anchors you have actually defined above.</div></div>

The reference skills prescribe a `robots` tabset carrying `<<: *_robots` (six SEO checkboxes: `noIndex`, `noFollow`, `noArchive`, `noSnippet`, `noOdp`, `excludeSitemap`). This is **recommended, not engine-enforced**, the live fil-rouge site does not use it.

## SEO metadata

There is **no native `metaTitle`, `metaDescription` or `ogImage`** at the page-type level. SEO metadata comes from one of two routes:

- MCP `update-page` `metas` (a JSON-stringified `{description, keywords}` object), read in templates as `page.metas.{title, description, robots}`.
- A custom `seo` tabset of page-level fields, read as `page.custom.<field>`.

## Detail pages

There is **no special "detail" page-type kind**. A detail page is an ordinary page-type that declares a `query` field resolving the current item by slug, paired with a [route](/en/docs/routing-and-urls). The route exposes `{{route.<param>}}` to the query config; the query writes its result to `page.custom.<queryKey>` for the template to read.

```yaml
article:
  name: Article (detail)
  template: 'layouts/article.liquid'
  icon: file
  sections:
    main:
    footer:
      type: rows
      name: Footer
      shared: true
      sections:
        col1:
        col2:
  tabsets:
    config:
      name: Article data
      fields:
        article:
          type: query
          label: Current article
          config:
            itemSet: articles
            limit: 1
            where:
              '&=':
                slug: '{{route.article.slug}}'
        author:
          type: query
          config:
            itemSet: auteurs
            limit: 1
            dependsOn: [article]
            where:
              '&=':
                slug: '{{article.author}}'
        related:
          type: query
          config:
            itemSet: articles
            limit: 3
            dependsOn: [article]
            where:
              and:
                - '&=':
                    category: '{{article.category}}'
                - '&!=':
                    slug: '{{route.article.slug}}'
            sort:
              - date: desc
```

`{{route.article.slug}}` is valid **only inside a query config**, never in a template. The template reads `page.custom.article[0]`, guarding with `and .size > 0` before indexing. See [Custom fields](/en/docs/custom-fields) for full `query` config and [Routing and URLs](/en/docs/routing-and-urls) for the route side.

## Related

<div class="q-cards">
  <a class="q-card reco" href="/en/docs/routing-and-urls"><span class="k">Recommended</span><span class="t">Routing and URLs</span><span class="d">Routes, params and detail-page wiring.</span></a>
  <a class="q-card" href="/en/docs/itemset-types"><span class="t">Collection-type schema</span><span class="d">The collections a query reads from.</span></a>
  <a class="q-card" href="/en/docs/template-variables"><span class="t">Template variables</span><span class="d">The <code>page</code> context object.</span></a>
</div>
