# Component Metadata Guide

This guide explains how to write `meta.js` files for Uniweb components.

## Overview

A `meta.js` file declares a section type's content interface:

- What the component does (for documentation and editor UI)
- What content it expects from markdown
- What parameters content authors can configure
- What presets are available for quick setup
- What data it receives — the `content.data` keys it declares

**In `src/sections/`**, `meta.js` is optional at the root level. A bare file (`Hero.jsx`) or folder (`Hero/Hero.jsx`) is automatically an addressable section type with an implicit empty content interface. Add `meta.js` when you need params, content expectations, or presets. Deeper nesting within `src/sections/` requires `meta.js` for a component to be addressable.

Components without `meta.js` (outside of `src/sections/` root) are ordinary React components — not selectable by content authors, but the foundation's actual rendering workhorses.

---

## Minimal Examples

The simplest section type — no `meta.js` at all:

```
src/sections/TextSection.jsx    ← addressable, title inferred as "Text Section"
```

When you need params or content expectations, add a `meta.js`:

```javascript
export default {
  title: 'Text Section',    // Optional — inferred from component name if omitted
}
```

---

## What's Required vs Optional

When `meta.js` is present:

| Field | Required | Default |
| --- | --- | --- |
| `title` | No | Inferred from component name (`TeamRoster` → "Team Roster") |
| `description` | No | — |
| `family` | No | — (the editor falls back to a generic illustration) |
| `hidden` | No | `false` |
| `background` | No | `false` |
| `sticky` | No | `false` |
| `inset` | No | `false` |
| `children` | No | — |
| `data` | No | — (the section receives no data of its own) |
| `content` | No | — |
| `params` | No | — |
| `presets` | No | — |
| `vars` | No | — |
| `context` | No | — |
| `initialState` | No | — |

When `meta.js` is absent (only at root of `src/sections/`), the section type has no params, no content expectations, and a title inferred from the file or folder name.

---

## Full Example

```javascript
// sections/Hero/meta.js
export default {
  title: 'Hero Banner',
  description: 'Bold hero section with headline and call-to-action',
  background: true,

  data: {
    events: '@std/event',  // the section receives content.data.events, shaped by @std/event
  },

  content: {
    title: 'Headline',
    pretitle: 'Eyebrow',
    paragraphs: 'Description [1-2]',
    links: 'CTA buttons [1-2]',
    image: 'Hero image [1]',
  },

  params: {
    variant: {
      type: 'select',
      label: 'Variant',
      options: ['gradient', 'glass', 'minimal'],
      default: 'gradient',
    },
    layout: {
      type: 'select',
      label: 'Layout',
      options: [
        'center',
        'left',
        { value: 'split-right', label: 'Split (image right)' },
        { value: 'split-left', label: 'Split (image left)' },
      ],
      default: 'center',
    },
  },

  vars: {
    'content-gap': {
      default: '2rem',
      label: 'Content Gap',
      type: 'select',
      options: ['1rem', '1.5rem', '2rem', '3rem'],
      group: 'Layout',
    },
  },

  presets: {
    default: {
      label: 'Centered Hero',
      params: { variant: 'gradient', layout: 'center' },
    },
    split: {
      label: 'Split Layout',
      params: { variant: 'gradient', layout: 'split-right' },
    },
  },

  // Static — neighbors read via getNextBlockInfo().context
  context: {
    allowTranslucentTop: true,
  },

  // Dynamic — neighbors read via getNextBlockInfo().state
  // Component can update with useBlockState()
  initialState: {
    allowTranslucentTop: true,
  },
}
```

---

## How Data Flows to Your Component

The meta.js describes the contract. Here's what your component actually receives:

```jsx
function Hero({ content, params, block, website }) {
  // ─── From markdown (content) ────────────────────────
  // Runtime guarantees all fields exist - no defensive checks needed
  const { title, pretitle, subtitle, paragraphs, links, images, items } = content

  // ─── From frontmatter (params) ──────────────────────
  const { variant, layout } = params

  // ─── From a declared data key (a query's records) ──────
  const events = content.data?.events || []

  // ─── From structured author data (tagged blocks or form UI) ──
  const navLinks = content.data?.['nav-links'] || []

  return (
    <section className={variant}>
      {pretitle && <span className="eyebrow">{pretitle}</span>}
      {title && <H1>{title}</H1>}
      {paragraphs.map((p, i) => <P key={i}>{p}</P>)}
    </section>
  )
}
```

**Runtime guarantees**: The runtime ensures all content fields exist with sensible defaults (empty strings/arrays). You don't need defensive null checks—content structure is guaranteed.

| Source | Declared in | Accessed via |
| --- | --- | --- |
| Markdown content | `content: { ... }` | `content.title`, `content.paragraphs`, `content.items` |
| Frontmatter params | `params: { ... }` | `params.paramName` |
| A query's records | `data: { events: '@std/event' }` | `content.data.events` |
| Structured author data | `data: { 'nav-links': { ... } }` | `content.data['nav-links']` |

> **Note:** `block.data` and `content.data` point at the same object at runtime. Use `content.data.*` in components — it matches how the rest of the framework surfaces the content tree.

---

## Schema Reference

### Identity Fields

| Field | Type | Description |
| --- | --- | --- |
| `title` | string | Display name in the editor. If omitted, inferred from the component name (`TeamRoster` → "Team Roster") |
| `description` | string | What the component does |
| `family` | string | The standard section family this component belongs to. Picks the editor's illustration, its icon and a label it can translate. Optional — see below |
| `hidden` | boolean | If true, component is excluded from export entirely (internal helpers, not-yet-ready components) |
| `inset` | boolean | If true, available for `@ComponentName` references in markdown |

#### Family

An editor that lets an author add a section has to draw something for each one. `family` is how it knows what to draw — an illustration and an icon — and which translated label to show. The icon is the family's: there is no `icon` to declare.

**Most components need no declaration.** The section type *is* the component name, so a component called `Hero`, `Footer` or `Pricing` already says what it is and resolves on its own. `family` is for the ones whose name does not give it away:

```javascript
// sections/ProfileHero/meta.js
export default {
  title: 'Researcher Profile',
  family: 'profile',
}
```

`ProfileHero`, `CvEntry` and `PublicationsByYear` are all good names that no standard list will ever contain. One line says which family they belong to.

**A name of your own is fine.** An unrecognized value — or none at all — falls back to a generic illustration. Nothing breaks, nothing is gated, and you are never waiting for a name to be added before you can ship. Plenty of sections are a thing your site has and nobody else's, and those should stay undeclared.

