Page Configuration

The page.yml file in each page folder defines metadata, layout options, SEO settings, and data sources for that page.

Quick Start

A minimal page.yml:

title: About Us

That's all you need. The page renders all .md files in the folder as sections.

Full Reference

# Identity
title: About Us
description: Learn about our company
label: About                    # Short nav label (defaults to title)
id: about                       # Stable ID for page: links
slug:                           # Localized URL segments (multilingual sites)
  fr: a-propos
  es: acerca-de

# Ordering
order: 2                        # Sort position in navigation

# Child Pages
pages: [team, history, ...]     # Order of child pages (... = the rest)

# Page Visibility
hidden: true                    # Draft: exclude from the published site (page + its subtree)
hideIn: ['*']                   # Reachable, but hidden from every nav menu
hideIn: [header]                # Hidden from named nav areas only (header, footer, sidebar, …)

# Agent Content
knowledge: true                 # Written for an AI agent, not a visitor: never rendered (page + its subtree)

# Layout
layout: DocsLayout              # Layout name (or use default)
layout:                         # Or expanded form:
  name: DocsLayout
  hide: [right]                 # Hide specific areas
  params:                       # Layout-specific params
    sidebarWidth: wide

# Sections
sections: '*'                   # Auto-discover (default)

# Section Nesting
nest:                           # Declare parent-child relationships
  features: [card-a, card-b]   # features gets card-a and card-b as children

# Data
query: articles                 # A query by name (recommended; a list names several)
fetch:                          # …or the full fetch config (one or the other, not both)
  query: team                   # A query declared in queries.yml
  limit: 6                      # Narrowed for this page: where, sort, limit

# Analytics
trackSections: true             # Override the site's `emit` for this page (true or false)

# SEO
seo:
  noindex: false
  image: /og-about.png            # Open Graph / social-card image
  ogTitle: About Acme             # Social title (defaults to the page title)
  ogDescription: Meet the team
  canonical: https://acme.com/about
  changefreq: monthly
  priority: 0.8

Identity

title: About Us
description: Learn about our company and team
label: About
id: about-page
FieldTypeDescription
titlestringPage title (browser tab, navigation)
descriptionstringMeta description for SEO
labelstringShort label for navigation (defaults to title)
idstringStable ID for page: links (see Linking)

Why Use Labels?

When your title is long, provide a shorter label for navigation:

title: Getting Started with Uniweb Components
label: Getting Started

Navigation shows "Getting Started" while the page title remains descriptive.

Why Use IDs?

IDs give a page stable identity that survives reorganization:

# pages/docs/setup/installation/page.yml
id: installation
title: Installation Guide

Now [Install](page:installation) works regardless of where the page moves.

IDs can be descriptive (like installation) or opaque (like a7f3e2b1); a descriptive one makes a meaningful page: link target.


Ordering

Control where the page appears in navigation.

order: 2

Sorting rules:

  1. Pages with explicit order are sorted by that number (lower first)
  2. Decimals are allowed: order: 1.5 sorts between 1 and 2
  3. Pages without order appear after all ordered pages
  4. Ties are broken alphabetically by title

Example:

# pages/about/page.yml
order: 1    # Appears first

# pages/research/page.yml
order: 2    # Appears second

# pages/blog/page.yml
order: 3    # Appears third

# pages/legal/page.yml
# No order - appears after ordered pages, sorted by title

At the site root it also picks the homepage when site.yml names none: the top-level page with the lowest order — see Site Configuration → Page Ordering.


Child Page Ordering

When a page has child pages (subfolders), control their order.

# pages/docs/page.yml
pages: [getting-started, guides, ...]

The ... wildcard means "all remaining pages here." Listed pages are pinned in position; everything else fills the gap.

  • Every child keeps its own route: /docs/getting-started, /docs/guides, /docs/api-reference
  • All unlisted pages still appear in navigation

Only the site root promotes a page to its parent's route — the homepage. Below it, a page with no sections of its own, such as /docs here, takes a visitor to its first child that has content.

# getting-started first, api-reference last, rest in middle
pages: [getting-started, ..., api-reference]

