uniweb 0.8.8 → 0.8.9

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,12 +1,39 @@
1
1
  # AGENTS.md
2
2
 
3
- > A comprehensive guide to building with Uniweb — for developers and AI assistants alike.
3
+ ## The Architecture in One Sentence
4
4
 
5
- Uniweb is a Component Content Architecture (CCA). Content lives in markdown, code lives in React components, and a runtime connects them. The runtime handles section wrapping, background rendering, context theming, and token resolution components receive pre-parsed content and render it with semantic tokens. Understanding what the runtime does (and therefore what components should *not* do) is the key to working effectively in this architecture.
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.
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.
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.
10
+
11
+ ### What this replaces
12
+
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.
14
+
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.
16
+
17
+ ### Before you start: what the runtime already does
18
+
19
+ The most common mistake is reimplementing what the framework provides for free. Check this before writing any component logic:
20
+
21
+ | The runtime handles | So components should NOT contain |
22
+ |---|---|
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>` |
29
+
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.
31
+
32
+ ---
6
33
 
7
34
  ## Documentation
8
35
 
9
- This project was created with [Uniweb](https://github.com/uniweb/cli). Full documentation (markdown, fetchable): https://github.com/uniweb/docs
36
+ This project was created with [Uniweb CLI](https://github.com/uniweb/cli). Full documentation (markdown, fetchable): https://github.com/uniweb/docs
10
37
 
11
38
  **To read a specific page:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
12
39
 
@@ -29,15 +56,15 @@ This project was created with [Uniweb](https://github.com/uniweb/cli). Full docu
29
56
 
30
57
  ```
31
58
  project/
32
- ├── foundation/ # React component library
33
- ├── site/ # Content (markdown pages)
59
+ ├── foundation/ # Component developer's domain
60
+ ├── site/ # Content author's domain
34
61
  └── pnpm-workspace.yaml
35
62
  ```
36
63
 
37
64
  Multi-site variant uses `foundations/` and `sites/` (plural) folders.
38
65
 
39
- - **Foundation**: React components. Those with `meta.js` are *section types* — selectable by content authors via `type:` in frontmatter. Everything else is ordinary React.
40
- - **Site**: Markdown content + configuration. Each section file references a section type.
66
+ - **Foundation** (developer): React components. Those with `meta.js` are *section types* — selectable by content authors via `type:` in frontmatter. Everything else is ordinary React.
67
+ - **Site** (content author): Markdown content + configuration. Each section file references a section type. Authors work here without touching foundation code.
41
68
 
42
69
  ## Project Setup
43
70
 
@@ -50,7 +77,7 @@ pnpm create uniweb my-project
50
77
  cd my-project && pnpm install
51
78
  ```
52
79
 
53
- This creates a workspace with foundation + site + starter content — two commands to a dev server. Use `--template <name>` for an official template (`marketing`, `docs`, `academic`, etc.), `--template none` for foundation + site with no content, or `--blank` for an empty workspace.
80
+ This creates a workspace with foundation + site + starter content — two commands to a dev server. Use `--template <n>` for an official template (`marketing`, `docs`, `academic`, etc.), `--template none` for foundation + site with no content, or `--blank` for an empty workspace.
54
81
 
55
82
  ### Adding a co-located project
56
83
 
@@ -70,7 +97,7 @@ pnpm uniweb add site # First site → ./site/
70
97
  pnpm uniweb add site blog # Named → ./blog/
71
98
  ```
72
99
 
73
- The name is both the directory name and the package name. Use `--project <name>` to co-locate under a project directory (e.g., `--project docs` → `docs/foundation/`).
100
+ The name is both the directory name and the package name. Use `--project <n>` to co-locate under a project directory (e.g., `--project docs` → `docs/foundation/`).
74
101
 
75
102
  ### Adding section types
76
103
 
@@ -104,8 +131,14 @@ pnpm preview # Preview production build (SSG + SPA)
104
131
 
105
132
  > **npm works too.** Projects include both `pnpm-workspace.yaml` and npm workspaces. Replace `pnpm` with `npm` in any command above.
106
133
 
134
+ ---
135
+
107
136
  ## Content Authoring
108
137
 
138
+ 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.
139
+
140
+ 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.
141
+
109
142
  ### Section Format
110
143
 
111
144
  Each `.md` file is a section. Frontmatter on top, content below:
@@ -172,7 +205,28 @@ Lightning quick. ← items[0].paragraphs[0]
172
205
  Enterprise-grade. ← items[1].paragraphs[0]
173
206
  ```
174
207
 
175
- Each item has the same content shape as the top level `title`, `paragraphs`, `icons`, `links`, `lists`, etc. are all available per item.
208
+ **Items have the full content shape** this is the most commonly overlooked feature. Each item has `title`, `pretitle`, `subtitle`, `paragraphs`, `links`, `icons`, `lists`, and even `data` (tagged blocks). You don't need workarounds for structured content within items:
209
+
210
+ ```markdown
211
+ ### The Problem ← items[0].pretitle
212
+ ## Content gets trapped ← items[0].title
213
+ Body text here. ← items[0].paragraphs[0]
214
+
215
+ ### The Solution ← items[1].pretitle
216
+ ## Separate content from code ← items[1].title
217
+ ```
218
+
219
+ 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:
220
+
221
+ ````markdown
222
+ ### Starter ← items[0].title
223
+ $9/month ← items[0].paragraphs[0]
224
+
225
+ ```yaml:details
226
+ trial: 14 days
227
+ seats: 1
228
+ ``` ← items[0].data.details = { trial: "14 days", seats: 1 }
229
+ ````
176
230
 
177
231
  **Complete example — markdown and resulting content shape side by side:**
178
232
 
@@ -196,83 +250,65 @@ Enterprise-grade security. │ content.items[1].paragraphs[0] = "Enterprise
196
250
 
197
251
  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.
198
252
 
199
- ### Multi-Line Headings
253
+ ### Choosing how to model content
200
254
 
201
- Consecutive headings at the same level merge into a title array a single heading split across visual lines:
255
+ You have three layers. Most of the design skill is choosing between them:
202
256
 
203
- ```markdown
204
- # Build the future │ content.title = ["Build the future", "with confidence"]
205
- # with confidence │
206
- ```
257
+ **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.
207
258
 
208
- Kit's `<H1>`, `<H2>`, etc. render arrays as a single tag with line breaks. This is how you create dramatic multi-line hero headlines.
259
+ **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.
209
260
 
210
- **Works with accent styling:**
261
+ **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.
211
262
 
212
- ```markdown
213
- # Build the future │ content.title = [
214
- # [with confidence]{accent} │ "Build the future",
215
- │ "<span accent=\"true\">with confidence</span>"
216
- │ ]
217
- ```
263
+ 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.
218
264
 
219
- **Works at any heading slot** title, subtitle, items:
265
+ **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.
220
266
 
221
- ```markdown
222
- ### Our Mission │ content.pretitle = "Our Mission"
223
- # Build the future │ content.title = ["Build the future",
224
- # with confidence │ "with confidence"]
225
- ## The platform for │ content.subtitle = ["The platform for",
226
- ## modern teams │ "modern teams"]
267
+ ```js
268
+ // You design this it's not a fixed schema
269
+ export default {
270
+ params: {
271
+ columns: { type: 'number', default: 3 },
272
+ cardStyle: { type: 'select', options: ['minimal', 'bordered', 'elevated'], default: 'minimal' },
273
+ showIcon: { type: 'boolean', default: true },
274
+ maxItems: { type: 'number', default: 6 },
275
+ }
276
+ }
227
277
  ```
228
278
 
229
- **Rule:** Same-level continuation only applies before going deeper. Once a subtitle level is reached, same-level headings start new items instead of merging:
230
-
231
- ```markdown
232
- # Features │ title = "Features"
233
-
234
- We built this for you. │ paragraph
235
-
236
- ### Fast │ items[0].title = "Fast"
237
- ### Secure │ items[1].title = "Secure" ← new item, not merged
279
+ ````markdown
280
+ <!-- You invent the tag name — the framework parses it -->
281
+ ```yaml:speakers
282
+ - name: Ada Lovelace
283
+ role: Keynote
284
+ topic: The Future of Computing
238
285
  ```
286
+ ````
287
+ Access: `content.data?.speakers` — an array of objects. You defined this. The framework parsed it.
239
288
 
240
- Use `---` to force separate items when same-level headings would otherwise merge:
289
+ **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.
241
290
 
242
- ```markdown
243
- # Line one │ title = "Line one"
244
- --- │ ← divider forces split
245
- # Line two │ items[0].title = "Line two"
246
- ```
291
+ ### Multi-Line Headings
247
292
 
248
- **Lists** contain bullet or ordered list items. Each list item is an object with the same content shape — not a plain string:
293
+ Consecutive headings at the same level merge into a title array a single heading split across visual lines:
249
294
 
250
295
  ```markdown
251
- # Features ← title
252
-
253
- - Fast builds ← lists[0][0].paragraphs[0]
254
- - **Hot** reload ← lists[0][1].paragraphs[0] (HTML: "<strong>Hot</strong> reload")
296
+ # Build the future │ content.title = ["Build the future", "with confidence"]
297
+ # with confidence │
255
298
  ```
256
299
 
257
- Items can contain lists:
300
+ Kit's `<H1>`, `<H2>`, etc. render arrays as a single tag with line breaks. This is how you create dramatic multi-line hero headlines.
258
301
 
