uniweb 0.12.61 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,326 +1,293 @@
1
1
  # AGENTS.md
2
2
 
3
- ## The Architecture in One Sentence
3
+ ## Part 0 Read this first
4
4
 
5
- A Uniweb project separates **what the site says** from **how it's built**. Content authors write markdown — choosing section types, setting params, composing layouts. Component developers build reusable section types that receive pre-parsed content and render it. Neither touches the other's files. Neither can break the other's work.
5
+ A Uniweb project separates **what the site says** from **how it's built**. Content authors write markdown — choosing section types, setting params, composing pages. Component developers build section types that receive pre-parsed content and render it. Neither touches the other's files.
6
6
 
7
- Every pattern in this guide serves that separation: markdown for content, frontmatter for configuration, `meta.js` for the contract between the two roles, semantic tokens for context adaptation, and a runtime that handles section wrapping, backgrounds, theming, and token resolution so components don't have to.
7
+ **Why that separation is cheap rather than ceremonial.** Every Uniweb site ships a runtime. A foundation is an ES module that plugs into it either **standalone** (inlined into the site's own bundle, one self-describing artifact) or **linked** (loaded by URL at runtime as a federated module). Same foundation either way; the packaging is a deploy-time decision, not something your components know about. The runtime owns the content pipeline, section wrapping, backgrounds, theming, light/dark, prerendering, routing, and i18n; it orchestrates rendering and bridges the author's world into yours. Your components receive `{ content, params }` and render. **These are enforced guarantees, not conventions you have to uphold** — so you can drop the defensive code you'd write elsewhere.
8
8
 
9
- Once the runtime parses content and hands it to your component as `{ content, params }`, **it's standard React.** Standard Tailwind. Standard anything — import any library, use any pattern, build any UI. The `{ content, params }` interface is only for section types (components that content authors select in markdown). Everything else in your foundation is ordinary React with ordinary props. The framework handles the content pipeline and the boilerplate; you handle the design and interaction.
9
+ | The runtime handles | So components should NOT contain |
10
+ |---|---|
11
+ | Section backgrounds (image, video, gradient, color, overlay) from `background:` | Background rendering code, `bg-white`/`bg-gray-900` on the wrapper |
12
+ | Context classes (`context-light`/`medium`/`dark`) on every section | Theme maps: `const themes = { light: {…}, dark: {…} }` |
13
+ | Token resolution — `text-heading` adapts automatically | Conditionals: `isDark ? 'text-white' : 'text-gray-900'` |
14
+ | Content parsing with a guaranteed shape | Defensive null checks on content fields |
15
+ | Section wrapping in `<section>` with context class | An outer `<section>` with background/theme classes |
16
+ | Light/dark scheme resolution before first paint | `localStorage` reads, `typeof window` guards |
17
+ | i18n via locale-specific content directories | String wrapping with `t()` or `<Trans>` |
10
18
 
11
- ### What this replaces
19
+ Concretely, the code you don't write: no theme maps, no `isDark ? … : …`, no null checks on content fields, no `t()` wrapping, no per-component background rendering, no SSR guards, no outer `<section>`. A hero that's 200 lines of undifferentiated React elsewhere is a 40-line component plus a markdown file here — and the markdown is editable by someone who has never opened your code.
12
20
 
13
- In conventional React, content lives in JSX or ad-hoc data files. Theming means conditional logic in every component. Dark mode means `isDark ? 'text-white' : 'text-gray-900'` scattered everywhere. Each component handles its own background, its own null checks, its own i18n wrapping. A "simple" marketing page becomes hundreds of lines of undifferentiated boilerplate — and when a non-developer needs to change a headline, they open a pull request into code they don't understand.
21
+ Components *should* contain: layout (`grid`, `flex`, `max-w-7xl`), spacing (`p-6`, `gap-8`), typography scale (`text-3xl`, `font-bold`), animations, border-radius anything that stays the same regardless of theme context.
14
22
 
15
- Uniweb eliminates these categories of work. The runtime handles theming, backgrounds, and context adaptation. Components receive guaranteed content shapes empty strings and arrays, never null. You build a *system* of section types, not individual pages. Authors compose pages from your system. That's what makes i18n, theming, and multi-site tractable: they're properties of the system, not things bolted onto individual components.
23
+ Once content reaches your component, **it's standard React.** Standard Tailwind. Import any library, use any pattern. The `{ content, params }` interface applies only to *section types* (the components authors select in markdown); everything else in your foundation is ordinary React with ordinary props.
16
24
 
17
- ### Before you start: what the runtime already does
25
+ You're building a *system* of section types, not individual pages. That's what makes theming, i18n, and multi-site tractable — they're properties of the system rather than things bolted onto each component.
18
26
 
19
- The most common mistake is reimplementing what the framework provides for free. Check this before writing any component logic:
27
+ ### Route yourself
20
28
 
21
- | The runtime handles | So components should NOT contain |
29
+ | Your situation | Start at |
22
30
  |---|---|
23
- | Section backgrounds (image, video, gradient, color, overlay) from `background:` | Background rendering code, `bg-white`/`bg-gray-900` on wrapper |
24
- | Context classes (`context-light`/`medium`/`dark`) on every section | Theme maps: `const themes = { light: {...}, dark: {...} }` |
25
- | Token resolution `text-heading` adapts automatically | Conditionals: `isDark ? 'text-white' : 'text-gray-900'` |
26
- | Content parsing with guaranteed shape | Defensive null checks on content fields |
27
- | Section wrapping in `<section>` with context class | Outer `<section>` with background/theme classes |
28
- | i18n via locale-specific content directories | String wrapping with `t()` or `<Trans>` |
31
+ | New or nearly empty project | **Part 1**, then Part 4 |
32
+ | Existing project, content task (pages, copy, colors, nav) | **Part 2**, then Part 3 |
33
+ | Existing project, component task (new section type, restyle) | **Part 2**, then Part 4 |
34
+ | Porting a design from React / Next / a generated site | **Part 1**, then Part 5 *Migrating* |
29
35
 
30
- Components *should* contain: layout (`grid`, `flex`, `max-w-7xl`), spacing (`p-6`, `gap-8`), typography scale (`text-3xl`, `font-bold`), animations, border-radius — anything that stays the same regardless of theme context.
36
+ ### Three commands that replace guessing
31
37
 
32
- ---
38
+ ```bash
39
+ uniweb inspect <path> # print the parsed content shape of a section or page
40
+ uniweb <command> --help # exact flags, always current — no side effects
41
+ uniweb doctor # diagnose project configuration (--fix to repair)
42
+ ```
33
43
 
34
- ## Documentation
44
+ `uniweb inspect` is the important one. Every parse rule in Part 3 is faster to *confirm* than to re-derive, and re-deriving from memory is where agents go wrong. Don't predict the shape — print it.
35
45
 
36
- This project was created with [Uniweb CLI](https://github.com/uniweb/cli). Full documentation (markdown, fetchable): https://github.com/uniweb/docs
46
+ ### The four silent failures
37
47
 
38
- **To read a specific page:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
48
+ Most Uniweb mistakes do not fail the build. The build goes green, the terminal is clean, and the page is wrong. If you don't look at the page or run `inspect`, you will report success on broken output.
39
49
 
40
- **By task:**
50
+ 1. **An unknown section type renders a red `Component not found: <Type>` box.** The build does not fail. Never write a `type:` you haven't confirmed exists — see Part 2, step 2.
51
+ 2. **An undeclared param is accepted and does nothing.** Frontmatter keys no `meta.js` declares are passed through and ignored, not rejected.
52
+ 3. **Content lands in the wrong field.** A heading becomes a `subtitle` when you wanted an item, or the reverse. That's the level rule (Part 3). `uniweb inspect` settles it.
53
+ 4. **A section silently doesn't render.** `@`-prefixed files are child sections (only rendered via `nest:`), `_`-prefixed files are drafts, and section types nested below the root of `sections/` without a `meta.js` are never discovered.
41
54
 
42
- | Task | Doc page |
43
- |------|----------|
44
- | Writing page content | `authoring/writing-content.md` |
45
- | Theming and styling | `authoring/theming.md` |
46
- | Building components | `development/creating-components.md` |
47
- | Kit API (hooks, components) | `reference/kit-reference.md` |
48
- | Site configuration | `reference/site-configuration.md` |
49
- | Content shape reference | `reference/content-structure.md` |
50
- | Component metadata (meta.js) | `reference/component-metadata.md` |
51
- | Migrating existing designs | `development/converting-existing.md` |
55
+ ### Documentation
52
56
 
53
- ## Project Structure
57
+ This guide covers what you can't derive by reading code. Everything else is in the full documentation — and **you can reach all of it without guessing at URLs.**
54
58
 
55
- Most projects start as a workspace with two packages:
59
+ **Start at the index.** One address, and it's the only documentation URL worth memorizing:
56
60
 
57
61
  ```
58
- project/
59
- ├── src/ # Component developer's domain (the foundation package)
60
- ├── site/ # Content author's domain
61
- └── pnpm-workspace.yaml
62
+ https://www.uniweb.io/llms.txt
62
63
  ```
63
64
 
64
- A site is pure content. A foundation is the site's source codethat's why it lives in `src/`. The foundation's `package.json::name` is `src` (a unique workspace package name; symmetric with `site`).
65
+ It lists every documentation page with a one-line description and a fetchable link. Go there whenever your question isn't answered in this file and *especially* when you don't know whether a feature exists at all. That's where guessing does the most damage: **a 404 on a path you invented tells you your guess was wrong, not that the feature is missing.** Concluding "the framework doesn't support X" from a failed fetch is a mistake worth actively guarding against; the index answers the existence question directly.
65
66
 
66
- - **Foundation** (developer, in `src/`): React components. Those in `src/sections` and `src/layouts` are *section types* selectable by content authors via `type:` in frontmatter, or used for site-level layout areas (header, footer, panel). Most have a `meta.js` with metadata in them. Everything in `src/components` (or elsewhere) is ordinary React the developer's workbench for helper components that section types import and compose internally.
67
- - **Site** (content author, in `site/`): Markdown content + configuration. Each section file references a section type. Authors work here without touching foundation code. It may also contain collections of structured content and/or references to external data sources.
67
+ Then **follow the link the index gives you** rather than constructing one yourself. The index is the source of truth for where pages live, so it stays correct when the site's URL conventions change. (Pages also serve markdown to any client that sends `Accept: text/markdown`.)
68
68
 
69
- **The composition boundary:** Authors compose pages from finished section types — choosing types, writing content, setting params. Developers compose section types from building blocks — importing helpers from `components/`, using libraries, writing JSX. These are two different levels of composition. The section type is the boundary between them. Don't expose building-block composition to authors; build complete, self-contained section types that handle their own internal structure.
69
+ **Clone the repo instead** when you expect several lookups, or may lose network access later:
70
70
 
71
- > Multi-site projects use sub-folders with site/foundation pairs in them (each project gets its own `src/` + `site/`), or segregate foundations and sites into separate folders (`foundations/`, `sites/`).
71
+ ```bash
72
+ git clone --depth 1 https://github.com/uniweb/docs .uniweb-docs # then gitignore it
73
+ ```
72
74
 
73
- ## Project Setup
75
+ Same content, greppable, and one network operation instead of many. `grep -ri "deferred" .uniweb-docs` beats any number of fetches while you're still exploring.
74
76
 
75
- Always use the CLI to scaffold projectsnever write `package.json`, `vite.config.js`, `entry.js`, or `index.html` manually. The CLI resolves correct versions and structure.
77
+ Documentation paths in this guide are given bare — `development/creating-components.md` so they resolve three ways: as a key in the index, as a file in a clone, or at `https://raw.githubusercontent.com/uniweb/docs/main/{path}`.
76
78
 
77
- **npm or pnpm.** Projects include both `pnpm-workspace.yaml` and npm workspaces. Replace `pnpm` with `npm` in any command below.
79
+ > **Don't confuse the two doc sites.** `www.uniweb.io/docs` is the **developer** documentation this is the one you want. `docs.uniweb.app` is the *author-facing* app documentation, written for non-technical people using the visual editor; it won't answer framework questions.
78
80
 
79
- ### New workspace
81
+ **The table below saves you the round trip** for the tasks that come up most:
80
82
 
81
- ```bash
82
- pnpm create uniweb my-project --template <n>
83
- cd my-project && pnpm install
84
- ```
85
- Use `--template <n>` for an official template (`none`, `starter`, `marketing`, `docs`, `academic`, etc.), `--template none` for foundation + site with no content, or `--blank` for an empty workspace.
83
+ | Task | Page |
84
+ |------|------|
85
+ | Writing page content | `authoring/writing-content.md` |
86
+ | Theming and styling | `authoring/theming.md` |
87
+ | Authoring collections | `authoring/collections.md` |
88
+ | Where-object predicate format | `authoring/predicates.md` |
89
+ | Connecting a backend / custom transports | `development/connecting-a-backend.md` |
90
+ | Building components | `development/creating-components.md` |
91
+ | Schemas in practice | `development/schemas-in-practice.md` |
92
+ | Workspace layouts and their wiring | `development/project-structures.md` |
93
+ | Migrating an existing design | `development/converting-existing.md` |
94
+ | Internationalization (full model) | `development/internationalization.md` |
95
+ | Kit API (hooks, components) | `reference/kit-reference.md` |
96
+ | Content shape reference | `reference/content-structure.md` |
97
+ | Component metadata (`meta.js`) | `reference/component-metadata.md` |
98
+ | Site configuration | `reference/site-configuration.md` |
99
+ | Data fetching model | `reference/data-fetching.md` |
100
+ | Navigation patterns | `reference/navigation-patterns.md` |
101
+
102
+ For anything not in that table, start at the index. For CLI flags, prefer `uniweb <command> --help` over any of this — it's always current.
103
+
104
+ ---
105
+
106
+ ## Part 1 — Starting a project
86
107
 
87
- ### Adding a co-located project
108
+ **Always use the CLI to scaffold — never hand-write `package.json`, `vite.config.js`, `entry.js`, or `index.html`.** The CLI resolves correct versions and structure; hand-written config is the most common way to end up with a project that can't build.
88
109
 
89
110
  ```bash
90
- uniweb add project docs
91
- pnpm install
111
+ npx uniweb create my-project --template marketing
112
+ cd my-project && pnpm install
113
+ uniweb dev
92
114
  ```
93
115
 
94
- This creates `docs/src/` + `docs/site/` with package names `docs-src` and `docs-site`. Use `--from <template>` to apply template content to both packages.
116
+ **Choosing a template.** `--template <name>` gives you a working site plus a foundation you can study and edit. `--template none` gives you the same two packages with no content — the right choice when you're building a foundation from scratch or porting a design. `--blank` gives you an empty workspace and assumes you'll add packages with `uniweb add`; use it only if you already know the framework.
95
117
 
96
- ### Adding individual packages
118
+ Official templates: `marketing` (tokens, insets, grids, multi-line headings), `docs` (sidebar nav, code highlighting), `dynamic` (live API data, loading states), `international` (i18n, collections, multi-locale routing), `store` (product grids, e-commerce), `academic` (publications, timeline, math), `extensions` (multi-foundation, runtime loading).
97
119
 
98
- The CLI creates exactly the folder you ask for. No silent nesting under `foundations/` or `sites/` the framework doesn't require a particular folder layout, so the CLI doesn't impose one.
120
+ **npm or pnpm.** Projects include both `pnpm-workspace.yaml` and npm workspaces. Replace `pnpm` with `npm` in any command in this guide.
99
121
 
100
- ```bash
101
- uniweb add foundation # No name → ./src/ (package: src)
102
- uniweb add foundation ui # Bare name → ./ui/ (package: ui)
103
- uniweb add foundation foundations/effects # Slash → folder is the path (package: effects)
104
- uniweb add foundation marketing --path libs # name + parent → ./libs/marketing/ (package: marketing)
122
+ ### What you get
105
123
 
106
- uniweb add site # No name → ./site/ (package: site)
107
- uniweb add site blog # Bare name → ./blog/ (package: blog)
108
- uniweb add site sites/store # Slash → folder is the path (package: store)
109
124
  ```
125
+ my-project/
126
+ ├── src/ # the foundation — component developer's domain
127
+ ├── site/ # the site — content author's domain
128
+ └── pnpm-workspace.yaml
129
+ ```
130
+
131
+ A site is pure content. A foundation is the site's source code — that's why it lives in `src/`. The foundation's `package.json::name` is `src`, symmetric with `site`.
110
132
 
111
- If the target folder already exists, or the package name is already taken by another package in the workspace, the CLI stops with a precise error and suggests alternatives. The check uses the same `classifyPackage` logic the build uses, so cross-type collisions are caught (you can't `add site marketing` if a foundation named `marketing` is already in the workspace).
133
+ - **Foundation** (`src/`): React components. Those in `sections/` and `layouts/` are *section types* selectable by authors via `type:`, or used for layout areas. Everything in `components/` and `utils/` is ordinary React and JS: the developer's workbench, not visible to authors.
134
+ - **Site** (`site/`): markdown content and configuration, plus optional collections of structured content and references to external data sources.
112
135
 
113
- Use `--project <n>` to co-locate a foundation+site pair under a project directory: `--project docs` `docs/src/` (package `docs-src`) + `docs/site/` (package `docs-site`). The `-src` / `-site` suffix is the convention for co-located projects only single-foundation workspaces use bare `src`.
136
+ **The composition boundary.** Authors compose pages from finished section types choosing types, writing content, setting params. Developers compose section types from building blocks — importing helpers, using libraries, writing JSX. Two different levels of composition, and the section type is the boundary between them. Don't expose building-block composition to authors; build complete, self-contained section types that handle their own internal structure.
114
137
 
115
- ### Adding section types
138
+ ### Growing the workspace
116
139
 
117
140
  ```bash
118
- uniweb add section Hero
119
- uniweb add section Hero --foundation ui # When multiple foundations exist
141
+ uniweb add project docs # co-located pair → docs/src + docs/site
142
+ uniweb add foundation [name|path] [--path parent] # no name ./src/, bare name → ./name/, slash → that path
143
+ uniweb add site [name|path]
144
+ uniweb add extension <name> [--site <name>] # secondary foundation, wired to a site
145
+ uniweb add section Hero [--foundation ui] # sections/Hero/{index.jsx,meta.js}
120
146
  ```
121
147
 
122
- Creates `sections/Hero/index.jsx` and `meta.js` with a minimal CCA-proper starter. The dev server picks it up automatically no build or install needed.
148
+ The CLI creates exactly the folder you ask for — no silent nesting under `foundations/` or `sites/`. If the target exists or the package name is taken, it stops with a precise error and suggests alternatives (using the same `classifyPackage` logic the build uses, so cross-type collisions are caught: you can't `add site marketing` when a foundation named `marketing` exists).
123
149
 
124
- ### What the CLI generates
150
+ `uniweb add section` scaffolds a CCA-proper starter, and the dev server picks it up with no build or install.
125
151
 
126
- **Foundation** (`vite.config.js`, `package.json`, `main.js`, `styles.css`):
127
- - `defineFoundationConfig()` in vite.config.js
128
- - Dependencies pinned to current npm versions
129
- - `@import "@uniweb/kit/theme-tokens.css"` in styles.css
152
+ ### Learn from a working template
130
153
 
131
- **Site** (`vite.config.js`, `package.json`, `entry.js`, `index.html`, `site.yml`):
132
- - `defineSiteConfig()` in vite.config.js
133
- - `react-router-dom` in devDependencies (required by pnpm strict mode)
134
- - Standard `start()` call in entry.js
135
-
136
- ## Commands
154
+ **When you're unsure how to implement a pattern — data fetching, i18n, layouts, insets, theming — read a real one instead of guessing.** Install an official template as a second project in your workspace:
137
155
 
138
156
  ```bash
139
- # Local development
140
- uniweb dev # Start dev server (picks the site for you)
141
- pnpm install # Install dependencies
142
- pnpm build # Build for production
143
- pnpm preview # Preview production build (SSG + SPA)
144
-
145
- # Ship a site — `uniweb deploy` asks where, if you haven't chosen yet
146
- uniweb deploy # Wizard: pick a destination, then deploy (or set up CI)
157
+ uniweb add project marketing --from marketing
158
+ pnpm install
159
+ ```
147
160
 
148
- # Ship a site to Uniweb hosting (needs `uniweb login`)
149
- uniweb publish # The smart path: bring the foundation along (releasing it to the
150
- # catalog if its code changed), sync content, and go live
161
+ That creates `marketing/src/` + `marketing/site/` alongside your project. You don't need to build or run it. Read:
151
162
 
152
- # Ship a site to a third-party host instead
153
- uniweb add ci --host=<adapter> # Set up CI so every push deploys — usually the best answer for a
154
- # free host. github-pages, cloudflare-pages, netlify, vercel
155
- uniweb deploy --host=<adapter> # Build + upload now, from this machine. Same four, plus
156
- # s3-cloudfront. Drives that host's CLI (wrangler/netlify/vercel/aws)
157
- uniweb export # Build dist/ for any static host (no Uniweb account)
163
+ - `marketing/src/sections/` components with `meta.js`: what content they expect, what params they accept, what presets they define
164
+ - `marketing/site/pages/` real content files, showing the markdown component mapping
165
+ - `marketing/site/theme.yml` and `site.yml` theming and configuration in practice
158
166
 
159
- # Publish a foundation for free, at permanent versioned URLs
160
- uniweb add ci --target foundation # GitHub Pages workflow → foundations/<name>/<version>/entry.js
167
+ This is the highest-bandwidth way to learn the framework, and it works with no network access. Install several if you like; each is independent. To run one: `cd marketing/site && pnpm dev`.
161
168
 
162
- # Sync a site with the Uniweb backend (git-style; needs `uniweb login`)
163
- uniweb push # Push local content to the backend (creates the site on first push)
164
- uniweb pull # Pull backend content back to local files
165
- uniweb clone <site-uuid> # Start a new local project from a site already in the backend
166
- uniweb status # Show what's unpushed / out of sync (local, offline)
169
+ ### The loop
167
170
 
168
- # Register a foundation + its data schemas to the Uniweb registry
169
- uniweb register # Register this foundation + the data schemas it defines
170
- uniweb register --scope @org # Register under @org (also works from a schemas-only package)
171
+ ```bash
172
+ uniweb dev # from the workspace root; it picks the site for you
173
+ pnpm build # production build (SSG + SPA)
174
+ pnpm preview # preview the production build
175
+ ```
171
176
 
172
- # Project health + maintenance
173
- uniweb doctor # Diagnose project configuration issues (--fix to auto-repair)
174
- uniweb validate # Check file-based data against your declared schemas (--strict for CI)
175
- uniweb update # Align @uniweb/* deps + this AGENTS.md with the CLI's matrix
176
- # (--dry-run to preview, --yes for non-interactive)
177
+ Markdown, `theme.yml`, and component edits hot-reload. New section types are picked up without a restart.
177
178
 
178
- # Help
179
- uniweb --help # Top-level help
180
- uniweb <command> --help # Per-command help (no side effects)
181
- ```
179
+ ---
182
180
 
183
- `uniweb publish` brings the site's local foundation along releasing it to the catalog under your `@org` when its code changed — so a single site needs no separate `uniweb register` step.
181
+ ## Part 2Working in an existing project
184
182
 
185
- **Choosing where a site goes.** `uniweb deploy` never assumes a host: with nothing configured it opens a picker listing only destinations it can actually act on, and records the choice in `deploy.yml` so later runs go straight there. For a free static host, prefer `uniweb add ci --host=<adapter>` over a manual `uniweb deploy` one command, then every push deploys, and on Cloudflare Pages / Netlify / Vercel it also adds a per-PR preview that comments the URL on the pull request. Destination config (bucket, project name, site id) lives in `deploy.yml` beside `site.yml`; host credentials come from the environment, never from that committed file.
183
+ The rest of this guide explains how Uniweb works. This part is what to do *first* when you've been handed a project that already uses it, plus a task.
186
184
 
187
- **Registering data schemas.** A foundation that defines data schemas (`@/article`, …) uses `uniweb register` to register the foundation together with those schemas in the Uniweb registry so content authors can create and manage entities of those types. It requires authentication: run `uniweb login`, or supply a bearer token directly with `--token <bearer>` (or the `UNIWEB_TOKEN` env var). Point at a specific registry with `--registry <url>` (or `UNIWEB_REGISTER_URL`). Preview without auth using `--dry-run` (or `-o <file>` to write the submission), and set the org scope with `--scope @org` (default: the foundation's `package.json` `uniweb.scope`).
185
+ ### 1. Orientwhat kind of workspace is this?
188
186
 
189
- **Registering standard or shared schemas (no foundation).** Data schemas can also be registered on their own — without a foundation — straight from a schemas package. Run `uniweb register` from a package that *exports* schemas (`@uniweb/schemas`, or any `@org/schemas` you maintain), or from a bare folder of `schemas/*.{yml,json,js}` files: it detects the schemas-only package automatically and submits just the data schemas (no foundation) under `--scope`. This is how the standard schemas are published under `@std`, and how an org publishes its own shared `@org/schemas` once for many foundations to reference. The same flags apply (`--scope`, `--dry-run`, `-o`, `--token`, `--registry`); `--dry-run` (or `-o <file>`) previews the exact submission without authenticating.
187
+ **There is no single layout. Read `pnpm-workspace.yaml` first** its `packages:` globs name the shape in one glance:
190
188
 
191
- **Staying current.** `uniweb update` aligns this project's `@uniweb/*` deps and `AGENTS.md` to the CLI that runs it; `uniweb doctor` reports drift without mutating. To pin to the newest published release, run `npx uniweb@latest update --yes` — no global install needed. The verb won't refresh AGENTS.md while declared deps still lag the CLI, or while edited deps haven't been installed: both would put the doc ahead of the code. Updating the CLI itself is your package manager's job (`npm i -g uniweb@latest`, `pnpm add -g uniweb@latest`, …); `uniweb update` does not do that.
189
+ | Globs | Layout | What it implies |
190
+ |---|---|---|
191
+ | `src`, `site` | **single** (the default) | foundation in `src/`, site in `site/` |
192
+ | `foundations/*`, `sites/*` | **segregated** | several sites may share one foundation — a foundation edit hits all of them |
193
+ | `*/src`, `*/site` | **co-located projects** | one self-contained pair per project: `docs/src` + `docs/site` |
194
+ | `extensions/*` (alongside any of the above) | extensions present | extra section types, runtime-loaded — see step 2 |
192
195
 
193
- ---
196
+ Those names are convention; the globs are the truth. Full guide with the wiring for each: `development/project-structures.md`.
194
197
 
195
- ## `package.json` `uniweb` configuration
198
+ **A site is any package with a `site.yml` and a `pages/` folder** — a workspace can hold several. Everything below calls yours `<site>/`.
196
199
 
197
- The `uniweb` block in `package.json` carries platform-specific configuration that doesn't belong in the npm-standard fields. All fields are optional; defaults apply when omitted.
200
+ **Find the foundation by following the wiring, not by guessing the folder.** `foundation:` in `site.yml` is a **package name**, not a path, and folder name matches package name only by convention — co-located projects break it deliberately (folder `docs/src`, package `docs-src`). Go from name to folder via the matching `file:` dependency in `<site>/package.json`:
198
201
 
199
202
  ```json
200
- {
201
- "name": "src",
202
- "version": "1.0.0",
203
- "uniweb": {
204
- "id": "marketing",
205
- "runtimePolicy": "auto-minor"
206
- },
207
- "dependencies": {
208
- "@uniweb/core": "0.7.8",
209
- "@uniweb/runtime": "0.8.9"
210
- }
211
- }
203
+ // docs/site/package.json → the foundation is at docs/src/
204
+ { "dependencies": { "docs-src": "file:../src" } }
212
205
  ```
213
206
 
214
- | Field | Where used | Default | Purpose |
215
- |---|---|---|---|
216
- | `id` | `uniweb register` | (the bare segment of a scoped `package.json::name`, or `uniweb.id`) | The foundation's registered id — the bare-name segment in `@org/<id>`. Decoupled from `package.json::name` (a workspace concern), so renaming the foundation on the registry doesn't ripple through site dependencies. |
217
- | `namespace` | `uniweb register` | (none — see scope resolution) | Legacy explicit org-namespace override. Equivalent to using a scoped `package.json::name` (`"@myorg/foundation"`). Rarely needed in modern foundations. |
218
- | `runtimePolicy` | `dist/runtime-pin.json` (foundation build) | `"auto-minor"` | Controls how sites using this foundation receive runtime updates. Three values: `"exact"`, `"auto-patch"`, `"auto-minor"`. See "Foundation runtime policy" below. |
219
-
220
- **How a foundation reaches the catalog.** Foundations on Uniweb hosting always live in the catalog as `@org/name@version`. Two ways to get one there, both from the same `dist/entry.js` artifact:
207
+ If `foundation:` isn't a workspace package name, the source isn't in this repo at all:
221
208
 
222
- - **Brought along by `uniweb publish`.** When a foundation powers a single site, don't run `uniweb register` yourself. Run `uniweb publish` from the site directory — it releases the site's local foundation to the catalog under your `@org` (when its code changed) and then goes live, in one step. No separate register ceremony.
223
- - **Registered deliberately with `uniweb register --scope @org`.** When the foundation is a product meant for multiple sites — listed in the catalog, consumable by other developers' sites — register it on its own schedule. Consuming sites then pin a versioned ref (`foundation: '@org/name@1.2.3'`).
224
-
225
- **On the split between `package.json::name` and `uniweb.id`:** the workspace name is what pnpm uses for `file:` linking and what `site.yml::foundation` references. The published id is what the registry stores. Keeping them separate means a workspace rename (e.g. `marketing` → `marketing-pro`) is a one-shot `uniweb rename foundation marketing marketing-pro` it updates the package, folder, and every dependent site, while the foundation's registered id (`uniweb.id`) stays independent.
209
+ | `foundation:` value | What it means | Can you add a section type? |
210
+ |---|---|---|
211
+ | a workspace package name (`src`, `docs-src`, `marketing`) | source is in this repo — follow the `file:` dep to it | **Yes** |
212
+ | a versioned registry ref (`@org/name@1.2.3`) | published; **its source is not in this repo** | **No**work within the types it offers |
213
+ | an `https://…` URL, or `{ url: … }` | same, loaded from that URL | **No** |
226
214
 
227
- These are the only fields the platform consumes today. Future platform features that need static configuration will land here too.
215
+ A versionless `@org/name` is an error rather than a shorthand the build rejects it and asks for a version.
228
216
 
229
- ---
217
+ > **Foundations are never npm packages.** They're runtime federated modules, not libraries. Don't `npm install` one, and don't add one to `dependencies` by hand. The four supported shapes are exactly the ones above: a workspace sibling, a `file:` dependency, a versioned registry ref, or a full URL.
230
218
 
231
- ## Foundation runtime policy
219
+ **Check `paths:` in `site.yml` before going looking for content.** It mounts outside directories into the page tree (`paths: { pages/docs: ../../../docs }`), so a route's markdown may live in another repo or a submodule rather than under `<site>/pages/`.
232
220
 
233
- (Foundation authors onlysites don't set this.)
221
+ ### 2. Learn this project's vocabulary before you write anything
234
222
 
235
- When a foundation builds, it pins the `@uniweb/runtime` version it was built against (from its own dependencies) into `dist/runtime-pin.json`. Foundations can also declare a *policy* that controls how the runtime version moves forward on already-published sites:
223
+ **A foundation is a fixed vocabulary of section types, and every project's is different.** Nothing in the framework tells you what a given project offers; you read it in the foundation folder you resolved in step 1, not a guessed `src/`:
236
224
 
237
- ```json
238
- // foundation's package.json
239
- {
240
- "name": "@uniweb/votiverse",
241
- "version": "0.1.1",
242
- "uniweb": {
243
- "runtimePolicy": "auto-minor"
244
- },
245
- "dependencies": {
246
- "@uniweb/core": "0.7.8"
247
- }
248
- }
225
+ ```bash
226
+ ls <foundation>/sections/ # the section types available
227
+ cat <foundation>/sections/Hero/meta.js # what one expects and accepts
249
228
  ```
250
229
 
251
- Three valid values:
230
+ **Extensions add to that vocabulary.** If `site.yml` carries an `extensions:` list, each entry is a second foundation contributing its own section types, usable by name in exactly the same way. Enumerate their `sections/` too. The primary foundation wins on a name collision, and extensions are checked in declared order.
252
231
 
253
- | Value | Meaning |
254
- |---|---|
255
- | `exact` | The site stays on exactly the runtime version this foundation built against. Newer runtime versions are not auto-applied. |
256
- | `auto-patch` | The site auto-updates within the same `MAJOR.MINOR.x` (e.g. `0.8.9` → `0.8.10`). Conservative; matches typical npm patch semantics. |
257
- | `auto-minor` | The site auto-updates within the same `MAJOR.x.y` (e.g. `0.8.9` → `0.9.0`). |
258
-
259
- **Default when unset:** `auto-minor`. Most foundations don't need to set this field — the platform's runtime is internally backwards-compatible at the minor level by convention, and `auto-minor` lets your foundation's sites pick up bug fixes and additive features without rebuilding the foundation.
232
+ Each `meta.js` is a catalog entry: `description` (what the type is for), `content:` (what markdown it expects), `params:` (what frontmatter it accepts, with defaults), `presets:` (named param bundles). Read them as a menu — that is what they are. There is no CLI command that lists them; reading the folder *is* the discovery step.
260
233
 
261
- Set `exact` if your foundation depends on undocumented runtime internals or has been audited against one specific runtime release and you don't want to allow drift.
234
+ > **Never write a `type:` or a param you haven't confirmed exists.** Both failures are silent (Part 0) invisible from the terminal, visible only on the page.
262
235
 
263
- The field is read by `@uniweb/build` at build time and emitted into `dist/runtime-pin.json` next to the runtime version:
264
-
265
- ```json
266
- // dist/runtime-pin.json (auto-generated)
267
- { "runtime": "0.8.9", "policy": "auto-minor" }
268
- ```
236
+ ### 3. Find your lane
269
237
 
270
- Sites using your foundation will see this pin + policy when they're served. Site owners cannot override your policy choice this is the foundation author's contract with the platform.
238
+ The architecture exists to keep content and code separate. Your task sits in one of them. Decide before you edit, then stay there.
271
239
 
272
- ### Where does the pinned runtime version come from?
240
+ | Task | Lane | Files you touch |
241
+ |---|---|---|
242
+ | Add / edit / reorder a section on a page | content | `<site>/pages/**` |
243
+ | Add a page | content | `<site>/pages/<name>/` |
244
+ | Change colors, fonts, light/dark | content | `<site>/theme.yml` |
245
+ | Change header, footer, or nav content | content | `<site>/layout/*.md`, or page order in `site.yml` |
246
+ | Change how a section type *looks* everywhere | foundation | `<foundation>/sections/<Type>/` |
247
+ | Add a new section type | foundation | `<foundation>/sections/<NewType>/` |
248
+ | Expose a new knob to authors | foundation | `<foundation>/sections/<Type>/meta.js` + the component |
273
249
 
274
- You may notice your foundation's `dist/runtime-pin.json` reports a runtime version (e.g. `0.8.9`) without your `package.json` declaring `@uniweb/runtime` anywhere. This is intentional.
250
+ **If you're on a content task and find yourself wanting to open the foundation, stop.** Usually it means you missed a param the section type already exposes — re-read its `meta.js`. If the knob genuinely doesn't exist, that's a foundation change: say so explicitly rather than quietly crossing the boundary. Editing a section type changes it for every page that uses it — and in a segregated workspace, for **every site in the repo sharing that foundation**, which is exactly the layout chosen when a foundation is the primary deliverable. Check who else depends on it before you edit.
275
251
 
276
- `@uniweb/runtime` is pulled in **transitively** through `@uniweb/build` (which every foundation has as a devDependency). The runtime version baked into your foundation's pin is whichever version your `@uniweb/build` version pulled in at install time:
252
+ ### 4. The loop
277
253
 
278
- ```
279
- your foundation
280
- └─ devDependencies: "@uniweb/build": "0.12.0"
281
- └─ pulls in @uniweb/runtime (whatever version that build version locks)
282
- → resolved at install time → version goes into dist/runtime-pin.json
254
+ ```bash
255
+ uniweb dev # from the workspace root; it picks the site
283
256
  ```
284
257
 
285
- Practical implications:
258
+ **When you're unsure what shape your markdown produces, don't reason about it — run it.** From the *site* directory (paths resolve against your shell's working directory):
286
259
 
287
- - **You don't need to add `@uniweb/runtime` to your foundation's dependencies.** Runtime is the host environment, not a foundation import.
288
- - **To bump the runtime version your foundation pins, bump your `@uniweb/build` dep.** When a newer build version ships pulling in a newer runtime, updating your devDependency is how you adopt it.
289
- - **You can override by adding `@uniweb/runtime` directly to your foundation's `dependencies`** — but this is rarely needed and creates two sources of truth for the runtime version. Don't do this unless you have a specific reason.
290
- - **Whichever runtime version is pinned, your `runtimePolicy` controls how sites can move forward beyond it.** Pinning `0.8.9` with `auto-minor` lets sites pick up `0.9.0` or higher (within the same major); pinning with `exact` locks them at `0.8.9` until you rebuild your foundation.
260
+ ```bash
261
+ uniweb inspect pages/home/hero.md # the parsed content shape
262
+ uniweb inspect pages/home/ # every section on the page
263
+ uniweb inspect pages/home/hero.md --full # include empty fields (matches runtime)
264
+ uniweb inspect pages/home/hero.md --sequence # include the sequence array
265
+ uniweb inspect pages/home/hero.md --raw # the ProseMirror AST
266
+ ```
291
267
 
292
- ### What happens when fields aren't set
268
+ This settles any question in Part 3 — whether a heading became a subtitle or an item, whether the parser saw your data block, why `content.title` is an array. One command beats re-deriving the rules.
293
269
 
294
- The system has multi-layer fallbacks so missing or partial information is always handled gracefully:
270
+ ### 5. Recipes
295
271
 
296
- | Scenario | What happens |
297
- |---|---|
298
- | **`uniweb.runtimePolicy` not set in `package.json`** | `dist/runtime-pin.json` is emitted with the runtime version but no `policy` field. At serve time the platform applies `auto-minor` as the implicit default. Most foundations don't need to set `runtimePolicy` — leaving it unset is the correct choice when you want default behavior. |
299
- | **`@uniweb/runtime` not resolvable at build time** | The build silently skips emitting `runtime-pin.json`. This was the pre-Strategy-S behavior — your foundation falls back to the legacy self-contained build path. New foundations created with `npx uniweb create` always have `@uniweb/runtime` as a dependency, so this only affects unusual workspace setups. |
300
- | **`runtime-pin.json` is missing or malformed** | The platform's edge dispatcher detects the absence and serves the foundation through the legacy bundling path (the foundation's own `ssr-worker-bundle.js` is used). Your sites still work; they just don't participate in runtime propagation. |
301
- | **`runtime-pin.json` has a `runtime` version that's not actually deployed to the platform** | The site publish flow rejects the publish with a clear error message asking you to deploy the pinned runtime version first. This is caught at publish time, not at serve time. |
302
- | **You set `policy: "auto-minor"` but no compatible newer version exists** | The site stays on the version you pinned. The resolver only moves forward when a newer version satisfying the policy is actually available. |
272
+ Paths are relative to the site package you identified in step 1 — `site/` in the default layout, `sites/main/` or `docs/site/` in others.
303
273
 
304
- Bottom line: a foundation that doesn't set `runtimePolicy` gets `auto-minor` behavior automatically. A foundation that doesn't ship `runtime-pin.json` at all (e.g. a legacy build) still serves correctly through the platform's compatibility path you just don't get the propagation benefits. Set `runtimePolicy` explicitly only when you want to override the default (typically to `exact` for stability-critical builds).
274
+ **Add a section to a page.** `ls <site>/pages/<page>/` to see the existing sections and their numeric prefixes pick a `type:` from the vocabulary (step 2) create `<site>/pages/<page>/N-name.md` with frontmatter plus markdown. Use a decimal (`2.5-…`) to slot between existing sections without renaming them.
305
275
 
306
- ---
276
+ **Add a page.** Create `<site>/pages/<name>/`, add `page.yml` (at minimum `title:`), add one or more section `.md` files. It gets the route `/<name>` automatically. Only touch `pages:` in the parent `page.yml` / `site.yml` if you need a specific order.
307
277
 
308
- ## Content Authoring
278
+ **Change the brand color.** `<site>/theme.yml` → `colors.primary`. One line, and every component follows. Do **not** edit Tailwind color classes in components to change brand color — that is the exact anti-pattern this system removes.
309
279
 
310
- The decision rule: **would a content author need to change this?** Yes it belongs in markdown, frontmatter, or a tagged data block. No it belongs in component code.
280
+ **Change a nav item.** First check how nav is produced. If `<site>/layout/header.md` lists the links (a markdown list, or a `yaml:nav` block), edit it there. If it doesn't, the Header is generating nav from the page hierarchy — change page titles and order in `site.yml` / `page.yml` instead.
311
281
 
312
- Start with the content, not the component. Write the markdown a content author would naturally write, check what content shape the parser produces, *then* build the component to receive it.
282
+ **Update the project's Uniweb dependencies — and this file.** `uniweb update`. One command aligns every `@uniweb/*` dependency *and* refreshes this AGENTS.md together, to the version matrix of the CLI that runs it. Preview with `--dry-run`. **Don't reach for `npm update` / `pnpm update`** — see *Staying current* in Part 5 for why that breaks things quietly.
313
283
 
314
- **Markdown order rendering order.** The parser extracts content into a flat structure (`title`, `icons`, `images`, `paragraphs`). The component decides how to arrange these visually. Don't write markdown in visual order write it in semantic order. Start sections with the heading, then add icons, images, and text in any order:
284
+ **Change one section's columns / spacing / variant.** Check that type's `meta.js` `params:` first. If the knob exists, set it in that section's frontmatter and you're done, in the content lane. If it doesn't, it's a foundation change see the warning in step 3.
315
285
 
316
- ```markdown
317
- # Site Name ← title — always start with the heading
318
- ![](lu-graduation-cap) ← icon — component controls where this renders
319
- ```
286
+ ---
320
287
 
321
- Placing content *before* the first heading changes the parse: headings after body content become items, not the section title. This is by design it's how repeating content groups (cards, features) are created.
288
+ ## Part 3The content lane
322
289
 
323
- ### Section Format
290
+ ### Section format
324
291
 
325
292
  Each `.md` file is a section. Frontmatter on top, content below:
326
293
 
@@ -334,7 +301,7 @@ theme: dark
334
301
 
335
302
  # Build the system. ← title (the big headline)
336
303
 
337
- ## Not every page. ← subtitle
304
+ ## Not every page. ← subtitle
338
305
 
339
306
  Description paragraph.
340
307
 
@@ -343,11 +310,15 @@ Description paragraph.
343
310
  ![Image](./image.jpg)
344
311
  ```
345
312
 
346
- Content authors don't need to understand *why* `###` means pretitle — just that putting a smaller heading before the main heading creates a small label above it. Heading levels set *structure* (pretitle, title, subtitle), not font size — the component controls visual sizing.
313
+ Heading levels set *structure* (pretitle, title, subtitle), not font size — the component controls visual sizing.
314
+
315
+ **Markdown order ≠ rendering order.** The parser extracts content into a flat structure; the component decides how to arrange it visually. Write markdown in semantic order, not visual order — start with the heading, then add icons, images, and text in any order.
347
316
 
348
- ### Content Shape
317
+ **Placing content *before* the first heading changes the parse:** headings after body content become items, not the section title. This is by design — it's how repeating content groups are created.
349
318
 
350
- The semantic parser extracts markdown into a flat, guaranteed structure. No null checks needed — empty strings/arrays if content is absent:
319
+ ### The parsed object
320
+
321
+ The semantic parser produces a flat, guaranteed structure. No null checks needed — empty strings and arrays when content is absent:
351
322
 
352
323
  ```js
353
324
  content = {
@@ -360,7 +331,7 @@ content = {
360
331
  icons: [], // { library, name, role }
361
332
  videos: [], // { src, alt, role, poster, href }
362
333
  insets: [], // Inline @Component references — { refId }
363
- lists: [], // [[{ paragraphs, links, lists, ... }]] — each list item is an object, not a string
334
+ lists: [], // [[{ paragraphs, links, lists, }]] — each item is an object, not a string
364
335
  quotes: [], // Blockquotes
365
336
  snippets: [], // Fenced code — [{ language, code }]
366
337
  data: {}, // From tagged data blocks (```yaml:tagname, ```json:tagname)
@@ -370,53 +341,16 @@ content = {
370
341
  }
371
342
  ```
372
343
 
373
- **Items** are repeating content groups (cards, features, FAQ entries). Created when a heading appears after body content:
374
-
375
- ```markdown
376
- # Our Features ← title
377
-
378
- We built this for you. ← paragraph
379
-
380
- ### Fast ← items[0].title
381
- ![](lu-zap) ← items[0].icons[0]
382
- Lightning quick. ← items[0].paragraphs[0]
383
-
384
- ### Secure ← items[1].title
385
- ![](lu-shield) ← items[1].icons[0]
386
- Enterprise-grade. ← items[1].paragraphs[0]
387
- ```
388
-
389
- **Items have the full content shape** — this is the most commonly overlooked feature. Each item has `title`, `pretitle`, `subtitle`, `paragraphs`, `links`, `icons`, `lists`, `snippets`, and even `data` (tagged data blocks). You don't need workarounds for structured content within items:
390
-
391
- ```markdown
392
- ### The Problem ← items[0].pretitle
393
- ## Content gets trapped ← items[0].title
394
- Body text here. ← items[0].paragraphs[0]
395
-
396
- ### The Solution ← items[1].pretitle
397
- ## Separate content from code ← items[1].title
398
- ```
399
-
400
- If you need an eyebrow label above an item's title, that's `pretitle` — the same heading hierarchy as the top level. Heading hierarchy within items follows the same rules — `####` within a `###` item becomes `items[0].subtitle`. If you need metadata per item, use a tagged block inside the item:
401
-
402
- ````markdown
403
- ### Starter ← items[0].title
404
- $9/month ← items[0].paragraphs[0]
405
-
406
- ```yaml:details
407
- trial: 14 days
408
- seats: 1
409
- ``` ← items[0].data.details = { trial: "14 days", seats: 1 }
410
- ````
344
+ > **Don't predict this print it.** `uniweb inspect <path>` shows the actual parsed shape of any section or page. The rules below are the ones most often gotten wrong from memory.
411
345
 
412
- **Complete example markdown and resulting content shape side by side:**
346
+ ### Markdown content, side by side
413
347
 
414
348
  ```markdown
415
349
  ### Eyebrow │ content.pretitle = "Eyebrow"
416
350
  # Our Features │ content.title = "Our Features"
417
351
  ## Build better products │ content.subtitle = "Build better products"
418
352
 
419
- We help teams ship faster. │ content.paragraphs[0] = "We help teams..."
353
+ We help teams ship faster. │ content.paragraphs[0] = "We help teams"
420
354
 
421
355
  [Get Started](/start) │ content.links[0] = { href: "/start", label: "Get Started" }
422
356
 
@@ -426,109 +360,105 @@ Lightning quick. │ content.items[0].paragraphs[0] = "Lightning
426
360
 
427
361
  ### Secure │ content.items[1].title = "Secure"
428
362
  ![](lu-shield) │ content.items[1].icons[0] = { library: "lu", name: "shield" }
429
- Enterprise-grade security. │ content.items[1].paragraphs[0] = "Enterprise-grade..."
363
+ Enterprise-grade security. │ content.items[1].paragraphs[0] = "Enterprise-grade"
430
364
  ```
431
365
 
432
- Headings before the main title become `pretitle`. Headings after the main title at a lower importance become `subtitle`. Headings that appear after body content (paragraphs, links, images) start the `items` array.
366
+ The three rules that produce this: headings *before* the main title become `pretitle`; a heading *after* the title at lower importance becomes `subtitle`; headings appearing *after body content* start the `items` array.
367
+
368
+ ### Items have the full content shape
369
+
370
+ This is the most commonly overlooked feature. Each item has `title`, `pretitle`, `subtitle`, `paragraphs`, `links`, `icons`, `lists`, `snippets`, and `data` — you don't need workarounds for structured content inside items. Heading hierarchy within an item follows the same rules (`####` inside a `###` item becomes that item's `subtitle`).
371
+
372
+ ````markdown
373
+ ### Starter ← items[0].title
374
+ $9/month ← items[0].paragraphs[0]
375
+
376
+ ```yaml:details
377
+ trial: 14 days
378
+ seats: 1
379
+ ``` ← items[0].data.details = { trial: "14 days", seats: 1 }
380
+ ````
381
+
382
+ ### Subtitle vs items — the level rule
433
383
 
434
- **Subtitle vs items:** A heading immediately after the title becomes `subtitle` only when it is **exactly one level deeper** (H1→H2, H2→H3). Skipping levels (H1→H3) breaks the group the deeper heading starts items instead. If you want items without a subtitle, use a `---` divider or a paragraph to close the title group:
384
+ A heading immediately after the title becomes `subtitle` **only when it is exactly one level deeper** (H1→H2, H2→H3). Skipping levels (H1→H3) breaks the group and the deeper heading starts items instead. To get items with no subtitle, close the title group with a `---` divider or a paragraph:
435
385
 
436
386
  ```markdown
437
387
  # Our Stats │ content.title = "Our Stats"
438
- --- │ ← divider closes the title group
388
+ --- │ ← divider closes the title group
439
389
  ## 15,000+ │ content.items[0].title = "15,000+"
440
390
  Students from 90 countries │ content.items[0].paragraphs[0]
441
-
391
+
442
392
  ## 200+ │ content.items[1].title = "200+"
443
393
  Programs offered │ content.items[1].paragraphs[0]
444
394
  ```
445
395
 
446
- Without the `---`, `## 15,000+` would become `content.subtitle` instead of an item.
396
+ Without the `---`, `## 15,000+` would become `content.subtitle`.
397
+
398
+ ### Multi-line headings
399
+
400
+ Consecutive headings at the same level merge into a title array — one heading split across visual lines. Kit's `<H1>`, `<H2>`, … render arrays as a single tag with line breaks.
401
+
402
+ ```markdown
403
+ # Build the future │ content.title = ["Build the future", "with confidence"]
404
+ # with confidence │
405
+
406
+ # Build the future │ content.title = [
407
+ # [with confidence]{accent} │ "Build the future",
408
+ │ "<span accent=\"true\">with confidence</span>"
409
+ │ ]
410
+ ```
411
+
412
+ **Rule:** same-level continuation only applies *before* going deeper. Once a subtitle level is reached, same-level headings start new items instead of merging. Use `---` to force separate items where headings would otherwise merge.
447
413
 
448
414
  ### Sequential content
449
415
 
450
- `content.sequence` is the flat, ordered list of all elements before any grouping. Each element has a `type` (`heading`, `paragraph`, `image`, `codeBlock`, `dataBlock`, `list`, `link`, `divider`, `inset`, etc.) and type-specific fields. Use it when grouping isn't the right lens — for example, rendering prose in document order with `<Prose>`, or finding specific elements regardless of which group they ended up in:
416
+ `content.sequence` is the flat, ordered list of all elements before any grouping. Each element has a `type` (`heading`, `paragraph`, `image`, `codeBlock`, `dataBlock`, `list`, `link`, `divider`, `inset`, ) plus type-specific fields. Use it when grouping isn't the right lens — rendering prose in document order, or finding elements regardless of which group they landed in:
451
417
 
452
418
  ```js
453
- // All data blocks, regardless of heading groups
454
419
  const allData = {}
455
420
  for (const el of content.sequence) {
456
421
  if (el.type === 'dataBlock') allData[el.tag] = el.data
457
422
  }
458
-
459
- // All headings in order
460
- const headings = content.sequence.filter(e => e.type === 'heading')
461
423
  ```
462
424
 
463
- The grouped fields (`title`, `paragraphs`, `items`, `data`) and the sequential view (`sequence`) are two interpretations of the same content. Grouped is better for structured layouts (cards, features). Sequential is better for prose rendering and for finding content without caring about group boundaries.
425
+ Grouped fields and `sequence` are two interpretations of the same content. Grouped suits structured layouts (cards, features); sequential suits prose and cross-group searching.
464
426
 
465
427
  ### Choosing how to model content
466
428
 
467
- You have three layers. Most of the design skill is choosing between them:
429
+ The decision rule: **would a content author need to change this?** Yes → markdown, frontmatter, or a tagged data block. No → component code.
468
430
 
469
- **Pure markdown** headings, paragraphs, links, images, lists, items. This is the default. If the content reads naturally as markdown and the parser's semantic structure captures it, stop here. Most sections live entirely in this layer.
431
+ Start with the content, not the component. Write the markdown an author would naturally write, check what shape the parser produces (`uniweb inspect <file>`), *then* build the component to receive it. You have three layers, and most of the design skill is choosing between them:
470
432
 
471
- **Frontmatter params** — `columns: 3`, `variant: centered`, `theme: dark`. Configuration that an author might change but that isn't *content*. Would changing this value change the section's *meaning*, or just its *presentation*? Presentation → param. Meaning → content.
433
+ **Pure markdown** — headings, paragraphs, links, images, lists, items. The default. If the content reads naturally as markdown and the parser captures it, stop here.
472
434
 
473
- **Tagged data blocks** — for content that doesn't fit markdown patterns. Products with SKUs, team members with roles, event schedules, pricing metadata, form definitions. When the information is genuinely structured data that a content author still owns, a well-named tagged block (`yaml:pricing`, `yaml:speakers`, `yaml:config`) is clearer than contorting markdown into a data format. Supported formats: `yaml` and `json`. The format is a serialization format (how to parse the data), not a language for display. Tagged blocks are parsed at build time into JS objects and delivered as `content.data.tagName`.
474
-
475
- Read the markdown out loud. If a content author would understand what every line does and how to edit it, you've chosen the right layer. The moment markdown feels like it's encoding data rather than expressing content, step up to a tagged block — that's fine. A well-documented `yaml:pricing` block is better than a markdown structure that puzzles the author.
476
-
477
- **You are designing these, not choosing from a menu.** The examples in this guide illustrate patterns, not exhaustive inventories. Any param name works in `meta.js`. Any tag name works for data blocks. Any section type name works. The framework has fixed mechanisms (the content shape, the context modes, the token system); nearly everything else is yours to define.
478
-
479
- ```js
480
- // You design this — it's not a fixed schema
481
- export default {
482
- params: {
483
- columns: { type: 'number', default: 3 },
484
- cardStyle: { type: 'select', options: ['minimal', 'bordered', 'elevated'], default: 'minimal' },
485
- showIcon: { type: 'boolean', default: true },
486
- maxItems: { type: 'number', default: 6 },
487
- }
488
- }
489
- ```
435
+ **Frontmatter params** — `columns: 3`, `variant: centered`. Configuration an author might change but that isn't *content*. Would changing this alter the section's *meaning* or just its *presentation*? Presentation param. Meaning content.
490
436
 
491
- ````markdown
492
- <!-- You invent the tag name — the framework parses it -->
493
- ```yaml:speakers
494
- - name: Ada Lovelace
495
- role: Keynote
496
- topic: The Future of Computing
497
- ```
498
- ````
499
- Access: `content.data?.speakers` — an array of objects. You defined this. The framework parsed it.
437
+ **Tagged data blocks** — for content that doesn't fit markdown patterns: products with SKUs, team members with roles, event schedules, form definitions. When information is genuinely structured data that an author still owns, a well-named block (`yaml:pricing`, `yaml:speakers`) is clearer than contorting markdown. Formats: `yaml` and `json`. Parsed at build time into `content.data.tagName`.
500
438
 
501
- **Parameter naming matters.** Would an author understand the param without reading code? `columns: 3` yes. `gridCols: 3` no. `variant: centered` yes. `renderMode: flex-center` no. `align: left` yes. `contentAlignment: flex-start` no.
439
+ Read the markdown out loud. If an author would understand what every line does, you've chosen right. The moment markdown feels like it's encoding data rather than expressing content, step up to a tagged block.
502
440
 
503
- ### Multi-Line Headings
441
+ **When you are building the foundation, you are designing these, not choosing from a menu.** The examples in this guide illustrate patterns, not exhaustive inventories. Any param name works in `meta.js`, any tag name works for data blocks, any section type name works. The framework has fixed mechanisms (content shape, context modes, token system); nearly everything else is yours to define.
504
442
 
505
- Consecutive headings at the same level merge into a title array — a single heading split across visual lines:
443
+ **When you are writing content against a foundation that already exists, the opposite holds.** The vocabulary is fixed and finite: the section types are exactly what `sections/**` declares, and their params are exactly what each `meta.js` lists. Inventing a name there doesn't extend the system it renders a red `Component not found` box on a build that otherwise reports success.
506
444
 
507
- ```markdown
508
- # Build the future │ content.title = ["Build the future", "with confidence"]
509
- # with confidence │
510
- ```
445
+ **Parameter naming matters.** Would an author understand it without reading code? `columns: 3` yes, `gridCols: 3` no. `variant: centered` yes, `renderMode: flex-center` no. `align: left` yes, `contentAlignment: flex-start` no.
511
446
 
512
- Kit's `<H1>`, `<H2>`, etc. render arrays as a single tag with line breaks. This is how you create dramatic multi-line hero headlines.
447
+ ### Icons
513
448
 
514
- **Works with accent styling:**
449
+ Image syntax with a library prefix — **two interchangeable spellings, the same everywhere** (markdown, and Kit's `<Icon name>`):
515
450
 
516
451
  ```markdown
517
- # Build the future │ content.title = [
518
- # [with confidence]{accent} │ "Build the future",
519
- │ "<span accent=\"true\">with confidence</span>"
520
- │ ]
452
+ ![](lucide:house) colon + friendly name every library
453
+ ![](lu:house) colon + short code
454
+ ![](lu-house) dash — short codes only, to stay unambiguous with ordinary paths
521
455
  ```
522
456
 
523
- **Rule:** Same-level continuation only applies before going deeper. Once a subtitle level is reached, same-level headings start new items instead of merging. Use `---` to force separate items when same-level headings would otherwise merge.
524
-
525
- ### Icons
526
-
527
- Use image syntax with library prefix: `![](lu-house)`. Supported libraries: `lu` (Lucide), `hi2` (Heroicons), `fi` (Feather), `pi` (Phosphor), `tb` (Tabler), `bs` (Bootstrap), `md` (Material), `fa6` (Font Awesome 6), and others. Browse at [react-icons.github.io/react-icons](https://react-icons.github.io/react-icons/).
457
+ Short codes: `lu` (Lucide), `hi` / `hi2` (Heroicons v1 / v2), `fi` (Feather), `pi` (Phosphor), `tb` (Tabler), `bs` (Bootstrap), `md` (Material), `ai` (Ant Design), `ri` (Remix), `si` (Simple Icons), `io5` (Ionicons), `bi` (Boxicons), `vsc` (VS Code), `wi` (Weather), `gi` (Game), `fa` / `fa6` (Font Awesome 5 / 6). Browse at [react-icons.github.io/react-icons](https://react-icons.github.io/react-icons/).
528
458
 
529
- Custom SVGs: `![Logo](./logo.svg){role=icon}`
459
+ Optional attributes: `{size=20}`, `{color=red}`. Custom SVGs: `![Logo](./logo.svg){role=icon}` (or `![Logo](icon:./logo.svg)`) — always available, no library needed.
530
460
 
531
- ### Links and Media Attributes
461
+ ### Links and media attributes
532
462
 
533
463
  ```markdown
534
464
  [text](url){target=_blank} <!-- Open in new tab -->
@@ -536,32 +466,32 @@ Custom SVGs: `![Logo](./logo.svg){role=icon}`
536
466
  ![alt](./img.jpg){role=banner} <!-- Role determines array: images, icons, or videos -->
537
467
  ```
538
468
 
539
- **Quote values that contain spaces:** `{note="Ready to go"}` not `{note=Ready to go}`. Unquoted values end at the first space.
469
+ **Quote values containing spaces:** `{note="Ready to go"}`, not `{note=Ready to go}` unquoted values end at the first space.
540
470
 
541
- Standalone links (alone on a line) become buttons in `content.links[]`. Inline links stay as `<a>` tags within `content.paragraphs[]`. Multiple links sharing a paragraph are all promoted to `content.links[]`:
471
+ Standalone links (alone on a line) become buttons in `content.links[]`. Inline links stay as `<a>` tags inside `content.paragraphs[]`. Multiple links sharing a paragraph are all promoted:
542
472
 
543
473
  ```markdown
544
474
  [Primary](/start) ← standalone → content.links[0]
545
475
  [One](/a) [Two](/b) ← links-only paragraph → both in content.links[]
546
- Check out [this](/a) link. ← inline → stays in paragraphs as <a> tag
476
+ Check out [this](/a) link. ← inline → stays in paragraphs as an <a> tag
547
477
  ```
548
478
 
549
- ### Inline Text Styling
479
+ ### Inline text styling
550
480
 
551
481
  ```markdown
552
482
  # Build [faster]{accent} with structure
553
483
  This is [less important]{muted} context.
554
484
  ```
555
485
 
556
- `accent` and `callout` (both accent-colored + bold) and `muted` (subtle) are built-in defaults that adapt to context automatically. `--accent` is your brand color unless you declare a separate `colors.accent`, and resolves to the shade you authored — not a darkened one, since accent is decorative emphasis rather than body-size link text. Components receive HTML strings with spans applied: `<span accent="true">faster</span>`.
486
+ `accent` and `callout` (both accent-colored + bold) and `muted` (subtle) are built-in defaults that adapt to context. `--accent` is your brand color unless you declare a separate `colors.accent`, and resolves to the shade you authored — not a darkened one, since accent is decorative emphasis rather than body-size link text. Components receive HTML strings with spans applied: `<span accent="true">faster</span>`.
557
487
 
558
- Sites can adjust these or define additional named styles in `theme.yml`'s `inline:` section. Overrides merge **property by property**, so declare only what differs (`accent: { font-weight: inherit }` keeps the default color); to drop a default property rather than change it, give it a neutral value (`initial`, `inherit`, `unset`).
488
+ Sites can adjust these or add named styles in `theme.yml`'s `inline:` section. Overrides merge **property by property**, so declare only what differs (`accent: { font-weight: inherit }` keeps the default color); to drop a default property rather than change it, give it a neutral value (`initial`, `inherit`, `unset`).
559
489
 
560
- ### Fenced Code in Content
490
+ ### Fenced code: data blocks vs snippets
561
491
 
562
- Fenced code in markdown serves two distinct purposes depending on whether it has a tag:
492
+ Fenced code serves two purposes depending on whether it carries a tag.
563
493
 
564
- **Tagged data blocks** — structured data parsed into JS objects. The format (`yaml`/`json`) is a serialization format, not a display language. The tag is the key in `content.data`:
494
+ **Tagged data blocks** — structured data parsed into JS objects. The tag is the key in `content.data`; the format (`yaml`/`yml`/`json`) is a serialization format, not a display language.
565
495
 
566
496
  ````markdown
567
497
  ```yaml:form
@@ -572,354 +502,552 @@ submitLabel: Send
572
502
  ```
573
503
  ````
574
504
 
575
- Access: `content.data?.form` `{ fields: [...], submitLabel: "Send" }`. Supported formats: `yaml` (or `yml`) and `json`.
505
+ `content.data?.form` = `{ fields: [...], submitLabel: "Send" }`
576
506
 
577
- **Code snippets** — display content with a language for syntax highlighting. Available in `content.snippets` as `[{ language, code }]`:
507
+ **Code snippets** — display content with a language for syntax highlighting, collected in `content.snippets` as `[{ language, code }]`. Filter with `content.snippets.filter(s => s.language === 'css')`.
578
508
 
579
- ````markdown
580
- ```jsx
581
- function Hello() {
582
- return <h1>Hello world</h1>
583
- }
584
- ```
585
- ````
509
+ Both appear in `content.sequence`. `<Prose>` handles the difference automatically — it renders snippets with highlighting and skips tagged data blocks, which components access separately via `content.data`.
586
510
 
587
- Access: `content.snippets[0]` → `{ language: 'jsx', code: 'function Hello() {...}' }`. The `language` attribute is a display hint for syntax highlighting, not a parsing format. Filter by language: `content.snippets.filter(s => s.language === 'css')`.
511
+ ### Math (LaTeX)
588
512
 
589
- Both appear in `content.sequence` for document-order rendering. The difference: tagged data blocks are parsed and extracted to `content.data`; code snippets are preserved and collected in `content.snippets`. `<Prose>` handles this automatically it renders code snippets with syntax highlighting and skips tagged data blocks, which components access separately via `content.data`.
513
+ Authors write LaTeX directly; it compiles to MathML Core **at build time** no runtime math library, no extra CSS. Three forms, matching Pandoc / GitHub / VS Code / Jupyter / Obsidian convention:
590
514
 
591
- ### Math (LaTeX)
515
+ | Form | Mode |
516
+ |---|---|
517
+ | `$x^2$` | Inline |
518
+ | `$$x^2$$` | Display (block on its own line, inline display mid-paragraph) |
519
+ | ` ```math ` fence | Display (multi-line friendly) |
520
+ | `\$` | Literal `$` |
592
521
 
593
- Authors write LaTeX directly in markdown; the browser renders real math natively. The LaTeX is compiled to MathML Core **at build time** no runtime math library ships to the browser, no CSS from a math package is required.
522
+ **Disambiguation gotcha:** a `$…$` span counts as math only when the body has no whitespace next to the delimiters *and* the closing `$` isn't immediately followed by a digit. So `It costs $5 and $10 total` and `Budget: $200` stay prose without escaping, while `Let $f(x) = 5$ be a function` is math. Use `\$` when the rules would otherwise trip.
594
523
 
595
- Three forms, matching Pandoc / GitHub / VS Code / Jupyter / Obsidian convention:
524
+ Math rides the normal content pipeline — it appears in prerendered HTML, survives `compile('epub')` and `compile('pagedjs')`, and roundtrips through the editor. Component code needs nothing special.
596
525
 
597
- | Form | Mode | Example |
598
- |---|---|---|
599
- | `$x^2$` | Inline | `Let $f(x) = ax + b$ be a function.` |
600
- | `$$x^2$$` | Display (block when on its own line, inline display mid-paragraph) | `$$\sum_{i=1}^n i$$` |
601
- | ` ```math ` fence | Display (multi-line friendly) | see below |
602
- | `\$` | Literal `$` | `The price is \$20.` |
526
+ ### Lists as navigation menus
527
+
528
+ Markdown lists model nav, menus, and grouped links. Each list item is a full content object with `paragraphs`, `links`, `icons`, and nested `lists`.
603
529
 
604
- Multi-line display math uses a `math` code fence:
530
+ ```markdown
531
+ - ![](lu-home) [Home](/) ← content.lists[0], each item.links[0] + item.icons[0]
532
+ - ![](lu-book) [Docs](/docs)
533
+
534
+ - Product ← nested: group.paragraphs[0] is the label,
535
+ - [Features](/features) ← group.lists[0] holds sub-items
536
+ - [Pricing](/pricing)
537
+ ```
538
+
539
+ **For richer navigation** with icons, descriptions, or hierarchy, use a `yaml:nav` tagged block:
605
540
 
606
541
  ````markdown
607
- ```math
608
- \int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}
542
+ ```yaml:nav
543
+ - label: Dashboard
544
+ href: /
545
+ icon: lu:layout-grid # same icon spellings as anywhere else —
546
+ - label: Docs # lu:name, lucide:name, lu-name, or an SVG path
547
+ href: /docs
548
+ icon: /icons/book.svg
549
+ children:
550
+ - label: Getting Started
551
+ href: /docs/quickstart
609
552
  ```
610
553
  ````
611
554
 
612
- Disambiguation for `$...$`: a dollar-delimited span counts as math only when the body has no whitespace next to the delimiters *and* the closing `$` is not immediately followed by a digit. So `It costs $5 and $10 total` and `Budget: $200` stay as prose without any escaping, while `Let $f(x) = 5$ be a function` is math. Use `\$` when you need a literal `$` in content where the rules would otherwise trip it up.
555
+ Access: `content.data?.nav` an array of `{ label, href, icon, text, children, target }`. Components can support both modes: use `content.data?.nav` when provided, fall back to `website.getPageHierarchy()`. Full pattern: `reference/navigation-patterns.md`.
613
556
 
614
- Math rides through the same content pipeline as everything else — it appears in prerendered HTML, survives `compile('epub')` and `compile('pagedjs')`, and roundtrips cleanly through the editor. Component code needs nothing special; `<Prose>` and `<Text>` already render the MathML that the pipeline embedded.
557
+ ### Section backgrounds
615
558
 
616
- ### Composition: Nesting and Embedding
559
+ Set `background` in frontmatter — the runtime renders it:
617
560
 
618
- Pages are sequences of sections — that's the obvious composition layer. But the framework supports real nesting: sections containing other sections, and sections containing embedded components. And it does this without leaving markdown.
561
+ ```yaml
562
+ background: /images/hero.jpg # Image
563
+ background: /videos/hero.mp4 # Video
564
+ background: linear-gradient(135deg, #667eea, #764ba2) # Gradient
565
+ background: '#1a1a2e' # Color (hex — quote in YAML)
566
+ background: primary-900 # Palette token (bare name or var())
619
567
 
620
- **Insets embedding components in content.** Many section types need a "visual" — a hero's illustration, a split-content section's media. The classic is an image or video. But what if it's a JSX + SVG diagram? A ThreeJS animation? An interactive code playground?
568
+ background: # Object form for more control
569
+ image: { src: /img.jpg, position: center top }
570
+ overlay: { enabled: true, type: dark, opacity: 0.5 }
571
+ ```
621
572
 
622
- In other frameworks, this is where you'd reach for MDX, or prop-drill a component. In Uniweb, the content author writes:
573
+ Components that render their own background declare `background: 'self'` in `meta.js`.
623
574
 
624
- ```markdown
625
- ![Architecture overview](@NetworkDiagram){variant=compact}
575
+ ### Page organization
576
+
577
+ ```
578
+ site/layout/ # header.md, footer.md, left.md — rendered on every page
579
+ site/pages/home/
580
+ ├── page.yml # title, description, order
581
+ ├── 1-hero.md # Numeric prefix sets order
582
+ ├── 2-features.md
583
+ └── 3-cta.md
626
584
  ```
627
585
 
628
- Standard markdown image syntax `![alt](@Component){attributes}`. The content author placed a full React component with content and params, and it looks like an image reference. The developer builds `NetworkDiagram` as an ordinary React component with `inset: true` in its `meta.js`. The kit's `<Visual>` component renders the first non-empty candidate — so the same section type works whether the author provides a static image, a video, or an interactive component:
586
+ Decimals insert between: `2.5-testimonials.md` goes between `2-` and `3-`. **Ignored:** `README.md`, and `_*.md` / `_*/` (drafts and private files).
629
587
 
630
- ```jsx
631
- <Visual inset={block.insets[0]} video={content.videos[0]} image={content.images[0]} className="rounded-2xl" />
632
- ```
588
+ ```yaml
589
+ # page.yml
590
+ title: About Us
591
+ id: about # Stable identity (for page: links, survives moves)
592
+ order: 2 # Navigation sort position
593
+ pages: [team, history, ...] # Child page order (... = rest). Without ... = strict (hides unlisted)
594
+ redirect: academic # Redirect to a child page (relative/absolute path, or URL)
595
+ slug: { fr: a-propos } # Localized URL segment per language
633
596
 
634
- The content author controls what goes in the visual slot. The developer's component doesn't need to know or care whether it's rendering an image or a ThreeJS scene.
597
+ # site.yml
598
+ index: home # Just set the homepage
599
+ pages: [home, about, ...] # Order pages (... = rest, first = homepage); without ... = strict
600
+ ```
635
601
 
636
- **Child sections composing layouts from reusable pieces.** You encounter a complex layouta 2:1 split with a panel and a main area, or a grid with different card types in each cell. Your instinct says: build a specialized component. But step back.
602
+ **Configuration cascades: `page.yml` `folder.yml` `site.yml` → foundation defaults.** Each level inherits from the one above and overrides specific values, the way CSS specificity works. This is what makes bulk assignment natural put `layout: marketing` in a `folder.yml` and every page in that folder inherits it, while one page can still override with its own `page.yml`. Reach for `folder.yml` before editing the same key into a dozen `page.yml` files.
637
603
 
638
- The panel? A reusable section type. The main area? Another one. The split? A Grid with `columns: "1fr 2fr"`. And your child components already adapt to narrow containers container queries handle that.
604
+ **Route mapping:** folder structure maps 1:1 to routes. Every folder keeps its natural route `pages:` controls **order only**, not which child "becomes" the parent. The only exception is the site root, where `index:` (or first in `pages:`) sets `/`.
639
605
 
640
- But if you hardcode which components go where, the author can't rearrange or swap them. This is where child sections solve it:
606
+ **Content-less containers:** folders with `page.yml` but no markdown are structural groups (`hasContent: false`). Visiting one auto-redirects to the first descendant with content this is what supports courses modules → lessons at any depth.
641
607
 
642
- ```
643
- pages/home/
644
- ├── 2-dashboard.md # type: Grid, columns: "1fr 2fr"
645
- ├── @sidebar-stats.md # type: StatPanel
646
- └── @main-chart.md # type: PerformanceChart
647
- ```
608
+ **SEO & social cards:** set site-wide defaults in `site.yml`; any page overrides per-field in `page.yml` (page wins, site fills gaps). These render into every page's static `<head>` — Open Graph, Twitter Card, canonical, robots — so shares and crawlers see them without running JS. The social `image` is the field most worth setting once at site level.
648
609
 
649
610
  ```yaml
611
+ # site.yml
612
+ keywords: [components, react, cms]
613
+ seo:
614
+ image: /og-default.png
615
+ noindex: false # true keeps the WHOLE site out of search
616
+
650
617
  # page.yml
651
- nest:
652
- dashboard: [sidebar-stats, main-chart]
618
+ seo:
619
+ image: /og-about.png
620
+ canonical: https://acme.com/about
653
621
  ```
654
622
 
655
- Each child is a regular section with its own type, params, and content. The Grid renders them with `<ChildBlocks from={block} />` and you're in the middle: you can wrap each child, filter by type, reorder, add container classes. The author decides *what* goes in the grid; your component decides *how* it's rendered.
623
+ **Localized URLs:** on a multilingual site (`languages:` in site.yml), `slug: { <lang>: <segment> }` gives a page a native URL segment per language; the folder name stays the canonical route. Nested folders compose automatically, and localized URLs flow through navigation, the language switcher, and the sitemap. See Part 5 for the translation workflow.
656
624
 
657
- The author can swap a child for a different section type tomorrow without the developer changing a line of code. And the developer's components are reusable wherever child sections are accepted, not locked to this one layout.
625
+ ### Collections and dynamic routes
658
626
 
659
- **Choosing the right pattern:**
627
+ Most content lives in `pages/` — a fixed composition of sections on a fixed set of pages. **Collections are the other kind: repeating content managed as a set of files**, one item per file, that pages pull from. Blog posts, team members, products, case studies, bibliographies.
660
628
 
661
- | Pattern | How authored | Use when |
662
- |---|---|---|
663
- | **Items** (`content.items`) | Heading groups within one `.md` file | Repeating content within one section: cards, features, FAQ entries |
664
- | **Child sections** (`block.childBlocks`) | `@`-prefixed `.md` files + `nest:` | Children that need their own section type, rich content, or independent editing |
665
- | **Insets** (`block.insets`) | `![](@Component)` in markdown | Self-contained visuals/widgets: charts, diagrams, code demos |
629
+ They're delivered through the **same data pipeline as remote APIs**, so from a component's point of view a locally-authored collection and a backend-served one look identical. Whether the records live in files or behind an endpoint is a transport concern (see *Fetching from other sources* in Part 4).
630
+
631
+ ```
632
+ site/
633
+ ├── pages/
634
+ ├── collections/
635
+ │ ├── articles/
636
+ │ │ ├── getting-started.md
637
+ │ │ └── design-tips.md
638
+ │ └── team/
639
+ │ └── alice.yml
640
+ └── site.yml
641
+ ```
666
642
 
667
- Does the content author write content *inside* the nested element? **Yes** child sections. **No** (self-contained, param-driven) inset. Repeating same-structure groups within one section → items. These compose: a child section can contain insets, items work inside children.
643
+ **Four formats, one shape.** All of these produce the same records at runtime:
668
644
 
669
- Inset components declare `inset: true` in meta.js. Don't use `hidden: true` on insets — `hidden` means "don't export this component at all" (for internal helpers), while `inset: true` means "available for `@Component` references in markdown."
645
+ | Format | Best for | Notes |
646
+ |---|---|---|
647
+ | `.md` | Items with prose — articles, case studies | Frontmatter holds fields, body holds text; auto-excerpt and first-image extraction |
648
+ | `.yml` / `.yaml` | Structured records with no prose body — team, products | Cleaner than a markdown file with an empty body |
649
+ | `.json` | Same as YAML | When the data comes out of another tool |
650
+ | `.bib` | Bibliographies | Each `@entry{key, …}` is a record, cite key is the slug; normalized to CSL-JSON, LaTeX accents converted to Unicode |
670
651
 
671
- **What inset components receive:** Insets are full section types they get `{ content, params, block }` like any other section. The alt text becomes `content.title`, and attributes become `params`:
652
+ **One record per file, or many.** A single mapping at the top of a file makes one record and the filename stem becomes its `slug`. A top-level array (YAML/JSON) or a multi-entry `.bib` makes many, each carrying its own `slug`. You can mix both in one folder an exported `refs.bib` beside a hand-written `extras.yml`.
672
653
 
673
- ```markdown
674
- ![npm create uniweb](@CommandBlock){note="Ready to go"}
675
- ```
676
- → CommandBlock receives `content.title = "npm create uniweb"` and `params.note = "Ready to go"`.
654
+ **Keep collection folders flat.** `collections/articles/design-tips.md` works; `collections/articles/2025/design-tips.md` does not.
677
655
 
678
- **SSG:** Insets, `<ChildBlocks>`, and `<Visual>` all render correctly during prerender. Inset components that use React hooks internally (useState, useEffect) will trigger prerender warnings this is expected and harmless; the page renders correctly client-side.
656
+ Item frontmatter conventionally uses `title`, `date`, `tags`, `image`, `description`, `published`, `author` plus any fields your content needs (`price`, `role`, `order`). Images can sit beside the item file and be referenced with `./`. `published: false` hides an item without deleting it; items with no `published` field are included.
679
657
 
680
- ### Section Nesting Details
658
+ **Declare each collection in `site.yml`:**
681
659
 
682
- ```
683
- pages/home/
684
- ├── page.yml
685
- ├── 1-hero.md
686
- ├── 2-features.md # Parent section (type: Grid)
687
- ├── 3-cta.md
688
- ├── @card-a.md # Child of features (@ = not top-level)
689
- ├── @card-b.md
690
- └── @card-c.md
660
+ ```yaml
661
+ collections:
662
+ articles: collections/articles # simple form — just point at the folder
663
+
664
+ team: # extended form
665
+ path: collections/team # or `url:` for a remote source
666
+ sort: order asc # `date desc`, `title asc`,
667
+ where: { published: { ne: false } } # a predicate — see authoring/predicates.md
668
+ limit: 100
691
669
  ```
692
670
 
671
+ **Show a collection on a page** with `data:` in `page.yml` (the whole collection), or `fetch:` in a section's frontmatter (a subset):
672
+
693
673
  ```yaml
694
- # page.yml
695
- nest:
696
- features: [card-a, card-b, card-c]
674
+ # pages/blog/page.yml | # a section on the homepage
675
+ title: Blog | ---
676
+ data: articles | type: ArticleTeaser
677
+ | fetch: { collection: articles, limit: 3, sort: date desc }
678
+ | ---
697
679
  ```
698
680
 
699
- **Rules:**
700
- - `@`-prefixed files are excluded from the top-level section list
701
- - `nest:` declares parent-child relationships (parent name → child names)
702
- - `@@` prefix for deeper nesting (grandchildren)
703
- - `nest:` is flat: `{ features: [a, b], a: [sub-1] }`
704
- - Children ordered by position in the `nest:` array
705
-
706
- ```jsx
707
- import { ChildBlocks } from '@uniweb/kit'
681
+ **Give each item its own page with a `[slug]/` folder** under the list page:
708
682
 
709
- export default function Grid({ block, params }) {
710
- return (
711
- <div className={`grid grid-cols-${params.columns || 2} gap-6`}>
712
- <ChildBlocks from={block} />
713
- </div>
714
- )
715
- }
683
+ ```
684
+ pages/blog/
685
+ ├── page.yml # title: Blog / data: articles
686
+ ├── list.md
687
+ └── [slug]/
688
+ ├── page.yml
689
+ └── article.md # just `type: Article` — the item arrives automatically
716
690
  ```
717
691
 
718
- `ChildBlocks` renders each child as a bare component by default — no wrapper element, no context classes, no background. This is the right behavior for grid cells, tab panels, carousel slides, and inline children where the parent controls the container.
692
+ `collections/articles/design-tips.md` becomes `/blog/design-tips`. The section inside `[slug]/` needs no special markdown the matched record is delivered to it. Generated pages are excluded from navigation menus.
719
693
 
720
- For the rare case where children should be independent sections with their own theming and backgrounds, pass `wrapAs`:
694
+ > **The record arrives as a single-element array under the collection key** `content.data.articles[0]`, not `content.data.article`. The runtime never coerces it to an object and never synthesizes a singular key. See *Data* in Part 4.
721
695
 
722
- ```jsx
723
- <ChildBlocks from={block} wrapAs="div" />
724
- ```
696
+ **Two options for bigger collections:**
697
+
698
+ `deferred: [body]` strips heavy fields from the list payload — cards stay light, while a `[slug]` page still receives the full record automatically and other components fetch on demand via `useEntityDetail`. For a remote collection, add `detailUrl: /api/articles/{slug}` so the framework knows how to fetch one full record; file-based collections emit per-record files at `/data/<name>/<slug>.json` and need no configuration.
725
699
 
726
- **Data and child blocks:** Page-level `data:` is available to all blocks on the page, including children. Each child block resolves data independently through the same page → site hierarchy. If a child component needs data, declare it in the child's `meta.js` or in the child section's frontmatter (`data: articles`).
700
+ `queryable:` declares which fields a reader may filter on, with enough metadata for the foundation to render controls:
727
701
 
728
- ### Dividers — Content Boundaries
702
+ ```yaml
703
+ collections:
704
+ members:
705
+ path: collections/members
706
+ queryable:
707
+ department: { type: enum, label: Department, options: [biology, physics, chemistry] }
708
+ tenured: { type: boolean, label: Tenured }
709
+ start_year: { type: range, label: Start year, min: 1800, max: 2025 }
710
+ ```
729
711
 
730
- The `---` (horizontal rule) in markdown creates a boundary between content regions. The developer decides what each region means. Two patterns:
712
+ The site declares the *surface*; the foundation reads the metadata, renders matching controls (dropdown, toggle, slider), and composes the predicate when the reader picks values.
731
713
 
732
- **Data-driven iteration (Loom).** Dividers separate header/body/footer in a repeated template. The content handler splits *before* Loom runs because each segment gets a different variable context — the body template contains item-level fields that don't exist on the top-level data. The header and footer are instantiated once against the full data; the body is repeated per data item.
714
+ Full author guide: `authoring/collections.md`. Predicate operators: `authoring/predicates.md`.
733
715
 
734
- ```markdown
735
- ---
736
- type: CvEntry
737
- source: education
738
716
  ---
739
- # Education
740
- {COUNT OF education} degrees.
741
- ---
742
- ## {degree}
743
- {institution} — {field} ({start}–{end})
717
+
718
+ ## Part 4 — The foundation lane
719
+
720
+ You're not building pages — you're building a **system** of section types that authors compose into pages. Name by purpose, not content: `Testimonial` not `WhatClientsSay`, `SplitContent` not `AboutSection`. Expect consolidation: a React site with 30+ components typically maps to 8–15 Uniweb section types.
721
+
722
+ ### Where things go
723
+
724
+ ```
725
+ src/ # the foundation package (folder name is `src`)
726
+ ├── sections/ # Section types (auto-discovered)
727
+ │ ├── Hero.jsx # Bare file — no folder needed
728
+ │ ├── Features/ # Folder when you need meta.js
729
+ │ │ ├── index.jsx
730
+ │ │ └── meta.js
731
+ │ └── insets/ # Organizational subdirectory (lowercase)
732
+ ├── layouts/ # Custom layouts (optional, auto-discovered)
733
+ ├── components/ # Your React components — any props you like, no meta.js
734
+ ├── utils/ # Helper functions, non-React logic
735
+ ├── main.js
736
+ ├── styles.css
737
+ └── package.json # name: "src"
744
738
  ```
745
739
 
746
- The `source` frontmatter param names the data array to iterate. The content handler (see "Content Handlers" below) reads it. A second `---` starts a footer, rendered once after all items.
740
+ **Discovery:** PascalCase files and folders at the root of `sections/` are auto-discovered. Nested levels require a `meta.js`. Lowercase directories are organizational only. `hidden: true` excludes a component entirely. Everything outside `sections/` and `layouts/` is ordinary React.
747
741
 
748
- **UI regions (component).** Dividers separate structural areas that the component renders differently e.g., lesson prose vs challenge content. `splitContent()` from `@uniweb/kit` splits the parsed content at divider elements in the sequence:
742
+ **Source root.** The foundation package's source lives at the package rootthe `src/` folder *is* the foundation. The build reads `package.json::main` (for new scaffolds, `main: "./_entry.generated.js"`). Older foundations nest source in `foundation/src/` with `main` pointing at `./src/_entry.generated.js`; both work through the same code path.
743
+
744
+ **Import aliases.** Foundations include subpath imports for shared internals — use them instead of brittle relative paths:
745
+
746
+ | Alias | Maps to | Use for |
747
+ |-------|---------|---------|
748
+ | `#components/*` | `./components/*` | Shared React components |
749
+ | `#utils/*` | `./utils/*` | Helper functions, non-React logic |
749
750
 
750
751
  ```jsx
751
- import { splitContent } from '@uniweb/kit'
752
+ import LessonHeader from '#components/LessonHeader' // ✅
753
+ import LessonHeader from '../../components/LessonHeader' // ❌ breaks if you reorganize sections/
754
+ ```
752
755
 
753
- function Lesson({ content, block }) {
754
- const [lesson, challenge] = splitContent(content)
755
- return (
756
- <div>
757
- <Prose content={lesson} block={block} />
758
- <aside><Prose content={challenge} block={block} /></aside>
759
- </div>
760
- )
756
+ Within the same directory, use normal relative imports (`./AIFeedbackCard`).
757
+
758
+ **Foundation entry (`main.js`).** A single `export default { … }` whose top-level keys are the capabilities the foundation provides — `name`, `description`, `defaultLayout`, `defaultSection`, `viewTransitions`, `props`, `defaultInsets`, `xref`, `outputs`, `handlers` — plus an optional named `vars` export. Section types and layouts are auto-discovered and merged in by `@uniweb/build`. The build wraps your default export under `default.capabilities` in `dist/entry.js`; you never write that wrapper. The one place it matters: when you import your **own** `main.js` from a component (e.g. a download button calling `compileDocument(website, { foundation })`), you get the bare default object — pass it through directly, Press handles both shapes.
759
+
760
+ ### Props interface
761
+
762
+ ```jsx
763
+ function MyComponent({ content, params, block }) {
764
+ const { title, paragraphs, links, items } = content // Guaranteed shape
765
+ const { columns, variant } = params // Defaults from meta.js
766
+ const { website } = useWebsite() // Or block.website
761
767
  }
762
768
  ```
763
769
 
764
- **When to use which:** Different data contexts per region Loom pre-parse split (handled by the content handler). Same data, different UI treatment kit post-parse split (`splitContent`). A foundation can use both — Loom splits and iterates to produce final content, then the component splits the result to route regions to different UI.
770
+ Frontmatter becomes `params`, minus the keys the framework consumes outright: `type` (and its legacy alias `component`), `preset`, `input`, `props`, `fetch`, `data`, `id`. `props:` is the one that isn't dropped but merged *into* params.
765
771
 
766
- ### Section Backgrounds
772
+ **Framework fields you'd expect to be stripped are not.** `background`, `theme`, `source`, `where`, and `vars` are acted on by the runtime *and* passed through — so `params.theme` is readable when a component needs logic beyond CSS tokens (a light vs. dark logo, say). Components ignore the keys they don't use, the same way they ignore unused `content.data` keys.
767
773
 
768
- Set `background` in frontmatter — the runtime renders it automatically:
774
+ ### Rendering content with Kit
769
775
 
770
- ```yaml
771
- background: /images/hero.jpg # Image
772
- background: /videos/hero.mp4 # Video
773
- background: linear-gradient(135deg, #667eea, #764ba2) # Gradient
774
- background: '#1a1a2e' # Color (hex — quote in YAML)
775
- background: primary-900 # Palette token (bare name or var())
776
- ```
776
+ Content fields are **HTML strings** — they contain `<strong>`, `<em>`, `<a>` from markdown. **Never render them with raw `{content.title}` in JSX** — that shows HTML tags as visible text. Use Kit components:
777
777
 
778
- Object form for more control:
778
+ ```jsx
779
+ import { H1, H2, H3, P, Span } from '@uniweb/kit'
779
780
 
780
- ```yaml
781
- background:
782
- image: { src: /img.jpg, position: center top }
783
- overlay: { enabled: true, type: dark, opacity: 0.5 }
781
+ <H1 text={content.title} className="text-heading text-5xl font-bold" />
782
+ <H2 text={content.subtitle} className="text-heading text-2xl" />
783
+ <P text={content.paragraphs} className="text-body" />
784
+ <Span text={listItem.paragraphs[0]} className="text-subtle" />
784
785
  ```
785
786
 
786
- Components that render their own background declare `background: 'self'` in `meta.js`.
787
+ Kit provides `H1`–`H6`. These render their own HTML tag **don't wrap them**: `<H2 text={…} />`, not `<h2><H2 text={…} /></h2>`.
787
788
 
788
- ### Page Organization
789
+ **Full content rendering** (article/docs sections where the author controls the flow):
790
+
791
+ ```jsx
792
+ import { Section, Prose } from '@uniweb/kit'
789
793
 
794
+ <Section block={block} width="lg" padding="md" />
795
+ <Prose content={content} block={block} />
790
796
  ```
791
- site/layout/
792
- ├── header.md # type: Header — rendered on every page
793
- ├── footer.md # type: Footer — rendered on every page
794
- └── left.md # type: Sidebar — optional sidebar
795
797
 
796
- site/pages/
797
- └── home/
798
- ├── page.yml # title, description, order
799
- ├── hero.md # Single section
800
- └── (or for multi-section pages:)
801
- ├── 1-hero.md # Numeric prefix sets order
802
- ├── 2-features.md
803
- └── 3-cta.md
798
+ `Prose` renders from the parsed sequence — headings, paragraphs, images, snippets, lists — with prose typography. Tagged data blocks are **skipped**; access them via `content.data` for custom rendering. Pass `content` (needs `.sequence`), and `block` too if the content uses insets. Also works as a pure typography wrapper: `<Prose>{children}</Prose>`.
799
+
800
+ `Article` is an older alternative rendering from `block.rawContent` (raw ProseMirror), including data blocks. Prefer `Prose` for new components.
801
+
802
+ ### The Kit, by use case
803
+
804
+ Names only — for signatures and props, read the package: it's on disk at `node_modules/@uniweb/kit`, and `reference/kit-reference.md` has the full API.
805
+
806
+ **Text:** `H1`–`H6`, `P`, `Span`, `Div`, `Text` (with `as`)
807
+ **Content:** `Section`, `Prose`, `Article`, `Render` (ProseMirror → React), `ChildBlocks`, `splitContent()`
808
+ **Media:** `Visual` (first non-empty: inset/video/image), `Image`, `Media`, `Icon`, `Asset`
809
+ **Navigation:** `Link`, `useActiveRoute()`, `useWebsite()`, `useRouting()`
810
+ **Header/layout:** `useScrolled(threshold)`, `useMobileMenu()`, `useAppearance()`
811
+ **Layout helpers:** `useGridLayout(columns, { gap })`, `useAccordion({ multiple, defaultOpen })`, `useTheme(name)`
812
+ **Theming data:** `useThemeData()`, `useColorContext(block)`
813
+ **Data fetching:** `useFetched`, `useCacheEntry`, `useEntityDetail`
814
+ **Utilities:** `cn()`, `SafeHtml`, `SocialIcon`, `filterSocialLinks(links)`, `getSocialPlatform(url)`, `getLocaleLabel(locale)`
815
+ **Other styled:** `SidebarLayout`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
816
+
817
+ Four things you won't discover by reading exports:
818
+
819
+ > **Content fields are HTML strings.** Covered above — it's the single most common rendering bug.
820
+
821
+ > **`<Link>` takes `to` or `href`.** `to="page:about"` resolves a page ID (survives moves), external URLs get `target="_blank"` automatically, and `reload` forces a full page load.
822
+
823
+ > **`cn()` gotcha — a later `text-<size>` silently drops an earlier `leading-*`.** Tailwind's size classes set line height too, so `cn()` treats the size as replacing the leading: `cn('leading-[1.1] text-4xl')` → `text-4xl`. Put the size first, or fold the leading into it (`text-[clamp(2rem,5vw,4rem)]/[1.1]`). Most likely to bite when the size comes from a lookup and the leading sits in a shared base string.
824
+
825
+ > **Kit hooks are SSR-safe by design.** If you hit "document is not defined" during a build, don't add `typeof document` guards — reach for the hook instead (`useAppearance()` for scheme, `useScrolled()` for scroll position).
826
+
827
+ ### Icon component
828
+
829
+ ```jsx
830
+ {content.icons.map((icon, i) => <Icon key={i} {...icon} />)} // From content
831
+ <Icon name="search" /> // Lucide (default)
832
+ <Icon name="hi2-arrow-right" /> // Other library by prefix
833
+ <Icon name="close" /> // Built-in (no network)
804
834
  ```
805
835
 
806
- Decimals insert between: `2.5-testimonials.md` goes between `2-` and `3-`.
836
+ Built-ins (instant, no network): `check`, `close`, `menu`, `chevronDown`, `chevronRight`, `externalLink`, `download`, `play`, and a few others. Other props: `svg`, `url`, `size` (default `'24'`), `className`.
807
837
 
808
- **Ignored:** `README.md` (repo docs), `_*.md` or `_*/` (drafts/private).
838
+ ### Section wrapper
809
839
 
810
- **page.yml:**
811
- ```yaml
812
- title: About Us
813
- description: Learn about our company
814
- id: about # Stable identity (for page: links, survives moves)
815
- order: 2 # Navigation sort position
816
- pages: [team, history, ...] # Child page order (... = rest). Without ... = strict (hides unlisted)
817
- redirect: academic # Redirect to child page (relative or absolute path, or URL)
818
- slug: { fr: a-propos } # Localized URL segment per language (multilingual sites)
840
+ The runtime wraps every section in `<section>` with its context class and background. Customize with static properties:
841
+
842
+ ```jsx
843
+ function Hero({ content, params }) {
844
+ return <div className="max-w-7xl mx-auto px-6">…</div>
845
+ }
846
+
847
+ Hero.className = 'pt-32 md:pt-48' // adds classes to the runtime wrapper
848
+ Hero.as = 'div' // changes the wrapper element
849
+ export default Hero
819
850
  ```
820
851
 
821
- **site.yml:**
822
- ```yaml
823
- index: home # Just set the homepage
824
- pages: [home, about, ...] # Order pages (... = rest, first = homepage)
825
- pages: [home, about] # Strict: only listed pages in nav
852
+ **Layout components typically need `p-0`** to suppress default padding:
853
+
854
+ ```jsx
855
+ Header.className = 'p-0'
856
+ Header.as = 'header'
826
857
  ```
827
858
 
828
- **SEO & social cards:** set the site's default social card + keywords in `site.yml`; any page overrides per-field in `page.yml` (page wins, site fills the gaps). These render into every page's static `<head>` — Open Graph, Twitter Card, canonical, robots — so shares and crawlers see them without running JS. The social `image` is the field most worth setting once at the site level.
829
- ```yaml
830
- # site.yml — defaults for the whole site (incl. the homepage card)
831
- keywords: [components, react, cms]
832
- seo:
833
- image: /og-default.png # social-card image
834
- ogTitle: Acme
835
- noindex: false # true keeps the WHOLE site out of search
859
+ ### meta.js
836
860
 
837
- # page.yml — overrides for one page
838
- seo:
839
- image: /og-about.png
840
- ogTitle: About Acme
841
- canonical: https://acme.com/about
861
+ ```javascript
862
+ export default {
863
+ title: 'Feature Grid',
864
+ description: 'Grid of feature cards with icons',
865
+ category: 'showcase', // free-form editor grouping — nothing validates it.
866
+ // impact / showcase / structure is a suggested set.
867
+ // It names the kind of *component*, not the kind of
868
+ // site: a genre like 'marketing' belongs to a template
869
+ purpose: 'Explain', // free-form; a single verb reads well
870
+ // hidden: true, // Exclude from export entirely (internal helpers)
871
+ // background: 'self', // Component renders its own background
872
+ // inset: true, // Available for @ComponentName in markdown
873
+ // visuals: 1, // Expects 1 visual
874
+ // children: true, // Accepts child sections
875
+
876
+ content: {
877
+ title: 'Section heading',
878
+ paragraphs: 'Introduction [0-1]',
879
+ items: 'Feature cards with icon, title, description',
880
+ },
881
+
882
+ params: {
883
+ columns: { type: 'number', default: 3 },
884
+ variant: { type: 'select', options: ['default', 'centered', 'split'], default: 'default' },
885
+ },
886
+
887
+ presets: {
888
+ default: { label: 'Standard', params: { columns: 3 } },
889
+ compact: { label: 'Compact', params: { columns: 4 } },
890
+ },
891
+
892
+ // context / initialState: keys are developer-defined, not framework fields.
893
+ context: {}, // Static — neighbors read via getNextBlockInfo().context
894
+ initialState: {}, // Dynamic — neighbors read .state; component updates via useBlockState()
895
+ }
842
896
  ```
843
897
 
844
- **Route mapping:** Folder structure maps 1:1 to routes. Every folder keeps its natural route `pages:` controls **order only**, not which child "becomes" the parent. The only exception is the site root: `index:` (or first in `pages:`) in site.yml sets the homepage at `/`.
898
+ **All defaults belong in `meta.js`, not inline in component code.** `meta.js` is also the catalog entry another agent reads to discover your section type (Part 2, step 2) write `description` and `content:` for that reader.
845
899
 
846
- **Localized URLs:** On a multilingual site (`languages:` in site.yml), a page's `slug: { <lang>: <segment> }` gives it a native URL segment per language (`/about` `/fr/a-propos`); the folder name stays the canonical route. Nested folders compose automatically, and localized URLs flow through navigation, the language switcher, and the sitemap.
900
+ > **`meta.js` is a user interface, not just metadata.** When a foundation is published, every `meta.js` is registered as the foundation's schema, and the visual editor builds its controls from it: the build generates a `schema.json` from every `meta.js` in the foundation, and the editor renders it: a `select` param with `options` becomes a dropdown, a `boolean` becomes a toggle, `label` and `description` become the words a non-technical author reads, and each entry in `presets` becomes a one-click choice. That's the real reason param naming matters — `variant: centered` and `renderMode: flex-center` aren't a style preference, they're the difference between a legible control and a baffling one. Write `meta.js` as though someone who will never see your code has to use it, because that's exactly who does.
847
901
 
848
- **Draft languages:** `publishLanguages: [en, fr]` in site.yml lists which declared languages a published build ships — unlisted ones stay dev-previewable drafts. Absent field = publish all declared; the default language must be listed.
902
+ ### Two worlds: section types and your own components
849
903
 
850
- **Content-less containers:** Folders with `page.yml` but no markdown are structural groups. They appear in `getPageHierarchy()` with `hasContent: false` and their own title/label. When visited directly, the runtime auto-redirects to the first descendant with content. This supports hierarchical navigation (courses modules lessons) at any depth.
904
+ The `{ content, params, block }` interface is not "how components work in Uniweb." It is how **section types** work the components an author names with `type:` in frontmatter. A section type is a *public interface*: its name, params, and content expectations are a contract with content authors, and `meta.js` is its documentation.
851
905
 
852
- ### Lists as Navigation Menus
906
+ **Everything under `components/` is yours.** It's a normal React project in there, and the framework has no opinion about it:
853
907
 
854
- Markdown lists model nav, menus, and grouped links. Each list item is a full content object with `paragraphs`, `links`, `icons`, and nested `lists`.
908
+ - **Any props you want** `<PricingCard tier={tier} price={price} featured />`. No `content`, no `params`, no `block` unless you choose to pass them.
909
+ - **Any composition** — children, render props, context, `forwardRef`, custom hooks, whatever the job needs.
910
+ - **Any library** — install it and import it.
911
+ - **No `meta.js`, no naming convention, no auto-discovery, no author visibility.**
855
912
 
856
- **Header nav:**
857
- ```markdown
858
- - ![](lu-home) [Home](/)
859
- - ![](lu-book) [Docs](/docs)
860
- - ![](lu-mail) [Contact](/contact)
861
- ```
862
- Access: `content.lists[0]` — each item has `item.links[0]` and `item.icons[0]`.
913
+ Two symptoms of getting this backwards, both worth checking yourself against:
863
914
 
864
- **Footer columns:**
865
- ```markdown
866
- - Product
867
- - [Features](/features)
868
- - [Pricing](/pricing)
869
- - Company
870
- - [About](/about)
871
- - [Careers](/careers)
872
- ```
873
- Access: `content.lists[0]` — `group.paragraphs[0]` (label), `group.lists[0]` (sub-items with `subItem.links[0]`).
915
+ > **Don't give helper components a `{ content, params }` signature.** Copying the section interface downward is the most common version of this mistake. It makes every helper harder to reuse and hides what it actually needs. A card that takes `title`, `price`, and `features` is better than one that takes `content` and digs through it.
874
916
 
875
- Render list item text with Kit components list items contain HTML strings, not plain text:
917
+ > **Don't put helpers in `sections/`.** Anything at the root of `sections/` becomes author-selectable vocabulary. A `Button` or `Card` appearing in the editor's section picker is a bug in your foundation's interface design, not a harmless extra.
918
+
919
+ **A section type is usually a composite.** Its job is to translate parsed content into whatever your own components expect, then arrange them:
876
920
 
877
921
  ```jsx
878
- content.lists[0]?.map((group, i) => (
879
- <div key={i}>
880
- <Span text={group.paragraphs[0]} className="font-semibold text-heading" />
881
- <ul>
882
- {group.lists[0]?.map((subItem, j) => (
883
- <li key={j}><Link to={subItem.links[0]?.href}>{subItem.links[0]?.label}</Link></li>
922
+ // sections/Pricing/index.jsx
923
+ import PricingCard from '#components/PricingCard'
924
+ import formatPrice from '#utils/formatPrice'
925
+
926
+ export default function Pricing({ content, params }) {
927
+ const currency = params.currency || 'USD'
928
+ return (
929
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-6xl mx-auto">
930
+ {content.items.map((tier, i) => (
931
+ <PricingCard key={i} tier={tier} price={formatPrice(tier.data, currency)} />
884
932
  ))}
885
- </ul>
886
- </div>
887
- ))
933
+ </div>
934
+ )
935
+ }
888
936
  ```
889
937
 
890
- **For richer navigation with icons, descriptions, or hierarchy**, use `yaml:nav` tagged blocks:
938
+ The author writes `type: Pricing` and defines tiers as content items. `PricingCard` knows nothing about Uniweb — it takes a tier and a price. That boundary is what makes it testable, reusable across section types, and replaceable without touching the author-facing contract.
891
939
 
892
- ````markdown
893
- ```yaml:nav
894
- - label: Dashboard
895
- href: /
896
- icon: lu:layout-grid
897
- - label: Docs
898
- href: /docs
899
- icon: lu:book-open
900
- children:
901
- - label: Getting Started
902
- href: /docs/quickstart
940
+ **There are many ways to use `components/`**, and no expected shape. Presentational primitives shared across section types (buttons, cards, badges). A thin wrapper adapting the parsed content shape to a third-party component's props. Layout scaffolding several section types share. Stateful widgets. Legacy components carried in during a migration. The Front Desk pattern below is *one* specific arrangement worth knowing — not a requirement, and not the main reason to have helper components.
941
+
942
+ ### The Front Desk pattern
943
+
944
+ One arrangement of the above is worth naming, because it comes up whenever a single `type:` needs to cover genuinely different renderings.
945
+
946
+ Section types naturally use params to adjust their own rendering — `variant: flipped` reverses a flex direction, `columns: 3` sets a grid. That's the baseline, not a pattern. The **Front Desk pattern** is the step beyond it: a section type that does virtually no rendering itself, instead reading the author's params, picking the right helper component, and translating author-friendly vocabulary into developer-oriented props.
947
+
948
+ The workers behind the desk need not share an interface. A `Hero` might delegate to a `SliderHero` (image carousel) and a `ContactHero` (quote form) expecting different content and different props. The front desk declares the **union** of everything its workers might need — some content goes unused for a given variant, and that's normal in CCA: params change behavior, and that includes not rendering some content.
949
+
950
+ ```js
951
+ // meta.js — the union of all variants' needs
952
+ export default {
953
+ params: {
954
+ variant: { type: 'select', options: ['slider', 'contact'], default: 'slider' },
955
+ slideInterval: { type: 'number', default: 5 },
956
+ density: { type: 'select', options: ['default', 'compact'], default: 'default' },
957
+ style: { type: 'select', options: ['default', 'dramatic'], default: 'default' },
958
+ }
959
+ }
903
960
  ```
904
- ````
905
961
 
906
- Access: `content.data?.nav` — array of `{ label, href, icon, text, children, target }`. Components can support both modes: use `content.data?.nav` when provided, fall back to `website.getPageHierarchy()` for automatic nav. See `reference/navigation-patterns.md` for the full pattern.
962
+ ```jsx
963
+ // sections/Hero/index.jsx — the front desk
964
+ import { SliderHero } from '#components/SliderHero'
965
+ import { ContactHero } from '#components/ContactHero'
907
966
 
908
- ---
967
+ const variants = { slider: SliderHero, contact: ContactHero }
968
+
969
+ export default function Hero({ content, block, params }) {
970
+ const Variant = variants[params.variant] || SliderHero
971
+ return (
972
+ <Variant
973
+ title={content.title} subtitle={content.paragraphs[0]} links={content.links} block={block}
974
+ images={content.images} // only some variants use these
975
+ formData={content.data?.quote}
976
+ interval={params.slideInterval} // author vocabulary → developer props
977
+ compact={params.density === 'compact'}
978
+ transition={params.style === 'dramatic' ? 'zoom' : 'fade'}
979
+ />
980
+ )
981
+ }
982
+ ```
909
983
 
910
- ## Semantic Theming
984
+ `SliderHero` uses `images`, `interval`, `transition` and ignores the rest; `ContactHero` uses `formData` and `compact` and ignores the rest. The author writes `variant: contact` — they don't know or care that `ContactHero` exists.
911
985
 
912
- Components use **semantic CSS tokens** instead of hardcoded colors. The runtime applies a context class (`context-light`, `context-medium`, `context-dark`) to each section based on `theme:` frontmatter. The `theme` value is also available as `params.theme` — useful when a component needs conditional logic beyond CSS tokens (e.g., switching between a light and dark logo).
986
+ This is the system-building pattern at its clearest: **section types are the public interface** (author-friendly names, documented in `meta.js`); **helper components are the implementation** (ordinary React props). The section type is the thin translation layer between the two worlds.
987
+
988
+
989
+ **When to reach for this pattern:** when a page type has consistent structural elements — header bars, navigation footers, contextual sidebars — that the content author shouldn't have to add as separate sections. If the author would otherwise repeat the same boilerplate sections on every page of a given type, the section component should compose them internally.
990
+
991
+ **Common mistake:** solving structural repetition at the layout level. If only some page types need a content header (lessons do, the homepage doesn't), that's a section concern. The layout owns page-wide chrome; the section owns its internal structure.
992
+
993
+ ### Params, component vars, foundation vars
994
+
995
+ Three places a value can live, and picking the right one is most of the design:
996
+
997
+ | | Declared in | Reaches the component as | Author overrides in | CSS scope |
998
+ |---|---|---|---|---|
999
+ | **Param** | `meta.js` `params:` | a JS prop | section frontmatter | — |
1000
+ | **Component var** | `meta.js` `vars:` | a CSS custom property | section frontmatter `vars:` | `#section-{id}` |
1001
+ | **Foundation var** | `main.js` `vars:` | a CSS custom property | `theme.yml` `vars:` | `:root` (global) |
1002
+
1003
+ **Param when the value drives logic** — a count, a variant name, a boolean the JSX branches on. **Component var when it only drives CSS** and belongs to one section type; it never enters JS, so it doesn't re-render anything:
1004
+
1005
+ ```js
1006
+ // meta.js — same schema as foundation vars (default, label, type, options, group, description)
1007
+ vars: {
1008
+ 'card-gap': { default: '1.5rem', label: 'Card Gap', type: 'select', options: ['1rem', '1.5rem', '2rem'] },
1009
+ 'card-radius': { default: 'var(--radius-md)', description: 'Inherits the foundation var by default' },
1010
+ }
1011
+ ```
913
1012
 
914
1013
  ```jsx
915
- // Hardcoded breaks in dark context
916
- <h2 className="text-slate-900">...</h2>
1014
+ <div className="grid gap-[var(--card-gap)]"> // emitted on #section-{id}
1015
+ ```
1016
+
1017
+ ```yaml
1018
+ ---
1019
+ type: PricingTable
1020
+ vars: { card-gap: 2rem } # author override — only declared names apply
1021
+ ---
1022
+ ```
1023
+
1024
+ Unknown var names in frontmatter are ignored, and component vars are always context-independent (unlike foundation `color`/`gradient` vars, which resolve per light/dark context).
1025
+
1026
+ **Foundation var when the value must stay consistent *across* components** — shared radii, spacing scales, extra typefaces. A header height is a layout param, not a foundation var; a sidebar width is a layout param too. Declare foundation vars in **`main.js`**, the single source of truth:
1027
+
1028
+ ```js
1029
+ export const vars = {
1030
+ 'radius-lg': { default: '1rem', description: 'Large border radius' },
1031
+ 'section-padding-y': { default: 'clamp(4rem, 6vw, 7rem)', description: 'Vertical section padding' },
1032
+ }
1033
+ ```
1034
+
1035
+ Each entry ships the default as a CSS custom property (so `var(--section-padding-y)` resolves everywhere), gives the visual editor a description and type, and is what a site overrides under `vars:` in `theme.yml`. You don't declare these anywhere else — the defaults reach the browser on their own, in dev and production, bundled or runtime-loaded.
1036
+
1037
+ A `styles.css` `@theme` block is a different tool: it registers a token so **Tailwind generates utilities** from it (`@theme { --breakpoint-xs: 30rem }` → `xs:` variants). Use it to extend Tailwind's vocabulary, not to ship a plain default.
1038
+
1039
+ **A name matching a Tailwind namespace is an intentional override.** A var named after a Tailwind v4 scale — `radius-*`, `shadow-*`, `spacing`, `font-*` — redefines that scale wherever the matching utility appears: declaring `radius-lg` retunes every `rounded-lg` in the foundation. That's the point when you mean it — name deliberately so you don't reshape a utility by accident.
1040
+
1041
+ ### Semantic theming
917
1042
 
918
- // Semantic adapts to any context and brand
919
- <h2 className="text-heading">...</h2>
1043
+ Components use **semantic CSS tokens** instead of hardcoded colors. The runtime applies a context class (`context-light`, `context-medium`, `context-dark`) to each section from its `theme:` frontmatter.
1044
+
1045
+ ```jsx
1046
+ <h2 className="text-slate-900">…</h2> // ❌ Hardcoded — breaks in dark context
1047
+ <h2 className="text-heading">…</h2> // ✅ Semantic — adapts to any context and brand
920
1048
  ```
921
1049
 
922
- **Semantic tokens** (available as Tailwind classes — `text-*`, `bg-*`, `border-*`):
1050
+ **Semantic tokens** (available as Tailwind `text-*`, `bg-*`, `border-*` classes):
923
1051
 
924
1052
  | Token | Purpose |
925
1053
  |-------|---------|
@@ -937,22 +1065,9 @@ Components use **semantic CSS tokens** instead of hardcoded colors. The runtime
937
1065
  | `success` / `warning` / `error` / `info` | Status colors |
938
1066
  | `success-subtle` / `warning-subtle` / `error-subtle` / `info-subtle` | Status backgrounds (alerts) |
939
1067
 
940
- Use with any Tailwind prefix: `text-heading`, `bg-section`, `border-border`, `bg-primary`, `text-primary-foreground`, `hover:bg-primary-hover`, `bg-error-subtle`, etc.
941
-
942
- **Palette shades** are also available: `text-primary-600`, `bg-neutral-100`, `border-accent-300` — 11 shades (50–950) for each palette color (primary, secondary, accent, neutral). See `theme-tokens.css` for the complete mapping.
943
-
944
- **Content authors control context** in frontmatter:
945
-
946
- ```markdown
947
- ---
948
- type: Testimonial
949
- theme: dark ← sets context-dark, all tokens resolve to dark values
950
- ---
951
- ```
952
-
953
- Alternate between `light` (default), `medium`, and `dark` across sections for visual rhythm.
1068
+ **Palette shades** are also available `text-primary-600`, `bg-neutral-100`, `border-accent-300` — 11 shades (50–950) per palette color (primary, secondary, accent, neutral). See `theme-tokens.css` for the complete mapping.
954
1069
 
955
- **But the three presets aren't the limit.** The object form gives fine-grained control per section:
1070
+ **Authors control context** via `theme: dark` in frontmatter, alternating `light` (default), `medium`, and `dark` across sections for visual rhythm. **The three presets aren't the limit** the object form overrides any token per section:
956
1071
 
957
1072
  ```yaml
958
1073
  theme:
@@ -962,89 +1077,28 @@ theme:
962
1077
  primary: neutral-900 # Dark buttons instead of brand color
963
1078
  ```
964
1079
 
965
- Any semantic token can be overridden. And `background:` accepts CSS variables and hex colors, so authors can alternate between `var(--neutral-50)`, `var(--neutral-100)`, and `var(--primary-50)` surfaces all without component code. If a source design uses subtle surface variations (e.g., `--surface-base` vs `--surface-sunken`), map those to specific backgrounds or token overrides in frontmatter, not to component code.
966
-
967
- ### theme.yml
968
-
969
- ```yaml
970
- # site/theme.yml
971
- colors:
972
- primary: '#3b82f6' # Your exact hex appears at shade 500
973
- secondary: '#64748b'
974
- accent: '#8b5cf6'
975
- neutral: stone # Named preset: stone, zinc, gray, slate, neutral
976
-
977
- contexts:
978
- light:
979
- section: '#fafaf9' # Override individual tokens per context
980
-
981
- appearance:
982
- default: light # 'light' | 'dark' | 'system'; see Light/dark appearance below
983
- allowToggle: true # offer a visitor light/dark switch
984
-
985
- fonts:
986
- import:
987
- - url: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'
988
- heading: "'Inter', system-ui, sans-serif"
989
- body: "'Inter', system-ui, sans-serif"
990
-
991
- inline:
992
- accent:
993
- color: var(--link)
994
- font-weight: '600'
995
- callout:
996
- color: var(--accent)
997
- font-weight: '600'
998
-
999
- vars:
1000
- radius: 0.75rem
1001
- ```
1002
-
1003
- Each color generates 11 OKLCH shades (50–950). `neutral` uses a named preset rather than hex. Shade 500 = your exact input color. Context override keys match token names: `section:` not `bg:`, `primary:` not `btn-primary-bg:`.
1004
-
1005
- ### How colors reach components
1006
-
1007
- Your hex → 11 shades (50–950) → semantic tokens → components.
1008
-
1009
- Semantic tokens map shades to roles. In light/medium: `--primary` uses shade 600, `--link` uses 600, `--ring` uses 500. In dark: `--primary` uses 500, `--link` uses 400.
1010
-
1011
- **Buttons use shade 600 — darker than your input color.** This is an accessibility choice for contrast with white text. For brand-exact buttons:
1012
-
1013
- ```yaml
1014
- colors:
1015
- primary: "#E35D25"
1016
- contexts:
1017
- light:
1018
- primary: primary-500 # Your exact color on buttons
1019
- primary-hover: primary-600
1020
- ```
1021
-
1022
- > **Contrast warning:** Bright brand colors (orange, yellow, light green) at shade 500 may not meet WCAG contrast (4.5:1) with white foreground text. Test buttons for readability — if contrast is insufficient, keep the default shade 600 mapping.
1080
+ `background:` also accepts CSS variables and hex, so authors can alternate `var(--neutral-50)` / `var(--primary-50)` surfaces with no component code. If a source design uses subtle surface variations (`--surface-base` vs `--surface-sunken`), map those to backgrounds or token overrides in frontmatter not to component code.
1023
1081
 
1024
- ### Light/dark appearance (scheme vs. context)
1082
+ ### Section context vs site scheme
1025
1083
 
1026
- Uniweb splits what other frameworks fuse into one "dark mode" into two independent ideas:
1084
+ Uniweb splits what other frameworks fuse into one "dark mode":
1027
1085
 
1028
- - **Section context** — the `theme:` field on a section (`light` / `medium` / `dark`). It sets the color tokens for *that one section*, so a single page can move through light, dark, and colored sections. Per-section, author-controlled, covered above.
1029
- - **Site scheme** — the global light/dark preference, configured under `appearance:` in `theme.yml`. This is the site-wide toggle a visitor flips (or that follows their OS).
1086
+ - **Section context** — the `theme:` field on one section (`light`/`medium`/`dark`). Per-section, author-controlled.
1087
+ - **Site scheme** — the global light/dark preference under `appearance:` in `theme.yml`. The site-wide toggle a visitor flips.
1030
1088
 
1031
- They compose rather than fight: a section with **no `theme:`** inherits the site scheme, so it follows the toggle; a section that **pins** `theme: dark` stays dark in either scheme. A dark-scheme site can still carry one bright white CTA; a light-scheme site can carry one dramatic dark hero. You never write `isDark ? … : …` — components built on semantic tokens (`text-heading`, `bg-card`, `border`, …) adapt to whichever scheme and context resolve around them.
1032
-
1033
- Configure the scheme in `theme.yml`:
1089
+ They compose rather than fight: a section with **no `theme:`** inherits the site scheme and follows the toggle; a section that **pins** `theme: dark` stays dark in either scheme. A dark site can carry one bright white CTA. You never write `isDark ? … : …` — semantic tokens adapt to whichever scheme and context resolve around them.
1034
1090
 
1035
1091
  ```yaml
1036
1092
  appearance:
1037
- default: light # 'light' | 'dark' | 'system' (start by following the OS)
1093
+ default: light # 'light' | 'dark' | 'system'
1038
1094
  allowToggle: true # offer a visitor switch (also generates the dark tokens)
1039
- respectSystemPreference: true # first visit follows the OS; set false to pin `default`
1095
+ respectSystemPreference: true # first visit follows the OS; false pins `default`
1040
1096
  schemes: [light, dark] # optional — declares the available set
1041
1097
  ```
1042
1098
 
1043
- Shorthand strings work too: `appearance: light` / `appearance: dark` (fixed, no toggle), `appearance: system` (follow the OS).
1044
-
1045
- A site **has dark mode** whenever it offers a toggle, defaults to `dark`/`system`, or lists `dark` in `schemes:`. Any such site follows the visitor's OS on first visit (unless `respectSystemPreference: false`) and remembers a manual choice. Precedence: a stored choice wins over the OS, which wins over `default:`.
1099
+ Shorthands: `appearance: light` / `dark` (fixed, no toggle), `appearance: system` (follow the OS).
1046
1100
 
1047
- Common intents:
1101
+ A site **has dark mode** whenever it offers a toggle, defaults to `dark`/`system`, or lists `dark` in `schemes:`. Precedence: a stored choice wins over the OS, which wins over `default:`. Note that `default: light` is a *fallback*, not a guarantee — a dark OS still wins on first visit unless `respectSystemPreference: false`. (`default: system` is just the honest way to say "there is no fixed default — use the OS.")
1048
1102
 
1049
1103
  | Goal | `theme.yml` |
1050
1104
  |---|---|
@@ -1053,9 +1107,7 @@ Common intents:
1053
1107
  | Toggle, follow the OS on first visit | `appearance: { default: system, allowToggle: true }` |
1054
1108
  | Toggle, but always start light | `appearance: { default: light, allowToggle: true, respectSystemPreference: false }` |
1055
1109
 
1056
- Note that `default: light` is a *fallback*, not a guarantee: a dark OS still wins on first visit unless you set `respectSystemPreference: false` (the last row). `default: system` is just the honest way to say "there is no fixed default use the OS."
1057
-
1058
- **Rendering a toggle (foundation side).** The runtime resolves the scheme and applies it to `<html>` (`scheme-dark` / `scheme-light`) *before the page paints* — you never touch `localStorage` or `document` yourself, and there's no flash of the wrong scheme. A section type only renders the button:
1110
+ **Rendering a toggle.** The runtime resolves the scheme and applies it to `<html>` (`scheme-dark`/`scheme-light`) *before the page paints* never touch `localStorage` or `document` yourself, and there's no flash of the wrong scheme. A section type only renders the button:
1059
1111
 
1060
1112
  ```jsx
1061
1113
  import { useAppearance } from '@uniweb/kit'
@@ -1067,1003 +1119,708 @@ function SchemeToggle() {
1067
1119
  }
1068
1120
  ```
1069
1121
 
1070
- **Tailwind `dark:` variant.** If your foundation uses Tailwind's `dark:` utilities, bind them to the site scheme (not the OS media query) so they track the toggle. In `styles.css`:
1122
+ **Tailwind `dark:` variant.** If your foundation uses `dark:` utilities, bind them to the site scheme so they track the toggle. In `styles.css`:
1071
1123
 
1072
1124
  ```css
1073
1125
  @custom-variant dark (&:where(.scheme-dark, .scheme-dark *));
1074
1126
  ```
1075
1127
 
1076
- Without this, `dark:` defaults to `@media (prefers-color-scheme: dark)` and ignores the site's own light/dark setting. Prefer semantic tokens over `dark:` where you can — they adapt to per-section context, which a global `dark:` cannot.
1077
-
1078
- ### Fonts
1079
-
1080
- Font families are a **site** setting, configured in `theme.yml` under `fonts:`. A foundation never installs a font package (no `@fontsource/*`) and never hardcodes a family in its code — if you reach for either, you're off the paved path. The framework provides three roles and wires each onto the standard elements for you:
1128
+ Without this, `dark:` falls back to `@media (prefers-color-scheme: dark)` and ignores the site's setting. Prefer semantic tokens over `dark:` where you can — they adapt to per-section context, which a global `dark:` cannot.
1081
1129
 
1082
- | Role | Wired onto | Typical use |
1083
- |---|---|---|
1084
- | `body` | `body` (all text by default) | paragraphs, UI |
1085
- | `heading` | `h1, h2, h3` | titles |
1086
- | `code` | `code, pre, kbd, samp` | code, monospace |
1130
+ ### theme.yml
1087
1131
 
1088
1132
  ```yaml
1089
1133
  # site/theme.yml
1134
+ colors:
1135
+ primary: '#3b82f6' # Your exact hex appears at shade 500
1136
+ secondary: '#64748b'
1137
+ accent: '#8b5cf6'
1138
+ neutral: stone # Named preset: stone, zinc, gray, slate, neutral
1139
+
1140
+ contexts:
1141
+ light:
1142
+ section: '#fafaf9' # Override individual tokens per context
1143
+
1144
+ appearance: { default: light, allowToggle: true }
1145
+
1090
1146
  fonts:
1091
- body: "Inter, system-ui, sans-serif"
1092
- heading: "Poppins, system-ui, sans-serif"
1093
- code: "'Fira Code', monospace"
1147
+ heading: "'Inter', system-ui, sans-serif"
1148
+ body: "'Inter', system-ui, sans-serif"
1149
+
1150
+ inline:
1151
+ callout: { color: var(--accent), font-weight: '600' }
1152
+
1153
+ vars:
1154
+ radius: 0.75rem
1094
1155
  ```
1095
1156
 
1096
- (`code` was previously named `mono`; `fonts.mono` is no longer an alias rename it to `fonts.code`.)
1157
+ Each color generates 11 OKLCH shades (50–950); shade 500 is your exact input. `neutral` takes a named preset rather than hex. Context override keys match token names: `section:` not `bg:`, `primary:` not `btn-primary-bg:`.
1097
1158
 
1098
- Because the site wires the families onto real elements, **the norm is that a foundation doesn't set fonts itself** — render semantic markup (`<H1>`, `<P>`, `<code>` from the kit) and the roles apply. (It stays Tailwind for everything else — layout, spacing, weights, color.) That convention is what authors can count on: `body`/`heading`/`code` control the fonts the same way in every foundation, and one foundation re-fonts per site with zero code changes.
1159
+ **How colors reach components:** your hex 11 shades semantic tokens components. In light/medium, `--primary` uses shade 600, `--link` 600, `--ring` 500; in dark, `--primary` uses 500 and `--link` 400.
1099
1160
 
1100
- **Loading the files**two site-side options, both config-only, neither adds a dependency:
1161
+ **Buttons use shade 600 darker than your input color.** That's an accessibility choice for contrast with white text. For brand-exact buttons:
1101
1162
 
1102
1163
  ```yaml
1103
- # Hosted (Google Fonts or any provider). The build auto-adds preconnect
1104
- # hints and drops families no role references.
1105
- fonts:
1106
- heading: "Poppins, sans-serif"
1107
- import:
1108
- - url: "https://fonts.googleapis.com/css2?family=Poppins:wght@600;700"
1109
-
1110
- # Self-hosted (no CDN — privacy / offline). Put the files under
1111
- # site/public/fonts/ and declare @font-face faces; the build emits the
1112
- # @font-face rules + preload hints, and skips any face whose family no
1113
- # role uses.
1114
- fonts:
1115
- heading: "Söhne, sans-serif"
1116
- faces:
1117
- - { family: "Söhne", src: /fonts/soehne-bold.woff2, weight: 700 }
1118
- - { family: "Söhne", src: /fonts/soehne-regular.woff2, weight: 400 }
1164
+ colors: { primary: "#E35D25" }
1165
+ contexts:
1166
+ light:
1167
+ primary: primary-500 # Your exact color on buttons
1168
+ primary-hover: primary-600
1119
1169
  ```
1120
1170
 
1121
- **In a component, weight is yours; family is the site's.** Weight / size / style utilities (`font-bold`, `font-black`, `italic`, `text-xl`) are ordinary design vocabulary use them freely. A font-**family** utility is a different matter: `font-sans` / `font-serif` resolve to Tailwind's built-in stacks unless the foundation makes them site-controlled (below), so a bare `font-serif` in a component silently hardcodes a typeface.
1171
+ > **Contrast warning:** bright brand colors (orange, yellow, light green) at shade 500 may fail WCAG 4.5:1 against white text. Test for readability if contrast is insufficient, keep the default 600 mapping.
1172
+
1173
+ ### Fonts
1122
1174
 
1123
- **When a design needs typefaces the three roles can't express** — an editorial serif on selected prose, a display face for hero titles, a decorative mono for metadata labels that aren't `<code>` — a foundation declares each as a **font var** in `main.js`. A `font-*`-named var is recognized as a *typeface* automatically (no `type` needed) a bare-named one takes `type: 'font'`. Either way the family loads (via the site's `fonts.import` / `fonts.faces`), it appears in the foundation's schema tagged as a font so the site and the visual editor's theme panel can set it, and the family stays site-controlled.
1175
+ Font families are a **site** setting, under `fonts:` in `theme.yml`. A foundation never installs a font package (no `@fontsource/*`) and never hardcodes a family if you reach for either, you're off the paved path. Three roles are wired onto standard elements for you:
1176
+
1177
+ | Role | Wired onto | Typical use |
1178
+ |---|---|---|
1179
+ | `body` | `body` (all text by default) | paragraphs, UI |
1180
+ | `heading` | `h1, h2, h3` | titles |
1181
+ | `code` | `code, pre, kbd, samp` | code, monospace |
1182
+
1183
+ (`code` was previously named `mono`; `fonts.mono` is no longer an alias — rename it.)
1184
+
1185
+ Because the site wires families onto real elements, **the norm is that a foundation doesn't set fonts itself** — render semantic markup (`<H1>`, `<P>`, `<code>` from the kit) and the roles apply. Everything else stays Tailwind.
1186
+
1187
+ **In a component, weight is yours; family is the site's.** Weight/size/style utilities (`font-bold`, `italic`, `text-xl`) are ordinary design vocabulary. A font-**family** utility is different: `font-sans`/`font-serif` resolve to Tailwind's built-in stacks unless the foundation makes them site-controlled, so a bare `font-serif` silently hardcodes a typeface.
1188
+
1189
+ **When a design needs typefaces the three roles can't express** — an editorial serif, a display face for hero titles — declare each as a **font var** in `main.js`. A `font-*`-named var is recognized as a typeface automatically (no `type` needed); a bare-named one takes `type: 'font'`. Either way the site loads and owns the family:
1124
1190
 
1125
1191
  ```js
1126
- // foundation src/main.js — typefaces this foundation manages itself.
1127
- // `font-*` names are recognized as fonts automatically (no `type` needed);
1128
- // defaults are OS stacks, so it renders native until a site opts in.
1192
+ // foundation src/main.js
1129
1193
  export const vars = {
1130
1194
  'font-serif': {
1131
1195
  default: 'ui-serif, Georgia, serif',
1132
1196
  description: 'Editorial serif — blurbs, taglines, quotes',
1133
- applyTo: ['blockquote', '.tagline'], // framework applies it here, like a built-in role
1197
+ applyTo: ['blockquote', '.tagline'], // framework emits the rule, like a built-in role
1134
1198
  },
1135
1199
  'font-display': {
1136
1200
  default: 'ui-sans-serif, system-ui, sans-serif',
1137
- description: 'Display — hero titles',
1138
1201
  // no applyTo → the component wires it (a `font-display` utility, or var(--font-display))
1139
1202
  },
1140
1203
  }
1141
1204
  ```
1142
1205
 
1143
- Two ways to apply a typed font varchoose per var:
1144
-
1145
- - **`applyTo: [selectors]`** — the framework emits the `font-family` rule for you, exactly like the three built-in roles. Declarative; no font-family classes in components.
1146
- - **Omit `applyTo`** — the component wires it: a Tailwind `font-*` utility when the var is named after a Tailwind scale (`font-serif` → the `font-serif` utility), or `font-family: var(--font-display)` for a custom name.
1147
-
1148
- Either way the site sets the family and loads it — no component changes. The `fonts:` block takes **any** role name, built-in or foundation-added, so a site sets its whole type system in one place:
1206
+ The `fonts:` block takes **any** role name, built-in or foundation-added, so a site sets its whole type system in one place. The three roles are defaults you can **retarget** too redeclaring a role's var with a new `applyTo` changes which elements it paints, while the site still owns the family.
1149
1207
 
1150
1208
  ```yaml
1151
- # site/theme.yml
1209
+ # site/theme.yml — loading, hosted or self-hosted; both config-only, no dependency
1152
1210
  fonts:
1153
- body: "Inter, system-ui, sans-serif"
1154
- serif: '"Fraunces", Georgia, serif' # a foundation-declared role, set by name
1211
+ heading: "Poppins, sans-serif"
1212
+ serif: '"Fraunces", Georgia, serif' # a foundation-declared role, set by name
1213
+ import:
1214
+ - url: "https://fonts.googleapis.com/css2?family=Poppins:wght@600;700"
1155
1215
  faces:
1156
1216
  - { family: "Fraunces", src: /fonts/fraunces.woff2, weight: "100 900" }
1157
1217
  ```
1158
1218
 
1159
- (`vars: { font-serif: }` still works same role. `serif` and `font-serif` are the same font var; the `font-` spelling is just the one Tailwind's `font-serif` utility reads.)
1219
+ The build auto-adds preconnect hints for imports, emits `@font-face` + preload for faces, and drops any family no role references.
1160
1220
 
1161
- **The three roles are defaults you can retarget.** `body`/`heading`/`code` and their selectors are *defaults*, not a fixed contract a foundation can **redefine** a role (change which elements it paints) or **add** roles, all through the same `type: 'font'` var. Declaring a role's var with a new `applyTo` retargets it; the site still owns the family.
1221
+ > **`serif` and `font-serif` are the same font var**the `font-` spelling is just the one Tailwind's `font-serif` utility reads. Declare it either way in `main.js`; the site sets it by the bare role name under `fonts:`.
1162
1222
 
1163
- ```js
1164
- // foundation src/main.js — retarget a built-in role, add a new one.
1165
- export const vars = {
1166
- heading: { type: 'font', applyTo: ['h1', 'h2', 'h3', 'h4'] }, // include h4 in the heading font
1167
- serif: { type: 'font', default: 'ui-serif, Georgia, serif', applyTo: ['blockquote'] },
1168
- }
1169
- ```
1223
+ > The `code` role owns `--font-code`. Tailwind's `font-mono` utility (which reads `--font-mono`) is a **separate** concern — control it by declaring your own `font-mono` font var. So `fonts.code` styles code without disturbing `font-mono`-styled labels, and vice-versa.
1170
1224
 
1171
- > The `code` role owns `--font-code`. Tailwind's `font-mono` utility (which reads `--font-mono`) is a **separate** concern — a foundation controls it by declaring its own `font-mono` font var. So setting `fonts.code` styles code without disturbing `font-mono`-styled labels, and vice-versa.
1225
+ ### Tailwind v4 and styles.css
1172
1226
 
1173
- ### Foundation variables
1174
-
1175
- Most customization is handled by component params. Both section components and layout components declare their own params in `meta.js` — layouts are full components with params, not just structural wrappers. A header height, for example, is typically a layout param, not a foundation var.
1176
-
1177
- Foundation-level CSS variables are for values that must stay consistent **across** multiple components — shared radii, spacing scales, or additional typefaces beyond the three built-in roles (body, heading, code), declared as typed font vars (`type: 'font'`, covered in **Fonts** above). Don't reach for foundation vars when a component or layout param would do.
1178
-
1179
- When you do need one, declare it in **`main.js`** — the single source of truth:
1227
+ ```css
1228
+ @import "tailwindcss";
1229
+ @import "@uniweb/kit/theme-tokens.css";
1230
+ @source "./sections/**/*.{js,jsx}";
1231
+ @source "./components/**/*.{js,jsx}";
1232
+ @source "../node_modules/@uniweb/kit/src/**/*.jsx";
1180
1233
 
1181
- ```js
1182
- export const vars = {
1183
- 'radius-lg': { default: '1rem', description: 'Large border radius' },
1184
- 'section-padding-y': { default: 'clamp(4rem, 6vw, 7rem)', description: 'Vertical section padding' },
1234
+ @theme {
1235
+ --breakpoint-xs: 30rem;
1185
1236
  }
1186
1237
  ```
1187
1238
 
1188
- Each entry does everything at once: it ships the default as a CSS custom property (`--radius-lg`, `--section-padding-y`) so `var(--section-padding-y)` resolves everywhere, it gives the visual editor a description and type, and it's the value a site overrides in `theme.yml` under `vars:` (the site's theme CSS wins). You don't declare these anywhere else the defaults reach the browser on their own, in dev and in production, whether the foundation is bundled or loaded at runtime.
1189
-
1190
- A `styles.css` `@theme` block is a separate tool for a different job: registering a token so **Tailwind generates utility classes** from it (`@theme { --breakpoint-xs: 30rem }` → `xs:` variants). Reach for it to extend Tailwind's vocabulary — not to ship a plain default.
1191
-
1192
- **A name that matches a Tailwind namespace is an intentional override.** A var named after a Tailwind v4 token scale — `radius-*`, `shadow-*`, `spacing`, `font-*` — redefines that scale wherever the matching utility appears: declaring `radius-lg` retunes every `rounded-lg` in the foundation from Tailwind's default to your value (sites can still override in `theme.yml`). That's the point when you mean it — just name deliberately so you don't reshape a utility by accident.
1193
-
1194
- **Common mistake:** Using foundation vars for values that belong to a specific component. A header height is a layout param, not a foundation var — the layout component owns it. A sidebar width is a layout param too. Foundation vars are for values that multiple unrelated components share — radii, spacing, shadows.
1195
-
1196
- ### Design richness beyond tokens
1239
+ Semantic tokens come from `theme-tokens.css` (populated from `theme.yml`). Use `@theme` only for values tokens don't cover. **Custom CSS is expected alongside Tailwind** shadow systems, border hierarchies, gradients, glassmorphism. Tailwind handles layout; tokens handle context; `styles.css` handles everything else.
1197
1240
 
1198
- Tokens handle context adaptation — the hard problem. **They are a floor, not a ceiling.** A great foundation adds design vocabulary on top:
1241
+ **Tokens are a floor, not a ceiling.** They solve context adaptation — the hard problem. A great foundation adds design vocabulary on top:
1199
1242
 
1200
1243
  ```css
1201
- /* styles.css */
1202
1244
  .border-subtle { border-color: color-mix(in oklch, var(--border), transparent 50%); }
1203
- .border-strong { border-color: color-mix(in oklch, var(--border), var(--heading) 30%); }
1204
1245
  .text-tertiary { color: color-mix(in oklch, var(--body), var(--subtle) 50%); }
1205
1246
  ```
1206
1247
 
1207
- These compose with tokens — they adapt per context because they reference token variables. But they add nuance the 24-token set doesn't provide. Use palette shades directly (`var(--primary-300)`, `bg-neutral-200`) for fine-grained color control.
1208
-
1209
- **The priority:** Design quality > portability > configurability. A beautiful foundation for one site is more valuable than a generic one that looks flat.
1248
+ These compose with tokens — they adapt per context because they reference token variables while adding nuance the token set doesn't provide. Use palette shades directly (`var(--primary-300)`, `bg-neutral-200`) for fine-grained control. **The priority: design quality > portability > configurability.** A beautiful foundation for one site is worth more than a generic one that looks flat.
1210
1249
 
1211
- ---
1250
+ Two lines a ported CSS reset will try to bring with it, both of which break things:
1212
1251
 
1213
- ## Component Development
1252
+ > **Don't set `scroll-behavior: smooth` globally.** The runtime owns scrolling: it already smooth-scrolls anchor targets itself (`scrollIntoView({ behavior: 'smooth' })`), so the CSS adds nothing there — but it resets and restores scroll on route changes with the two-argument `scrollTo(x, y)`, which *inherits* the property. Route changes then animate their scroll-to-top, and back-button restoration (which scrolls, checks the position on the next frame, and retries) keeps interrupting its own animation. Scope it to a specific scrollable element if you need it; never to `html` or `body`.
1214
1253
 
1215
- You're not building pages you're building a **system** of section types that content authors compose into pages. Name by purpose, not content: `Testimonial` not `WhatClientsSay`, `SplitContent` not `AboutSection`. Expect consolidation: a React site with 30+ components typically maps to 8–15 Uniweb section types.
1254
+ > **Font smoothing is a per-scheme decision, not a reset.** `-webkit-font-smoothing: antialiased` (macOS only) forces grayscale rasterization, which thins strokes. On dark surfaces that usefully counteracts the bloom of light text on near-black; on light surfaces it costs contrast and makes body text spindly. Scope it — `.scheme-dark { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }` rather than putting it on `html`.
1216
1255
 
1217
- ### Props Interface
1256
+ ### Composition: items, child sections, insets
1218
1257
 
1219
- ```jsx
1220
- function MyComponent({ content, params, block }) {
1221
- const { title, paragraphs, links, items } = content // Guaranteed shape
1222
- const { columns, variant } = params // Defaults from meta.js
1223
- const { website } = useWebsite() // Or block.website
1224
- }
1225
- ```
1258
+ Pages are sequences of sections — the obvious layer. The framework also supports real nesting, without leaving markdown.
1226
1259
 
1227
- All non-reserved frontmatter fields become `params`. Reserved: `type`, `preset`, `input`, `data`, `id`, `background`, `theme`, `source`, `where`. Everything else flows to the component.
1228
-
1229
- ### Data
1230
-
1231
- A component on a page with a `data:` or `fetch:` declaration automatically receives that data in `content.data.{key}`. No opt-in required in `meta.js`. **Bound collections always arrive as arrays.** On a list page, `content.data.articles` is the full collection. On a template page (`[slug]/`), the matched record is delivered under the *same* collection key as a single-element array the detail section reads `content.data.articles[0]`. When nothing matches, the key is `[]`. The runtime never coerces the array to a single object and never synthesizes a separate singular key.
1232
-
1233
- ```jsx
1234
- function Article({ content, block }) {
1235
- if (block.dataLoading) return <DataPlaceholder />
1236
- const article = content.data.articles?.[0] // focused record on a [slug] page
1237
- if (!article) return <NotFound />
1238
- return <ArticleView article={article} />
1239
- }
1240
- ```
1260
+ | Pattern | How authored | Use when |
1261
+ |---|---|---|
1262
+ | **Items** (`content.items`) | Heading groups within one `.md` | Repeating content in one section: cards, features, FAQ entries |
1263
+ | **Child sections** (`block.childBlocks`) | `@`-prefixed `.md` files + `nest:` | Children needing their own section type, rich content, or independent editing |
1264
+ | **Insets** (`block.insets`) | `![](@Component)` in markdown | Self-contained visuals/widgets: charts, diagrams, code demos |
1241
1265
 
1242
- Components can ignore keys in `content.data` they don't need the same way unused `params` are ignored.
1266
+ Does the author write content *inside* the nested element? **Yes** → child sections. **No** (self-contained, param-driven) inset. Repeating same-structure groups items. These compose: a child section can contain insets; items work inside children.
1243
1267
 
1244
- **Declaring data schemas.** `meta.js` declares the schema for each `content.data` key with a single `data:` field there is no separate `schemas:` key. Each entry's value is one of: a **named ref** (`'@/article'` resolves to this foundation's `foundation/schemas/article.{js,json,yml,yaml}`; `'@std/person'` is a shared standard), an **inline field map** (`{ field: { type, default } }`), or an **inline rich-form** (`{ fields: [...] }`, an editor form). Refs use Uniweb namespacing, resolved on disk at build time, never fetched: `@/name` (this foundation's own `foundation/schemas/`), `@std/name` (the shared standard schemas, shipped in the `@uniweb/schemas` package), and `@org/name` (an org's own schemas, from its `@org/schemas` package define a schema once and share it across foundations). The schema is a hint: it supplies field defaults and drives the editor, not delivery (which is default-on). For an explicit opt-out (rare), set `data: false`.
1268
+ **Insets embedding components in content.** Many section types need a "visual"a hero's illustration, a split-content section's media. Classically an image or video. But what if it's a JSX + SVG diagram, a ThreeJS animation, an interactive playground? Elsewhere you'd reach for MDX or prop-drilling. Here the author writes standard image syntax:
1245
1269
 
1246
- ```js
1247
- // meta.js
1248
- export default {
1249
- data: {
1250
- articles: '@/article', // named ref (this foundation)
1251
- authors: '@std/person', // named ref (shared standard)
1252
- pricing: { tier: { type: 'string', default: '' } }, // inline field map
1253
- },
1254
- }
1270
+ ```markdown
1271
+ ![Architecture overview](@NetworkDiagram){variant=compact}
1255
1272
  ```
1256
1273
 
1257
- **Routing a scope to a folder.** By default `@org/name` resolves from that org's `@org/schemas` package. A foundation can instead route a scope to a plain folder of schema files anywhere on disk, no package, no install — via an optional `schemas.config.js` at its root. This is how a team shares one set of schemas across many foundations:
1274
+ The developer builds `NetworkDiagram` as an ordinary React component with `inset: true` in `meta.js`. Kit's `<Visual>` renders the first non-empty candidate, so one section type works whether the author supplies an image, a video, or an interactive component:
1258
1275
 
1259
- ```js
1260
- // schemas.config.js — @acme/person ../shared/acme-schemas/person.{js,yml}
1261
- export default { '@acme': '../shared/acme-schemas', '@brand': process.env.BRAND_SCHEMAS }
1276
+ ```jsx
1277
+ <Visual inset={block.insets[0]} video={content.videos[0]} image={content.images[0]} className="rounded-2xl" />
1262
1278
  ```
1263
1279
 
1264
- Plain JS, so paths can be relative, absolute, or read from an env var. A routed scope wins over the package convention; `@/` and `@uniweb` are never routable; an empty value (e.g. an unset env var) falls back to the package convention. A routed scope has no package fallback for a *missing* schema (it errors rather than silently load a different definition). To override a single schema — keeping the rest of the scope routed to the shared folder — add a per-schema key pointing at a file: `'@acme/person': './schemas/acme-person.yml'` (most-specific wins: file › directory › package).
1280
+ **Insets are full section types** they receive `{ content, params, block }`. The alt text becomes `content.title` and attributes become `params`: `![npm create uniweb](@CommandBlock){note="Ready to go"}` `content.title = "npm create uniweb"`, `params.note = "Ready to go"`.
1265
1281
 
1266
- **Validate your data.** `uniweb validate` checks your file-based data against these declared schemas — missing required fields, type/enum/format mismatches, nested fields — before you ship. Warns by default, `--strict` for a non-zero CI exit. It's distinct from `uniweb doctor` (which checks project structure): `validate` checks your *data* against the schemas you *declared*. Remote (`url:`), `ref`/`options`, and rich `sections`-form inputs are reported deferred — validate those against live data.
1282
+ **Don't use `hidden: true` on insets.** `hidden` means "don't export this component at all" (internal helpers); `inset: true` means "available for `@Component` references in markdown."
1267
1283
 
1268
- When the same record needs to be a single object rather than a one-element array, that's the foundation's job: read `content.data.articles[0]`, or reshape `content.data` once with a `handlers.data` hook.
1284
+ **Child sections.** You hit a complex layout a 2:1 split with a panel and a main area. Your instinct says build a specialized component. Step back: the panel is a reusable section type, the main area is another, and the split is a Grid with `columns: "1fr 2fr"`. Your child components already adapt to narrow containers — container queries handle that. But hardcoding which components go where means the author can't rearrange or swap them. Child sections solve that:
1269
1285
 
1270
- **Authoring queries.** Fetch declarations (`fetch:` or the `data:` shorthand) accept query operators that describe which records you want, in what order, how many: `where:` (a where-object predicate), `sort:` (e.g. `date desc`), `limit:` (first N records). Whether the source evaluates them or the framework applies them as a runtime fallback is a transport detail controlled by the site's `fetcher.supports:` declaration.
1286
+ ```
1287
+ pages/home/
1288
+ ├── page.yml
1289
+ ├── 2-dashboard.md # Parent section (type: Grid, columns: "1fr 2fr")
1290
+ ├── @sidebar-stats.md # Child (@ = not top-level)
1291
+ └── @main-chart.md
1292
+ ```
1271
1293
 
1272
1294
  ```yaml
1273
- # pages/blog/page.yml
1274
- fetch:
1275
- collection: articles
1276
- where: { published: true, tags: featured }
1277
- sort: date desc
1278
- limit: 3
1295
+ # page.yml
1296
+ nest:
1297
+ dashboard: [sidebar-stats, main-chart]
1279
1298
  ```
1280
1299
 
1281
- **Lean lists with `deferred:`.** Collections with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]` in `site.yml`. The cascade payload omits those fields; per-record full files are emitted at `/data/<name>/<slug>.json` (file-based collections — markdown, YAML, or JSON) or fetched from an author-declared `detailUrl:` pattern (API-backed collections). On dynamic-route pages the focused entity's full record is delivered automatically; elsewhere components fetch on demand via the `useEntityDetail` kit hook.
1282
-
1283
- **Component-side fetching.** When a component genuinely needs to fetch something on its own (a search box, a "load more" button, a popover that lazy-loads), use the kit hooks (`useFetched`, `useCacheEntry`, `useEntityDetail`). They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically.
1284
-
1285
- See [Data Fetching](https://github.com/uniweb/docs/blob/main/reference/data-fetching.md) for the full model and [Predicates](https://github.com/uniweb/docs/blob/main/authoring/predicates.md) for the where-object format with examples.
1286
-
1287
- ### block properties
1288
-
1289
- | Property | Type | Description |
1290
- |----------|------|-------------|
1291
- | `block.page` | Page | Parent page |
1292
- | `block.website` | Website | Site-level data and navigation |
1293
- | `block.type` | string | Component type name |
1294
- | `block.childBlocks` | Block[] | File-based child sections |
1295
- | `block.insets` | Block[] | Inline `@Component` references |
1296
- | `block.getInset(refId)` | Block | Lookup inset by refId |
1297
- | `block.properties` | object | Raw frontmatter |
1298
- | `block.rawContent` | object | ProseMirror document — passed internally when using `<Article block={block} />` |
1299
- | `block.themeName` | string | `"light"`, `"medium"`, `"dark"` |
1300
- | `block.stableId` | string | Stable ID from filename or `id:` |
1301
- | `block.key` | string | Unique key across pages (path + id) — use as React key |
1302
- | `block.path` | string | Page route this block belongs to |
1303
-
1304
- ### Section Wrapper
1305
-
1306
- The runtime wraps every section in `<section>` with context class and background. Customize with static properties:
1300
+ **Rules:**
1301
+ - `@`-prefixed files are excluded from the top-level section list; `@@` nests deeper (grandchildren)
1302
+ - `nest:` maps parent name child names, and is **flat**: `{ features: [a, b], a: [sub-1] }`
1303
+ - Children are ordered by position in the `nest:` array
1307
1304
 
1308
1305
  ```jsx
1309
- function Hero({ content, params }) {
1310
- return (
1311
- <div className="max-w-7xl mx-auto px-6">
1312
- <h1 className="text-heading text-5xl font-bold">{content.title}</h1>
1313
- </div>
1314
- )
1315
- }
1316
-
1317
- Hero.className = 'pt-32 md:pt-48' // Override spacing
1318
- Hero.as = 'div' // Change wrapper element
1306
+ import { ChildBlocks } from '@uniweb/kit'
1319
1307
 
1320
- export default Hero
1308
+ export default function Grid({ block, params }) {
1309
+ return <div className={`grid grid-cols-${params.columns || 2} gap-6`}><ChildBlocks from={block} /></div>
1310
+ }
1321
1311
  ```
1322
1312
 
1323
- - `Component.className` — adds classes to the runtime wrapper. Section-level spacing, borders, overflow.
1324
- - `Component.as` — changes wrapper element: `'nav'` for headers, `'footer'` for footers.
1313
+ `ChildBlocks` renders each child as a **bare component by default** no wrapper, no context class, no background. That's right for grid cells, tab panels, carousel slides. For the rare case where children should be independent sections with their own theming and backgrounds, pass `wrapAs="div"`.
1325
1314
 
1326
- **Layout components** typically need `p-0` to suppress default padding:
1315
+ Each child is a regular section with its own type, params, and content — and you're in the middle: wrap each child, filter by type, reorder, add container classes. The author decides *what* goes in the grid; your component decides *how* it renders. Tomorrow the author can swap a child for a different section type with no code change, and your components stay reusable wherever child sections are accepted.
1327
1316
 
1328
- ```jsx
1329
- Header.className = 'p-0'
1330
- Header.as = 'header'
1331
- ```
1317
+ **Data and child blocks:** page-level `data:` is available to all blocks including children, and each child resolves data independently through the page → site hierarchy. If a child needs data, declare it in the child's `meta.js` or its frontmatter (`data: articles`).
1332
1318
 
1333
- ### Rendering Content with Kit
1319
+ **SSG:** insets, `<ChildBlocks>`, and `<Visual>` all render correctly during prerender. Inset components using React hooks internally trigger prerender warnings — expected and harmless; the page renders correctly client-side.
1334
1320
 
1335
- Content fields are **HTML strings** they contain `<strong>`, `<em>`, `<a>` from markdown. Never render them with raw `{content.title}` in JSX — that shows HTML tags as visible text. Use Kit components:
1321
+ ### Dividers — content boundaries
1336
1322
 
1337
- **Extracted fields** (most common custom layout with content from markdown):
1323
+ `---` creates a boundary between content regions; the developer decides what each region means.
1338
1324
 
1339
- ```jsx
1340
- import { H1, H2, H3, P, Span } from '@uniweb/kit'
1325
+ **UI regions.** `splitContent()` from `@uniweb/kit` splits parsed content at divider elements — e.g. lesson prose vs challenge content:
1341
1326
 
1342
- <H1 text={content.title} className="text-heading text-5xl font-bold" />
1343
- <H2 text={content.subtitle} className="text-heading text-2xl" />
1344
- <H3 text={item.title} className="text-heading text-lg font-semibold" />
1345
- <P text={content.paragraphs} className="text-body" />
1346
- <Span text={listItem.paragraphs[0]} className="text-subtle" />
1327
+ ```jsx
1328
+ const [lesson, challenge] = splitContent(content)
1347
1329
  ```
1348
1330
 
1349
- Kit provides `H1` through `H6` — use the appropriate level for semantic hierarchy. These render their own HTML tag — don't wrap: `<H2 text={...} />` not `<h2><H2 text={...} /></h2>`.
1331
+ <!-- template:loom -->
1332
+ **Data-driven iteration (Loom).** Dividers separate header/body/footer in a repeated template. The split happens *before* Loom runs, because each segment gets a different variable context — the body contains item-level fields that don't exist on the top-level data. Header and footer are instantiated once against the full data; the body repeats per item.
1350
1333
 
1351
- **Full content rendering** (article/docs sections where the author controls the flow):
1334
+ ```markdown
1335
+ ---
1336
+ type: CvEntry
1337
+ source: education
1338
+ ---
1339
+ # Education
1340
+ {COUNT OF education} degrees.
1341
+ ---
1342
+ ## {degree}
1343
+ {institution} — {field} ({start}–{end})
1344
+ ```
1352
1345
 
1353
- ```jsx
1354
- import { Section, Prose } from '@uniweb/kit'
1346
+ **Which to use:** different data contexts per region → Loom pre-parse split (content handler). Same data, different UI treatment → kit post-parse split (`splitContent`). A foundation can use both — Loom splits and iterates to produce final content, then the component splits the result to route regions to different UI.
1347
+ <!-- /template:loom -->
1355
1348
 
1356
- <Section block={block} width="lg" padding="md" />
1357
- <Prose content={content} block={block} />
1358
- ```
1349
+ ### Custom layouts
1359
1350
 
1360
- `Prose` renders from the parsed content sequence headings, paragraphs, images, code snippets, lists, etc. — with prose typography. Tagged data blocks are **skipped** (they're structured data, not prose). Access them via `content.data` for custom rendering:
1351
+ Layouts live in `layouts/` inside the foundation and are auto-discovered. Set `defaultLayout` in `main.js`.
1361
1352
 
1362
1353
  ```jsx
1363
- function Lesson({ content, block }) {
1354
+ // layouts/DocsLayout/index.jsx
1355
+ export default function DocsLayout({ header, body, footer, left, right, params }) {
1364
1356
  return (
1365
- <div>
1366
- <Prose content={content} block={block} />
1367
- {content.data.quiz && <Quiz data={content.data.quiz} />}
1357
+ <div className="min-h-screen flex flex-col">
1358
+ {header && <header>{header}</header>}
1359
+ <div className="flex-1 flex">
1360
+ {left && <aside className="w-64">{left}</aside>}
1361
+ <main className="flex-1">{body}</main>
1362
+ {right && <aside className="w-64">{right}</aside>}
1363
+ </div>
1364
+ {footer && <footer>{footer}</footer>}
1368
1365
  </div>
1369
1366
  )
1370
1367
  }
1371
1368
  ```
1372
1369
 
1373
- Pass `content` (the parsed content object has `.sequence`). Pass `block` too if the content uses insets. Also works as a pure typography wrapper: `<Prose>{children}</Prose>`.
1374
-
1375
- `Article` is an older alternative that renders from `block.rawContent` (raw ProseMirror nodes) — it renders everything including data blocks. Prefer `Prose` for new components.
1376
-
1377
- **Visuals:**
1378
-
1379
- ```jsx
1380
- import { Visual } from '@uniweb/kit'
1381
-
1382
- <Visual inset={block.insets[0]} video={content.videos[0]} image={content.images[0]} className="rounded-2xl" />
1383
- ```
1384
-
1385
- ### Kit API by Use Case
1386
-
1387
- **Rendering text:** `H1`–`H6`, `P`, `Span`, `Div`, `Text` (with `as` prop)
1388
-
1389
- **Rendering content:** `Section` (full section with prose + layout), `Prose` (prose from parsed content sequence, skips data blocks), `Article` (raw ProseMirror rendering), `Render` (ProseMirror nodes → React), `ChildBlocks` (render child sections)
1390
-
1391
- **Rendering media:** `Visual` (first non-empty: inset/video/image), `Image`, `Media`, `Icon`
1392
-
1393
- **Navigation and routing:** `Link` (`to`/`href`, `to="page:about"` for page ID resolution, auto `target="_blank"` for external, `reload` for full page reload), `useActiveRoute()`, `useWebsite()`, `useRouting()`
1394
-
1395
- **Header and layout:** `useScrolled(threshold)`, `useMobileMenu()`, `useAppearance()`
1396
-
1397
- **Layout helpers:** `useGridLayout(columns, { gap })`, `useAccordion({ multiple, defaultOpen })`, `useTheme(name)`
1398
-
1399
- **Data and theming:** `useThemeData()` (programmatic color access), `useColorContext(block)`
1400
-
1401
- **Utilities:** `cn()` (Tailwind class merge — `cn('px-4', condition && 'bg-primary')` resolves conflicts), `Link`, `Image`, `Asset`, `SafeHtml`, `SocialIcon`, `filterSocialLinks(links)`, `getSocialPlatform(url)`
1402
-
1403
- > **`cn()` gotcha — a later `text-<size>` silently drops an earlier `leading-*`.** Tailwind's size classes set line height too, so `cn()` treats the size as replacing the leading: `cn('leading-[1.1] text-4xl')` → `text-4xl`. Put the size first, or fold the leading into it (`text-[clamp(2rem,5vw,4rem)]/[1.1]`). Most likely to bite when the size comes from a lookup and the leading sits in a shared base string.
1404
-
1405
- **Other styled:** `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
1406
-
1407
- ### Hook Signatures
1408
-
1409
- ```js
1410
- useActiveRoute() → { route, rootSegment, isActive(pageOrRoute), isActiveOrAncestor(pageOrRoute) }
1411
- useMobileMenu() → { isOpen, open, close, toggle } // auto-closes on route change
1412
- useScrolled(threshold?) → boolean // true when scrolled past threshold (px)
1413
- useAppearance() → { scheme, setScheme, toggle, canToggle, schemes }
1414
- useWebsite() → { website } // the Website object
1415
- useThemeData() → Theme // programmatic color access
1416
- useColorContext(block) → 'light' | 'medium' | 'dark' // current section context
1417
- ```
1418
-
1419
- `isActive` and `isActiveOrAncestor` accept a Page object or a route string. `useAppearance` reflects the site scheme from `appearance:` in `theme.yml` — `scheme` is `'light'`|`'dark'`, `canToggle` reflects `allowToggle`. The runtime applies the scheme to `<html>` before paint; the hook reads it, `toggle`/`setScheme` flip it and persist the choice to localStorage. First visit follows the OS when the site has dark mode. Full model: *Light/dark appearance* above.
1370
+ Layouts are full components with their own `params` in `meta.js`, not just structural wrappers a header height or sidebar width is a layout param.
1420
1371
 
1421
- ### Icon Component
1372
+ **Layout `meta.js`** declares areas and optional scroll behavior: `{ areas: ['header', 'footer', 'left'], scroll: 'self' }`. Area names are arbitrary. `scroll` controls scroll restoration: unset = runtime manages `window` (default), `'self'` = the layout scrolls itself, or a CSS selector (`'main'`) = runtime manages that element.
1422
1373
 
1423
- The `<Icon>` renders icons from content or explicit props:
1374
+ **Layout content** lives in `site/layout/` `header.md`, `footer.md` for the default layout, or a named subdirectory (`site/layout/marketing/`) for named layouts. Named subdirectories are self-contained — no inheritance. Cascade: `page.yml` → `folder.yml` → `site.yml` → foundation `defaultLayout` → `"default"`.
1424
1375
 
1425
- ```jsx
1426
- {content.icons.map((icon, i) => <Icon key={i} {...icon} />)} // From content
1427
- <Icon name="search" /> // Lucide (default)
1428
- <Icon name="hi2-arrow-right" /> // Other library
1429
- <Icon name="close" /> // Built-in (no network)
1430
- ```
1431
-
1432
- The `name` prop handles everything: built-in names, Lucide icons (default when no library prefix), and other libraries via prefix (`hi2-arrow-right`, `tb-star`). From content, spread the icon object which has `library` + `name` fields.
1433
-
1434
- Other props: `svg` (direct SVG string), `url` (fetch from URL), `size` (default `'24'`), `className`.
1435
-
1436
- Built-in icons (instant, no network): `check`, `close`, `menu`, `chevronDown`, `chevronRight`, `externalLink`, `download`, `play`, and a few others.
1437
-
1438
- ### Content Patterns for Header and Footer
1439
-
1440
- Layout sections (`header.md`, `footer.md`) are regular section types — they support the full content shape including tagged data blocks, lists, links, icons, and items. The only difference is they render on every page instead of one.
1441
-
1442
- Header and Footer combine several content categories. Use different parts of the content shape for each role:
1443
-
1444
- **Header** — title for logo, list for nav, standalone link for CTA:
1376
+ Layout sections are regular section types — they support the full content shape, including tagged data blocks, lists, links, and items. The only difference is they render on every page. Each content category takes a different role:
1445
1377
 
1446
1378
  ````markdown
1447
1379
  ---
1448
1380
  type: Header
1449
1381
  ---
1382
+ # Acme Inc ← content.title — the logo
1450
1383
 
1451
- # Acme Inc
1452
-
1453
- - ![](lu-search) [How It Works](/how-it-works)
1454
- - ![](lu-users) [For Teams](/for-teams)
1384
+ - ![](lu-search) [How It Works](/how) ← content.lists[0] — the nav
1455
1385
  - ![](lu-book) [Docs](/docs)
1456
1386
 
1457
- [Get Started](/docs/quickstart)
1387
+ [Get Started](/docs/quickstart) ← content.links[0] — the CTA
1458
1388
 
1459
1389
  ```yaml:config
1460
- github: https://github.com/acme
1461
- version: v2.1.0
1390
+ github: https://github.com/acme ← content.data.config — everything else
1462
1391
  ```
1463
1392
  ````
1464
1393
 
1465
1394
  ```jsx
1466
- function Header({ content, block }) {
1395
+ function Header({ content }) {
1467
1396
  const logo = content.title
1468
1397
  const navItems = content.lists[0] || []
1469
1398
  const cta = content.links[0]
1470
1399
  const config = content.data?.config
1471
1400
  }
1401
+
1402
+ function Footer({ content }) {
1403
+ const tagline = content.paragraphs[0] // a plain paragraph
1404
+ const columns = (content.lists[0] || []).map(group => ({
1405
+ label: group.paragraphs[0], // nested list → columns
1406
+ links: group.lists[0]?.map(item => item.links[0])
1407
+ }))
1408
+ }
1472
1409
  ```
1473
1410
 
1474
- **Footer** paragraph for tagline, nested list for columns, YAML for legal:
1475
-
1476
- ````markdown
1477
- ---
1478
- type: Footer
1479
- ---
1411
+ ### Cross-block communication
1480
1412
 
1481
- Build something great.
1413
+ Section types sometimes coordinate — the typical case is a Header that needs to know whether the section below supports a floating translucent overlay (a Hero with a full-bleed background does, a plain text section doesn't). The section that **owns the capability declares it**; the section that **needs to adapt reads it**. `getNextBlockInfo()` — and `getPrevBlockInfo()`, `page.getFirstBodyBlockInfo()` — expose two channels:
1482
1414
 
1483
- - Product
1484
- - [Features](/features)
1485
- - [Pricing](/pricing)
1486
- - Developers
1487
- - [Docs](/docs)
1488
- - [GitHub](https://github.com/acme){target=_blank}
1415
+ - **`context`** — static capabilities from `meta.js`. Never changes.
1416
+ - **`state`** — dynamic runtime state via `useBlockState()`. Initial value from `initialState` in `meta.js`.
1489
1417
 
1490
- ```yaml:legal
1491
- copyright: © 2025 Acme Inc
1418
+ ```js
1419
+ // Hero/meta.js
1420
+ export default {
1421
+ context: { allowTranslucentTop: true }, // "I always support this"
1422
+ initialState: { allowTranslucentTop: true }, // …or start true and let logic change it
1423
+ }
1492
1424
  ```
1493
- ````
1494
1425
 
1495
1426
  ```jsx
1496
- function Footer({ content }) {
1497
- const tagline = content.paragraphs[0]
1498
- const columns = content.lists[0] || []
1499
- const legal = content.data?.legal
1427
+ // Hero/index.jsx dynamic case
1428
+ const [state, setState] = block.useBlockState(useState)
1500
1429
 
1501
- columns.map(group => ({
1502
- label: group.paragraphs[0],
1503
- links: group.lists[0]?.map(item => item.links[0])
1504
- }))
1505
- }
1430
+ // Header/index.jsx reads dynamic state, falls back to static context
1431
+ const info = block.getNextBlockInfo()
1432
+ const isFloating = info?.state?.allowTranslucentTop ?? info?.context?.allowTranslucentTop ?? false
1506
1433
  ```
1507
1434
 
1508
- ### meta.js Structure
1509
-
1510
- ```javascript
1511
- export default {
1512
- title: 'Feature Grid',
1513
- description: 'Grid of feature cards with icons',
1514
- category: 'marketing',
1515
- // hidden: true, // Exclude from export
1516
- // background: 'self', // Component renders its own background
1517
- // inset: true, // Available for @ComponentName in markdown
1518
- // visuals: 1, // Expects 1 visual
1519
- // children: true, // Accepts child sections
1435
+ The key names are yours to design — they're not framework fields.
1520
1436
 
1521
- content: {
1522
- title: 'Section heading',
1523
- paragraphs: 'Introduction [0-1]',
1524
- items: 'Feature cards with icon, title, description',
1525
- },
1437
+ ### block, website, and page
1526
1438
 
1527
- params: {
1528
- columns: { type: 'number', default: 3 },
1529
- variant: { type: 'select', options: ['default', 'centered', 'split'], default: 'default' },
1530
- },
1439
+ ```jsx
1440
+ const { website } = useWebsite()
1441
+ const page = website.activePage
1442
+ ```
1531
1443
 
1532
- presets: {
1533
- default: { label: 'Standard', params: { columns: 3 } },
1534
- compact: { label: 'Compact', params: { columns: 4 } },
1535
- },
1444
+ | `block` property | Description |
1445
+ |----------|-------------|
1446
+ | `block.page` / `block.website` | Parent page / site-level data and navigation |
1447
+ | `block.type` | Component type name |
1448
+ | `block.childBlocks` / `block.insets` | Child sections / inline `@Component` references |
1449
+ | `block.getInset(refId)` | Lookup an inset by refId |
1450
+ | `block.properties` | Raw frontmatter |
1451
+ | `block.rawContent` | ProseMirror document — passed internally by `<Article block={block} />` |
1452
+ | `block.themeName` | `"light"`, `"medium"`, `"dark"` |
1453
+ | `block.stableId` / `block.key` | Stable ID from filename or `id:` / unique key across pages — use as React key |
1454
+ | `block.path` | Page route this block belongs to |
1455
+ | `block.dataLoading` | True while declared data is still resolving |
1536
1456
 
1537
- // context and initialState: keys are developer-defined, not framework fields.
1538
- // Design your own names for your foundation's cross-block communication.
1457
+ ```jsx
1458
+ // getPageHierarchy(options)
1459
+ // [{ id, route, navigableRoute, translatedRoute, title, label, description, hasContent, version, children }]
1460
+ // Options: for: 'header'|'footer'|<area> (respects hideIn) · nested: true (default)
1461
+ // includeHidden: false · filter: (page) => bool · sort: (a, b) => number
1462
+ website.getPageHierarchy({ for: 'header' }) // = website.getHeaderPages()
1463
+ website.getPageHierarchy({ nested: false }) // = website.getAllPages()
1539
1464
 
1540
- // Static — neighbors read via getNextBlockInfo().context
1541
- context: {
1542
- // Example: a Hero might declare this so a Header knows it can float.
1543
- // allowTranslucentTop: true,
1544
- },
1465
+ website.name, website.basePath
1466
+ website.hasMultipleLocales(), website.getLocales(), website.getActiveLocale(), website.getLocaleUrl('es')
1545
1467
 
1546
- // Dynamic neighbors read via getNextBlockInfo().state
1547
- // Component can update with useBlockState()
1548
- initialState: {
1549
- // Example: Hero starts translucent-ready, but component logic may disable it.
1550
- // allowTranslucentTop: true,
1551
- },
1552
- }
1468
+ page.title, page.label, page.route, page.description
1469
+ page.isHidden(), page.showInHeader(), page.showInFooter()
1470
+ page.hasContent() // true if the page has its own content (not just a folder)
1471
+ page.hasChildren(), page.children, page.parent
1472
+ page.getNavigableRoute() // first descendant route with content (for linking)
1553
1473
  ```
1554
1474
 
1555
- All defaults belong in `meta.js`, not inline in component code.
1475
+ Content-less containers appear as group nodes (`hasContent: false`) use `navigableRoute` for links, `title` for display, `hasContent` to style differently.
1556
1476
 
1557
- ### The Front Desk Pattern
1477
+ ### Data
1478
+
1479
+ A component on a page with a `data:` or `fetch:` declaration automatically receives that data in `content.data.{key}` — no opt-in in `meta.js`.
1558
1480
 
1559
- Section types naturally use params to adjust their own rendering `variant: flipped` reverses a flex direction, `columns: 3` sets a grid. That's not a pattern, that's the baseline.
1481
+ **Bound collections always arrive as arrays.** On a list page, `content.data.articles` is the full collection. On a template page (`[slug]/`), the matched record is delivered under the *same* key as a single-element array — the detail section reads `content.data.articles[0]`. When nothing matches, the key is `[]`. The runtime never coerces to a single object and never synthesizes a singular key.
1482
+
1483
+ ```jsx
1484
+ function Article({ content, block }) {
1485
+ if (block.dataLoading) return <DataPlaceholder />
1486
+ const article = content.data.articles?.[0] // focused record on a [slug] page
1487
+ if (!article) return <NotFound />
1488
+ return <ArticleView article={article} />
1489
+ }
1490
+ ```
1560
1491
 
1561
- The **Front Desk pattern** is when a section type does virtually no rendering itself. It reads the author's params, picks the right helper component, and translates author-friendly vocabulary into developer-oriented props. The section type is a front desk it greets the request and routes it to the right specialist.
1492
+ Components can ignore keys in `content.data` they don't need, the same way unused `params` are ignored. When a record genuinely needs to be a single object, that's the foundation's job read `[0]`, or reshape once with a `handlers.data` hook.
1562
1493
 
1563
- The workers behind the front desk don't need to share the same interface. A `Hero` might delegate to a `SliderHero` that renders an image carousel and a `ContactHero` that renders a quote request form. They expect different content and different props that's fine. The front desk declares the **union** of all content its workers might need. Some content won't be used for a given variant, and that's perfectly normal in CCA params change behavior, and that includes not rendering some content:
1494
+ **Declaring schemas.** `meta.js` declares the schema for each `content.data` key with a single `data:` field — there is no separate `schemas:` key. Each value is a **named ref**, an **inline field map**, or an **inline rich-form** (`{ fields: [...] }`, an editor form). Refs resolve on disk at build time, never fetched: `@/name` (this foundation's `schemas/`), `@std/name` (shared standards, from `@uniweb/schemas`), `@org/name` (an org's own `@org/schemas` package). The schema is a hint it supplies field defaults and drives the editor, not delivery, which is default-on. For an explicit opt-out (rare), set `data: false`.
1564
1495
 
1565
1496
  ```js
1566
- // meta.js — the union of all variants' needs
1497
+ // meta.js
1567
1498
  export default {
1568
- params: {
1569
- variant: { type: 'select', options: ['slider', 'contact'], default: 'slider' },
1570
- slideInterval: { type: 'number', default: 5 },
1571
- density: { type: 'select', options: ['default', 'compact'], default: 'default' },
1572
- style: { type: 'select', options: ['default', 'dramatic'], default: 'default' },
1573
- }
1499
+ data: {
1500
+ articles: '@/article', // named ref (this foundation)
1501
+ authors: '@std/person', // named ref (shared standard)
1502
+ pricing: { tier: { type: 'string', default: '' } }, // inline field map
1503
+ },
1574
1504
  }
1575
1505
  ```
1576
1506
 
1577
- ```jsx
1578
- // sections/Hero/index.jsx — the front desk
1579
- import { SliderHero } from '../../components/SliderHero'
1580
- import { ContactHero } from '../../components/ContactHero'
1581
-
1582
- const variants = { slider: SliderHero, contact: ContactHero }
1507
+ A foundation can route a scope to a plain folder of schema files instead of a package via an optional `schemas.config.js` at its root — `export default { '@acme': '../shared/acme-schemas' }`. A routed scope wins over the package convention; `@/` and `@uniweb` are never routable; a routed scope has no package fallback for a missing schema (it errors rather than silently loading a different definition). Per-schema keys override single entries (most-specific wins: file › directory › package). Worked examples: `development/schemas-in-practice.md`.
1583
1508
 
1584
- export default function Hero({ content, block, params }) {
1585
- const Variant = variants[params.variant] || SliderHero
1509
+ **Authoring queries.** Fetch declarations accept `where:` (a where-object predicate), `sort:` (e.g. `date desc`), and `limit:`. Whether the source evaluates them or the framework applies them as a runtime fallback is a transport detail controlled by the site's `fetcher.supports:` declaration.
1586
1510
 
1587
- return (
1588
- <Variant
1589
- // Shared — every variant gets these
1590
- title={content.title}
1591
- subtitle={content.paragraphs[0]}
1592
- links={content.links}
1593
- block={block}
1594
- // Content that only some variants use
1595
- images={content.images}
1596
- formData={content.data?.quote}
1597
- // Translated params — author vocabulary → developer props
1598
- interval={params.slideInterval}
1599
- compact={params.density === 'compact'}
1600
- transition={params.style === 'dramatic' ? 'zoom' : 'fade'}
1601
- />
1602
- )
1603
- }
1511
+ ```yaml
1512
+ # pages/blog/page.yml
1513
+ fetch:
1514
+ collection: articles
1515
+ where: { published: true, tags: featured }
1516
+ sort: date desc
1517
+ limit: 3
1604
1518
  ```
1605
1519
 
1606
- `SliderHero` uses `images`, `interval`, and `transition`; it ignores `formData` and `compact`. `ContactHero` uses `formData` and `compact`; it ignores `images` and `interval`. Each worker takes what it needs. Some params only matter for certain variants (`slideInterval` for slider, `density` for contact). Some are high-level names that the front desk translates into developer-oriented values (`style: dramatic` `transition="zoom"`). The content author writes `variant: contact` they don't know or care about `ContactHero`.
1520
+ **Lean lists with `deferred:`.** Collections with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]` in `site.yml`. The cascade payload omits those fields; per-record full files are emitted at `/data/<name>/<slug>.json` (file-based collections) or fetched from an author-declared `detailUrl:` (API-backed). On dynamic-route pages the focused record's full data is delivered automatically; elsewhere components fetch on demand via `useEntityDetail`.
1607
1521
 
1608
- This is the system-building pattern at its clearest: **section types are the public interface** to your content system (author-friendly names, documented in `meta.js`). **Helper components are the implementation** (developer-friendly APIs, ordinary React props). The section type is the thin translation layer that connects the two worlds.
1522
+ **Component-side fetching.** When a component genuinely needs to fetch on its own (a search box, "load more", a lazy popover), use the kit hooks `useFetched`, `useCacheEntry`, `useEntityDetail`. They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically.
1609
1523
 
1610
- ### Section components are composites
1524
+ **Validate before shipping.** `uniweb validate` checks file-based data against your declared schemas — missing required fields, type/enum/format mismatches, nested fields. Warns by default; `--strict` for a non-zero CI exit. Distinct from `uniweb doctor` (project structure): `validate` checks your *data* against the schemas you *declared*. Remote (`url:`), `ref`/`options`, and rich `sections`-form inputs are reported deferred.
1611
1525
 
1612
- A section component is rarely a single flat render. It imports helper components from `components/` and utilities from `utils/` to build a complex UI while presenting a single `type:` to the content author. These directories are the developer's workbench — ordinary React and JS, not selectable by authors, not auto-discovered.
1526
+ ### Fetching from other sources (`fetcher:`)
1613
1527
 
1614
- ```jsx
1615
- // sections/Pricing/index.jsx
1616
- import PricingCard from '#components/PricingCard'
1617
- import formatPrice from '#utils/formatPrice'
1528
+ A site isn't limited to file-based collections. The `fetcher:` block in `site.yml` tunes the framework's default fetcher and opts into foundation-provided **named transports** per schema:
1618
1529
 
1619
- export default function Pricing({ content, params }) {
1620
- const currency = params.currency || 'USD'
1621
- return (
1622
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-6xl mx-auto">
1623
- {content.items.map((tier, i) => (
1624
- <PricingCard key={i} tier={tier} price={formatPrice(tier.data, currency)} />
1625
- ))}
1626
- </div>
1627
- )
1628
- }
1530
+ ```yaml
1531
+ # site.yml
1532
+ fetcher:
1533
+ baseUrl: https://api.example.com
1534
+ headers: { X-Tenant: acme }
1535
+ envelope: { collection: data.items, item: data.article, error: errors.0.message }
1536
+
1537
+ supports: [where, limit, sort] # which operators the source evaluates natively
1538
+
1539
+ transports:
1540
+ articles: uniweb # a foundation-registered transport handles `data: articles`
1541
+ events: default # explicitly route back to the default fetcher
1542
+ uniweb: # binding config that transport reads
1543
+ siteFolder: abc-123-def
1629
1544
  ```
1630
1545
 
1631
- The content author writes `type: Pricing` and defines tiers as content items. The section component maps items to cards using a helper component from `components/` and a formatting utility from `utils/`. Neither is selectable by authorsthey're implementation details behind the section type boundary.
1546
+ **`supports:` is a capability declaration, not a switch.** With `supports: []` (the default) the source is treated as static: the whole collection is fetched and the framework applies `where` / `sort` / `limit` in JS afterward, so two pages with different predicates share one cache entry. With `supports: [where]` the predicate ships in the request and the cache splits per predicate. With `[where, limit, sort]` the source returns the final result and the framework passes it through. Pushdown applies only to remote `url:` sources local `path:` reads are static files and always evaluate operators as a runtime fallback.
1632
1547
 
1633
- **When to reach for this pattern:** When a page type has consistent structural elements (header bars, navigation footers, contextual sidebars) that the content author shouldn't need to add as separate sections. If the author would have to add the same boilerplate sections to every page of a certain type, the section component should compose them internally.
1548
+ **Selection is explicit and site-owned.** For each request: `fetcher.transports[schema]` wins if set; otherwise `fetcher.transports.default` if set; otherwise the framework's default fetcher applies `baseUrl` / `headers` / `envelope`. No route-walking, no `match()` predicates, no silent foundation-owned routing the site picks.
1634
1549
 
1635
- **Common mistake:** Solving structural repetition at the layout level. If only some page types need a content header (lessons do, the homepage doesn't), it's a section concern, not a layout concern. The layout owns the page-wide chrome (header area, sidebar area). The section owns its own internal structure.
1550
+ > **Never put secrets in `site.yml`** every value in it is public to the browser. Sites needing private credentials proxy through the same origin at the deployment layer, so the site fetches `/api/…` and the proxy attaches the credential server-side.
1636
1551
 
1637
- ### Foundation Organization
1552
+ **Failures degrade rather than break:** a failed fetch falls back to `[]`, logs a build warning, and the page still renders. Components should handle the empty case — which the guaranteed content shape already encourages.
1638
1553
 
1639
- ```
1640
- src/ # the foundation package (folder name is `src`)
1641
- ├── sections/ # Section types (auto-discovered)
1642
- │ ├── Hero.jsx # Bare file — no folder needed
1643
- │ ├── Features/ # Folder when you need meta.js
1644
- │ │ ├── index.jsx
1645
- │ │ └── meta.js
1646
- │ └── insets/ # Organizational subdirectory (lowercase)
1647
- │ └── Diagram/
1648
- │ ├── index.jsx
1649
- │ └── meta.js
1650
- ├── layouts/ # Custom layouts (optional, auto-discovered)
1651
- │ └── DocsLayout/
1652
- │ ├── index.jsx
1653
- │ └── meta.js
1654
- ├── components/ # Your React components (no meta.js, not selectable)
1655
- │ ├── ui/
1656
- │ │ └── button.jsx
1657
- │ └── Card.jsx
1658
- ├── utils/ # Helper functions, non-React logic
1659
- │ └── splitContent.js
1660
- ├── main.js
1661
- ├── styles.css
1662
- └── package.json # name: "src"
1663
- ```
1554
+ Recipes for staying on the default fetcher, and for writing a custom transport: `development/connecting-a-backend.md`.
1664
1555
 
1665
- **Discovery:** PascalCase files/folders at root of `src/sections/` are auto-discovered. Nested levels require `meta.js`. Lowercase directories are organizational only. `hidden: true` excludes a component entirely. Everything outside `src/sections/` is ordinary React.
1556
+ Full model: `reference/data-fetching.md`. Where-object format with examples: `authoring/predicates.md`.
1666
1557
 
1667
- **Source root.** The foundation package's source files live at the package root — the `src/` folder *is* the foundation. The build reads `package.json::main` to know that (for new scaffolds, `main: "./_entry.generated.js"`). Older foundations may use an even-more-nested layout where the source lives in `foundation/src/` and `main` points to `./src/_entry.generated.js`; both shapes work through the same code path.
1558
+ <!-- template:loom -->
1559
+ ### Content handlers
1668
1560
 
1669
- **Import aliases:** Foundations include subpath imports in `package.json` for shared internals. Use them instead of brittle relative paths:
1561
+ Content handlers are a transform layer between data assembly and the component, declared in `main.js` and applied to every section in the foundation. The standard content shape is the default; handlers reshape it. All three are optional, run per block, and are error-isolated — a failing handler logs a warning and falls back to default behavior.
1670
1562
 
1671
- | Alias | Maps to | Use for |
1672
- |-------|---------|---------|
1673
- | `#components/*` | `./components/*` | Shared React components |
1674
- | `#utils/*` | `./utils/*` | Helper functions, non-React logic |
1563
+ | Handler | When it runs | Receives | Returns | Purpose |
1564
+ |---|---|---|---|---|
1565
+ | `data` | After data assembly, before content transform | `(data, block)` | New data object, or null | Filter, reshape, or augment assembled data |
1566
+ | `content` | After the data handler | `(data, block)` | ProseMirror document, or null | Transform raw content (Loom instantiation, template expansion) |
1567
+ | `props` | After parsing, defaults, and guarantees | `(content, params, block)` | `{ content, params }`, or null | Post-process the final shape before the component sees it |
1675
1568
 
1676
- ```jsx
1677
- // ✅ Clean — use aliases
1678
- import LessonHeader from '#components/LessonHeader'
1679
- import splitContent from '#utils/splitContent'
1569
+ The `content` handler receives `block.parsedContent.data` and reads raw ProseMirror from `block.rawContent`, returning a new ProseMirror document that the framework re-parses through the semantic parser. Returning `null` — or the same reference as `block.rawContent` — signals no change.
1570
+
1571
+ > **`block.rawContent` may or may not be wrapped.** Unwrap it defensively — `const doc = block.rawContent?.doc ?? block.rawContent` — before passing it to `instantiateContent` / `instantiateRepeated`. This is the first thing a hand-written handler gets wrong.
1572
+
1573
+ **Loom integration** is the most common use: resolving `{placeholder}` expressions against live data. `@uniweb/loom` provides a factory:
1574
+
1575
+ ```js
1576
+ import { createLoomHandlers } from '@uniweb/loom'
1680
1577
 
1681
- // Fragile — breaks if you reorganize sections/
1682
- import LessonHeader from '../../components/LessonHeader'
1578
+ export default {
1579
+ handlers: createLoomHandlers({ vars: (data) => data?.profile?.[0] }),
1580
+ }
1683
1581
  ```
1684
1582
 
1685
- Within the same directory (e.g., one component importing a sibling), use normal relative imports (`./AIFeedbackCard`).
1583
+ `vars` extracts the Loom variable namespace. The returned `content` handler reads the `source` and `where` frontmatter params: without `source` it does simple substitution; with `source` it splits the markdown at `---` dividers and repeats the body per data item. `where` filters the source array first — `type = 'book'` (equality), `year > 1870` (comparison), `refereed` (truthy), `type = 'book' AND refereed` (combination). Aggregates like `{COUNT OF publications}` reflect the filtered set.
1686
1584
 
1687
- **Foundation entry shape (`main.js`).** A single `export default { … }` whose top-level keys are the capabilities the foundation provides e.g. `name`, `description`, `defaultLayout`, `defaultSection`, `viewTransitions`, `props`, `defaultInsets`, `xref`, `outputs`, `handlers`. Optionally a named `vars` export for theme-variable metadata (see *Foundation variables*). Everything else (section types, layouts) is auto-discovered from `sections/` and `layouts/` and merged in by `@uniweb/build`. The build wraps your default export under `default.capabilities` in the produced `dist/entry.js`; you don't write that wrapper yourself, and most foundation code never sees it. The one place it matters: when you import your **own** `main.js` from a foundation component (e.g., a download button calling `compileDocument(website, { foundation })`), you get the bare default object — pass it through directly, Press handles both shapes.
1585
+ For cases the factory doesn't cover, write handlers directly using `Loom`, `instantiateContent`, and `instantiateRepeated` from `@uniweb/loom`.
1688
1586
 
1689
- ### Website and Page APIs
1587
+ > **`instantiateContent` resolves `{placeholders}` in text nodes only** — not in link `href`s or other node/mark attributes. So `[{email}](mailto:{email})` fills the visible label but leaves the `mailto:` URL literal. For dynamic URLs, emit the value as plain text and let the component linkify it, or build the href in the handler yourself.
1690
1588
 
1691
- ```jsx
1692
- const { website } = useWebsite()
1693
- const page = website.activePage
1589
+ **Reserved frontmatter:** `source` and `where` are convention-level reserved fields — they flow to both `block.properties` (for handler access) and `params` (visible to components), consistent with `background` and `theme`. Components can ignore them. List them in `meta.js` params with descriptions so the editor and schema recognize them.
1590
+ <!-- /template:loom -->
1694
1591
 
1695
- // Navigation — getPageHierarchy(options)
1696
- // Returns [{ id, route, navigableRoute, translatedRoute, title, label, description, hasContent, version, children }]
1697
- //
1698
- // Options:
1699
- // for: 'header' | 'footer' | <area> — filter by nav area (respects the page's hideIn)
1700
- // nested: true (default) — nested hierarchy with children; false = flat list
1701
- // includeHidden: false — include hidden pages
1702
- // filter: (page) => bool — custom filter function
1703
- // sort: (a, b) => number — custom sort function
1704
- //
1705
- // Convenience methods:
1706
- // website.getHeaderPages() — same as getPageHierarchy({ for: 'header' })
1707
- // website.getFooterPages() — same as getPageHierarchy({ for: 'footer' })
1708
- // website.getAllPages() — flat list: getPageHierarchy({ nested: false })
1709
- //
1710
- // Common patterns:
1711
- website.getPageHierarchy({ for: 'header' }) // Header nav (excludes pages with 'header' in hideIn)
1712
- website.getPageHierarchy() // Full nested hierarchy (no nav filtering)
1713
- website.getPageHierarchy({ nested: false }) // Flat list of all visible pages
1714
- website.getPageHierarchy({ nested: false, includeHidden: true }) // Everything including hidden
1715
-
1716
- // Core properties
1717
- website.name // Site name from site.yml
1718
- website.basePath // Deployment base path (e.g., '/docs/')
1719
-
1720
- // Locale
1721
- website.hasMultipleLocales()
1722
- website.getLocales() // [{ code, label, isDefault }]
1723
- website.getActiveLocale()
1724
- website.getLocaleUrl('es')
1725
-
1726
- // Route detection
1727
- const { isActive, isActiveOrAncestor } = useActiveRoute()
1728
-
1729
- // Appearance
1730
- const { scheme, toggle, canToggle } = useAppearance()
1731
-
1732
- // Page properties
1733
- page.title, page.label, page.route, page.description
1734
- page.isHidden(), page.showInHeader(), page.showInFooter()
1735
- page.hasContent() // True if page has its own content (not just a folder)
1736
- page.hasChildren(), page.children // Direct child Page instances
1737
- page.parent // Parent Page instance (null for root pages)
1738
- page.getNavigableRoute() // First descendant route with content (for linking)
1592
+ ---
1739
1593
 
1740
- // Hierarchical navigationcontent-less containers are group nodes:
1741
- // { route: '/courses/intro', title: 'Introduction', hasContent: false,
1742
- // navigableRoute: '/courses/intro/lesson-1', children: [...] }
1743
- // Use navigableRoute for links, title for display, hasContent to style differently.
1744
- ```
1594
+ ## Part 5Commands, shipping, and migration
1745
1595
 
1746
- ### Cross-Block Communication
1596
+ ```bash
1597
+ uniweb dev # Start dev server (picks the site for you)
1598
+ pnpm build / pnpm preview # Build for production / preview the build (SSG + SPA)
1747
1599
 
1748
- Section types sometimes need to coordinate. The typical case: a Header needs to know whether the section below it supports a floating translucent overlay — a Hero with a full-bleed background does, a plain text section doesn't. The section that **owns the capability declares it**; the section that **needs to adapt reads it**.
1600
+ uniweb deploy # Wizard: pick a destination, then deploy (or set up CI)
1601
+ uniweb deploy --host=<adapter> # Build + upload now, from this machine
1602
+ uniweb add ci --host=<adapter> # CI so every push deploys — usually best for a free host
1603
+ uniweb export # Build dist/ for any static host (no Uniweb account)
1604
+ uniweb publish # Ship to Uniweb hosting, foundation included (needs `uniweb login`)
1749
1605
 
1750
- `getBlockInfo()` exposes two channels:
1606
+ uniweb add ci --target foundation # Publish a foundation for free at permanent versioned URLs
1607
+ # (GitHub Pages → foundations/<name>/<version>/entry.js)
1751
1608
 
1752
- - **`context`** Static capabilities from `meta.js`. Never changes. The declaring section type always has this capability.
1753
- - **`state`** Dynamic runtime state via `useBlockState()`. Can change based on component logic. Initial value comes from `initialState` in `meta.js`.
1609
+ uniweb push / pull / clone / status # Git-style content sync with the Uniweb backend
1610
+ uniweb register [--scope @org] # Register a foundation + its data schemas to the registry
1754
1611
 
1755
- ```jsx
1756
- // Header reads the next section's info to decide how to render
1757
- const nextBlockInfo = block.getNextBlockInfo()
1758
- // nextBlockInfo.context → static (meta.js)
1759
- // nextBlockInfo.state → dynamic (useBlockState)
1760
- ```
1612
+ uniweb rename <foundation|site|extension> <old> <new> # Rename across the whole workspace
1613
+ uniweb i18n extract / init / sync / status / audit # Translation workflow (build first)
1614
+ uniweb i18n init-freeform / update-hash / move / rename / prune --freeform
1761
1615
 
1762
- **Static context**Hero declares a permanent capability, Header reads it:
1616
+ uniweb -v # Installed CLI version and whether a newer one exists
1617
+ uniweb doctor # Diagnose project configuration (--fix to auto-repair)
1618
+ uniweb validate # Check file-based data against declared schemas (--strict for CI)
1619
+ uniweb update # Align @uniweb/* deps + AGENTS.md to the CLI (--dry-run, --yes)
1620
+ uniweb inspect <path> # Show parsed content for a section or page (--raw for the AST)
1763
1621
 
1764
- ```js
1765
- // Hero/meta.js — "I always support a translucent header over me"
1766
- export default {
1767
- context: { allowTranslucentTop: true },
1768
- }
1622
+ uniweb <command> --help # Per-command flags — no side effects. Prefer this over guessing.
1769
1623
  ```
1770
1624
 
1771
- ```jsx
1772
- // Header/index.jsx — adapts based on what's below
1773
- const nextBlockInfo = block.getNextBlockInfo()
1774
- const isFloating = nextBlockInfo?.context?.allowTranslucentTop || false
1775
- ```
1625
+ ### Where a site can live
1776
1626
 
1777
- **Dynamic state** Hero declares an initial value but can change it at runtime:
1627
+ **There is no lock-in, and no default you're pushed toward.** Four independent paths, and a project can change its mind:
1778
1628
 
1779
- ```js
1780
- // Hero/meta.js — starts as true, but component logic may change it
1781
- export default {
1782
- initialState: { allowTranslucentTop: true },
1783
- }
1784
- ```
1629
+ | Path | Command | Good for |
1630
+ |---|---|---|
1631
+ | Free static host, via CI | `uniweb add ci --host=<adapter>` | most self-hosted sites — one command, then every push deploys |
1632
+ | Free static host, from this machine | `uniweb deploy --host=<adapter>` | one-off or manual deploys |
1633
+ | Any static host at all | `uniweb export` | full control; no Uniweb account needed |
1634
+ | Uniweb Cloud | `uniweb publish` | teams with non-technical content authors, or client work |
1785
1635
 
1786
- ```jsx
1787
- // Hero/index.jsx — conditionally updates
1788
- function Hero({ content, block }) {
1789
- const [state, setState] = block.useBlockState(useState)
1790
- // state.allowTranslucentTop is true initially (from meta.js)
1791
- // Component logic can change it: setState({ allowTranslucentTop: false })
1792
- }
1793
- ```
1636
+ `uniweb deploy` never assumes a host: with nothing configured it opens a picker listing only destinations it can act on, and records the choice in `deploy.yml` so later runs go straight there. On Cloudflare Pages / Netlify / Vercel, `add ci` also adds per-PR previews that comment the URL. Adapters: `github-pages`, `cloudflare-pages`, `netlify`, `vercel`, plus `s3-cloudfront` for `deploy`. Destination config lives in `deploy.yml` beside `site.yml`; host credentials come from the environment, never from that committed file.
1794
1637
 
1795
- ```jsx
1796
- // Header/index.jsx — reads dynamic state, falls back to static context
1797
- const nextBlockInfo = block.getNextBlockInfo()
1798
- const isFloating = nextBlockInfo?.state?.allowTranslucentTop
1799
- ?? nextBlockInfo?.context?.allowTranslucentTop
1800
- ?? false
1801
- ```
1638
+ Foundations have their own free path too: `uniweb add ci --target foundation` publishes to permanent versioned URLs on GitHub Pages.
1802
1639
 
1803
- The key names (`allowTranslucentTop`, `expanded`, etc.) are yours to design — they're not framework fields. Define whatever protocol your foundation's sections need.
1640
+ ### Uniweb Cloud and the two-sided workflow
1804
1641
 
1805
- Other navigation methods: `block.getPrevBlockInfo()`, `block.page.getFirstBodyBlockInfo()`.
1642
+ `uniweb publish` ships to Uniweb Cloud, and it's worth understanding what that buys, because it isn't only hosting — it's how a team of technical and non-technical people works on one site.
1806
1643
 
1807
- ### Custom Layouts
1644
+ **Content authors work visually in the Uniweb App.** They compose the same extended markdown and set the same component params you defined, through a visual editor — never touching code, git, or the CLI. They see exactly the section types your foundation offers and exactly the knobs each one exposes, because **every `meta.js` is registered as the foundation's schema** when you publish. Your `meta.js` is the app's UI (see *meta.js* in Part 4).
1808
1645
 
1809
- Layouts live in `layouts/` (inside the foundation package) and are auto-discovered:
1646
+ **Sync is developer-only and one-sided by design.** `uniweb push` and `uniweb pull` are your commands, not theirs:
1810
1647
 
1811
- ```js
1812
- // main.js
1813
- export default {
1814
- name: 'My Template',
1815
- description: 'A brief description',
1816
- defaultLayout: 'DocsLayout',
1817
- }
1818
- ```
1648
+ - **The app is the live source of truth for content** — where authors work and where your push lands live.
1649
+ - **Git is the reviewed, durable record** — you `pull` content back, read it with `git diff`, and commit.
1650
+ - **Authors never push or pull.** For them, content simply updates, whether the change came from another author or from a developer's CLI.
1819
1651
 
1820
- ```jsx
1821
- // layouts/DocsLayout/index.jsx
1822
- export default function DocsLayout({ header, body, footer, left, right, params }) {
1823
- return (
1824
- <div className="min-h-screen flex flex-col">
1825
- {header && <header>{header}</header>}
1826
- <div className="flex-1 flex">
1827
- {left && <aside className="w-64">{left}</aside>}
1828
- <main className="flex-1">{body}</main>
1829
- {right && <aside className="w-64">{right}</aside>}
1830
- </div>
1831
- {footer && <footer>{footer}</footer>}
1832
- </div>
1833
- )
1834
- }
1835
- ```
1652
+ **Conflicts behave like a collaborative document, not like git.** A developer's push arrives the way a live collaborator's edit would. Different sections never conflict, and different params of the same section never conflict — last edit wins. The app warns only when two content edits target the same section at the same time.
1836
1653
 
1837
- **Layout meta.js** declares areas and optional scroll behavior: `{ areas: ['header', 'footer', 'left'], scroll: 'self' }`. Area names are arbitrary. The `scroll` property controls how the runtime manages scroll restoration: not set = runtime manages on `window` (default), `'self'` = layout handles its own scrolling, or a CSS selector (e.g. `'main'`) = runtime manages scroll on that element.
1654
+ **The Cloud also provides a real backend for structured data:** a database for every registered data schema, and a CMS that edits both static page content and dynamic data entities typed by those schemas. That's the piece that makes it viable for teams and client work the client manages records, not markdown files.
1838
1655
 
1839
- **Layout content** each layout has section files in `site/layout/`:
1656
+ Either side can publish. Nothing about this changes how you build: the same foundation and the same site run under `uniweb dev`, `uniweb export`, or a CI deploy with no account at all.
1840
1657
 
1841
- ```
1842
- site/layout/
1843
- ├── header.md # Default layout
1844
- ├── footer.md
1845
- └── marketing/ # Named layout sections
1846
- ├── header.md
1847
- └── footer.md
1658
+ **Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login`, `--token`, or `UNIWEB_TOKEN`; preview with `--dry-run`.
1659
+
1660
+ ### Staying current
1661
+
1662
+ ```bash
1663
+ uniweb -v # installed CLI version, and whether a newer one is available
1664
+ uniweb doctor # report drift in this project, changing nothing
1665
+ uniweb update --dry-run # preview exactly what update would change
1666
+ uniweb update # apply: align @uniweb/* deps AND refresh AGENTS.md
1848
1667
  ```
1849
1668
 
1850
- Named subdirectories are self-contained no inheritance. Layout cascade: `page.yml` `folder.yml` `site.yml` foundation `defaultLayout` `"default"`.
1669
+ **`uniweb update` is the command for bringing a project up to date.** It aligns the project's `@uniweb/*` dependencies *and* this AGENTS.md to the version matrix of the CLI that runs it. Deps and documentation move together — that's the whole point of the verb.
1851
1670
 
1852
- ---
1671
+ > **Don't run `npm update` or `pnpm update` on the `@uniweb/*` packages.** They're a matched set resolved by the CLI's version matrix, not independently versioned libraries you upgrade one at a time. Updating them directly gets you a combination nobody tested, and it won't refresh AGENTS.md — so this guide silently drifts out of sync with the code it describes, which is worse than being out of date, because nothing looks wrong.
1853
1672
 
1854
- ## Content Handlers
1673
+ Two ordering rules: `update` won't refresh AGENTS.md while declared deps still lag the CLI, or while edited deps haven't been installed — either would put the doc ahead of the code. And updating the CLI itself is your package manager's job (`npm i -g uniweb@latest`, `pnpm add -g uniweb@latest`); `uniweb update` does not do that. To pin a project to the newest published release with no global install: `npx uniweb@latest update --yes`.
1855
1674
 
1856
- Content handlers are a transform layer that runs between data assembly and the component. They're declared in `main.js` and apply to every section in the foundation. The standard content shape (title, paragraphs, items, sequence) is the default — handlers can reshape it.
1675
+ ### `package.json` `uniweb` block
1857
1676
 
1858
- ### The three hooks
1677
+ Platform-specific configuration that doesn't belong in npm-standard fields. All fields optional.
1859
1678
 
1860
- The foundation declares handlers as an object in its default export:
1679
+ | Field | Where used | Default | Purpose |
1680
+ |---|---|---|---|
1681
+ | `id` | `uniweb register` | bare segment of a scoped `name` | The foundation's registered id — the bare name in `@org/<id>`. Decoupled from `package.json::name` (a workspace concern), so renaming on the registry doesn't ripple through site dependencies. |
1682
+ | `namespace` | `uniweb register` | none | Legacy explicit org-namespace override; equivalent to a scoped `package.json::name`. Rarely needed. |
1683
+ | `runtimePolicy` | `dist/runtime-pin.json` | `"auto-minor"` | How sites using this foundation receive runtime updates. |
1861
1684
 
1862
- ```js
1863
- // main.js
1864
- export default {
1865
- handlers: {
1866
- data: (data, block) => { /* ... */ },
1867
- content: (data, block) => { /* ... */ },
1868
- props: (content, params, block) => { /* ... */ },
1869
- },
1870
- }
1871
- ```
1685
+ **Runtime policy** (foundation authors only — sites don't set this). At build time a foundation pins the `@uniweb/runtime` version it built against into `dist/runtime-pin.json`, alongside a policy controlling how that version moves forward on already-published sites: `exact` (stay put), `auto-patch` (within `MAJOR.MINOR.x`), `auto-minor` (within `MAJOR.x.y`, the default). Most foundations should leave it unset — the runtime is backwards-compatible at the minor level by convention, so `auto-minor` lets sites pick up fixes without a foundation rebuild. Set `exact` only if you depend on undocumented runtime internals or have audited against one release. Site owners cannot override your choice.
1872
1686
 
1873
- All three are optional. Each runs per block and is error-isolated (a failing handler logs a warning and falls back to the default behavior).
1687
+ `@uniweb/runtime` arrives **transitively** through `@uniweb/build`, so your foundation pins a runtime version without declaring one that's intentional. **Don't add `@uniweb/runtime` to your foundation's dependencies**; to bump the pinned version, bump `@uniweb/build`. If the pin is missing or malformed, the platform serves the foundation through its legacy compatibility path — sites still work, they just don't participate in runtime propagation.
1874
1688
 
1875
- | Handler | When it runs | Receives | Returns | Purpose |
1876
- |---|---|---|---|---|
1877
- | `data` | After data assembly, before content transform | `(data, block)` | New data object, or null | Filter, reshape, or augment the assembled data |
1878
- | `content` | After data handler | `(data, block)` | ProseMirror document, or null | Transform raw content (Loom instantiation, template expansion) |
1879
- | `props` | After parsing, defaults, and guarantees | `(content, params, block)` | `{ content, params }`, or null | Post-process the final shape before the component sees it |
1689
+ ### Localization
1880
1690
 
1881
- ### Loom integration
1691
+ **Content reaches components already translated.** There is no `t()`, no runtime lookup, no string wrapping — each language is a complete static build with its own HTML, `hreflang` tags, and search index. The default language has no URL prefix (`/about`); others are prefixed (`/es/about`).
1882
1692
 
1883
- The most common use of content handlers is Loom-based content instantiation resolving `{placeholder}` expressions in markdown against live data. `@uniweb/loom` provides a factory that creates the handler for you:
1693
+ **Translations live in `locales/`, a sibling of `pages/` at the site rootnever inside `pages/`.** The markdown under `pages/` is one language: the default.
1884
1694
 
1885
- ```js
1886
- import { createLoomHandlers } from '@uniweb/loom'
1695
+ ```yaml
1696
+ # site.yml
1697
+ defaultLanguage: en
1698
+ languages: [en, es, fr] # or [{ code: es, label: Español }, …], or '*' to
1699
+ # auto-discover from the locales/ folder
1700
+ publishLanguages: [en, es] # optional — fr stays a dev-previewable draft,
1701
+ # excluded from production output entirely
1702
+ ```
1887
1703
 
1888
- export default {
1889
- handlers: createLoomHandlers({
1890
- vars: (data) => data?.profile?.[0],
1891
- }),
1892
- }
1704
+ ```bash
1705
+ uniweb build # FIRST — extract scans built content, not source
1706
+ uniweb i18n extract # → locales/manifest.json, keyed by 8-char content hash
1707
+ uniweb i18n init es fr # → locales/es.json, locales/fr.json, pre-filled with source
1708
+ # …translate the values…
1709
+ uniweb build # merges translations, emits dist/es/, dist/fr/
1893
1710
  ```
1894
1711
 
1895
- The `vars` function extracts the Loom variable namespace from the assembled data. The factory returns a `content` handler that reads the `source` and `where` frontmatter params without `source`, the handler does simple substitution; with `source`, the handler splits the markdown at `---` dividers and repeats the body per data item (see "Dividers — Content Boundaries" above). When `where` is also set, the source array is filtered first only items where the expression evaluates to truthy are iterated:
1712
+ Running `extract` before a build is the usual first mistake it reads the compiled content, so a stale build yields a stale manifest. After content changes, `uniweb i18n sync` updates the manifest and `init` refreshes the language files; `status` and `audit` report coverage and stale entries.
1896
1713
 
1897
- ```yaml
1898
- ---
1899
- type: PublicationList
1900
- source: publications
1901
- where: "type = 'book'"
1902
- ---
1714
+ **Two mechanisms.** *Hash-based strings* (the default) — `locales/{locale}.json` maps content hash → translation, so structure stays identical across languages. When one source string needs different translations depending on where it appears, the value takes an override form:
1715
+
1716
+ ```json
1717
+ { "e5f6g7h8": { "default": "Learn More", "overrides": { "/pricing:cta": "See Pricing" } } }
1903
1718
  ```
1904
1719
 
1905
- `where` expressions use Loom Plain form: `type = 'book'` (equality), `year > 1870` (comparison), `refereed` (truthy check), `type = 'book' AND refereed` (boolean combination). Aggregate expressions in the header (like `{COUNT OF publications}`) reflect the filtered set.
1720
+ *Free-form bodies* when a language needs different structure, different images, or copy rewritten rather than translated. The file replaces one section's content:
1906
1721
 
1907
- ### Writing a custom handler
1722
+ ```
1723
+ locales/freeform/es/pages/about/hero.md # by page route
1724
+ locales/freeform/es/collections/articles/x.md # collections work too
1725
+ ```
1908
1726
 
1909
- When the factory doesn't cover your case, write handlers directly:
1727
+ These are **body only — no frontmatter**; params and config still come from the source section. `uniweb i18n init-freeform es pages/about hero` creates one pre-filled and records a source hash, so `uniweb i18n status --freeform` can tell you when the original moved on (`update-hash` to acknowledge). `move`, `rename`, and `prune --freeform` keep them aligned when pages get reorganized.
1910
1728
 
1911
- ```js
1912
- import { Loom, instantiateContent, instantiateRepeated } from '@uniweb/loom'
1729
+ **Collections translate in the same `extract` run**, into their own manifest at `locales/collections/manifest.json`.
1913
1730
 
1914
- const loom = new Loom()
1731
+ **Component side.** Nothing to do: `content.title` arrives in the active language. The one thing a foundation builds is a switcher.
1915
1732
 
1916
- export default {
1917
- handlers: {
1918
- content: (data, block) => {
1919
- const profile = data?.profile?.[0]
1920
- if (!profile) return null
1733
+ ```jsx
1734
+ import { useWebsite, Link, getLocaleLabel } from '@uniweb/kit'
1921
1735
 
1922
- const doc = block.rawContent?.doc ?? block.rawContent
1923
- const source = block.properties?.source
1736
+ function LanguageSwitcher() {
1737
+ const { website } = useWebsite()
1738
+ if (!website.hasMultipleLocales()) return null
1924
1739
 
1925
- if (!source) return instantiateContent(doc, loom, profile)
1926
- return instantiateRepeated(doc, loom, profile, source)
1927
- },
1928
- },
1740
+ return website.getLocales().map(locale => (
1741
+ <Link reload key={locale.code} href={website.getLocaleUrl(locale.code)}>
1742
+ {getLocaleLabel(locale)}
1743
+ </Link>
1744
+ ))
1929
1745
  }
1930
1746
  ```
1931
1747
 
1932
- The content handler receives `block.parsedContent.data` and reads raw ProseMirror from `block.rawContent`. It returns a new ProseMirror document the framework re-parses it through the semantic parser. Returning `null` or the same reference as `block.rawContent` signals no change.
1933
-
1934
- > **`instantiateContent` resolves `{placeholders}` in text nodes only** — not in link `href`s or other node/mark attributes. So `[{email}](mailto:{email})` fills the visible label but leaves the `mailto:` URL literal. For dynamic URLs, emit the value as plain text and let the component linkify it, or build the href in the handler yourself.
1935
-
1936
- ### Reserved frontmatter fields
1937
-
1938
- `source` and `where` are convention-level reserved fields — they flow through to both `block.properties` (for handler access) and `params` (visible to components). Components can ignore them. This is consistent with how `background` and `theme` work. List them in `meta.js` params with descriptions so the editor and schema recognize them.
1748
+ > **Use `<Link reload>` not a plain `<a>`, and not `window.location.href`.** `getLocaleUrl()` returns a root-relative path that does **not** include the deployment base path, so under a subdirectory deploy (`base: /docs/` in `site.yml`) a raw href sends the visitor outside the site. `<Link reload>` prepends `website.basePath` and forces the full page load a locale switch requires — an SPA `<Link>` won't do, because the other language is a different build.
1939
1749
 
1940
- ---
1750
+ Full model: `development/internationalization.md`. Author's view: `authoring/translating.md`.
1941
1751
 
1942
- ## Migrating From Other Frameworks
1752
+ ### Migrating from other frameworks
1943
1753
 
1944
1754
  Don't port line-by-line. Study the source, then rebuild from first principles. Other frameworks produce far more components than Uniweb needs — expect consolidation, not 1:1 correspondence.
1945
1755
 
1946
- ### The mental model shift
1947
-
1948
1756
  | React / conventional | Uniweb equivalent |
1949
1757
  |---|---|
1950
1758
  | Props with typed data | Frontmatter params + `meta.js` |
1951
- | Component variants via props | `variant` param in frontmatter; Front Desk pattern for complex routing |
1759
+ | Component variants via props | `variant` param; Front Desk pattern for complex routing |
1952
1760
  | Context / ThemeProvider | `theme:` frontmatter + semantic tokens (automatic) |
1953
1761
  | Wrapper/layout components | Section nesting or custom layouts |
1954
1762
  | Prop-drilling visuals into containers | Insets — `![](@Component)` rendered via `<Visual>` |
1955
1763
  | Content in JSX or `.js` data files | Markdown → parser → `content` prop |
1956
1764
  | CSS color tokens / design systems | `theme.yml` → palette shades + semantic tokens |
1957
- | `isDark ? ... : ...` conditionals | `text-heading` — context classes handle it |
1765
+ | `isDark ? : …` conditionals | `text-heading` — context classes handle it |
1958
1766
  | Per-component backgrounds | `background:` in frontmatter |
1959
- | Multiple near-identical components | One section type + `variant` param, or Front Desk pattern |
1767
+ | Multiple near-identical components | One section type + `variant` param, or Front Desk |
1960
1768
  | i18n wrapping (`t()` / `<Trans>`) | Locale-specific content directories |
1961
1769
 
1962
- ### Migration approach
1770
+ **Approach:** scaffold with `--template none` → use named layouts for different page groups → dump legacy components under `components/` (they're not section types; import them from section types during transition) → create section types one at a time.
1963
1771
 
1964
- 1. **Scaffold the workspace:**
1965
- ```bash
1966
- pnpm create uniweb my-project --template none
1967
- ```
1772
+ **It's incremental, not a rewrite.** Level **0** — paste the original as one section type; routing and dev tooling work immediately, and you have a running site. Level **1** — decompose into section types, consolidating duplicates via `variant` params or Front Desk. Level **2** move content from JSX to markdown, so authors can edit without code. Level **3** — replace hardcoded colors with semantic tokens, so components work in any context. Each level is shippable; stopping at 1 is a legitimate outcome.
1968
1773
 
1969
- 2. **Use named layouts** for different page groupsmarketing layout for landing pages, docs layout for `/docs/*`.
1774
+ **The most common mistake** is recreating source colors as CSS custom properties that bypasses the token system. Instead: primary color → `colors.primary` in `theme.yml`, neutral tone → `colors.neutral`, context needs → `theme:` frontmatter.
1970
1775
 
1971
- 3. **Dump legacy components under `components/`** they're not section types. Import from section types during transition.
1776
+ Also: name by purpose, not content (`TheModel` `SplitContent`, `WorkModes` `FeatureColumns`), and put UI helpers (buttons, badges, cards) in `components/` with no `meta.js`.
1972
1777
 
1973
- 4. **Create section types one at a time.** Migration levels:
1974
- - **Level 0**: Paste the original as one section type. Routing and dev tooling work immediately.
1975
- - **Level 1**: Decompose into section types. Consolidate duplicates — use `variant` params or the Front Desk pattern.
1976
- - **Level 2**: Move content from JSX to markdown. Authors can now edit without code.
1977
- - **Level 3**: Replace hardcoded colors with semantic tokens. Components work in any context.
1978
-
1979
- 5. **Map source colors to `theme.yml`.** The most common mistake is recreating source colors as CSS custom properties — this bypasses the token system. Instead: primary color → `colors.primary` in theme.yml. Neutral tone → `colors.neutral`. Context needs → `theme:` frontmatter.
1980
-
1981
- 6. **Name by purpose, not content** — `TheModel` → `SplitContent`, `WorkModes` → `FeatureColumns`.
1982
-
1983
- 7. **UI helpers → `components/`** — Buttons, badges, cards in `components/` (no `meta.js`, not selectable by authors).
1778
+ Full guide: `development/converting-existing.md`.
1984
1779
 
1985
1780
  ---
1986
1781
 
1987
- ## Tailwind CSS v4
1988
-
1989
- Foundation styles in `styles.css`:
1990
-
1991
- ```css
1992
- @import "tailwindcss";
1993
- @import "@uniweb/kit/theme-tokens.css";
1994
- @source "./sections/**/*.{js,jsx}";
1995
- @source "./components/**/*.{js,jsx}";
1996
- @source "../node_modules/@uniweb/kit/src/**/*.jsx";
1997
-
1998
- @theme {
1999
- --breakpoint-xs: 30rem;
2000
- }
2001
- ```
2002
-
2003
- Semantic tokens come from `theme-tokens.css` (populated from `theme.yml`). Use `@theme` only for values tokens don't cover. **Custom CSS is expected alongside Tailwind** — shadow systems, border hierarchies, gradients, glassmorphism. Tailwind handles layout; tokens handle context; `styles.css` handles everything else.
2004
-
2005
- **Don't set `scroll-behavior: smooth` globally.** It's a common line in a hand-written `html { … }` reset, and porting one into a foundation breaks navigation. The runtime owns scrolling: it already smooth-scrolls anchor targets itself (`scrollIntoView({ behavior: 'smooth' })`), so the CSS adds nothing there — but it resets and restores scroll on route changes with the two-argument `scrollTo(x, y)`, which *inherits* the property. Route changes then animate their scroll-to-top, and back-button restoration (which scrolls, checks the position on the next frame, and retries) keeps interrupting its own animation. Scope it to a specific scrollable element if you need it; never to `html` or `body`.
2006
-
2007
- **Font smoothing is a per-scheme decision, not a reset.** `-webkit-font-smoothing: antialiased` (macOS only) forces grayscale rasterization, which thins strokes. On dark surfaces that usefully counteracts the bloom of light text on near-black; on light surfaces it costs contrast and makes body text spindly. If you want it, scope it — `.scheme-dark { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }` — rather than putting it on `html` the way most CSS resets do.
1782
+ ## Part 6 — Troubleshooting
2008
1783
 
2009
- ## Troubleshooting
1784
+ Most Uniweb failures are **silent** — the build succeeds and the page is wrong. Check these first, because nothing in the terminal will point you at them.
2010
1785
 
2011
- **"Could not load foundation"** — Check `site/package.json` has `"foundation": "file:../foundation"`.
1786
+ **A red `Component not found: <Type>` box where a section should be** — the `type:` in that section's frontmatter names a section type this foundation doesn't have. `ls <foundation>/sections/` for the real list. The build does not fail on this, so it only surfaces by looking at the page.
2012
1787
 
2013
- **Component not appearing** — Verify `meta.js` exists. Check for `hidden: true`. Rebuild: `cd foundation && pnpm build`.
1788
+ **A param has no effect** — it isn't declared in that section type's `meta.js` `params:`. Undeclared frontmatter is passed through and ignored rather than rejected. Read the `meta.js` for the real knobs; if the one you want doesn't exist, exposing it is a foundation change.
2014
1789
 
2015
- **Styles not applying** — Verify `@source` includes your component paths.
1790
+ **A section doesn't appear at all** — the file is `@`-prefixed (a child section, only rendered via `nest:`), or `_`-prefixed (treated as a draft and skipped), or it's a section type nested below the root of `sections/` without a `meta.js`, which means it was never discovered.
2016
1791
 
2017
- **Prerender warnings about hooks** — Components with useState/useEffect show SSG warnings during build in local symlinked mode. Expected and harmless the page renders correctly client-side.
1792
+ **Content lands in the wrong field** — a heading became a `subtitle` when you wanted an item, or the reverse. That's the level rule (exactly one level deeper = subtitle; skipping a level, or any body content first, starts items). Run `uniweb inspect <path>` rather than re-deriving it.
2018
1793
 
2019
- **"document is not defined" during build** — Your component accesses `document`, `window`, or `localStorage` during render (not inside `useEffect`). Don't add `typeof document` guards — use the kit hook instead. Dark mode? `useAppearance()`. Scroll detection? `useScrolled()`. Kit hooks are SSR-safe by design.
1794
+ **A var in frontmatter does nothing** — component vars only apply when declared in that section type's `meta.js` `vars:`. Unknown names are ignored silently.
2020
1795
 
2021
- **Content not appearing as expected?**
2022
- ```bash
2023
- uniweb inspect pages/home/hero.md # Single section
2024
- uniweb inspect pages/home/ # Whole page
2025
- uniweb inspect pages/home/hero.md --raw # ProseMirror AST
2026
- ```
1796
+ Loud failures:
2027
1797
 
2028
- ## Learning from Official Templates
1798
+ **"Could not load foundation"** — check that the site's `package.json` depends on the foundation *by its workspace package name*. For the default layout that's `"src": "file:../src"`; for a co-located project (`docs/src` + `docs/site`) it's `"docs-src": "file:../src"`. The key must match the foundation's `package.json::name`, not the folder it happens to sit in.
2029
1799
 
2030
- When you're unsure how to implement a pattern data fetching, i18n, layouts, insets, theming install an official template as a reference project in your workspace:
1800
+ **Component not appearing**verify `meta.js` exists; check for `hidden: true`; rebuild the foundation.
2031
1801
 
2032
- ```bash
2033
- uniweb add project marketing --from marketing
2034
- pnpm install
2035
- ```
1802
+ **Styles not applying** — verify `@source` in `styles.css` includes your component paths.
2036
1803
 
2037
- This creates `marketing/src/` (the foundation, package `marketing-src`) + `marketing/site/` (the site, package `marketing-site`) alongside your existing project. You don't need to build or run it just read the source files to see how working components handle content, params, theming, and data.
1804
+ **Prerender warnings about hooks** components with `useState`/`useEffect` show SSG warnings during build in local symlinked mode. Expected and harmless.
2038
1805
 
2039
- **What to study:**
2040
- - `{name}/src/sections/` — components with meta.js (content expectations, params, presets)
2041
- - `{name}/site/pages/` — real content files showing markdown → component mapping
2042
- - `{name}/site/theme.yml` + `site.yml` — theming and configuration patterns
1806
+ **"document is not defined" during build** — your component touches `document`, `window`, or `localStorage` during render rather than inside `useEffect`. **Don't add `typeof document` guards** — use the kit hook instead: dark mode → `useAppearance()`, scroll detection → `useScrolled()`. Kit hooks are SSR-safe by design.
2043
1807
 
2044
- **Available templates:**
1808
+ **Content not parsing as expected** — `uniweb inspect pages/home/hero.md` (add `--raw` for the ProseMirror AST), or point it at a folder for a whole page.
2045
1809
 
2046
- | Template | Demonstrates |
2047
- |----------|-------------|
2048
- | `marketing` | Semantic tokens, insets, grids, multi-line headings, inline styling |
2049
- | `docs` | Sidebar navigation, navigation levels, code highlighting |
2050
- | `dynamic` | Live API data fetching, loading states, transforms |
2051
- | `international` | i18n, blog with collections, multi-locale routing |
2052
- | `store` | Product grid, collections, e-commerce patterns |
2053
- | `academic` | Publications, team grid, timeline, math |
2054
- | `extensions` | Multi-foundation architecture, runtime loading |
1810
+ ---
2055
1811
 
2056
- You can install multiple templates. Each becomes an independent project in the workspace. To run one in dev: `cd {name}/site && pnpm dev`
1812
+ ## Documentation index
2057
1813
 
2058
- ## Further Documentation
1814
+ **Index of every page: https://www.uniweb.io/llms.txt** — start there when you don't know which page you need.
2059
1815
 
2060
- Full documentation: **https://github.com/uniweb/docs**
1816
+ Source repo (public, cloneable): **https://github.com/uniweb/docs** · any page as raw markdown at `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`. See *Documentation* in Part 0 for when to fetch versus clone.
2061
1817
 
2062
- | Section | Path | Topics |
2063
- |---------|------|--------|
2064
- | **Getting Started** | `getting-started/` | What is Uniweb, quickstart, templates |
2065
- | **Authoring** | `authoring/` | Writing content, site setup, collections, theming, translations |
2066
- | **Development** | `development/` | Foundations, component patterns, data fetching, layouts, i18n |
2067
- | **Reference** | `reference/` | site.yml, page.yml, content structure, meta.js, kit API, CLI, deployment |
1818
+ | Section | Covers |
1819
+ |---------|--------|
1820
+ | `architecture/` | Component Content Architecture the why behind the patterns in this file |
1821
+ | `getting-started/` | What is Uniweb, quickstart, templates |
1822
+ | `authoring/` | Writing content, site setup, collections, theming, translations, predicates |
1823
+ | `development/` | Foundations, component patterns, project structures, data, layouts, i18n, migration, schemas |
1824
+ | `reference/` | site.yml, page.yml, content structure, meta.js, kit API, navigation, data fetching, CLI, deployment |
2068
1825
 
2069
- **Quick access:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
1826
+ The by-task table is in Part 0. For CLI flags, prefer `uniweb <command> --help` over this file — it's always current.