Strict Order

pages: [getting-started, guides, api-reference]

Without ..., only listed pages appear in navigation. Unlisted pages are still built and accessible by URL, but hidden from navigation.

Auto-Discovery (Default)

Omit pages to auto-discover children. They're sorted by their order property.


Page Visibility

A page has two independent visibility controls. Keep them distinct:

  • hidden — reachability. hidden: true keeps a page out of the published site entirely — it isn't built, routed, or reachable. Use it for drafts / work in progress. It cascades: hiding a folder hides its whole subtree. The page still renders in uniweb dev so you can preview it while you work.
  • hideIn — nav placement. The page stays routed and reachable; you only control which navigation menus list it.
hidden: true          # Draft — excluded from the published site (and its subtree)
hideIn: ['*']         # Reachable by URL, but shown in no nav menu
hideIn: [header]      # Hidden from specific named nav areas (a list of area names)

hideIn lists the nav areas to suppress the page from — header, footer, or any area a foundation's layout declares (e.g. sidebar). The '*' wildcard means every area.

OptionHeader NavFooter NavReachable by URL
(default)✓✓✓
hideIn: [header]✗✓✓
hideIn: [footer]✓✗✓
hideIn: ['*']✗✗✓
hidden: true✗✗✗ (not published)

⛔ This applies to the 404 page too. pages/404/ marked hidden: true is a draft 404: it is not published, and the site falls back to the generic 404.html the build always writes. If you want an authored 404 that no menu lists — which is almost always what you want — it needs no flag at all: a 404 page is never listed in navigation to begin with. (Until 2026-09-12 the flag was ignored for this one page and a "draft" 404 shipped anyway.)