259
- ```markdown
260
- ### Starter ← items[0].title
261
- $9/month ← items[0].paragraphs[0]
302
+ **Works with accent styling:**
262
303
 
263
- - Feature A ← items[0].lists[0][0].paragraphs[0]
264
- - Feature B ← items[0].lists[0][1].paragraphs[0]
304
+ ```markdown
305
+ # Build the future │ content.title = [
306
+ # [with confidence]{accent} │ "Build the future",
307
+ │ "<span accent=\"true\">with confidence</span>"
308
+ │ ]
265
309
  ```
266
310
 
267
- Render list item text with kit components (see [kit section](#uniwebkit) below):
268
-
269
- ```jsx
270
- import { Span } from '@uniweb/kit'
271
-
272
- content.lists[0]?.map((listItem, i) => (
273
- <li key={i}><Span text={listItem.paragraphs[0]} /></li>
274
- ))
275
- ```
311
+ **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.
276
312
 
277
313
  ### Icons
278
314
 
@@ -280,29 +316,6 @@ Use image syntax with library prefix: `![](lu-house)`. Supported libraries: `lu`
280
316
 
281
317
  Custom SVGs: `![Logo](./logo.svg){role=icon}`
282
318
 
283
- ### Insets (Component References)
284
-
285
- Place a foundation component inline within content using `@` syntax:
286
-
287
- ```markdown
288
- ![description](@ComponentName)
289
- ![description](@ComponentName){param=value other=thing}
290
- ```
291
-
292
- The three parts carry distinct information:
293
- - `[description]` — text passed to the component as `block.content.title`
294
- - `(@Name)` — foundation component to render
295
- - `{params}` — configuration attributes passed as `block.properties`
296
-
297
- ```markdown
298
- ![Architecture diagram](@NetworkDiagram){variant=compact}
299
- ![Cache metrics](@PerformanceChart){period=30d}
300
- ![](@GradientBlob){position=top-right}
301
- ![npm create uniweb](@CommandBlock){note="Vite + React + Routing — ready to go"}
302
- ```
303
-
304
- Inset components must declare `inset: true` in their `meta.js`. They render at the exact position in the content flow where the author placed them. See meta.js section below for details.
305
-
306
319
  ### Links and Media Attributes
307
320
 
308
321
  ```markdown
@@ -313,66 +326,24 @@ Inset components must declare `inset: true` in their `meta.js`. They render at t
313
326
 
314
327
  **Quote values that contain spaces:** `{note="Ready to go"}` not `{note=Ready to go}`. Unquoted values end at the first space.
315
328
 
316
- Standalone links (alone on a line) become buttons. Inline links stay as text links.
317
-
318
- **Standalone links** — paragraphs that contain *only* links (no other text) are promoted to `content.links[]`. This works for single links and for multiple links sharing a paragraph:
329
+ 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[]`:
319
330
 
320
331
  ```markdown
321
332
  [Primary](/start) ← standalone → content.links[0]
322
-
323
- [Secondary](/learn) standalonecontent.links[1]
324
-
325
- [One](/a) [Two](/b) ← links-only paragraph → content.links[0], content.links[1]
326
- ```
327
-
328
- Links mixed with non-link text stay as inline `<a>` tags within `content.paragraphs[]`:
329
-
330
- ```markdown
331
- Check out [this](/a) and [that](/b). ← inline links in paragraph text, NOT in content.links[]
333
+ [One](/a) [Two](/b) ← links-only paragraph → both in content.links[]
334
+ Check out [this](/a) link. inlinestays in paragraphs as <a> tag
332
335
  ```
333
336
 
334
337
  ### Inline Text Styling
335
338
 
336
- Style specific words or phrases using bracketed spans with boolean attributes:
337
-
338
339
  ```markdown
339
340
  # Build [faster]{accent} with structure
340
-
341
341
  This is [less important]{muted} context.