**One value, the dominant shape.** A `HeroWithEstimate` is in the `hero` family; the picker draws one picture, so name the shape that dominates.

`family` is a claim about **shape** and nothing else. No part of the build, the runtime or the delivered site reads it — a foundation that declares none renders identically.

Run `uniweb families` to see the list, or `uniweb doctor` to see what each of your sections resolved to and what it would suggest for the rest.

| Group | Families |
| --- | --- |
| **Opening** — say what this page is | `hero` · `profile` · `statement` · `announcement` · `countdown` · `marquee` · `canvas` |
| **Explaining** — your own words and pictures | `article` · `rich-text` · `story` · `editorial` · `figure` · `gallery` · `video` · `code-block` · `callout` · `steps` · `process` · `roadmap` · `faq` |
| **Listing** — show many of something | `card-grid` · `team` · `products` · `jobs` · `schedule` · `menu` · `social-feed` · `changelog` · `downloads` · `bibliography` · `teaser` · `recommendations` |
| **Convincing** — evidence and proof | `features` · `testimonials` · `evidence` · `logo-cloud` · `integrations` · `spotlight` · `hotspots` · `pricing` · `stats` · `metrics` · `data-table` · `comparison` |
| **Acting** — get the visitor to do something | `cta` · `contact` · `newsletter` · `booking` · `wizard` · `quiz` · `calculator` · `estimate` · `search` · `auth` · `paywall` · `app` |
| **Navigating** — help them get around | `header` · `footer` · `pathways` · `toc` |
| **Embedding** — bring in something external | `map` · `embed` · `raw-code` · `widget` |
| **Organizing** — arrange other content on the page | `grid` · `tabs` · `accordion` |

Whatever you pick describes the **kind of section** — not the kind of site it suits. A genre like `marketing` or `docs` belongs to a template, not to a section type.

---

### Content

The `content` object says what your component expects from its section's markdown — which elements it reads, how many, and what an author should write there.

```javascript
content: {
  title: 'Headline [1]',                 // a label, with a count
  paragraphs: 'Description [1-2]',
  links: 'CTA buttons [0-2]',
  media: 'Photo, video or diagram [1]',  // an image, a video, or an embedded component

  // The object form: a hint, and for `items`, what each entry holds
  items: {
    label: 'Feature cards [3-6]',
    hint: 'Each ### heading starts a card',
    content: { title: 'Feature', paragraphs: 'Description', icon: 'Icon [1]' },
  },
}
```

A value is one of:

| Form | Example | Means |
| --- | --- | --- |
| A label | `'Headline'` | The element, named for the author |
| A label with a count | `'Cards [3-6]'` | …and how many — see [Count Syntax](#count-syntax) |
| `true` | `title: true` | The element, with no label of yours — the editor uses its own words |
| An object | `{ label, hint }` | A label and a longer hint. `media` also takes `types`, `sequence` takes `except` and `placement`, and `items` takes `content` |

**A component that takes no content says so with `content: {}`** (or `content: []`) — one that renders only what it receives in `content.data`, say. The visual editor then offers no text editor for its sections. Leaving `content` out says nothing about what the component reads, which is different.

**The label is for authors.** A visual editor shows it to the person writing the section, so write what they need to know — "Product photo", "Rich content" — not a variable name. It is shown as you wrote it, in every language, so write it in your authors' language.

#### Count Syntax

Append count in brackets at the end of the label:

| Syntax | Meaning |
| --- | --- |
| `'Label'` | Any count (default) |
| `'Label [1]'` | Exactly 1 |
| `'Label [1-3]'` | 1 to 3 |
| `'Label [2+]'` | 2 or more |

Nothing rejects content that ignores a count — a section renders with whatever arrived. But a count is not inert: it is what the visual editor reads to decide **how much starter content** to put in a new section, and what it shows an author as the section's expectations. Writing `[3-6]` on a card grid is therefore worth doing.

⭐ **The editor generates for the upper bound.** A component need not render everything it is handed, and a param that reduces (`layout: compact`) simply renders less — so surplus content costs nothing, while a shortfall leaves a visible hole. `[0-2]` produces two, and a floor of `0` still produces one: the floor is the author's option to delete, not an instruction to start empty.

#### Standard Content Elements

These names are a **fixed vocabulary** — each is something the semantic parser extracts from markdown, and the component reads it as `content.<name>`:

| Element | Source | Description |
| --- | --- | --- |
| `title` | H1 | Main headline |
| `pretitle` | A `#>` label line | Eyebrow/kicker text |
| `subtitle` | Heading after title | Secondary headline |
| `paragraphs` | Body text | Description paragraphs |
| `links` | `[text](url)` | Markdown links (become buttons/links) |
| `lists` | `- item` | Bullet or numbered lists |
| `items` | Subsequent headings | Content groups within the markdown — see [What each entry holds](#what-each-entry-holds) |
| `media` | `![…](…)` on its own line | An image, a video, or an embedded component — see [Media](#media) |
| `icons` | `![](lu-star)` | Icons an author picks |
| `documents` | `![…](file.pdf){role=pdf}` | Files, with an optional preview |
| `snippets` | ` ```js ` | Code samples |
| `tables` | A markdown table | Tables |
| `math` | `$$ … $$` | Displayed equations |
| `quotes` | `> …` | Block quotes |
| `sequence` | The whole section | Content rendered as written — see [Sequence](#sequence--content-rendered-as-written) |

Use these exact names. The meta.js describes which of these your component uses — you're not inventing new names, you're declaring which parsed elements you consume. Child sections from separate files are not content elements: they arrive as `block.childBlocks`, and a section type that accepts them declares [`children`](#children-composition). A tagged block the author writes is declared in [`data`](#data) — a [concept block](#concept-blocks) as `'md:<tag>'`.

#### Media

`media` is the section's visual media: **an image, a video, or an embedded component**, each on its own line. Declared without `types` it takes any of the three, and narrowing is the extra work:

```javascript
content: {
  media: 'Hero media [1]',                                    // any of the three
  // media: { label: 'Product photo [1]', types: ['image'] },  // images only
}
```

`types` takes `'image'`, `'video'` and `'inset'` (an embedded component). The familiar spellings are the same declaration, narrowed to one type:

| Spelling | Is |
| --- | --- |
| `image`, `images`, `thumbnail` | `media` with `types: ['image']` |
| `videos` | `media` with `types: ['video']` |
| `insets` | `media` with `types: ['inset']` |

A component reads the slot as **`content.media`** — its items in the order the author wrote them, each with its `kind` (`'image'`, `'video'` or `'inset'`) — or by kind, as `content.images`, `content.videos` and `content.insets`. Kit's `<Visual>` renders an item by its kind; handed the whole list it renders the first, because a slot of one is the first thing the author placed in it:

```jsx
<Visual media={content.media} block={block} className="rounded-lg" />
```

⚠️ **The one mistake the name invites:** rendering the slot with an image or video component and dropping a component the author placed there. Kit's `<Media>` is a video player — for the slot, use `<Visual>`.

- **An icon is not media** — it is a glyph an author picks. Declare `icons` (or `icon`).
- **A document is not media** — declare `documents`. A `{role=pdf}` file arrives in `content.documents`, never among the images.
- **An image inside a sentence belongs to the text.** It arrives in `content.images` and in `content.sequence`, but not in `content.media`.
- **Declare a thing once.** `media` beside `image` would count one image toward both; the build says so.

> **`visuals` is retired.** The slot is the `media` element: `visuals: 1` is `media: '… [0-1]'`, and `visuals: { types: ['image'], count: 'many' }` is `media: { label: '…', types: ['image'] }`. The build refuses a top-level `visuals` and names the equivalent.

#### Sequence — content rendered as written

A component that renders its content in the order the author wrote it — prose with images, tables, equations and embedded components in between — declares `sequence`:

```javascript
content: {
  title: 'Headline [1]',
  sequence: 'Prose and media',
  // leaving out kinds it does not render:
  // sequence: { label: 'Rich content', except: ['table', 'math'] },
}
```

- It covers **the whole section, in order — the headline included**. The component renders `content.sequence`, as kit's `<Render>`, `<Prose>` and `<Article>` do. `title` beside it says the section should begin with one; it does not mean the title renders apart from the rest.
- **`except` lists the kinds of content it does not render** — `heading`, `prose`, `list`, `link`, `image`, `video`, `inset`, `icon`, `document`, `code`, `table`, `math`, `quote`. Every other kind reaches the page, including one the framework adds later.
- A count on `sequence` means nothing, so it takes none.

**`placement` says where an author may place a block** — an image, a video or a table — in the flow your component renders ([Placement](./content-structure.md#placement--where-a-block-sits)):

```javascript
content: {
  sequence: { label: 'The article', placement: true },
}
```

| Written | Means |
| --- | --- |
| `placement: true` | `left`, `right`, `wide` and `full`, on every kind that takes one |
| `placement: ['wide', 'full']` | those, on every kind |
| `placement: { image: ['left', 'right'], table: ['wide'] }` | those, per kind — `image`, `video`, `table` |
| left out | none |

A visual editor offers an author only what you declare, so declare what your component lays out: kit's `<Render>`, `<Prose>` and `<Article>` honour all four, so a component built on them declares `true`. A kind `except` leaves out takes none. The build writes the declaration as a map from each kind to its values, and says when a section's file asks for a placement its type does not offer. It is for authoring only, and never reaches a published page, which renders whatever placement its content holds. A record's body is not covered: a record is shown by whatever section a site gives it.

A `content:` declared without `sequence` says only what it names, and a component with no `content:` says nothing about its content at all.

#### What each entry holds

`items` takes `content:` — the same map a section's is, for one entry:

```javascript
content: {
  title: 'Headline',
  items: {
    label: 'Team members [2+]',
    content: { title: 'Name', subtitle: 'Role', image: 'Portrait [1]', paragraphs: 'Bio' },
  },
}
```

Each entry is built the way a section is, less the grouping, so an entry's `content:` holds neither `items` nor a `sequence`. Each entry in `content.items` has its own `media`, `images` and the rest.

#### What the build does with it

The build checks the declaration and **lowers** it into one form in the foundation's schema — what a visual editor reads: every media spelling becomes `media` with its `types`, a count becomes a minimum and a maximum, and a label appears only where you wrote one. Anything it cannot read — an unknown name, a count on `sequence`, two declarations of one thing — is a build warning, and the rest is still used. `@uniweb/schemas/content`'s `describeContent` reads a `meta.js` the same way.

#### Background Media

Background images and videos are handled at the engine level so components don't repeat this logic. Use the top-level `background` field:

```javascript
export default {
  title: 'Hero',
  background: true,  // Engine renders background media

  content: {
    title: 'Headline',
    image: 'Hero image [1]',
  },
}
```

| Value | Behavior |
| --- | --- |
| `'self'` | The component draws its own background; the runtime draws none behind the section. To draw the author's background itself, the component renders kit's `<SectionBackground block={block} />` |
| anything else, or left out | The runtime draws the author's background behind the component |

`true` and `false` change nothing at render; an editor may read them to decide whether to offer a background setting. ⛔ This table listed `'manual'` for a component that draws its own background until 2026-09-28 — the runtime has only ever read `'self'`.

**Frontmatter Background Options**

When a component has `background: true`, authors can configure backgrounds in section frontmatter:

```yaml
---
type: Hero
background:
  # Image background
  image: /images/hero.jpg
  position: center          # CSS background-position
  size: cover               # CSS background-size

  # Video background (falls back to image on mobile or reduced-motion)
  video: /videos/hero.mp4   # Auto-detects webm fallback if available
  poster: /images/hero.jpg  # Shown while video loads

  # Or provide explicit video sources
  sources:
    - src: /videos/hero.webm
      type: video/webm
    - src: /videos/hero.mp4
      type: video/mp4

  # Overlay on top of background
  overlay:
    enabled: true
    type: dark              # 'dark' or 'light'
    opacity: 0.5
    # Or gradient overlay
    gradient:
      start: 'rgba(0,0,0,0.7)'
      end: 'rgba(0,0,0,0)'
      angle: 180
---
```

**Accessibility**: Video backgrounds automatically respect `prefers-reduced-motion`. When users have reduced motion enabled, the poster image is shown instead of video.

**Format Fallback**: When you provide a `.mp4` video, the engine automatically tries a `.webm` version first (better compression). Just ensure both formats exist at the same path.

---

### Data

The `data` field declares **the `content.data` keys your component receives**, each with its schema. It is the single declaration surface for a component's structured data — there is no separate `schemas:` key.

A section's `content.data` holds the keys its component declares, and nothing else (plus any its foundation declares in `main.js`). A component that reads `content.data.events` declares `events`. Each key is filled from the fetches and tagged blocks that reach the section — by name, or, for a fetch under another name, by the query's schema — and a key nothing fills is `null`. The schema tells the editor what shape to expect and `uniweb validate` what to check; it changes no record — a field a record lacks reaches the component absent, and what it renders as is the component's choice. See [Data Fetching → What a section receives](./data-fetching.md#what-a-section-receives).

```javascript
data: {
  events: '@std/event',                            // named ref (shared standard)
  specs:  { cpu: { type: 'string' } },                // inline field map
  signup: { fields: [{ id: 'email', type: 'text' }] },// inline rich-form (editor form)
}
```

Each entry's **key** is the `content.data` key. Its **value** is one of three forms (below), or `{}` for a key whose records have no schema — an external API's. A key written `'md:<tag>'` declares a [concept block](#concept-blocks) the author writes, read as `content.data.<tag>`. The site, author, or editor decides *how* each key is filled — a query's records, a tagged code block, or an editor form — and the schema is the same regardless of source.

A component with no `data` field, or `data: false`, receives no keys of its own.

> **Changed:** delivery used to be default-on, and `data:` a hint for the editor and defaults. It is now what the section receives. The frontmatter form `fetch: { query: articles, current: exclude, limit: 3 }` in a `.md` file is a different mechanism — what a section fetches, and on a parametric page how it uses the page's record. See [Data Fetching](./data-fetching.md) for details.

#### The three value forms

**1. Named ref** — points at a schema module, resolved at build time. Refs use Uniweb namespacing:

```javascript
data: {
  members: '@/member',       // '@/' = this foundation's own foundation/schemas/member.{js,json,yml}
  authors: '@std/person',    // '@std/' = a shared standard schema
}
```

`@/name` is the **self** namespace — a schema this foundation owns, in `foundation/schemas/`. The empty scope means "this foundation," so a foundation never writes its own org name in source; `@/`-refs are portable. `@std/name` names the **shared standards** namespace (shipped in the `@uniweb/schemas` package); `@org/name` resolves from that org's `@org/schemas` package, letting a team share schemas across foundations. A ref names a *namespace*, not a package path.

A foundation-local schema file (`.js`, `.json`, `.yml`, or `.yaml`) holds the canonical shape:

```yaml
# foundation/schemas/member.yml   — referenced as '@/member'
name: member
version: 1.0.0
description: A team member
fields:
  name:   { type: string, required: true }
  role:   { type: string, default: '' }
  bio:    { type: string, default: '' }
  avatar: { type: string, default: '' }
```

The `name`/`version` are the schema's identity (recorded in the foundation's published metadata) — they are **not** `content.data` keys.

**2. Inline field map** — a flat keyed object, written directly in `meta.js`. Good for one-off foundation-local shapes:

```javascript
data: {
  pricing: {
    name:        { type: 'string', required: true },
    price:       { type: 'number', required: true },
    period:      { type: 'string', default: 'month' },
    features:    { type: 'array', items: { type: 'string' } },
    highlighted: { type: 'boolean', default: false },
  },
}
```

**Nested shapes.** An `array` declares its element type with `items`; an `object` declares its known keys with `fields`:

```javascript
data: {
  page: {
    seo:   { type: 'object', fields: { title: { type: 'string' } } },
    tags:  { type: 'array',  items:  { type: 'string' } },
  },
}
```

When the keys are the author's rather than yours — a map of arbitrary names — declare `values` instead of `fields`. It is to an object what `items` is to an array:

```javascript
// A map keyed by whatever the author calls each entry; every value the same shape.
rates: {
  type: 'object',
  values: { type: 'object', fields: { label: { type: 'string' }, amount: { type: 'number' } } },
}
```

Validation then checks every entry and names the offending key (`rates.overnight.amount`). Keys the value shape doesn't declare are left alone rather than rejected, so data can carry more than you described without failing a build. Declare `fields` or `values`, not both.

**3. Inline rich-form** — an editor form, distinguished by a `fields` **array**. See [Structured Author Data](#structured-author-data) below for the full rich-form reference.

#### Standard schema refs

The shared `@uniweb/schemas` package provides a common vocabulary any foundation can reference:

| Ref | Description |
| --- | --- |
| `@std/person` | Team members, authors, people |
| `@std/article` | Blog posts, news items |
| `@std/event` | Calendar events |
| `@std/project` | Portfolio/case studies |
| `@std/publication` | Academic papers, research |
| `@std/opportunity` | Jobs, grants, calls |
| `@std/service` | Services a business offers |
| `@std/product` | Things a business sells |
| `@std/testimonial` | What a customer, client or patient said |
| `@std/position` | A role someone held: a job, an appointment or a volunteer post |
| `@std/qualification` | A degree, diploma or certificate someone earned |

Add `@uniweb/schemas` as a dependency when a `@std/<name>` ref is used. Foundations that only use `@/`-refs or inline schemas don't need it.

#### Routing a scope with `schemas.config.js`

By default an `@org/name` ref resolves to a *package* — the `@org/schemas` package in the foundation's `node_modules`. A foundation can instead **route a scope to a directory**: a plain folder of schema files anywhere on disk, with no package and no install. Add an optional `schemas.config.js` at the foundation root:

```js
// foundation/schemas.config.js
export default {
  '@acme':  '../shared/acme-schemas',   // relative to the foundation
  '@brand': process.env.BRAND_SCHEMAS,   // absolute / machine-specific, via env
}
```

`@acme/person` then resolves to `../shared/acme-schemas/person.{js,json,yml,yaml}` — a bare schema file, not a package. The config is plain JS (like `main.js` and `vite.config.js`), so a path can be relative to the foundation, absolute, read from an environment variable, or computed — whatever the layout needs.

This is how a team reuses one set of schemas across many foundations, or across many workspaces, without publishing a package: keep the schemas in one folder and point each foundation's `schemas.config.js` at it.

A key can also name a **single schema** and point it at an exact file, overriding just that one while the rest of the scope stays routed to the shared folder:

```js
export default {
  '@acme':        '../shared/acme-schemas',      // the whole scope → a folder
  '@acme/person': './schemas/acme-person.yml',   // …but this one → a specific file
}
```

- Precedence is most-specific first: a per-schema **file** beats a scope **directory**, which beats the `@org/schemas` **package**.
- A routed scope does **not** fall back to the package for a *missing* schema — it errors, rather than silently load a different definition. Fill a gap with a per-schema key, not a fallback.
- A scope value is a directory (the schema name is appended); a per-schema value is a file (the extension is optional).
- `@/` (self) and `@uniweb` (reserved) are never routable.
- A key whose value is empty — for example, an unset environment variable — is skipped, and it falls back to the next source.
- Foundations without a `schemas.config.js` resolve exactly as before.

#### Example: Event Listing

```javascript
export default {
  title: 'Event Grid',

  // Renders event-shaped data; its shape comes from the standard event schema.
  data: { events: '@std/event' },

  content: {
    title: 'Section title',
    paragraphs: 'Intro text [1]',
  },

  params: {
    layout: {
      type: 'select',
      options: ['grid', 'list', 'calendar'],
      default: 'grid',
    },
  },
}
```

The component reads the events from `content.data.events` and renders them alongside markdown content.

#### One record, or a list

A `data:` entry describes the shape of *each record*. How many records the key holds is the entry's to say too:

```javascript
data: {
  events: '@std/event',                             // a list of records
  event:  { schema: '@std/event', single: true },   // one record, or null
}
```

- **A list, by default.** A section that shows a query's records receives every record it takes: `content.data.events`. When the query holds none, the key is `[]` — an answer with no records.
- **One record, with `single: true`.** The key holds the first record its binding selects, or `null` when there is none. On a [parametric page](./dynamic-routes.md) that is the record the URL names: `content.data.event`.

The runtime asks for one record for a `single` key, rather than the whole list. When the binding selects several, the key holds the first, so sort the query or give the binding `limit: 1` to choose which. When the URL of a parametric page names no record, the page is marked not found, and its key is `null` (`[]` for a list key).

A key that holds a tagged data block's value holds what was written, and `uniweb validate` checks the block against the key: a list of records under a list key, one record under a `single` key. The shape the key declares also decides what of a translated site's block is translated: its text fields, never an enum, a URL or a `translatable: false` field ([Internationalization](../development/internationalization.md)). A key declared `{}` leaves that to a heuristic. A key nothing fills is `null`. Beyond what a key declares, the runtime does not reshape what it holds; reshape `content.data` once with a foundation `handlers.data` hook. See [Data Fetching → What a section receives](./data-fetching.md#what-a-section-receives).

#### Briefs or whole records

A query's record reaches a component in one of two shapes, and the key says which the component expects ([Entity Content → What a component receives](./entity-content.md#what-a-component-receives)):

```javascript
data: {
  articles: '@std/article',                                         // briefs: the brief's fields at the top — article.title
  archive:  { schema: '@std/article', whole: true },                // whole records, as stored — article.brief.title, article.body.content
  article:  { schema: '@std/article', single: true, whole: true },  // the one article, whole
}
```

- **The default** — each record is its **brief**: the fields of the schema's brief section at the top, the section not named. What a list, a card or a row needs.
- **With `whole: true`** each record arrives **whole**: every section under its own name, the brief's included. What a page that shows one record in full needs — an article's body is not in its brief.

`single` and `whole` are independent, and the long form takes those two and `schema`, nothing else: the build stops on any other property, naming the key. A ref written alone, `'@std/article'`, is the same as `{ schema: '@std/article' }`.

The runtime asks for what the key declares — on a [parametric page](./dynamic-routes.md) as much as anywhere — so a component never receives the other shape. A key may also name **`@uniweb/link`** or **`@uniweb/file`**, the schemas of a site's [link records](./content-collections.md#link-records) and [file records](./content-collections.md#file-records): a link arrives as `{ url, $name, $label, $tags }`, a file as `{ file: { url, name, mime, size }, $name, $label, $tags }`. A key declared `whole: true` whose source cannot answer a whole record is `null`. A schema of one section (the `fields:` form) has a brief that is the whole record, so its component can leave `whole` off; records of an external query have no schema, and arrive as their source answers them. Either shape arrives as the records hold it: no field is filled with a default.

A schema whose root is a list, such as `@std/nav` or `@std/form`, describes the key's whole value: its key holds that list, and takes neither flag.

#### Loading states

When a data fetch runs at runtime, the component renders immediately — static content (title, paragraphs, items) is available on first render, and the declared key is `null` until the fetch completes.

The component checks `block.dataLoading` to know whether a fetch is still in progress:

```jsx
function ArticleList({ content, block }) {
  if (block.dataLoading) {
    return <DataPlaceholder lines={4} />
  }

  const articles = content.data.articles || []
  return <ArticleGrid articles={articles} />
}
```

This prevents layout shifts — instead of the page collapsing while data loads, the component holds its space with a placeholder. See [Kit Reference](./kit-reference.md) for `block.dataLoading` and `DataPlaceholder`.

---

### Params

Parameters are configurable options set in frontmatter:

```yaml
---
type: Hero
variant: glass
layout: split-right
---
```

Define them in `params`:

```javascript
params: {
  variant: {
    type: 'select',
    label: 'Variant',
    hint: 'The hero’s visual style',  // optional guidance
    options: ['gradient', 'glass', 'minimal'],
    default: 'gradient',
  },
  showPattern: {
    type: 'boolean',
    label: 'Show background pattern',
    default: true,
  },
  maxItems: {
    type: 'number',
    label: 'Maximum items to display',
    default: 6,
  },
  customClass: {
    type: 'string',
    label: 'Custom CSS class',
    hint: 'Added to the section wrapper element',
  },
}
```

**Runtime guarantees**: Param defaults from meta.js are automatically applied by the runtime. Your component receives `params` with defaults already merged in—no need for `variant || 'gradient'` fallbacks.

**Names a param can't take.** Every key in a section's frontmatter is a param except these, which every section can be given, whatever its type. None reaches your component as a param, so don't declare one in `params`:

| in a section's frontmatter | what it is | a component reads it from |
| --- | --- | --- |
| `type`, `id`, `hidden` | the section itself — its section type, its id, and a draft (`hidden: true`) | — |
| `query`, `fetch` | the section's data | `content.data`, under the keys `data:` declares |
| `theme`, `background`, `grid`, `vars`, `sticky` | the section's own settings, which framework applies — the color context and the background around your component, the section's theme and your component's CSS variables in the page stylesheet, the child layout, and whether the section stays in view as the page scrolls | `block`, when it needs one for its own logic: `useColorContext(block)` for the context (a light or dark logo, say), `<SectionBackground block={block} />` to draw the background itself, `<ChildGrid from={block} />` for the grid, `block.sticky` |
| `props` | params written as one map | `params` — its keys are merged in |
| `data`, `preset`, `input` | nothing: `data:` is refused (it was `query:`'s old name); `preset:` and `input:` are ignored with a warning | — |

The build warns on a param declared as any of them. A preset may still set `theme` or `background`: a preset's params become the section's frontmatter when an author starts a section from it.

#### Param Types

| Type | Editor UI | Value |
| --- | --- | --- |
| `select` | Dropdown | String from options |
| `boolean` | Toggle | true/false |
| `string` | Text input | Any string |
| `number` | Number input | Numeric value |

#### Options Shorthand

When option value equals label, use strings:

```javascript
// Full form
options: [
  { value: 'dark', label: 'Dark' },
  { value: 'light', label: 'Light' },
]

// Shorthand (value === label)
options: ['dark', 'light']

// Mixed
options: [
  'dark',
  'light',
  { value: 'glass', label: 'Glassmorphism' },
]
```

---

### Vars

Component-level CSS custom properties, scoped to `#section-{id}`. These are distinct from params — vars become CSS variables that your component's styles can reference, while params are JS values passed as props.

```javascript
vars: {
  'card-gap': {
    default: '1.5rem',
    label: 'Card Gap',
    type: 'select',
    options: ['1rem', '1.5rem', '2rem'],
    group: 'Layout',
  },
  'card-radius': {
    default: 'var(--radius-md)',
    description: 'Inherits from foundation var by default',
  },
}
```

The var schema is the same as [foundation vars](./foundation-config.md#variable-schema) — `default`, `label`, `type`, `options`, `group`, `description` all work the same way.

#### CSS output

Component vars emit on the section's CSS selector:

```css
#section-42 {
  --card-gap: 1.5rem;
  --card-radius: var(--radius-md);
}
```

Use them in your component's CSS or Tailwind classes:

```jsx
function PricingTable({ content }) {
  return (
    <div className="grid gap-[var(--card-gap)]">
      {content.items.map((item, i) => (
        <div key={i} className="rounded-[var(--card-radius)]">
          {/* ... */}
        </div>
      ))}
    </div>
  )
}
```

#### Frontmatter overrides

Content authors override component vars in section frontmatter:

```yaml
---
type: PricingTable
vars:
  card-gap: 2rem
---
```

Only vars declared in `meta.js` are emitted — unknown var names in frontmatter are ignored.

#### Foundation vars vs component vars

|  | Foundation vars | Component vars |
| --- | --- | --- |
| Declared in | `main.js` | `meta.js` |
| CSS scope | `:root` (global) | `#section-{id}` (scoped) |
| Overridden by | `theme.yml` (`vars:`) | Section frontmatter (`vars:`) |
| Context-aware types | Yes (`color`, `gradient`) | No (always context-independent) |

---

### Sticky

Whether a section of this type stays in view as the page scrolls, unless its author says otherwise:

```javascript
// sections/Header/meta.js
export default {
  title: 'Header',
  sticky: true,
}
```

An author turns it off with `sticky: false` in the section's frontmatter (or on with `sticky: true`, for a type that declares nothing) — it is one of the section's own settings, so your component never receives it as a param.

The runtime makes sticky the box that can stay in view:

- **a section in the header** (or another layout area) — the area's box, stacked above the areas that scroll under it;
- **a section in the body** — its own wrapper, which stays in view below whatever is stuck at the top: a sub-navigation sits under a sticky header with nothing to declare;
- **a child section** that its parent renders as a section (kit's `<ChildGrid>`, or `<ChildBlocks wrapAs="div">`) — its own wrapper, which stays in view within the box its parent places it in. In `<ChildGrid>` that is its row: a side panel stays beside a long column and leaves with it, and on a phone, where the grid is one column, it scrolls with the page. A child rendered bare has no box of the runtime's; the component reads `block.sticky` and draws it itself.

How far the stuck chrome reaches is measured in the browser and published as `--uw-sticky-top`. On a page whose header sticks, a link to a section (`#section-…`, as a search result or a contents entry writes it) lands below it, since each section in the body carries that much scroll margin. And a sticky section uses it unless its type sets a `top` of its own — one CSS length, say for a gap below the header:

```javascript
export default {
  sticky: { default: true, top: 'calc(var(--uw-sticky-top, 0px) + 1rem)' },
}
```

A `sticky top-0` on your component's own markup cannot stick: it sits in boxes as tall as itself, so it scrolls away with the page. Nor can a sticky section inside a box that clips with `overflow: hidden` — use `overflow: clip`, which clips the same and keeps it sticking. `uniweb dev` names both in the console. See [Component Patterns](../development/component-patterns.md#header-positioning-sticky-vs-fixed).

---

### Presets

Presets are pre-configured parameter combinations, which an editor offers by `label` — picking one sets those params on the section:

```javascript
presets: {
  default: {
    label: 'Centered Hero',
    params: { variant: 'gradient', layout: 'center' },
  },
  glass: {
    label: 'Glassmorphism',
    params: { variant: 'glass', layout: 'center' },
  },
  minimal: {
    label: 'Minimal Dark',
    params: { variant: 'minimal', layout: 'left', showPattern: false, theme: 'dark' },
  },
}
```

In a file, write the params themselves. `uniweb add section Hero --starter --preset glass` prints starter content with that preset's params as its frontmatter:

```yaml
---
type: Hero
variant: glass
layout: center
---
```

A preset may set one of the section's own settings too — `minimal` above pins the dark context with `theme: dark`.

A section keeps its params, not the name of the preset they came from: a `preset:` key in its frontmatter has no effect, and the build warns and ignores it.

---

### Structured Author Data

The same `data:` keys that receive a query's records (above) also declare the shapes of structured data a component accepts directly from authors. There is no separate `schemas:` key — a `data:` entry is the one declaration surface, and a tagged block under a key the component does not declare is left out of `content.data`. Two input paths converge on the same `content.data[key]`:

1. **Tagged markdown blocks** — authors write YAML/JSON in a fenced block prefixed with the key. Great for filesystem-edited projects.
2. **Form UI** — the Uniweb editor renders a form from the schema. Great for non-technical authors editing a published site.

Both produce the same shape at `content.data[key]`, so components are agnostic to how the data was entered. The value of a `data:` entry is either an **inline field map** (a keyed object, below) or an **inline rich-form** (a `fields` array — the editor form). It can also be a named ref (`'@/…'`, `'@std/…'`) when the shape is shared/versioned.

#### Concept blocks

A concept block is prose an author writes under a tag — a ` ```md:faq ` fence, or a GitHub alert (`> [!WARNING]`) — and it arrives at `content.data.<tag>` as `{ items, sequence }` ([Concept Blocks](./content-structure.md#concept-blocks)). Declare one by writing its key as the fence is written:

```javascript
data: {
  'md:faq': 'Questions and answers [3+]',
}
```

The component still reads `content.data.faq`. The value is the author's label — its count counts the entries — or an object that also says what each entry holds:

```javascript
data: {
  'md:faq': {
    label: 'Questions and answers [3+]',
    hint: 'One heading per question',
    content: { title: 'Question', paragraphs: 'Answer' },
  },
}
```

`content:` takes the vocabulary a section's does, for one entry — it holds neither `items` nor a `sequence`. A concept block has no schema, since its shape comes from the fence, so the value is never a schema ref. `faq: {}` still declares the key, with nothing said about the block.

#### Inline field map — flat key/value maps

Use a field map for flat row-like data (e.g. nav links). Fields are declared as a keyed object directly on the `data:` entry:

````markdown
```yaml:nav-links
- label: Home
  href: /
- label: About
  href: /about
  type: button
```
````

```javascript
// meta.js
data: {
  'nav-links': {
    label: { type: 'string' },
    href: { type: 'string' },
    type: { type: 'string', enum: ['plain', 'button', 'dropdown'], default: 'plain' },
    icon: 'string',  // shorthand for { type: 'string' }
    children: {      // one level of sub-links
      type: 'array',
      items: { type: 'object', fields: { label: 'string', href: 'string' } },
    },
  },
}
```

```jsx
function Header({ content }) {
  const navLinks = content.data['nav-links'] || []
}
```

#### Rich-form schemas — ordered fields with labels, options, conditions

Use a rich-form schema when the data has an ordered field list (labels, required markers, conditional visibility, nested arrays). Rich-form schemas drive both tagged-block authoring *and* the FormBlock editor widget. The distinguishing marker is a `fields` **array** (instead of a keyed object), or any of `isComposite` / `childSchema`.

```javascript
// meta.js
data: {
  stats: {
    name: { en: 'Stats', fr: 'Statistiques' },     // shown in editor
    isComposite: true,                              // value is an array
    childSchema: {
      name: { en: 'Stat', fr: 'Statistique' },
      fields: [
        {
          id: 'number',
          type: 'text',
          label: { en: 'Number', fr: 'Nombre' },
          required: true,
        },
        {
          id: 'text',
          type: 'text',
          label: 'Label',
          default: 'metric',
        },
      ],
    },
  },

  'side-content': {
    name: 'Side Content',
    fields: [
      {
        id: 'for',
        type: 'select',
        options: [
          { label: 'Scholar', value: 'scholar' },
          { label: 'News',    value: 'news' },
        ],
      },
      {
        id: 'department',
        type: 'text',
        condition: { for: 'scholar' },              // only when for=scholar
      },
      {
        id: 'headline',
        type: 'text',
        condition: { for: { $in: ['news', 'feature'] } },
      },
    ],
  },
}
```

Components read the filled data by key:

```jsx
function Hero({ content }) {
  const stats = content.data.stats || []
  // [{ number: '42', text: 'users' }, { number: '1M', text: 'queries' }, ...]
}
```

#### Rich schema — field properties

| Property | Type | Notes |
| --- | --- | --- |
| `id` | string (required) | field identifier (data key) |
| `type` | string (required) | see field types below |
| `label` | string or `{en,fr}` | shown in editor |
| `description` | string or `{en,fr}` | editor tooltip |
| `placeholder` | string or `{en,fr}` | input placeholder |
| `required` | boolean | editor validation |
| `default` | any | what the editor's form starts the field with — never applied at runtime |
| `options` | `[{label, value}]` or `[string]` | for `select` |
| `min` / `max` | number | for `number` |
| `condition` | object | visibility predicate — see below |
| `fields` | array | for `type: nestedObject` (inline nested fields) |
| `childSchema` | object | for `type: form` (nested composite array) |

#### Rich schema — field types

- `text`, `email`, `textarea`, `number`, `checkbox`, `select` — primitives
- `form` — nested composite array; requires `childSchema: { fields: [...] }`
- `nestedObject` — single nested object; requires `fields: [...]`

#### Rich schema — composite (`isComposite: true`)

The stored value is an **array** of rows; each row matches `childSchema.fields`. Add/remove rows in the editor, or author the array directly in a tagged markdown block.

#### Rich schema — conditions (editor-side)

Any field (root or nested) can declare a `condition` that controls **visibility in the editor form**:

```javascript
{ id: 'department', type: 'text', condition: { for: 'scholar' } }
{ id: 'label',      type: 'text', condition: { for: { $in: ['a','b'] } } }
```

Supported operators: `$eq`, `$neq`, `$in`, `$nin`, `$truthy`, `$falsy`. Shorthand `{ key: value }` is implicit `$eq`. Multiple keys are AND'd.

Conditions are a UX concern: the editor hides fields whose condition doesn't hold so the author sees a relevant form. **At runtime, all stored fields reach the component as-is** — there is no condition-based stripping. Components that render differently based on a controller field should branch on that field directly (e.g. `data.for === 'news'`) and ignore what isn't relevant.

#### Localized labels

Anywhere a `label`, `name`, or `description` is accepted (on schemas, child schemas, fields, and option entries), you may pass either a plain string or an `{en, fr, ...}` object. The editor renders the user's active locale.

#### Inline field-map field types (keyed-object form)

An inline field map uses the **same shape as a named data schema** (the `@/`-ref form): nested objects nest via `fields:`, lists via `items:`, and a closed value set is `enum:`. A one-off map and a shared `@/`-ref schema are interchangeable — anything you write inline you can move into a `foundation/schemas/*.yml` file and reference by name, except `default:`. A `default:` is what an editor's form starts the field with, so it belongs only inline; a named schema declares none (the build refuses one), and the runtime applies neither — a component receives the data as written. It must be a valid data schema — an `object` field declares `fields` or `values`, and an opaque value is `json` (`richtext` for ProseMirror content) — or `uniweb register` refuses the foundation, naming the key; the build warns of it first.

```javascript
// Full form
field: { type: 'string', default: 'value' }

// Shorthand
field: 'string'
field: 'number'
field: 'boolean'

// Closed value set (an inline list of allowed values)
field: { type: 'string', enum: ['a', 'b', 'c'], default: 'a' }

// Nested object — nests via `fields:`
field: { type: 'object', fields: { name: 'string', value: 'number' } }

// Array — element type via `items:`
field: { type: 'array', items: { type: 'string' } }
field: { type: 'array', items: { type: 'object', fields: { name: 'string' } } }
```

---

## Insets

The `inset` flag declares that a component is available for inline `@ComponentName` references in markdown. Content authors can place it within another section's content:

```markdown
![Architecture diagram](@NetworkDiagram){variant=compact}
```

```javascript
// sections/insets/NetworkDiagram/meta.js
export default {
  title: 'Network Diagram',
  inset: true,
  params: {
    variant: {
      type: 'select',
      options: ['full', 'compact'],
      default: 'full',
    },
  },
}
```

Where an inset is offered is a concern of the section it is placed in — a [`media`](#media) slot that takes embedded components, or a [`sequence`](#sequence--content-rendered-as-written) that does not leave them out — not a property of the inset itself. (`children` is about child sections, not insets.) Don't use `hidden` on insets — `hidden` means "exclude from export entirely" (for internal helpers or work-in-progress components), so a hidden component is not a section type and `@ComponentName` cannot resolve to it.

A component with `inset: true` is meant to be placed as an inset: a visual editor offers it among insets and leaves it out of the list of sections. A component meant for both adds `section: true`:

```javascript
// sections/Testimonial/meta.js
export default {
  inset: true,               // offered among insets in an editor
  section: true,             // and still offered as a section
}
```

This guides editors; it does not restrict markdown. The runtime renders any section type in either position.

Inset components receive `content.title` (from the `[description]` text) and `params` (from `{key=value}` attributes). At runtime, the parent section accesses insets via `block.insets` (separate from `block.childBlocks`).

---

## Children (Composition)

The `children` field declares that a section type accepts file-based child sections. This is editor metadata — at runtime, `block.childBlocks` is always available regardless of whether `children` is declared.

```javascript
// sections/Grid/meta.js
export default {
  title: 'Grid',
  description: 'Arrange components in a responsive layout',
  children: {
    label: 'Grid items',
    hint: 'Each child section becomes a grid cell. Use any component type.',
    grid: [3, 2, 4, '40/60', '60/40'],
  },
}
```

The object form takes `label`, `hint` (each a string, or an `{ en, fr, … }` map), `min`, `max`, `types` (the section types allowed as children), and `grid`. Shorthands: `children: true` (any number, any type) or `children: 3` (up to three).

### Grid layouts

`grid` lists the layouts a component offers for its children. Each is a column count (`3` — equal columns) or relative widths (`'40/60'` — two columns, the part count being the column count). **The first is the layout a section gets when it chooses none.**

A section chooses one with the `grid` key in its frontmatter — a section key like `type`, never a param:

```markdown
---
type: Grid
grid: '40/60'
---
```

Lay the children out with kit's `ChildGrid`, which reads that choice from `block.grid` and uses your default when there is none:

```jsx
import { ChildGrid } from '@uniweb/kit'

export default function Grid({ block }) {
  return <ChildGrid from={block} fallback={3} />
}
```

`ChildGrid` shows one column on narrow screens, a layout of up to two columns from the `md` breakpoint (a wider one as two equal columns), and the chosen layout from `lg`. `headerRow` makes the first child span every column; `className` replaces the default `gap-8`. Each child renders as a section, so its own `theme:` and `background:` apply. Don't declare a param named `grid` — the section key takes the name, so the param would never receive it.

In markdown, child sections use the `@` prefix and `nest:` in page.yml:

```
pages/home/
├── page.yml
├── 1-intro.md          # type: Grid (parent)
├── 2-features.md       # type: Features
├── @text.md            # type: TextBox (child of intro)
└── @media.md           # type: Media (child of intro)
```

```yaml
# page.yml
nest:
  intro: [text, media]
```

Or using inline nesting in `sections:`:

```yaml
sections:
  - intro:              # Grid
      - text            # Resolves to @text.md
      - media           # Resolves to @media.md
  - features            # Features
```

---

## Cross-Block Communication

Section types sometimes need to coordinate with their neighbors. 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** in its `meta.js`. The section that **needs to adapt reads it** via `getNextBlockInfo()`, `getPrevBlockInfo()`, or `getFirstBodyBlockInfo()`.

### context (static)

Declares permanent facts about the component type. Never changes at runtime.

```javascript
// Hero/meta.js — "I always support a translucent header over me"
export default {
  context: {
    allowTranslucentTop: true,
  },
}
```

```jsx
// Header/index.jsx — adapts based on what's below
const nextBlockInfo = block.getNextBlockInfo()
// nextBlockInfo.context  → static (meta.js)
const isFloating = nextBlockInfo?.context?.allowTranslucentTop || false
```

Use `context` when the capability is inherent to the component type — every instance of Hero always supports translucent headers, regardless of its content or state.

### initialState (dynamic)

Declares an initial value that the component can change at runtime via `useBlockState()`. Neighbors see changes immediately.

```javascript
// Hero/meta.js — starts translucent-ready, but component logic may disable it
export default {
  initialState: {
    allowTranslucentTop: true,
  },
}
```

```jsx
// Hero/index.jsx — updates state based on runtime conditions
function Hero({ content, block }) {
  const [state, setState] = block.useBlockState(useState)
  // state.allowTranslucentTop is true initially (from meta.js)
  // Component logic can change it: setState({ allowTranslucentTop: false })
}
```

```jsx
// Header/index.jsx — reads dynamic state, falls back to static context
const nextBlockInfo = block.getNextBlockInfo()
// nextBlockInfo.state    → dynamic (useBlockState)
const isFloating = nextBlockInfo?.state?.allowTranslucentTop
  ?? nextBlockInfo?.context?.allowTranslucentTop
  ?? false
```

Use `initialState` when the capability depends on runtime conditions — a Hero might disable the translucent overlay after a user interaction or based on loaded data.

### Key names are yours to design

`allowTranslucentTop`, `expanded`, `playing` — these are not framework fields. Design whatever protocol your foundation's sections need to coordinate.

---

## Design Principles

1. **Graceful degradation** — Components handle missing content without errors
2. **Sensible defaults** — Every param should have a good default
3. **Intent over implementation** — Params describe purpose (`variant: featured`) not CSS (`color: #1a1a1a`)
4. **Minimal metadata** — Only include what the editor needs; implementation details stay in code
5. **Composition over configuration** — Use Grid + simple components instead of mega-components with many options

---

## See Also

- [Content Structure](./content-structure.md) — How content is parsed and structured
- [Kit Reference](./kit-reference.md) — Hooks, components, and utilities from @uniweb/kit
- [Foundation Configuration](./foundation-config.md) — CSS variables and custom Layout
- [Data Fetching](./data-fetching.md) — Loading external data into components