A section takes hidden: true too, in its own frontmatter, with the same meaning one level down: a draft section. uniweb dev renders it, and a published build leaves it out together with its child sections — the page itself stays. Sync carries the flag both ways. (It is not the hidden in a component's meta.js, which keeps a component out of the foundation.)

Use cases:

  • Draft / in-progress pages: hidden: true (invisible on the live site; previewable in dev)
  • Landing / thank-you pages (reached only via a direct link): hideIn: ['*']
  • Legal pages: hideIn: [header] (show only in footer)
  • Header-only pages: hideIn: [footer] (show only in header)

The older hideInHeader: true / hideInFooter: true booleans still work as shorthand for hideIn: [header] / hideIn: [footer].


Agent Content (knowledge)

A knowledge page is content you write for an AI agent rather than for a visitor — reference material, background, house style, answers to questions people actually ask. It is never rendered: no route, no HTML, nothing a browser can navigate to.

⚠️ It is not published to anyone — it powers a service the site offers.

What your site publishes, it publishes on equal terms: an AI visiting the site sees exactly what a person sees. llms.txt and the per-page .md files are a convenience for that reader, not a privileged tier; no visitor, human or machine, gets more than another.

A knowledge page is different in kind, not in audience. It is never a page anyone reads — it is source material for an assistant the site offers to its visitors, who ask a question and receive an answer while the material behind it stays unpublished. That is why it is excluded from llms.txt, the .md projections and the search index: not because some readers are trusted less, but because a knowledge page was never a page.

If you want content readable by any AI that visits, write an ordinary page — it is already published to them, on the same terms as to everyone else.

⛔ knowledge: is not a security control — do not put secrets there

"Not published" is about who the prose is written for, not about confidentiality. The assistant's whole job is to answer visitors using this material, and it can quote it back to whoever asks. A visitor who prompts the assistant can therefore surface what a knowledge page says — by design, not by leak.

So the test for what belongs here is "is this written for the assistant to reason with?" — explanations, background, house style, the answers to questions people actually ask. Not "do I want to keep this from people?" Anything that genuinely must not reach a visitor does not belong in your site's content at all.

# pages/kb/page.yml   (or folder.yml — both carry it)
title: Product Knowledge
knowledge: true

It cascades to the whole subtree, by route prefix. One marker on a parent covers every page beneath it, and the children need no marker of their own. Prefix matching is by segment, so /kb claims /kb/pricing and does not claim /kbase.

pages/
├── about/        # rendered normally
└── kb/           # knowledge: true
    ├── page.yml
    ├── pricing/  # inherits — no marker needed
    └── faq/      # inherits

A section type: in a knowledge page's frontmatter never selects a component, because nothing renders these pages. Write plain markdown.

What reaches an agent depends on your host

knowledge: describes your intent; whether anything can act on it is a property of where the site is deployed.

DeploymentWhat happens
uniweb deploy — a backend-hosted siteThe content travels with the site, so an assistant running over it can grep and read the knowledge material alongside the public content when answering a visitor.
uniweb export, uniweb deploy --host <adapter> — a static hostThe build drops these pages and tells you. Files are served as files; there is no assistant here, so nothing would read them — and publishing them would turn prose written for a machine reader into pages for the only readers this host has.

The static-host build prints the routes it dropped:

[site-content] Dropped 2 knowledge page(s) from this build: /kb, /kb/pricing

That warning is the expected outcome, not an error — it means the flag was honoured. If you want that content readable by an agent, deploy to a host that provides an endpoint for it.

Knowledge pages stay out of the public artifacts

The artifacts your build emits to describe the public site never name a knowledge page: llms.txt, the per-page .md projections, and the search index. Those describe pages a visitor can reach, and a knowledge page is not one.

Scoped deliberately. This is a statement about what uniweb build produces. A host may derive these artifacts itself from the content you deploy, rather than serving the copies your build made — in which case its rules apply, not these. If that matters to you, ask your host how it treats knowledge:.

Two exclusions outrank knowledge:, so a contradiction resolves toward less exposure:

  • agents.exclude in site.yml — the one setting that means "keep agents out"
  • an _-prefixed route segment — a draft is a draft

knowledge vs hidden vs hideIn

Three ways a page is "not for a visitor", and they are not interchangeable:

Built?Reachable by URL?In nav?Intended reader
knowledge: truenever rendered✗✗an AI agent
hidden: true✗ (not published)✗✗nobody yet — it's a draft
hideIn: ['*']✓✓✗a visitor you send the link to

A site that is nothing but knowledge

A site is a set of routes. It does not have to have HTML pages at all — and marking the root knowledge: true gives you a site that renders nothing and exists to be an agent: a /_agent/chat URL that a web app, a mobile app, or another backend sends requests to.

site/pages/
├── page.yml          # knowledge: true — cascades to everything below
├── house-style.md
├── pricing.md
└── support.md

The cascade does the rest. On that shape:

agent corpusevery page, with its full text
llms.txtthe site name, and no page list
search index0 entries
per-page .mdnone emitted
HTML routesnone

Nothing leaks by construction rather than by configuration — there is no public surface for it to leak onto. You are not switching things off one at a time and hoping you got them all; the site simply has no public half.

⛔ This shape needs a host that actually runs an agent — and deploying successfully is not the same as having one.

On a static host every page is dropped, because nothing there can read them, and you get an empty SPA shell. The build still reports success: Collected 0 pages, zero pre-rendered. Zero pages is not an error, so nothing will stop you.

On a backend-hosted deployment, whether an agent endpoint exists is the host's decision, per site — some deployments do not offer the service at all. Nothing in your build can tell you; resolveService(website, 'assistant') returns a url only where the host declared one, and on a knowledge-only site there is no component around to ask. Confirm with your host that the agent is enabled for that site before building an integration against it.


Section Analytics (trackSections)

Report which sections of this page visitors actually reach:

trackSections: true

A section_view is sent the first time each section is at least half visible — once per section per page view, with no duration attached. Read as counts across a page (seen by 80% / 40% / 15%), it tells you where people stopped.

This is an override, not a switch. Say nothing and the site's emit decides — which by default includes section_view. Set it explicitly to disagree with the site on this one page:

trackSections: false    # exempt a page whose sections are noisy or uninteresting

Why this one event has a per-page control and the others don't. A page view is one event whatever the page; an outbound click is bounded by what a visitor does. A section view is one event per section, so a long page costs twenty times what a short one does. The control sits where the cost actually varies — which is why a site with five landing pages and four hundred documentation pages can instrument the five and leave the rest alone.

It does nothing unless the site has a services.tracking destination. With none — the default — nothing is collected and nothing is sent.

Localized URLs

On a multilingual site, give a page a localized URL segment per language with slug::

# pages/About-Us/page.yml
slug:
  fr: a-propos      # /About-Us (en) → /a-propos (fr)
  es: acerca-de     # → /acerca-de (es)

The folder name stays the page's stable, canonical route — used in the default language and for internal page: links — and each entry sets the URL segment for that locale. Only list the non-default languages; the default language always uses the folder name.

  • Each value is a single path segment (no slashes or spaces).
  • Nesting composes automatically: if a parent folder is localized (/blog → /blogue), its children follow (/blog/my-post → /blogue/my-post) without repeating the parent. Give a child its own slug: only to localize its own segment too.
  • Localized URLs flow through navigation, the language switcher, and the sitemap (hreflang).
  • One URL per page per language: the folder name under another language's prefix — /fr/About-Us, where French shows /fr/a-propos — sends the visitor to the localized URL. A host that answers with a status sends a permanent redirect (301), as it does for the default language's own prefix.

This controls only the URL. Translating page content (titles, text) is separate — see Internationalization.


Layout Options

Control which layout is used and which areas appear on this page.

Selecting a Layout

layout: MarketingLayout

If the foundation provides multiple layouts, set which one to use. Pages without an explicit layout: use the foundation's defaultLayout — else its layout named Default, else the built-in layout, which draws header, the page and footer.

Hiding Areas

layout:
  hide: [left, right]

The hide array suppresses specific areas on this page. Hidden areas are passed as null to the Layout component.

Layout Parameters

layout:
  name: DocsLayout
  params:
    sidebarWidth: wide

Set values for parameters declared in the layout's meta.js. Defaults from meta.js apply when not overridden.

Use Cases

Full-screen landing page:

title: Welcome
layout:
  hide: [header, footer]

Documentation without sidebars:

title: Quick Reference
layout:
  hide: [left, right]

Different layout for landing pages:

title: Product
layout: MarketingLayout

Section Ordering

Control which sections appear and in what order.

Auto-Discovery (Default)

sections: '*'

Discovers all .md files in the folder, sorted by numeric prefix:

pages/home/
├── 1-hero.md         # First
├── 2-features.md     # Second
├── 2.5-testimonials.md  # Between 2 and 3
└── 3-cta.md          # Third

Inclusive Order

sections:
  - hero
  - ...
  - cta

Pin hero first and cta last. Everything else appears in between, in their natural (numeric prefix) order. Reference sections by stable name (without numeric prefix and extension) — hero finds both hero.md and 1-hero.md.

# hero first, rest after
sections: [hero, ...]

# rest first, cta last
sections: [..., cta]

Strict Order

sections:
  - hero
  - features
  - testimonials
  - cta

Without ..., only listed sections are included. Reference by stable name (prefix-independent).

Section Nesting (nest:)

Use the @ prefix on filenames and nest: in page.yml to create parent-child section relationships:

pages/home/
├── page.yml
├── 1-hero.md
├── 2-features.md       # Parent section
├── 3-pricing.md
├── @logocloud.md       # Child of features (@ = not top-level)
└── @stats.md           # Child of features
# page.yml
nest:
  features: [logocloud, stats]

Child files use the @ prefix to signal they're not top-level. The nest: property declares which parent owns them. This works with all section modes (auto-discovery, inclusive, strict).

Inline nesting in sections: also works for declaring children directly:

sections:
  - hero
  - features:          # Parent section
      - logocloud      # Resolves to @logocloud.md
      - stats          # Resolves to @stats.md
  - ...
  - pricing

When both sections: inline nesting and nest: declare children for the same parent, nest: wins.

A uniweb pull keeps your nest: as you wrote it. When the nesting was changed elsewhere, it writes the new nesting as a sections: list and removes the nest: that would override it.

No Sections

sections: []

Pure route page with no content sections (useful for pages that only have child pages).


Data Fetching

Name the queries this page's sections use.

Naming a query

query: articles

Names the articles query. Its records reach the sections of this page — and of its child pages — whose component declares articles, or a key of the query's schema. On a site with no backend they come from the file the build generates from the query; a host that serves records live answers the same query.

A list names several, each under its own key:

query: [team, articles]

Each section receives the ones its component declares, so one page-level declaration serves sections that need different data.

Narrowing a query

fetch:
  query: team
  as: members                # The content.data key it fills (defaults to the query name)
  where: { role: faculty }   # Only the query's records that also match
  sort: name asc             # Put them in another order
  limit: 12                  # The first 12 of them — never more than the query selects

A fetch always names a query, and takes from the records it selects — never adding one. It never names a file or a URL. current:, which says how a section on a parametric page uses the page's record, belongs on a section's own fetch: and is refused here.

External Data

A public JSON API is an external query — declared once with its url:, and named by the page like any other query:

# queries.yml
apiData:
  url: https://api.example.com/data
  transform: data.items      # Dot-path to the records in the response
# page.yml
query: apiData

See Data Fetching for all options, and Queries for what a query can say.


SEO Configuration

Fine-tune search engine optimization.

seo:
  noindex: false           # Allow indexing (default)
  image: /og-about.png     # Open Graph / social-card image
  ogTitle: About Acme      # Social title (defaults to the page title)
  ogDescription: Meet the team behind Acme
  canonical: https://acme.com/about
  changefreq: monthly      # Sitemap change frequency
  priority: 0.8            # Sitemap priority (0.0-1.0)

Options

OptionTypeDescription
noindexbooleanPrevent search engine indexing
imagestringOpen Graph / social sharing image
ogTitlestringSocial-card title (defaults to the page title)
ogDescriptionstringSocial-card description (defaults to the page description)
canonicalstringCanonical URL for this page
changefreqstringSitemap hint: always, hourly, daily, weekly, monthly, yearly, never
prioritynumberSitemap priority (0.0 to 1.0, default 0.5)

Set site-wide defaults for image, ogTitle, ogDescription, keywords, and noindex in site.yml — a page overrides any field it sets.

Noindex Pages

# pages/admin/page.yml
title: Admin Dashboard
seo:
  noindex: true

This page won't appear in search results or the search index.


Parametric Pages

A folder named in brackets is a parametric page: one page, with a URL for each record of a query.

pages/blog/
├── page.yml              # query: articles
└── [slug]/               # /blog/:slug — one URL per article
    └── article.md

The [slug] page needs no data configuration: it takes its parent's query, and each URL renders the record it names. The page's title and description come from that record. A page.yml inside [slug]/ is only needed for what the record doesn't supply — an id, a layout.

See Parametric Pages for the full guide.


Versioned Documentation

For versioned docs, folder structure triggers version detection:

pages/docs/
├── page.yml              # Optional version metadata
├── v1/
│   └── intro/
└── v2/
    └── intro/

Configure version labels in the parent:

# pages/docs/page.yml
title: Documentation
versions:
  v2:
    label: "2.0 (Current)"
    latest: true
  v1:
    label: "1.0 (Legacy)"
    deprecated: true

See Versioning for details.


Complete Examples

Marketing Page

title: About Us
description: Learn about our mission and team
order: 2

seo:
  image: /og-about.png
  priority: 0.8

Documentation Section

title: Documentation
label: Docs
order: 3

pages: [getting-started, guides, ..., api]

layout:
  hide: [right]

Blog Listing

title: Blog
description: Latest articles and tutorials

query: articles

seo:
  changefreq: weekly

Admin Page (reachable, not in any menu)

title: Admin Dashboard
hideIn: ['*']         # reachable by URL, but shown in no nav menu

layout:
  header: false

seo:
  noindex: true

Use hideIn: ['*'] (not hidden: true) when the page must stay reachable by direct URL. hidden: true would exclude it from the published site entirely.


See Also