342
342
  ```
343
343
 
344
- The framework provides two defaults: `accent` (colored + bold) and `muted` (subtle). These adapt to context automatically in dark sections, `accent` resolves to a lighter shade.
345
-
346
- **What you write → what components receive:**
347
-
348
- | Markdown | HTML in content string |
349
- |----------|----------------------|
350
- | `[text]{accent}` | `<span accent="true">text</span>` |
351
- | `[text]{muted}` | `<span muted="true">text</span>` |
352
- | `[text]{color=red}` | `<span style="color: red">text</span>` |
353
-
354
- CSS is generated from `theme.yml`'s `inline:` section using attribute selectors (`span[accent] { ... }`). Sites can define additional named styles:
355
-
356
- ```yaml
357
- inline:
358
- accent:
359
- color: var(--link)
360
- font-weight: '600'
361
- callout:
362
- color: var(--accent-600)
363
- font-style: italic
364
- ```
365
-
366
- **Common pattern — accented multi-line hero heading:**
367
-
368
- ```markdown
369
- # Build the future
370
- # [with confidence]{accent}
371
- ```
372
-
373
- This produces `content.title = ["Build the future", "<span accent=\"true\">with confidence</span>"]` — an array rendered as a single `<h1>` with visual line breaks. See [Multi-Line Headings](#multi-line-headings) for details.
344
+ `accent` (colored + bold) and `muted` (subtle) adapt to context automatically. Components receive HTML strings with spans applied: `<span accent="true">faster</span>`.
374
345
 
375
- Components receive HTML strings with the spans already applied. Kit's `<H1>`, `<P>`, etc. render them correctly via `dangerouslySetInnerHTML`.
346
+ Sites can define additional named styles in `theme.yml`'s `inline:` section.
376
347
 
377
348
  ### Structured Data
378
349
 
@@ -389,73 +360,116 @@ submitLabel: Send
389
360
 
390
361
  Access: `content.data?.form` → `{ fields: [...], submitLabel: "Send" }`
391
362
 
392
- **Code blocks need tags too.** Untagged code blocks (plain ```js) are only visible to sequential-rendering components like Article or DocSection. If a component needs to access code blocks by name, tag them:
363
+ **Untagged code blocks** (plain ``` js) are only visible to sequential-rendering components. If a component needs to access code blocks by name, tag them (`jsx:before`, `jsx:after` → `content.data?.before`, `content.data?.after`).
393
364
 
394
- ````markdown
395
- ```jsx:before
396
- const old = fetch('/api')
365
+ ### Composition: Nesting and Embedding
366
+
367
+ 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.
368
+
369
+ **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?
370
+
371
+ In other frameworks, this is where you'd reach for MDX, or prop-drill a component. In Uniweb, the content author writes:
372
+
373
+ ```markdown
374
+ ![Architecture overview](@NetworkDiagram){variant=compact}
397
375
  ```
398
376
 
399
- ```jsx:after
400
- const data = useData()
377
+ 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:
378
+
379
+ ```jsx
380
+ <Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="rounded-2xl" />
401
381
  ```
402
- ````
403
382
 
404
- Access: `content.data?.before`, `content.data?.after` raw code strings.
383
+ 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.
405
384
 
406
- ### Lists as Navigation Menus
385
+ **Child sections composing layouts from reusable pieces.** You encounter a complex layout — a 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.
407
386
 
408
- Markdown lists are ideal for navigation, menus, and grouped link structures. Each list item is a full content object with `paragraphs`, `links`, `icons`, and nested `lists`.
387
+ 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.
409
388
 
410
- **Header nav flat list with icons and links:**
389
+ But if you hardcode which components go where, the author can't rearrange or swap them. This is where child sections solve it:
411
390
 
412
- ```markdown
413
- - ![](lu-home) [Home](/)
414
- - ![](lu-book) [Docs](/docs)
415
- - ![](lu-mail) [Contact](/contact)
391
+ ```
392
+ pages/home/
393
+ ├── 2-dashboard.md # type: Grid, columns: "1fr 2fr"
394
+ ├── @sidebar-stats.md # type: StatPanel
395
+ └── @main-chart.md # type: PerformanceChart
416
396
  ```
417
397
 
418
- Access: `content.lists[0]` — each item has `item.links[0]` (href + label) and `item.icons[0]` (icon).
398
+ ```yaml
399
+ # page.yml
400
+ nest:
401
+ dashboard: [sidebar-stats, main-chart]
402
+ ```
419
403
 
420
- **Footernested list for grouped links:**
404
+ 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.
421
405
 
422
- ```markdown
423
- - Product
424
- - [Features](/features)
425
- - [Pricing](/pricing)
426
- - Company
427
- - [About](/about)
428
- - [Careers](/careers)
406
+ 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.
407
+
408
+ **Choosing the right pattern:**
409
+
410
+ | Pattern | How authored | Use when |
411
+ |---|---|---|
412
+ | **Items** (`content.items`) | Heading groups within one `.md` file | Repeating content within one section: cards, features, FAQ entries |
413
+ | **Child sections** (`block.childBlocks`) | `@`-prefixed `.md` files + `nest:` | Children that need their own section type, rich content, or independent editing |
414
+ | **Insets** (`block.insets`) | `![](@Component)` in markdown | Self-contained visuals/widgets: charts, diagrams, code demos |
415
+
416
+ 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.
417
+
418
+ 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."
419
+
420
+ **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.
421
+
422
+ ### Section Nesting Details
423
+
424
+ ```
425
+ pages/home/
426
+ ├── page.yml
427
+ ├── 1-hero.md
428
+ ├── 2-features.md # Parent section (type: Grid)
429
+ ├── 3-cta.md
430
+ ├── @card-a.md # Child of features (@ = not top-level)
431
+ ├── @card-b.md
432
+ └── @card-c.md
429
433
  ```
430
434
 
431
- Access: `content.lists[0]` — each top-level item has `item.paragraphs[0]` (group label) and `item.lists[0]` (array of sub-items, each with `subItem.links[0]`).
435
+ ```yaml
436
+ # page.yml
437
+ nest:
438
+ features: [card-a, card-b, card-c]
439
+ ```
440
+
441
+ **Rules:**
442
+ - `@`-prefixed files are excluded from the top-level section list
443
+ - `nest:` declares parent-child relationships (parent name → child names)
444
+ - `@@` prefix for deeper nesting (grandchildren)
445
+ - `nest:` is flat: `{ features: [a, b], a: [sub-1] }`
446
+ - Children ordered by position in the `nest:` array
432
447
 
433
448
  ```jsx
434
- content.lists[0]?.map((group, i) => (
435
- <div key={i}>
436
- <Span text={group.paragraphs[0]} className="font-semibold text-heading" />
437
- <ul>
438
- {group.lists[0]?.map((subItem, j) => (
439
- <li key={j}><Link to={subItem.links[0]?.href}>{subItem.links[0]?.label}</Link></li>
440
- ))}
441
- </ul>
442
- </div>
443
- ))
449
+ import { ChildBlocks } from '@uniweb/kit'
450
+
451
+ export default function Grid({ block, params }) {
452
+ return (
453
+ <div className={`grid grid-cols-${params.columns || 2} gap-6`}>
454
+ <ChildBlocks from={block} />
455
+ </div>
456
+ )
457
+ }
444
458
  ```
445
459
 
446
460
  ### Section Backgrounds
447
461
 
448
- Set `background` in frontmatter — the runtime renders it automatically. The string form auto-detects the type:
462
+ Set `background` in frontmatter — the runtime renders it automatically:
449
463
 
450
464
  ```yaml
451
- background: /images/hero.jpg # Image (by extension)
452
- background: /videos/hero.mp4 # Video (by extension)
453
- background: linear-gradient(135deg, #667eea, #764ba2) # CSS gradient
465
+ background: /images/hero.jpg # Image
466
+ background: /videos/hero.mp4 # Video
467
+ background: linear-gradient(135deg, #667eea, #764ba2) # Gradient
454
468
  background: '#1a1a2e' # Color (hex — quote in YAML)
455
- background: var(--primary-900) # Color (CSS variable)
469
+ background: var(--primary-900) # CSS variable
456
470
  ```
457
471
 
458
- The object form gives more control:
472
+ Object form for more control:
459
473
 
460
474
  ```yaml
461
475
  background:
@@ -463,8 +477,6 @@ background:
463
477
  overlay: { enabled: true, type: dark, opacity: 0.5 }
464
478
  ```
465
479
 
466
- Overlay shorthand — `overlay: 0.5` is equivalent to `{ enabled: true, type: dark, opacity: 0.5 }`.
467
-
468
480
  Components that render their own background declare `background: 'self'` in `meta.js`.
469
481
 
470
482
  ### Page Organization
@@ -478,7 +490,7 @@ site/layout/
478
490
  site/pages/
479
491
  └── home/
480
492
  ├── page.yml # title, description, order
481
- ├── hero.md # Single section — no prefix needed
493
+ ├── hero.md # Single section
482
494
  └── (or for multi-section pages:)
483
495
  ├── 1-hero.md # Numeric prefix sets order
484
496
  ├── 2-features.md
@@ -487,9 +499,7 @@ site/pages/
487
499
 
488
500
  Decimals insert between: `2.5-testimonials.md` goes between `2-` and `3-`.
489
501
 
490
- **Ignored files/folders:**
491
- - `README.md` — repo documentation, not site content
492
- - `_*.md` or `_*/` — drafts and private content (e.g., `_drafts/`, `_old-hero.md`)
502
+ **Ignored:** `README.md` (repo docs), `_*.md` or `_*/` (drafts/private).
493
503
 
494
504
  **page.yml:**
495
505
  ```yaml
@@ -507,97 +517,73 @@ pages: [home, about, ...] # Order pages (... = rest, first = homepage)
507
517
  pages: [home, about] # Strict: only listed pages in nav
508
518
  ```
509
519
 
510
- Use `pages:` with `...` for ordering, without `...` for strict visibility control. Use `index:` for simple homepage selection.
511
-
512
- ### Section Nesting (Child Sections)
520
+ ### Lists as Navigation Menus
513
521
 
514
- Some section types need children a Grid that arranges cards, a TabGroup that holds panels. Use the `@` prefix and `nest:` property:
522
+ Markdown lists model nav, menus, and grouped links. Each list item is a full content object with `paragraphs`, `links`, `icons`, and nested `lists`.
515
523
 
524
+ **Header nav:**
525
+ ```markdown
526
+ - ![](lu-home) [Home](/)
527
+ - ![](lu-book) [Docs](/docs)
528
+ - ![](lu-mail) [Contact](/contact)
516
529
  ```
517
- pages/home/
518
- ├── page.yml
519
- ├── 1-hero.md
520
- ├── 2-features.md # Parent section (type: Grid)
521
- ├── 3-cta.md
522
- ├── @card-a.md # Child of features (@ = not top-level)
523
- ├── @card-b.md
524
- └── @card-c.md
525
- ```
530
+ Access: `content.lists[0]` — each item has `item.links[0]` and `item.icons[0]`.
526
531
 
527
- ```yaml
528
- # page.yml
529
- nest:
530
- features: [card-a, card-b, card-c]
532
+ **Footer columns:**
533
+ ```markdown
534
+ - Product
535
+ - [Features](/features)
536
+ - [Pricing](/pricing)
537
+ - Company
538
+ - [About](/about)
539
+ - [Careers](/careers)
531
540
  ```
541
+ Access: `content.lists[0]` — `group.paragraphs[0]` (label), `group.lists[0]` (sub-items with `subItem.links[0]`).
532
542
 
533
- **Rules:**
534
- - `@`-prefixed files are excluded from the top-level section list
535
- - `nest:` declares parent-child relationships (parent name → array of child names)
536
- - Child files **must** use the `@` prefix — the filename and YAML must agree
537
- - `@@` prefix signals deeper nesting (e.g., `@@sub-item.md` for grandchildren)
538
- - `nest:` is flat — each key is a parent: `nest: { features: [a, b], a: [sub-1] }`
539
- - Children are ordered by their position in the `nest:` array
540
- - Orphaned `@` files (no parent in `nest:`) appear at top-level with a warning
541
-
542
- Components receive children via `block.childBlocks`. Use `ChildBlocks` from kit to render them — the runtime handles component resolution:
543
+ Render list item text with Kit components — list items contain HTML strings, not plain text:
543
544
 
544
545
  ```jsx
545
- import { ChildBlocks } from '@uniweb/kit'
546
-
547
- export default function Grid({ block, params }) {
548
- return (
549
- <div className={`grid grid-cols-${params.columns || 2} gap-6`}>
550
- <ChildBlocks from={block} />
551
- </div>
552
- )
553
- }
546
+ content.lists[0]?.map((group, i) => (
547
+ <div key={i}>
548
+ <Span text={group.paragraphs[0]} className="font-semibold text-heading" />
549
+ <ul>
550
+ {group.lists[0]?.map((subItem, j) => (
551
+ <li key={j}><Link to={subItem.links[0]?.href}>{subItem.links[0]?.label}</Link></li>
552
+ ))}
553
+ </ul>
554
+ </div>
555
+ ))
554
556
  ```
555
557
 
556
- ### Composition in practice
558
+ **For richer navigation with icons, descriptions, or hierarchy**, use `yaml:nav` tagged blocks:
557
559
 
558
- Section nesting and insets together give content authors significant layout power without requiring new components. A single Grid section type composes *any* combination of children — each child is its own section type with its own content:
559
-
560
- ```
561
- pages/home/
562
- ├── page.yml
563
- ├── 1-hero.md
564
- ├── 2-highlights.md # type: Grid, columns: 3
565
- ├── 3-cta.md
566
- ├── @stats.md # type: StatCard — numbers and labels
567
- ├── @testimonial.md # type: Testimonial — quote with attribution
568
- └── @demo.md # type: SplitContent — text + ![](@LiveDemo) inset
569
- ```
570
-
571
- ```yaml
572
- nest:
573
- highlights: [stats, testimonial, demo]
560
+ ````markdown
561
+ ```yaml:nav
562
+ - label: Dashboard
563
+ href: /
564
+ icon: lu:layout-grid
565
+ - label: Docs
566
+ href: /docs
567
+ icon: lu:book-open
568
+ children:
569
+ - label: Getting Started
570
+ href: /docs/quickstart
574
571
  ```
572
+ ````
575
573
 
576
- The content author chose three different section types as children, arranged them in a grid, and embedded an interactive component inside one of them all through markdown and YAML. The developer wrote one Grid component, a few card-level section types, and an inset. No bespoke "highlights" component needed.
577
-
578
- This is functional composition applied to content: small, focused section types that combine into richer layouts. The developer builds reusable pieces (Grid, StatCard, Testimonial, SplitContent); the content author composes them. Adding a fourth card means creating one `@`-prefixed file and adding its name to the `nest:` array.
579
-
580
- ### When to use which pattern
581
-
582
- | Pattern | Authoring | Use when |
583
- |---------|-----------|----------|
584
- | **Items** (`content.items`) | Heading groups in one `.md` file | Repeating content within one section (cards, FAQ entries) |
585
- | **Insets** (`block.insets`) | `![](@Component)` in markdown | Embedding a self-contained visual (chart, diagram, widget) |
586
- | **Child sections** (`block.childBlocks`) | `@`-prefixed `.md` files + `nest:` | Children with rich authored content (testimonials, carousel slides) |
587
-
588
- Does the content author write content *inside* the nested component? **Yes** → child sections. **No** (self-contained, driven by params/data) → insets. Repeating groups within one section → items. These patterns compose: a child section can contain insets, and items work inside children.
574
+ 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.
589
575
 
590
- **Inset rule of thumb:** If the same interactive widget or self-contained visual appears inside multiple different sections (a copy-able command block, a chart, a demo player), make it an inset. The content author places it with `![](@CommandBlock)` wherever it's needed — no prop drilling, no imports.
576
+ ---
591
577
 
592
578
  ## Semantic Theming
593
579
 
594
- CCA separates theme from code. 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.
580
+ 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).
595
581
 
596
582
  ```jsx
597
- // ❌ Hardcoded — breaks in dark context, locked to one palette
583
+ // ❌ Hardcoded — breaks in dark context
598
584
  <h2 className="text-slate-900">...</h2>
599
585
 
600
- // ✅ Semantic — adapts to any context and brand automatically
586
+ // ✅ Semantic — adapts to any context and brand
601
587
  <h2 className="text-heading">...</h2>
602
588
  ```
603
589
 
@@ -613,48 +599,9 @@ CCA separates theme from code. Components use **semantic CSS tokens** instead of
613
599
  | `bg-muted` | Hover states, zebra rows |
614
600
  | `border-border` | Borders |
615
601
  | `text-link` | Link color |
616
- | `bg-primary` | Primary action background |
617
- | `text-primary-foreground` | Text on primary background |
618
- | `hover:bg-primary-hover` | Primary hover state |
619
- | `border-primary-border` | Primary border (transparent by default) |
620
- | `bg-secondary` | Secondary action background |
621
- | `text-secondary-foreground` | Text on secondary background |
622
- | `hover:bg-secondary-hover` | Secondary hover state |
623
- | `border-secondary-border` | Secondary border |
624
- | `text-success` / `bg-success-subtle` | Status: success |
625
- | `text-error` / `bg-error-subtle` | Status: error |
626
- | `text-warning` / `bg-warning-subtle` | Status: warning |
627
- | `text-info` / `bg-info-subtle` | Status: info |
628
-
629
- ### What the runtime handles (don't write this yourself)
630
-
631
- The runtime does significant work that other frameworks push onto components. Understanding this prevents writing unnecessary code:
632
-
633
- 1. **Section backgrounds** — The runtime renders image, video, gradient, color, and overlay backgrounds from frontmatter. Components never set their own section background.
634
- 2. **Context classes** — The runtime wraps every section in `<section class="context-{theme}">`, which auto-applies `background-color: var(--section)` and sets all token values.
635
- 3. **Token resolution** — All 24+ semantic tokens resolve automatically per context. A component using `text-heading` gets dark text in light context, white text in dark context — zero conditional logic.
636
- 4. **Colored section backgrounds** — Content authors create tinted sections via frontmatter, not component code:
637
- ```yaml
638
- ---
639
- type: Features
640
- theme: light
641
- background:
642
- color: var(--primary-50) # Light blue tint with light-context tokens
643
- ---
644
- ```
645
-
646
- **What components should NOT contain:**
647
-
648
- | Don't write | Why |
649
- |-------------|-----|
650
- | `bg-white` or `bg-gray-900` on section wrapper | Engine applies `bg-section` via context class |
651
- | `const themes = { light: {...}, dark: {...} }` | Context system replaces theme maps entirely |
652
- | `isDark ? 'text-white' : 'text-gray-900'` | Just write `text-heading` — it adapts |
653
- | Background rendering code | Declare `background:` in frontmatter instead |
654
- | Color constants / tokens files | Colors come from `theme.yml` |
655
- | Parallel color system (`--ink`, `--paper`) that duplicates what tokens already provide | Map source color roles to `theme.yml` colors/neutral. The build generates `--primary-50` through `--primary-950`, `--neutral-50` through `--neutral-950`, etc. Use palette shades directly (`var(--primary-300)`) for specific tones. Additive design classes that BUILD ON tokens are fine — a parallel system that REPLACES them bypasses context adaptation. |
656
-
657
- **What to hardcode** (not semantic — same in every context): layout (`grid`, `flex`, `max-w-6xl`), spacing (`p-6`, `gap-8`), typography scale (`text-3xl`, `font-bold`), animations, border-radius.
602
+ | `bg-primary` / `text-primary-foreground` / `hover:bg-primary-hover` | Primary actions |
603
+ | `bg-secondary` / `text-secondary-foreground` / `hover:bg-secondary-hover` | Secondary actions |
604
+ | `text-success` / `text-error` / `text-warning` / `text-info` | Status colors |
658
605
 
659
606
  **Content authors control context** in frontmatter:
660
607
 
@@ -665,34 +612,19 @@ theme: dark ← sets context-dark, all tokens resolve to dark values
665
612
  ---
666
613
  ```
667
614
 
668
- Alternate between `light` (default), `medium`, and `dark` across sections for visual rhythm — no CSS needed. A typical marketing page:
669
-
670
- ```markdown
671
- <!-- 1-hero.md -->
672
- theme: dark
673
-
674
- <!-- 2-features.md -->
675
- (no theme — defaults to light)
676
-
677
- <!-- 3-testimonials.md -->
678
- theme: medium
679
-
680
- <!-- 4-cta.md -->
681
- theme: dark
682
- ```
615
+ Alternate between `light` (default), `medium`, and `dark` across sections for visual rhythm.
683
616
 
684
- **Per-section token overrides** the object form lets authors fine-tune individual tokens for a specific section:
617
+ **But the three presets aren't the limit.** The object form gives fine-grained control per section:
685
618
 
686
619
  ```yaml
687
620
  theme:
688
621
  mode: light
689
- primary: neutral-900 # Dark buttons in a light section
690
- primary-hover: neutral-800
622
+ section: neutral-100 # Subtle off-white surface
623
+ card: neutral-50 # Cards lighter than surface
624
+ primary: neutral-900 # Dark buttons instead of brand color
691
625
  ```
692
626
 
693
- Any semantic token (`section`, `heading`, `body`, `primary`, `link`, etc.) can be overridden this way. The overrides are applied as inline CSS custom properties on the section wrapper components don't need to know about them.
694
-
695
- **Site controls the palette** in `theme.yml`. The same foundation looks different across sites because tokens resolve from the site's color configuration, not from component code.
627
+ 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.
696
628
 
697
629
  ### theme.yml
698
630
 
@@ -715,98 +647,70 @@ fonts:
715
647
  body: "'Inter', system-ui, sans-serif"
716
648
 
717
649
  inline:
718
- accent: # For [text]{accent} in markdown
650
+ accent:
719
651
  color: var(--link)
720
652
  font-weight: '600'
721
653
 
722
- vars: # Override foundation-declared variables
654
+ vars:
723
655
  header-height: 5rem
724
656
  ```
725
657
 
726
- Each color generates 11 OKLCH shades (50–950). The `neutral` palette is special — use a named preset (`stone` for warm) rather than a hex value. Three contexts are built-in: `light` (default), `medium`, `dark`. Context override keys match token names `section:` not `bg:`, `primary:` not `btn-primary-bg:`.
658
+ 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:`.
727
659
 
728
660
  ### How colors reach components
729
661
 
730
- Your hex color → 11 shades (50–950) → semantic tokens → components.
731
-
732
- **Shade 500 = your exact input color.** The build generates lighter shades (50–400) above it and darker shades (600–950) below it, redistributing lightness proportionally to maintain a smooth scale. Set `exactMatch: false` on a color to opt out and use fixed lightness values instead.
733
-
734
- Semantic tokens map shades to roles. The defaults for light/medium contexts:
735
-
736
- | Token | Shade | Purpose |
737
- |-------|-------|---------|
738
- | `--primary` | 600 | Button background |
739
- | `--primary-hover` | 700 | Button hover |
740
- | `--link` | 600 | Link color |
741
- | `--ring` | 500 | Focus ring |
742
-
743
- In dark contexts, `--primary` uses shade 500 and `--link` uses shade 400.
662
+ Your hex → 11 shades (50–950) → semantic tokens → components.
744
663
 
745
- **Buttons and links use shade 600 darker than your input.** This is an accessibility choice: shade 600 provides better contrast with white button text. For medium-bright brand colors like orange, buttons will be noticeably darker than the brand color.
664
+ 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.
746
665
 
747
- **Recipe — brand-exact buttons:**
666
+ **Buttons use shade 600 darker than your input color.** This is an accessibility choice for contrast with white text. For brand-exact buttons:
748
667
 
749
668
  ```yaml
750
669
  colors:
751
670
  primary: "#E35D25"
752
-
753
671
  contexts:
754
672
  light:
755
673
  primary: primary-500 # Your exact color on buttons
756
- primary-hover: primary-600 # Darker on hover
674
+ primary-hover: primary-600
757
675
  ```
758
676
 
759
- > **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 or darken your base color.
677
+ > **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.
760
678
 
761
679
  ### Foundation variables
762
680
 
763
- Foundations declare customizable layout/spacing values in `foundation.js`. The starter includes:
681
+ Foundations declare customizable layout values in `foundation.js`:
764
682
 
765
683
  ```js
766
684
  export const vars = {
767
685
  'header-height': { default: '4rem', description: 'Fixed header height' },
768
686
  'max-content-width': { default: '80rem', description: 'Maximum content width' },
769
- 'section-padding-y': { default: 'clamp(4rem, 6vw, 7rem)', description: 'Vertical padding for sections' },
687
+ 'section-padding-y': { default: 'clamp(4rem, 6vw, 7rem)', description: 'Vertical section padding' },
770
688
  }
771
689
  ```
772
690
 
773
- Sites override them in `theme.yml` under `vars:`. Components use them via Tailwind arbitrary values or CSS: `py-[var(--section-padding-y)]`, `h-[var(--header-height)]`, etc.
774
-
775
- The `section-padding-y` default uses `clamp()` for fluid spacing — tighter on mobile, more breathing room on large screens. Use this variable for consistent section spacing instead of hardcoding padding in each component. Sites can override to a fixed value (`section-padding-y: 3rem`) or a different clamp in `theme.yml`.
776
-
777
- **When to break the rules:** Header/footer components that float over content may need direct color logic (reading the first section's theme). Decorative elements with fixed branding (logos) use literal colors.
691
+ Sites override in `theme.yml` under `vars:`. Components use: `py-[var(--section-padding-y)]`, `h-[var(--header-height)]`.
778
692
 
779
693
  ### Design richness beyond tokens
780
694
 
781
- Semantic tokens handle context adaptation — the hard problem of making colors work in light, medium, and dark sections. **They are a floor, not a ceiling.** A great foundation adds its own design vocabulary on top.
782
-
783
- The token set is deliberately small (24 tokens). It covers the dimensions that change per context. Everything that stays constant across contexts — border weights, shadow depth, radius scales, gradient angles, accent borders, glassmorphism, elevation layers — belongs in your foundation's `styles.css` or component code.
784
-
785
- **Don't flatten a rich design to fit the token set.** If a source design has 4 border tones, create them:
695
+ Tokens handle context adaptation — the hard problem. **They are a floor, not a ceiling.** A great foundation adds design vocabulary on top:
786
696
 
787
697
  ```css
788
698
  /* foundation/src/styles.css */
789
699
  .border-subtle { border-color: color-mix(in oklch, var(--border), transparent 50%); }
790
700
  .border-strong { border-color: color-mix(in oklch, var(--border), var(--heading) 30%); }
791
- .border-accent { border-color: var(--primary-300); }
701
+ .text-tertiary { color: color-mix(in oklch, var(--body), var(--subtle) 50%); }
792
702
  ```
793
703
 
794
- These compose with semantic tokens — they adapt per context because they reference `--border`, `--heading`, or palette shades. But they add design nuance the token set alone doesn't provide.
795
-
796
- **The priority:** Design quality > portability > configurability. It's better to ship a foundation with beautiful, detailed design that's less configurable than to ship a generic one that looks flat. A foundation that looks great for one site is more valuable than one that looks mediocre for any site.
704
+ 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.
797
705
 
798
- **Text tones beyond the 3-token set.** Source designs often have 4+ text tones (primary, secondary, tertiary, disabled). Uniweb provides 3 (`text-heading`, `text-body`, `text-subtle`). Don't collapse the extras create them with `color-mix()` so they still adapt per context:
706
+ **The priority:** Design quality > portability > configurability. A beautiful foundation for one site is more valuable than a generic one that looks flat.
799
707
 
800
- ```css
801
- /* foundation/src/styles.css */
802
- .text-tertiary { color: color-mix(in oklch, var(--body), var(--subtle) 50%); }
803
- .text-disabled { color: color-mix(in oklch, var(--subtle), transparent 40%); }
804
- ```
805
-
806
- **When migrating from an existing design**, map every visual detail — not just the ones that have a semantic token. Shadow systems, border hierarchies, custom hover effects, accent tints: create CSS classes or Tailwind utilities in `styles.css` for anything the original has that tokens don't cover. Use palette shades directly (`var(--primary-300)`, `bg-neutral-200`) for fine-grained color control beyond the semantic tokens.
708
+ ---
807
709
 
808
710
  ## Component Development
809
711
 
712
+ 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.
713
+
810
714
  ### Props Interface
811
715
 
812
716
  ```jsx
@@ -817,9 +721,25 @@ function MyComponent({ content, params, block }) {
817
721
  }
818
722
  ```
819
723
 
724
+ All non-reserved frontmatter fields become `params`. Reserved: `type`, `preset`, `input`, `data`, `id`, `background`, `theme`. Everything else flows to the component.
725
+
726
+ ### block properties
727
+
728
+ | Property | Type | Description |
729
+ |----------|------|-------------|
730
+ | `block.page` | Page | Parent page |
731
+ | `block.website` | Website | Site-level data and navigation |
732
+ | `block.type` | string | Component type name |
733
+ | `block.childBlocks` | Block[] | File-based child sections |
734
+ | `block.insets` | Block[] | Inline `@Component` references |
735
+ | `block.getInset(refId)` | Block | Lookup inset by refId |
736
+ | `block.properties` | object | Raw frontmatter |
737
+ | `block.themeName` | string | `"light"`, `"medium"`, `"dark"` |
738
+ | `block.stableId` | string | Stable ID from filename or `id:` |
739
+
820
740
  ### Section Wrapper
821
741
 
822
- The runtime wraps every section type in a `<section>` element with context class, background, and semantic tokens. Use static properties to customize this wrapper:
742
+ The runtime wraps every section in `<section>` with context class and background. Customize with static properties:
823
743
 
824
744
  ```jsx
825
745
  function Hero({ content, params }) {
@@ -830,29 +750,80 @@ function Hero({ content, params }) {
830
750
  )
831
751
  }
832
752
 
833
- Hero.className = 'pt-32 md:pt-48' // Override spacing for hero (more top padding)
834
- Hero.as = 'div' // Change wrapper element (default: 'section')
753
+ Hero.className = 'pt-32 md:pt-48' // Override spacing
754
+ Hero.as = 'div' // Change wrapper element
835
755
 
836
756
  export default Hero
837
757
  ```
838
758
 
839
- - `Component.className` — adds classes to the runtime's wrapper. Use for section-level spacing, borders, overflow. Set `py-[var(--section-padding-y)]` for consistent spacing from the theme variable, or override for specific sections (e.g., hero needs extra top padding). The component's own JSX handles inner layout only (`max-w-7xl mx-auto px-6`).
840
- - `Component.as` — changes the wrapper element. Use `'nav'` for headers, `'footer'` for footers, `'div'` when `<section>` isn't semantically appropriate.
759
+ - `Component.className` — adds classes to the runtime wrapper. Section-level spacing, borders, overflow.
760
+ - `Component.as` — changes wrapper element: `'nav'` for headers, `'footer'` for footers.
841
761
 
842
- **Layout components** (Header, Footer) typically need `Component.className = 'p-0'` to suppress the runtime's default section padding, since they control their own padding. Also set `Component.as = 'header'` or `'footer'` for semantic HTML:
762
+ **Layout components** typically need `p-0` to suppress default padding:
843
763
 
844
764
  ```jsx
845
- function Header({ content, block }) { /* ... */ }
846
765
  Header.className = 'p-0'
847
766
  Header.as = 'header'
848
- export default Header
849
767
  ```
850
768
 
769
+ ### Rendering Content with Kit
770
+
771
+ 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:
772
+
773
+ **Extracted fields** (most common — custom layout with content from markdown):
774
+
775
+ ```jsx
776
+ import { H1, H2, P, Span } from '@uniweb/kit'
777
+
778
+ <H1 text={content.title} className="text-heading text-5xl font-bold" />
779
+ <P text={content.paragraphs} className="text-body" />
780
+ <Span text={listItem.paragraphs[0]} className="text-subtle" />
781
+ ```
782
+
783
+ These render their own HTML tag — don't wrap: `<H2 text={...} />` not `<h2><H2 text={...} /></h2>`.
784
+
785
+ **Full content rendering** (article/docs sections where the author controls the flow):
786
+
787
+ ```jsx
788
+ import { Section, Render } from '@uniweb/kit'
789
+
790
+ <Section block={block} width="lg" padding="md" />
791
+ <Render content={block.parsedContent} block={block} />
792
+ ```
793
+
794
+ **Visuals:**
795
+
796
+ ```jsx
797
+ import { Visual } from '@uniweb/kit'
798
+
799
+ <Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="rounded-2xl" />
800
+ ```
801
+
802
+ ### Kit API by Use Case
803
+
804
+ **Rendering text:** `H1`–`H6`, `P`, `Span`, `Div`, `Text` (with `as` prop)
805
+
806
+ **Rendering content:** `Section` (Render + prose + layout), `Render` (ProseMirror → React), `ChildBlocks` (render child sections)
807
+
808
+ **Rendering media:** `Visual` (first non-empty: inset/video/image), `Image`, `Media`, `Icon`
809
+
810
+ **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()`
811
+
812
+ **Header and layout:** `useScrolled(threshold)`, `useMobileMenu()`, `useAppearance()` (light/dark mode)
813
+
814
+ **Layout helpers:** `useGridLayout(columns, { gap })`, `useAccordion({ multiple, defaultOpen })`, `useTheme(name)`
815
+
816
+ **Data and theming:** `useThemeData()` (programmatic color access), `useColorContext(block)`
817
+
818
+ **Utilities:** `cn()` (Tailwind class merge), `Link`, `Image`, `Asset`, `SafeHtml`, `SocialIcon`, `filterSocialLinks(links)`, `getSocialPlatform(url)`
819
+
820
+ **Other styled:** `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
821
+
851
822
  ### Content Patterns for Header and Footer
852
823
 
853
- Header and Footer are the hardest components to content-model because they combine several content categories. Use different parts of the content shape for each role:
824
+ Header and Footer combine several content categories. Use different parts of the content shape for each role:
854
825
 
855
- **Header** — title for logo, list for nav links, standalone link for CTA, tagged YAML for metadata:
826
+ **Header** — title for logo, list for nav, standalone link for CTA:
856
827
 
857
828
  ````markdown
858
829
  ---
@@ -875,15 +846,14 @@ version: v2.1.0
875
846
 
876
847
  ```jsx
877
848
  function Header({ content, block }) {
878
- const logo = content.title // "Acme Inc"
879
- const navItems = content.lists[0] || [] // [{icons, links}, ...]
880
- const cta = content.links[0] // {href, label}
881
- const config = content.data?.config // {github, version}
882
- // ...
849
+ const logo = content.title
850
+ const navItems = content.lists[0] || []
851
+ const cta = content.links[0]
852
+ const config = content.data?.config
883
853
  }
884
854
  ```
885
855
 
886
- **Footer** — paragraph for tagline, nested list for grouped columns, tagged YAML for legal:
856
+ **Footer** — paragraph for tagline, nested list for columns, YAML for legal:
887
857
 
888
858
  ````markdown
889
859
  ---
@@ -898,9 +868,6 @@ Build something great.
898
868
  - Developers
899
869
  - [Docs](/docs)
900
870
  - [GitHub](https://github.com/acme){target=_blank}
901
- - Community
902
- - [Discord](#)
903
- - [Blog](/blog)
904
871
 
905
872
  ```yaml:legal
906
873
  copyright: © 2025 Acme Inc
@@ -908,12 +875,11 @@ copyright: © 2025 Acme Inc
908
875
  ````
909
876
 
910
877
  ```jsx
911
- function Footer({ content, block }) {
912
- const tagline = content.paragraphs[0] // "Build something great."
913
- const columns = content.lists[0] || [] // [{paragraphs, lists}, ...]
914
- const legal = content.data?.legal // {copyright}
878
+ function Footer({ content }) {
879
+ const tagline = content.paragraphs[0]
880
+ const columns = content.lists[0] || []
881
+ const legal = content.data?.legal
915
882
 
916
- // Each column: group.paragraphs[0] = label, group.lists[0] = links
917
883
  columns.map(group => ({
918
884
  label: group.paragraphs[0],
919
885
  links: group.lists[0]?.map(item => item.links[0])
@@ -928,11 +894,11 @@ export default {
928
894
  title: 'Feature Grid',
929
895
  description: 'Grid of feature cards with icons',
930
896
  category: 'marketing',
931
- // hidden: true, // Exclude from export (internal/helper component)
897
+ // hidden: true, // Exclude from export
932
898
  // background: 'self', // Component renders its own background
933
- // inset: true, // Available for @ComponentName references in markdown
934
- // visuals: 1, // Expects 1 visual (image, video, or inset)
935
- // children: true, // Accepts file-based child sections
899
+ // inset: true, // Available for @ComponentName in markdown
900
+ // visuals: 1, // Expects 1 visual
901
+ // children: true, // Accepts child sections
936
902
 
937
903
  content: {
938
904
  title: 'Section heading',
@@ -950,241 +916,131 @@ export default {
950
916
  compact: { label: 'Compact', params: { columns: 4 } },
951
917
  },
952
918
 
953
- // Static capabilities for cross-block coordination
954
919
  context: {
955
- allowTranslucentTop: true, // Header can overlay this section
920
+ allowTranslucentTop: true,
956
921
  },
957
922
  }
958
923
  ```
959
924
 
960
925
  All defaults belong in `meta.js`, not inline in component code.
961
926
 
962
- ### @uniweb/kit
963
-
964
- Content fields (`title`, `pretitle`, `paragraphs[]`, list item text) are **HTML strings** — they contain markup like `<strong>`, `<em>`, `<a>` from the author's markdown. The kit provides components to render them correctly.
927
+ ### The Front Desk Pattern
965
928
 
966
- **Rendering text** (`@uniweb/kit`):
929
+ 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.
967
930
 
968
- ```jsx
969
- import { H1, H2, P, Span } from '@uniweb/kit'
970
-
971
- <H1 text={content.title} className="text-heading text-5xl font-bold" />
972
- // string → single <h1>, array → single <h1> with line breaks (multi-line headings)
973
- <H2 text={content.subtitle} className="text-heading text-3xl font-bold" />
974
- <P text={content.paragraphs} className="text-body" />
975
- // array → each string becomes its own <p>
976
- <Span text={listItem.paragraphs[0]} className="text-subtle" />
977
- ```
978
-
979
- `H1`–`H6`, `P`, `Span`, `Div` are all wrappers around `Text` with a preset tag:
931
+ 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 from `src/components/`, 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:
980
932
 
981
933
  ```jsx
982
- <Text text={content.title} as="h2" className="..." /> // explicit tag
983
- ```
984
-
985
- These components render their own HTML tag — don't wrap them in a matching tag. `<h2><H2 text={...} /></h2>` creates a nested `<h2><h2>...</h2></h2>`, which is invalid HTML. Just use `<H2 text={...} />` directly.
934
+ // sections/Features/index.jsx the front desk
935
+ import { CardGrid } from '../../components/CardGrid'
936
+ import { CardList } from '../../components/CardList'
937
+ import { ComparisonTable } from '../../components/ComparisonTable'
986
938
 
987
- Don't render content strings with `{content.paragraphs[0]}` in JSX that shows HTML tags as visible text. Use `<P>`, `<H2>`, `<Span>`, etc. for content strings.
939
+ const variants = { grid: CardGrid, list: CardList, comparison: ComparisonTable }
988
940
 
989
- **Rendering full content** (`@uniweb/kit`):
990
-
991
- ```jsx
992
- import { Section, Render } from '@uniweb/kit'
941
+ export default function Features({ content, block, params }) {
942
+ const Layout = variants[params.variant] || CardGrid
993
943
 
994
- <Render content={block.parsedContent} block={block} /> // ProseMirror nodes → React
995
- <Section block={block} width="lg" padding="md" /> // Render + prose styling + layout
944
+ return (
945
+ <Layout
946
+ title={content.title}
947
+ subtitle={content.paragraphs[0]}
948
+ items={content.items}
949
+ block={block}
950
+ columns={params.columns}
951
+ showIcons={params.showIcon !== false}
952
+ compact={params.density === 'compact'}
953
+ />
954
+ )
955
+ }
996
956
  ```
997
957
 
998
- `Render` processes ProseMirror nodes into React elements — paragraphs, headings, images, code blocks, lists, tables, alerts, and insets. `Section` wraps `Render` with prose typography and layout options. Use these when rendering a block's complete content. Use `P`, `H2`, etc. when you extract specific fields and arrange them with custom layout.
999
-
1000
- **Rendering visuals** (`@uniweb/kit`):
1001
-
1002
- `<Visual>` renders the first non-empty candidate from the props you pass (inset, video, image). See Insets section below.
1003
-
1004
- **Other primitives** (`@uniweb/kit`): `Link`, `Image`, `Icon`, `Media`, `Asset`, `SafeHtml`, `SocialIcon`, `FileLogo`, `cn()`
1005
-
1006
- `Link` props: `to` (or `href`), `target`, `reload`, `download`, `className`, `children`:
1007
-
1008
- ```jsx
1009
- <Link to="/about">About</Link> // SPA navigation via React Router
1010
- <Link to="page:about">About</Link> // Resolves page ID to route
1011
- <Link reload href={localeUrl}>ES</Link> // Full page reload, prepends basePath
1012
- // External URLs auto-get target="_blank" and rel="noopener noreferrer"
958
+ ```js
959
+ // meta.js — author-friendly language
960
+ export default {
961
+ params: {
962
+ variant: { type: 'select', options: ['grid', 'list', 'comparison'], default: 'grid' },
963
+ columns: { type: 'number', default: 3 },
964
+ showIcon: { type: 'boolean', default: true },
965
+ density: { type: 'select', options: ['default', 'compact'], default: 'default' },
966
+ }
967
+ }
1013
968
  ```
1014
969
 
1015
- **Other styled** (`@uniweb/kit`): `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
1016
-
1017
- **Hooks:**
1018
- - `useScrolled(threshold)` → boolean for scroll-based header styling
1019
- - `useMobileMenu()` → `{ isOpen, toggle, close }` with auto-close on navigation
1020
- - `useAccordion({ multiple, defaultOpen })` → `{ isOpen, toggle }` for expand/collapse
1021
- - `useActiveRoute()` → `{ route, rootSegment, isActive(page), isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
1022
- - `useGridLayout(columns, { gap })` → responsive grid class string
1023
- - `useTheme(name)` → standardized theme classes
1024
- - `useAppearance()` → `{ scheme, toggle, canToggle, setScheme, schemes }` — light/dark mode control with localStorage persistence
1025
- - `useRouting()` → `{ useLocation, useParams, useNavigate, Link, isRoutingAvailable }` — SSG-safe routing access (returns no-op fallbacks during prerender)
1026
- - `useWebsite()` → `{ website, localize, makeHref, getLanguage, getLanguages, getRoutingComponents }` — primary runtime hook
1027
- - `useThemeData()` → Theme instance for programmatic color access (`getColor(name, shade)`, `getPalette(name)`)
1028
- - `useColorContext(block)` → `'light' | 'medium' | 'dark'` — current section's color context
970
+ The content author writes `variant: comparison` — they don't know or care about `ComparisonTable`. The section type translates `density: compact` into a `compact={true}` prop. `CardGrid`, `CardList`, `ComparisonTable` live in `src/components/` — ordinary React, reusable across multiple section types, testable independently.
1029
971
 
1030
- **Utilities:** `cn()` (Tailwind class merge), `filterSocialLinks(links)`, `getSocialPlatform(url)`
972
+ 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.
1031
973
 
1032
974
  ### Foundation Organization
1033
975
 
1034
976
  ```
1035
977
  foundation/src/
1036
- ├── sections/ # Section types (auto-discovered via meta.js)
1037
- │ ├── Hero/
1038
- ├── Hero.jsx # Entry or index.jsx, both work
978
+ ├── sections/ # Section types (auto-discovered)
979
+ │ ├── Hero.jsx # Bare file — no folder needed
980
+ │ ├── Features/ # Folder when you need meta.js
981
+ │ │ ├── index.jsx
1039
982
  │ │ └── meta.js
1040
- │ └── Features/
1041
- ├── Features.jsx
1042
- └── meta.js
983
+ │ └── insets/ # Organizational subdirectory (lowercase)
984
+ └── Diagram/
985
+ ├── index.jsx
986
+ │ └── meta.js
1043
987
  ├── components/ # Your React components (no meta.js, not selectable)
1044
- │ ├── ui/ # shadcn-compatible primitives
988
+ │ ├── ui/
1045
989
  │ │ └── button.jsx
1046
990
  │ └── Card.jsx
1047
991
  └── styles.css
1048
992
  ```
1049
993
 
1050
- Only folders with `meta.js` in `sections/` (or `components/` for older foundations) become section types. Everything else is ordinary React — organize however you like.
994
+ **Discovery:** PascalCase files/folders at root of `sections/` are auto-discovered. Nested levels require `meta.js`. Lowercase directories are organizational only. `hidden: true` excludes a component entirely. Everything outside `sections/` is ordinary React.
1051
995
 
1052
996
  ### Website and Page APIs
1053
997
 
1054
998
  ```jsx
1055
- const { website } = useWebsite() // or block.website
1056
- const page = website.activePage // or block.page
999
+ const { website } = useWebsite()
1000
+ const page = website.activePage
1057
1001
 
1058
1002
  // Navigation
1059
- const pages = website.getPageHierarchy({ for: 'header' }) // or 'footer'
1003
+ website.getPageHierarchy({ for: 'header' })
1060
1004
  // → [{ route, navigableRoute, label, hasContent, children }]
1061
1005
 
1006
+ // Core properties
1007
+ website.name // Site name from site.yml
1008
+ website.basePath // Deployment base path (e.g., '/docs/')
1009
+
1062
1010
  // Locale
1063
1011
  website.hasMultipleLocales()
1064
1012
  website.getLocales() // [{ code, label, isDefault }]
1065
- website.getActiveLocale() // 'en'
1013
+ website.getActiveLocale()
1066
1014
  website.getLocaleUrl('es')
1067
1015
 
1068
- // Core properties
1069
- website.name // Site name from site.yml
1070
- website.basePath // Deployment base path (e.g., '/docs/')
1071
-
1072
1016
  // Route detection
1073
1017
  const { isActive, isActiveOrAncestor } = useActiveRoute()
1074
- isActive(page) // Exact match
1075
- isActiveOrAncestor(page) // Ancestor match (for parent highlighting in nav)
1076
1018
 
1077
- // Appearance (light/dark mode)
1019
+ // Appearance
1078
1020
  const { scheme, toggle, canToggle } = useAppearance()
1079
1021
 
1080
1022
  // Page properties
1081
- page.title // Page title
1082
- page.label // Short label for nav (falls back to title)
1083
- page.route // Route path
1084
- page.isHidden() // Hidden from navigation
1085
- page.showInHeader() // Visible in header nav
1086
- page.showInFooter() // Visible in footer nav
1087
- page.hasChildren() // Has child pages
1088
- page.children // Array of child Page objects
1089
- ```
1090
-
1091
- ### Insets and the Visual Component
1092
-
1093
- Components access inline `@` references via `block.insets` (separate from `block.childBlocks`):
1094
-
1095
- ```jsx
1096
- import { Visual } from '@uniweb/kit'
1097
-
1098
- // Visual renders the first non-empty candidate: inset > video > image
1099
- function SplitContent({ content, block }) {
1100
- return (
1101
- <div className="flex gap-12">
1102
- <div className="flex-1">
1103
- <h2 className="text-heading">{content.title}</h2>
1104
- </div>
1105
- <Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="flex-1 rounded-lg" />
1106
- </div>
1107
- )
1108
- }
1109
- ```
1110
-
1111
- - `<Visual>` — renders first non-empty candidate from the props you pass (`inset`, `video`, `image`)
1112
- - `<Render>` / `<Section>` — automatically handles `@Component` references placed in content flow
1113
- - `block.insets` — array of Block instances from `@` references
1114
- - `block.getInset(refId)` — lookup by refId (used by sequential renderers)
1115
- - `content.insets` — flat array of `{ refId }` entries (parallel to `content.imgs`)
1116
-
1117
- **SSG and hooks:** Inset components that use React hooks (useState, useEffect) will trigger prerender warnings during `pnpm build`. This is expected — the SSG pipeline cannot render hooks due to dual React instances in the build. The warnings are informational; the page renders correctly client-side. If you see `"Skipped SSG for /..."` or `"Invalid hook call"`, this is the cause.
1118
-
1119
- Inset components declare `inset: true` in meta.js:
1120
-
1121
- ```js
1122
- // sections/insets/NetworkDiagram/meta.js
1123
- export default {
1124
- inset: true,
1125
- params: { variant: { type: 'select', options: ['full', 'compact'], default: 'full' } },
1126
- }
1127
- ```
1128
-
1129
- Whether an inset appears in a section palette is a concern of the parent component (via `children` and `insets` in its meta.js), not a property of the inset itself. Don't use `hidden: true` on insets — `hidden` means "don't export this component at all" (internal helpers, not-yet-ready components).
1130
-
1131
- ### Dispatcher Pattern
1132
-
1133
- One section type with a `variant` param replaces multiple near-duplicates. Instead of `HeroLeft`, `HeroCentered`, `HeroSplit` — one `Hero` with `variant: left | centered | split`:
1134
-
1135
- ```jsx
1136
- function SplitContent({ content, block, params }) {
1137
- const flipped = params.variant === 'flipped'
1138
- return (
1139
- <div className={`flex gap-16 items-center ${flipped ? 'flex-row-reverse' : ''}`}>
1140
- <div className="flex-1">
1141
- {content.pretitle && (
1142
- <p className="text-xs font-bold uppercase tracking-widest text-subtle mb-4">
1143
- {content.pretitle}
1144
- </p>
1145
- )}
1146
- <h2 className="text-heading text-3xl font-bold">{content.title}</h2>
1147
- <p className="text-body mt-4">{content.paragraphs[0]}</p>
1148
- </div>
1149
- <Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="flex-1 rounded-2xl" />
1150
- </div>
1151
- )
1152
- }
1023
+ page.title, page.label, page.route
1024
+ page.isHidden(), page.showInHeader(), page.showInFooter()
1025
+ page.hasChildren(), page.children
1153
1026
  ```
1154
1027
 
1155
- ```js
1156
- // meta.js
1157
- export default {
1158
- title: 'Split Content',
1159
- content: { pretitle: 'Eyebrow label', title: 'Section heading', paragraphs: 'Description' },
1160
- params: {
1161
- variant: { type: 'select', options: ['default', 'flipped'], default: 'default' },
1162
- },
1163
- }
1164
- ```
1165
-
1166
- Content authors choose the variant in frontmatter (`variant: flipped`), or the site can alternate it across sections. One component serves every "text + visual" layout on the site.
1167
-
1168
1028
  ### Cross-Block Communication
1169
1029
 
1170
- Components read neighboring blocks for adaptive behavior (e.g., translucent header over hero):
1171
-
1172
1030
  ```jsx
1173
1031
  const firstBody = block.page.getFirstBodyBlockInfo()
1174
1032
  // → { type, theme, context: { allowTranslucentTop }, state }
1175
-
1176
- // context = static (from meta.js), state = dynamic (from useBlockState)
1177
1033
  ```
1178
1034
 
1179
1035
  ### Custom Layouts
1180
1036
 
1181
- Layouts live in `foundation/src/layouts/` and are auto-discovered. Set the default in `foundation.js`:
1037
+ Layouts live in `foundation/src/layouts/` and are auto-discovered:
1182
1038
 
1183
1039
  ```js
1184
1040
  // foundation/src/foundation.js
1185
1041
  export default {
1186
- name: 'My Template', // Display name (falls back to package.json name)
1187
- description: 'A brief description', // Falls back to package.json description
1042
+ name: 'My Template',
1043
+ description: 'A brief description',
1188
1044
  defaultLayout: 'DocsLayout',
1189
1045
  }
1190
1046
  ```
@@ -1206,80 +1062,67 @@ export default function DocsLayout({ header, body, footer, left, right, params }
1206
1062
  }
1207
1063
  ```
1208
1064
 
1209
- Layout receives pre-rendered areas as props plus `params`, `page`, and `website`. The `body` area is always implicit.
1210
-
1211
- **Layout meta.js** declares which areas the layout renders:
1212
-
1213
- ```js
1214
- // foundation/src/layouts/DocsLayout/meta.js
1215
- export default {
1216
- areas: ['header', 'footer', 'left'],
1217
- }
1218
- ```
1219
-
1220
- Area names are arbitrary strings — `header`, `footer`, `left`, `right` are conventional, but a dashboard layout could use `topbar`, `sidebar`, `statusbar`.
1065
+ **Layout meta.js** declares areas: `{ areas: ['header', 'footer', 'left'] }`. Area names are arbitrary.
1221
1066
 
1222
- **Site-side layout content** — each layout can have its own section files:
1067
+ **Layout content** — each layout has section files in `site/layout/`:
1223
1068
 
1224
1069
  ```
1225
1070
  site/layout/
1226
- ├── header.md # Default layout sections
1071
+ ├── header.md # Default layout
1227
1072
  ├── footer.md
1228
- ├── left.md
1229
- └── marketing/ # Sections for the "marketing" layout
1230
- ├── header.md # Different header for marketing pages
1073
+ └── marketing/ # Named layout sections
1074
+ ├── header.md
1231
1075
  └── footer.md
1232
1076
  ```
1233
1077
 
1234
- Named subdirectories are self-contained — no inheritance from the root. If `marketing/` has no `left.md`, marketing pages have no left panel.
1235
-
1236
- **Layout cascade** (first match wins): `page.yml` → `folder.yml` → `site.yml` → foundation `defaultLayout` → `"default"`.
1078
+ Named subdirectories are self-contained — no inheritance. Layout cascade: `page.yml` `folder.yml` `site.yml` foundation `defaultLayout` `"default"`.
1237
1079
 
1238
- ```yaml
1239
- # page.yml — select layout and hide areas
1240
- layout:
1241
- name: MarketingLayout
1242
- hide: [left, right]
1243
- ```
1080
+ ---
1244
1081
 
1245
1082
  ## Migrating From Other Frameworks
1246
1083
 
1247
- Don't port line-by-line. Study the original implementation, then plan a new one in Uniweb from first principles. Other frameworks produce far more components than Uniweb needs — expect consolidation, not 1:1 correspondence.
1084
+ 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.
1248
1085
 
1249
- ### Why fewer components
1086
+ ### The mental model shift
1250
1087
 
1251
- Uniweb section types do more with less because the framework handles concerns that other frameworks push onto components:
1252
-
1253
- - **Dispatcher pattern** — one section type with a `variant` param replaces multiple near-duplicate components (`HeroHomepage` + `HeroPricing` → `Hero` with `variant: homepage | pricing`)
1254
- - **Section nesting** `@`-prefixed child files replace wrapper components that exist only to arrange children
1255
- - **Insets** `![](@ComponentName)` replaces prop-drilling of visual components into containers
1256
- - **Visual component** `<Visual>` renders the first non-empty visual from explicit candidates (inset, video, image), replacing manual media handling
1257
- - **Semantic theming** the runtime orchestrates context classes and token resolution, replacing per-component dark mode logic
1258
- - **Engine backgrounds** the runtime renders section backgrounds from frontmatter, replacing background-handling code in every section
1259
- - **Rich params** `meta.js` params with types, defaults, and presets replace config objects and conditional logic
1088
+ | React / conventional | Uniweb equivalent |
1089
+ |---|---|
1090
+ | Props with typed data | Frontmatter params + `meta.js` |
1091
+ | Component variants via props | `variant` param in frontmatter; Front Desk pattern for complex routing |
1092
+ | Context / ThemeProvider | `theme:` frontmatter + semantic tokens (automatic) |
1093
+ | Wrapper/layout components | Section nesting or custom layouts |
1094
+ | Prop-drilling visuals into containers | Insets `![](@Component)` rendered via `<Visual>` |
1095
+ | Content in JSX or `.js` data files | Markdown parser `content` prop |
1096
+ | CSS color tokens / design systems | `theme.yml` palette shades + semantic tokens |
1097
+ | `isDark ? ... : ...` conditionals | `text-heading` — context classes handle it |
1098
+ | Per-component backgrounds | `background:` in frontmatter |
1099
+ | Multiple near-identical components | One section type + `variant` param, or Front Desk pattern |
1100
+ | i18n wrapping (`t()` / `<Trans>`) | Locale-specific content directories |
1260
1101
 
1261
1102
  ### Migration approach
1262
1103
 
1263
- 1. **Check if you're inside an existing Uniweb workspace** (look for `pnpm-workspace.yaml` and a `package.json` with `uniweb` as a dependency). If yes, use `pnpm uniweb add` to create projects inside it. If no, create a new workspace:
1104
+ 1. **Scaffold the workspace:**
1264
1105
  ```bash
1265
1106
  pnpm create uniweb my-project --template none
1266
1107
  ```
1267
1108
 
1268
- 3. **Use named layouts** for different page groups — a marketing layout for landing pages, a docs layout for `/docs/*`. One site, multiple layouts, each with its own header/footer/sidebar content.
1109
+ 2. **Use named layouts** for different page groups — marketing layout for landing pages, docs layout for `/docs/*`.
1110
+
1111
+ 3. **Dump legacy components under `src/components/`** — they're not section types. Import from section types during transition.
1269
1112
 
1270
- 4. **Dump legacy components under `src/components/`** — they're not section types. Import them from section types as needed during the transition.
1113
+ 4. **Create section types one at a time.** Migration levels:
1114
+ - **Level 0**: Paste the original as one section type. Routing and dev tooling work immediately.
1115
+ - **Level 1**: Decompose into section types. Consolidate duplicates — use `variant` params or the Front Desk pattern.
1116
+ - **Level 2**: Move content from JSX to markdown. Authors can now edit without code.
1117
+ - **Level 3**: Replace hardcoded colors with semantic tokens. Components work in any context.
1271
1118
 
1272
- 5. **Create section types one at a time.** Each is independentone can use hardcoded content while another reads from markdown. Staged migration levels:
1273
- - **Level 0**: Paste the whole original file as one section type. You get routing and dev tooling immediately.
1274
- - **Level 1**: Decompose into section types. Name by purpose (`Institutions` → `Testimonial`). Consolidate duplicates via dispatcher pattern.
1275
- - **Level 2**: Move content from JSX to markdown. Components read from `content` instead of hardcoded strings. Content authors can now edit without touching code.
1276
- - **Level 3**: Replace hardcoded Tailwind colors with semantic tokens. Components work in any context and any brand.
1119
+ 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.
1277
1120
 
1278
- 6. **Map source colors to `theme.yml`, not to foundation CSS.** The most common migration mistake is recreating the source site's color tokens as CSS custom properties in `styles.css` (e.g., `--ink`, `--paper`, `--accent`). This creates a parallel color system that bypasses CCA's semantic tokens, context classes, and site-level theming entirely. Instead: identify the source's primary color set it as `colors.primary` in theme.yml. Identify the neutral tone → set it as `colors.neutral` (e.g., `stone` for warm). Identify context needs use `theme:` frontmatter per section. Components use `text-heading`, `bg-section`, `bg-card` — never custom color variables.
1121
+ 6. **Name by purpose, not content** `TheModel` → `SplitContent`, `WorkModes` → `FeatureColumns`.
1279
1122
 
1280
- 7. **Name by purpose, not by content** `TheModel` `SplitContent`, `WorkModes` `FeatureColumns`, `FinalCTA` `CallToAction`. Components render a *kind* of content, not specific content.
1123
+ 7. **UI helpers `components/`** Buttons, badges, cards in `src/components/` (no `meta.js`, not selectable by authors).
1281
1124
 
1282
- 8. **UI helpers → `components/`** — Buttons, badges, cards go in `src/components/` (no `meta.js` needed, not selectable by content authors).
1125
+ ---
1283
1126
 
1284
1127
  ## Tailwind CSS v4
1285
1128
 
@@ -1287,55 +1130,74 @@ Foundation styles in `foundation/src/styles.css`:
1287
1130
 
1288
1131
  ```css
1289
1132
  @import "tailwindcss";
1290
- @import "@uniweb/kit/theme-tokens.css"; /* Semantic tokens from theme.yml */
1133
+ @import "@uniweb/kit/theme-tokens.css";
1291
1134
  @source "./sections/**/*.{js,jsx}";
1292
- @source "./components/**/*.{js,jsx}"; /* UI helpers (Button, Card, etc.) */
1135
+ @source "./components/**/*.{js,jsx}";
1293
1136
  @source "../node_modules/@uniweb/kit/src/**/*.jsx";
1294
1137
 
1295
1138
  @theme {
1296
- /* Additional custom values — NOT for colors already in theme.yml */
1297
1139
  --breakpoint-xs: 30rem;
1298
1140
  }
1299
1141
  ```
1300
1142
 
1301
- Semantic color tokens (`text-heading`, `bg-section`, `bg-primary`, etc.) come from `theme-tokens.css` which the runtime populates from the site's `theme.yml`. Don't redefine colors here that belong in `theme.yml`. Use `@theme` only for values the token system doesn't cover (custom breakpoints, animations, shadows).
1302
-
1303
- **Custom CSS is expected alongside Tailwind.** Your foundation's `styles.css` is the design layer — shadow systems, border hierarchies, gradient effects, accent treatments, elevation scales, glassmorphism. If the source design has a visual detail, create a class for it. Tailwind handles layout and spacing; semantic tokens handle context adaptation; `styles.css` handles everything else that makes the design rich and distinctive.
1143
+ 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.
1304
1144
 
1305
1145
  ## Troubleshooting
1306
1146
 
1307
- **"Could not load foundation"** — Check `site/package.json` has `"foundation": "file:../foundation"` (or `"default": "file:../../foundations/default"` for multi-site).
1147
+ **"Could not load foundation"** — Check `site/package.json` has `"foundation": "file:../foundation"`.
1308
1148
 
1309
- **Component not appearing** — Verify `meta.js` exists. Check for `hidden: true` (means component is excluded from export — only use for internal helpers). Rebuild: `cd foundation && pnpm build`.
1149
+ **Component not appearing** — Verify `meta.js` exists. Check for `hidden: true`. Rebuild: `cd foundation && pnpm build`.
1310
1150
 
1311
- **Styles not applying** — Verify `@source` in `styles.css` includes your component paths. Check custom colors match `@theme` definitions.
1151
+ **Styles not applying** — Verify `@source` includes your component paths.
1312
1152
 
1313
- **Prerender warnings about hooks/useState** — Components with React hooks (useState/useEffect) — especially insets — will show SSG warnings during `pnpm build`. This is expected and harmless; see the note in the Insets section above.
1153
+ **Prerender warnings about hooks** — Components with useState/useEffect show SSG warnings during build. Expected and harmless.
1314
1154
 
1315
- **Content not appearing as expected?** In dev mode, open the browser console and inspect the parsed content shape your component receives:
1155
+ **Content not appearing as expected?**
1156
+ ```bash
1157
+ uniweb inspect pages/home/hero.md # Single section
1158
+ uniweb inspect pages/home/ # Whole page
1159
+ uniweb inspect pages/home/hero.md --raw # ProseMirror AST
1160
+ ```
1316
1161
 
1317
- ```js
1318
- globalThis.uniweb.activeWebsite.activePage.bodyBlocks[0].parsedContent
1162
+ ## Learning from Official Templates
1163
+
1164
+ 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:
1165
+
1166
+ ```bash
1167
+ uniweb add project marketing --from marketing
1168
+ pnpm install
1319
1169
  ```
1320
1170
 
1321
- Compare with the Content Shape table above to identify mapping issues (e.g., headings becoming items instead of title, links inline in paragraphs instead of in `links[]`).
1171
+ This creates `marketing/foundation/` + `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.
1172
+
1173
+ **What to study:**
1174
+ - `{name}/foundation/src/sections/` — components with meta.js (content expectations, params, presets)
1175
+ - `{name}/site/pages/` — real content files showing markdown → component mapping
1176
+ - `{name}/site/theme.yml` + `site.yml` — theming and configuration patterns
1177
+
1178
+ **Available templates:**
1179
+
1180
+ | Template | Demonstrates |
1181
+ |----------|-------------|
1182
+ | `marketing` | Semantic tokens, insets, grids, multi-line headings, inline styling |
1183
+ | `docs` | Sidebar navigation, navigation levels, code highlighting |
1184
+ | `dynamic` | Live API data fetching, loading states, transforms |
1185
+ | `international` | i18n, blog with collections, multi-locale routing |
1186
+ | `store` | Product grid, collections, e-commerce patterns |
1187
+ | `academic` | Publications, team grid, timeline, math |
1188
+ | `extensions` | Multi-foundation architecture, runtime loading |
1189
+
1190
+ You can install multiple templates. Each becomes an independent project in the workspace. To run one in dev: `cd {name}/site && pnpm dev`
1322
1191
 
1323
1192
  ## Further Documentation
1324
1193
 
1325
- Full Uniweb documentation is available at **https://github.com/uniweb/docs** — raw markdown files you can fetch directly.
1194
+ Full documentation: **https://github.com/uniweb/docs**
1326
1195
 
1327
1196
  | Section | Path | Topics |
1328
1197
  |---------|------|--------|
1329
- | **Getting Started** | `getting-started/` | What is Uniweb, quickstart guide, templates overview |
1330
- | **Authoring** | `authoring/` | Writing content, site setup, collections, theming, linking, search, recipes, translations |
1331
- | **Development** | `development/` | Building foundations, component patterns, data fetching, custom layouts, i18n, converting existing designs |
1332
- | **Reference** | `reference/` | site.yml, page.yml, content structure, meta.js, kit hooks/components, theming tokens, CLI commands, deployment |
1333
-
1334
- **Quick access pattern:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
1335
-
1336
- Examples:
1337
- - Content structure details: `reference/content-structure.md`
1338
- - Component metadata (meta.js): `reference/component-metadata.md`
1339
- - Kit hooks and components: `reference/kit-reference.md`
1340
- - Theming tokens: `reference/site-theming.md`
1341
- - Data fetching patterns: `reference/data-fetching.md`
1198
+ | **Getting Started** | `getting-started/` | What is Uniweb, quickstart, templates |
1199
+ | **Authoring** | `authoring/` | Writing content, site setup, collections, theming, translations |
1200
+ | **Development** | `development/` | Foundations, component patterns, data fetching, layouts, i18n |
1201
+ | **Reference** | `reference/` | site.yml, page.yml, content structure, meta.js, kit API, CLI, deployment |
1202
+
1203
+ **Quick access:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`