# Foundation Configuration

The `main.js` file at the foundation package's source root defines customizable CSS variables and configuration for your foundation. Layout components live in `src/layouts/` and are auto-discovered.

## Overview

Foundations can expose configuration points that sites customize in their `theme.yml`:

```
src/                       # the foundation package
├── main.js                # Name, description, variables, defaultLayout
├── sections/              # Section types
├── components/            # Internal components
├── layouts/               # Layout components (auto-discovered)
├── styles.css             # Global styles
├── package.json           # name: "src" — the workspace name, not the foundation's
└── vite.config.js
```

---

## Identity

A foundation's **name** is what it registers as — `@org/<name>` — and what sites pin (`foundation: '@acme/marketing@1.2.0'`). Set it in `main.js`:

```js
// src/main.js
export default {
  name: 'marketing',
  description: 'A modern marketing site with hero, features, and pricing sections.',
}
```

`uniweb create` and `uniweb add` write it for you — the project's name, or the name you gave `add` — and `uniweb register` asks for one when a foundation has none. The first time you register, it also writes the organization the foundation registers under into the name: `name: '@acme/marketing'`.

- **Form:** lowercase letters, digits and hyphens (`marketing`, `acme-docs`), with the organization it registers under once it has one (`@acme/marketing`). The scope is part of the name — see [Identity](#identity-scope--name-resolution).
- **Not `src` or `foundation`.** Those name the folder the code lives in, and every project in an org would register the same one. `uniweb register` refuses them and asks for a real name (or, when it cannot ask, prints the line to add).
- **Fallback:** with no `name` in `main.js`, the name is `package.json`'s `name`.
- The `version` always comes from `package.json`, and so does the `description` unless `main.js` sets one.

The `name` in the foundation's `package.json` is a **workspace** name: it is how the site depends on the foundation (`"src": "file:../src"`) and how `site.yml` refers to it (`foundation: src`). It can stay `src`. Once `main.js` names the foundation, the two are independent — `uniweb rename foundation` renames the workspace package without changing what the foundation registers as, and changing `main.js`'s `name` touches nothing in the workspace.

---

## `package.json` configuration

The `uniweb` block in `package.json` carries platform-specific configuration that doesn't belong in the npm-standard fields. It is optional, and most foundations need none: the platform falls back to sensible defaults when a field is omitted. What a foundation *is* — its name and the organization it registers under — lives in `main.js`, above.

### Supported fields

| Field | Type | Default | Purpose |
| --- | --- | --- | --- |
| `runtimePolicy` | `"exact"` | (unset) | **Escape hatch, rarely needed.** Sites using this foundation are given a compatible runtime automatically; compatibility is the framework's determination, not a per-foundation setting. Set `"exact"` only if the foundation depends on undocumented runtime internals or was audited against exactly one runtime build — it freezes sites on the recorded version and they stop receiving runtime fixes. See [`uniweb register`](./cli-commands.md#foundation-runtime-policy). |

> **Removed:** `uniweb.id`, which set the registered name from `package.json`, and `uniweb.scope`, which set the organization it registers under. Both live in `main.js`'s `name` now (`name: '@acme/marketing'`); `uniweb register` refuses a leftover and says what to write instead. (A schemas-only package, which has no `main.js`, still records its scope in `uniweb.scope` — see [`uniweb register`](./cli-commands.md#scope).)

### Identity (scope + name) resolution

A registered foundation's name has two pieces — a **scope** (the org it's registered under) and its **name** — written together in `main.js`: `@acme/marketing`. Full details in [`uniweb register` → Identity](./cli-commands.md#identity-scope--name). The summary:

**Name**: `main.js`'s `name`, else `package.json`'s `name` — never `src` or `foundation`.

**Scope**: the one in the name — a namespace, either your personal scope (`@<your handle>`, no organization needed) or an organization's. A name without one has not been registered yet: the first `uniweb register` takes `--scope @scope`, else the workspace you work in — an organization's scope, or your personal one — and writes it into the name. A `--scope` naming a different scope than the name's is refused. The foundation's own data schemas register under the same scope (`@/article` → `@acme/article`), which is the name a site's records and queries use for them. When you publish a site whose local foundation changed, `uniweb publish` releases it to the catalog automatically — so the same rules apply there.

### Why a separate `uniweb` block

These fields are platform configuration, not standard npm metadata. Keeping them under `uniweb` (as opposed to spreading them across the top-level package.json or various dotfiles) gives a single, discoverable home and avoids polluting the npm-tooling-recognized surface. Future platform features that need static configuration will land here too — additions will be documented in this section.

### Where does the pinned runtime version come from?

You may notice your foundation's `dist/runtime-pin.json` reports a runtime version (e.g. `0.8.9`) without your `package.json` declaring `@uniweb/runtime` anywhere. This is intentional.

`@uniweb/runtime` is pulled in **transitively** through `@uniweb/build` (which every foundation has as a devDependency). The runtime version baked into your foundation's pin is whichever version your `@uniweb/build` version pulled in at install time:

```
your foundation
  └─ devDependencies: "@uniweb/build": "0.12.0"
       └─ pulls in @uniweb/runtime  (whatever version that build version locks)
            → resolved at install time → version goes into dist/runtime-pin.json
```

The foundation build looks for `@uniweb/runtime` in two places:

1. **Transitive resolution from `@uniweb/build`'s location** — the common case. This is what every foundation hits unless it explicitly opts in to (2).
2. **Direct dependency in your foundation's own `node_modules`** — opt-in escape hatch.

#### Practical implications

- **You don't need to add `@uniweb/runtime` to your foundation's dependencies.** This is by design — runtime is the host environment, not a foundation import.
- **To bump the runtime version your foundation pins, bump your `@uniweb/build` dep.** When `@uniweb/build@0.13.0` ships pulling in a newer runtime, updating your devDependency is how you adopt it.
- **You can override by adding `@uniweb/runtime` directly to your foundation's `dependencies`** — but this is rarely needed and creates a source of confusion (now there are two places that know about the runtime version). Don't do this unless you have a specific reason.
- **You don't declare a compatibility rule — the pin is enough.** `uniweb register` carries the recorded version with your foundation, and sites using it are given a compatible runtime. Whether two runtime versions are compatible is the framework's determination, not a per-foundation setting: you have no way to evaluate whether anything your code can reach changed between two releases, so you aren't asked to. The only knob is the `"exact"` escape hatch above, for a foundation that depends on undocumented internals or was audited against exactly one build.

### Build outputs

`uniweb build` (or `vite build`) writes your foundation's `dist/`:

| Path | What it is |
| --- | --- |
| `dist/entry.js` | The foundation module the browser loads (code-split — kit's optional features like syntax highlighting and search lazy-load as separate chunks). |
| `dist/entry-ssr.js` | A **single-file server-render build** of the same foundation, for hosts that render your foundation on the server (SSR). Same components as `entry.js` in one module, with React, the runtime, and the browser-only lazy libraries left external — so it stays foundation-sized and those libraries (which only run in the browser) are never pulled in. Emitted automatically; you don't reference it directly. |
| `dist/assets/style.css` | The foundation's compiled stylesheet (carries your theme-var defaults). |
| `dist/meta/schema.json` | The compiled component + theme schema (params, defaults, layouts) the runtime reads. |
| `dist/runtime-pin.json` | The `@uniweb/runtime` version this build resolved (see above). |

You normally don't touch these — the CLI's deploy/export flow ships the right ones for the target.

---

## CSS Variables (vars)

Most customization is handled by component params. Both section components and layout components declare their own params in `meta.js` — layouts are full components with params, not just structural wrappers. A header height, for example, is typically a layout param, not a foundation var.

Foundation-level CSS variables are for values that must stay consistent **across** multiple components — shared radii, spacing scales, or a **typeface** the site retunes (beyond the three roles the theming system already provides as `body`/`heading`/`code`; see [Font family vars](#font-family-vars)). Don't reach for foundation vars when a component or layout param would do.

### Defining Variables

```js
// src/main.js

/**
 * CSS custom properties that sites can override in theme.yml
 */
export const vars = {
  'radius': {
    default: '0.5rem',
    description: 'Default border radius for cards and buttons',
  },
  'radius-lg': {
    default: '1rem',
    description: 'Large border radius for panels and modals',
  },
  'section-padding-y': {
    default: 'clamp(4rem, 6vw, 7rem)',
    description: 'Vertical padding for sections (fluid)',
  },
  'shadow': {
    default: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
    description: 'Default shadow for cards and elevated elements',
  },
}
```

### Variable Schema

Each variable is an object with:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `default` | string | Yes | Default CSS value |
| `description` | string | No | What the variable controls |
| `label` | string | No | Display name in editor (falls back to humanized var name) |
| `type` | string | No | `'select'`, `'color'`, or default text input |
| `options` | array | When `type: 'select'` | Dropdown options |
| `group` | string | No | Visual grouping in editor (e.g. `'Layout'`, `'Visual'`) |
| `globalOnly` | boolean | No | If true, hidden from section-level panel |

When `label` is omitted, the editor generates one from the var name: `radius-lg` → "Radius Lg".

#### Type mapping

| Var type | Editor control | Context-aware? |
| --- | --- | --- |
| (default) | Text input with default as placeholder | No |
| `select` | Dropdown with options | No |
| `color` | Color picker | Yes — stored per light/dark context |

Color-type vars are stored separately from non-color vars. The processor routes them to `colorVars` (per-context) rather than `foundationVars` (flat). This means color vars can have different values for light and dark schemes.

> **Foundation vars vs component vars:** Foundation vars are global — they emit on `:root` and apply site-wide. Component vars (declared in `meta.js`) are scoped to `#section-{id}`. See [Component Metadata](./component-metadata.md#vars) for component-level vars.

### Font family vars

The theming system provides three **font roles** — `body`, `heading`, `code` — that the framework paints onto elements (`body`, `h1–h3`, `code/pre/kbd/samp`) and the site sets in `theme.yml`. A foundation can go further: declare a **font var** to **add** a role (an editorial serif, a display face) or **redefine** a built-in one. A `font-*`-named var is recognized as a typeface automatically — no `type` needed; a bare-named one takes `type: 'font'`. Either way the family loads and the schema tags it as a font:

```js
// src/main.js
export const vars = {
  // `font-*` name → recognized as a font automatically (no `type` needed)
  'font-serif': {
    default: 'ui-serif, Georgia, serif',
    description: 'Editorial serif for pull-quotes and taglines',
    applyTo: ['blockquote', '.tagline'],   // framework paints it here, like a built-in role
  },
}
```

- **`applyTo: [selectors]`** — the framework emits the `font-family` rule for you, exactly like a built-in role. Omit it and the component wires the typeface itself (a Tailwind `font-*` utility, or `var(--font-…)`).
- **Naming & inferred type** — `serif` and `font-serif` are the same role (both emit `--font-serif`); the `font-serif` spelling is what Tailwind's `font-serif` utility reads **and** infers `type: 'font'` for you. So a `font-*` name needs no `type`; a bare name (`serif`, `display`) takes an explicit `type: 'font'`. A custom name with no built-in utility is referenced with `var(--font-display)`. (CSS longhands like `font-weight` are never inferred as typefaces.)
- **Redefining a built-in role** — declare `heading` / `body` / `code` as a `type: 'font'` var with your own `applyTo` to retarget it (e.g. include `<h4>` in the heading font). The site still owns the family.

The site sets any role — built-in or foundation-added — by name in `theme.yml` under `fonts:` (or `vars:`), and loads the file with `fonts.import` / `fonts.faces`. Defaults should be OS stacks (`ui-serif, Georgia, serif`) so the foundation renders natively before a site opts into brand faces. See [Site Theming → Fonts beyond the three roles](./site-theming.md#fonts-beyond-the-three-roles) for the site side.

### Registering Defaults in `styles.css`

The `main.js` declaration is metadata — descriptions, types, and editor UI hints that end up in `schema.json`. To ensure the default values are present in the foundation's actual CSS output, register them in `styles.css` via `@theme inline`:

```css
/* src/styles.css */
@theme inline {
  --radius: 0.5rem;
  --radius-lg: 1rem;
  --section-padding-y: clamp(4rem, 6vw, 7rem);
  --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
```

This is best practice for two reasons:

1. **Guarantees defaults are available.** The `@theme inline` block compiles into the foundation's CSS (`dist/assets/style.css`, shipped alongside `entry.js`). The defaults are present regardless of whether the site's `theme.yml` sets them — important for runtime-loaded foundations where the site build doesn't have access to `schema.json`.

2. **Enables Tailwind shorthand.** With registration, you can use `py-(--section-padding-y)` instead of `py-[var(--section-padding-y)]`.

When a site overrides a variable in `theme.yml`, the site's theme CSS takes priority over the foundation defaults.

### Using Variables in Components

Components reference foundation vars via CSS or Tailwind:

```jsx
function Card({ children }) {
  return (
    <div className="rounded-(--radius) shadow-(--shadow) p-6">
      {children}
    </div>
  )
}
```

Or in CSS:

```css
section {
  padding: var(--section-padding-y) 0;
}
```

The same vars apply consistently across all components that use them. Changing `radius` in `theme.yml` updates every card, button, and panel at once.

### Site Overrides

Sites override variables in `theme.yml`:

```yaml
# site/theme.yml
vars:
  radius: 0.75rem
  radius-lg: 1.25rem
  section-padding-y: clamp(3rem, 5vw, 5rem)   # tighter spacing
```

The build generates theme CSS with the site's values. These override the foundation defaults (from `@theme inline` in `styles.css`) because the site's theme CSS loads with higher priority:

```css
/* Site theme CSS — overrides foundation @theme inline defaults */
:root {
  --radius: 0.75rem;
  --radius-lg: 1.25rem;
  --section-padding-y: clamp(3rem, 5vw, 5rem);
}
/* --shadow keeps its foundation default since the site didn't override it */
```

---

## Default Section Type

When a content section doesn't specify a `type:` in its frontmatter, the runtime needs to know which section type to use. By default it looks for a component called `Section`, but you can change this with `defaultSection`:

```js
// src/main.js
export default {
  defaultSection: 'FeatureGrid',
}
```

This is useful when your foundation has a general-purpose component that should handle untyped content. If not set, the fallback is `'Section'`.

---

## Data Every Section Receives

A section's `content.data` holds the keys its component declares in `meta.js` `data:`. Keys your foundation's [content handlers](../development/content-handlers.md) — or a hook every section uses — read are declared once, in `main.js` `data:`, and every section receives them:

```js
// src/main.js
export default {
  data: { profile: {}, members: '@/member' },
}
```

Each value is a schema ref, an inline field map, or `{}` for records with no schema. A section's own `data:` wins for a key both declare. See [Data Fetching → What a section receives](./data-fetching.md#what-a-section-receives).

---

## Custom Layout

Foundations can provide custom Layout components that control page structure. Layouts live in `src/layouts/` and are auto-discovered.

### Default Behavior

Without a custom Layout, the runtime uses a simple wrapper:

```jsx
// Default layout
function Layout({ body }) {
  return <>{body}</>
}
```

### Creating a Layout

Place layout components in `src/layouts/`:

```
src/layouts/
├── DocsLayout/
│   ├── index.jsx
│   └── meta.js        # Optional: declares areas, params
└── MarketingLayout.jsx # Bare file works too
```

Set the default in `main.js`:

```js
// src/main.js
export default {
  defaultLayout: 'DocsLayout',
}
```

With no `defaultLayout`, a layout named `Default` (or `DefaultLayout`) is the default. With neither, a page that names no layout gets the built-in one: `header`, the page, then `footer`, in a single column — other areas are not drawn. A `defaultLayout` that names none of your layouts stops the build.

```jsx
// src/layouts/DocsLayout/index.jsx
export default function DocsLayout({ header, footer, left, right, body }) {
  return (
    <div className="min-h-screen flex flex-col">
      {header && <header>{header}</header>}

      <div className="flex-1 flex">
        {left && (
          <aside className="w-64 border-r">
            {left}
          </aside>
        )}

        <main className="flex-1">
          {body}
        </main>

        {right && (
          <aside className="w-64 border-l">
            {right}
          </aside>
        )}
      </div>

      {footer && <footer>{footer}</footer>}
    </div>
  )
}
```

### Layout Props

| Prop | Type | Description |
| --- | --- | --- |
| `header` | ReactNode | Rendered header area (or null) |
| `footer` | ReactNode | Rendered footer area (or null) |
| `left` | ReactNode | Rendered left area (or null) |
| `right` | ReactNode | Rendered right area (or null) |
| `body` | ReactNode | Page content sections |
| `params` | object | Layout parameters (merged with meta.js defaults) |
| `page` | Page | Current page instance |
| `website` | Website | Website instance |

Area names aren't fixed — foundations can declare any areas in `meta.js`. The props above are the conventional names. See [Custom Layouts](../development/custom-layouts.md#general-named-areas) for details.

### Layout meta.js

Layouts can optionally declare which areas they use and what parameters they accept:

```js
// src/layouts/DocsLayout/meta.js
export default {
  title: 'Documentation',
  description: 'Three-column layout with sidebar navigation',
  areas: ['header', 'footer', 'left', 'right'],
  params: {
    sidebarWidth: {
      type: 'select',
      options: ['narrow', 'wide'],
      default: 'narrow',
    },
  },
}
```

### Per-Page Layout Selection

Pages select a layout and configure it in `page.yml`:

```yaml
layout: MarketingLayout

# Or with options:
layout:
  name: MarketingLayout
  hide: [left, right]
  params:
    sidebarWidth: wide
```

The `hide` array suppresses specific areas on that page. Hidden areas are passed as null to the Layout component — check props directly:

```jsx
export default function DocsLayout({ header, footer, left, right, body }) {
  return (
    <div className="min-h-screen flex flex-col">
      {header && <header>{header}</header>}

      <div className="flex-1 flex">
        {left && <aside className="w-64">{left}</aside>}
        <main className="flex-1">{body}</main>
        {right && <aside className="w-64">{right}</aside>}
      </div>

      {footer && <footer>{footer}</footer>}
    </div>
  )
}
```

---

## Document Outputs

Foundations that compile their content into documents (PDF, DOCX, EPUB, Typst source bundles, HTML for Paged.js, etc.) declare the formats they support under `outputs:`. This is what powers in-page Download buttons and headless tools like `unipress` — both call `compileDocument(website, { format, foundation })` from `@uniweb/press`, which reads the map and does the rest.

```js
// src/main.js
import { buildTypstOptions, buildEpubOptions } from './compile-options.js'

export default {
  defaultLayout: 'BookLayout',
  outputs: {
    typst: {
      extension: 'zip',
      getOptions: buildTypstOptions,
    },
    pdf: {
      extension: 'pdf',
      via: 'typst',                      // routes through the typst adapter
      getOptions: buildTypstOptions,
    },
    epub: {
      extension: 'epub',
      getOptions: buildEpubOptions,
    },
  },
}
```

### Per-output fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `getOptions` | `(website, hostOptions) → { adapterOptions }` (can be async) | No | Foundation-specific assembly: meta from `website.config`, typst preamble, stylesheets, cover image bytes, etc. The returned `adapterOptions` go to the Press format adapter. `hostOptions` are the rest-args the host passes to `compileDocument` — use them to branch on caller intent (e.g., `mode: 'server'` vs `mode: 'sources'` for typst). Omit when the default Press adapter output suits the format unchanged. |
| `via` | string | No | Press format to compile through. Defaults to the output key itself. Use when the user-facing name differs from the Press adapter — `pdf` aliased to `typst`, for instance, so a headless host can fetch the typst source bundle and finish compile with its own typst binary. |
| `extension` | string | No | Default file extension the host uses when deriving an output filename. Convention only — the host can override. |

### How the shape fits the compile flow

```
  compileDocument(website, { format: 'pdf', foundation, mode: 'sources' })
    │
    ├─ reads    foundation.outputs.pdf
    │             via: 'typst'         → Press uses the typst adapter
    │             getOptions(website, { format: 'pdf', mode: 'sources' })
    │
    ├─ calls    getOptions(...)
    │             → { adapterOptions: { mode, meta, preamble, template, assets } }
    │
    ├─ gathers  website.pages.flatMap(p => p.bodyBlocks)
    │
    └─ dispatches  compileSubtree(<ChildBlocks blocks={...}/>, 'typst', { adapterOptions })
                   → Blob (a typst source-bundle zip; the host binary turns it into a PDF)
```

### Relationship to other foundation callables

`outputs.*.getOptions` is a per-document callable, invoked once per compile. It is not the same as:

- `handlers.{data, content, props}` — per-block callables in the **content pipeline**, invoked on every block's prepare-props pass. See [Content Handlers](../development/content-handlers.md).
- `transports[name].resolve` — per-request callables in the **data pipeline**, invoked on every fetch. See [Data Transports](#data-transports).

Each subsystem owns its lifecycle moment; `outputs` owns end-of-pipeline document compile.

---

## Data Transports

A foundation can export one or more **named transports** — reusable fetchers that a site can opt into by name. The site keeps authority: `site.yml` picks which transport handles which data key (a fetch's `as`). A foundation never silently intercepts a site's data request.

When a site declares no transport for a key, the framework's default fetcher handles it — the file the build compiles from a query, a host's live records, or an external query's `url` (GET, or `method: POST` with its `body`) and `transform:`, with every query operator evaluated over what arrived. It takes no site-level vocabulary for a backend of your own (a base URL, headers, a wire of its own): that is exactly what a transport is for — see [Data Sources](../development/data-sources.md). Most foundations need no transports at all.

Declared on the default export of `main.js` alongside identity, theme, and layout fields:

```js
// src/main.js
export default {
  defaultLayout: 'MarketingLayout',

  transports: {
    acme: {
      resolve: async (request, ctx) => {
        const baseUrl = ctx.website.config?.fetcher?.acme?.baseUrl
        const res = await fetch(`${baseUrl}/${request.query ?? request.as}`, {
          signal: ctx.signal,
        })
        if (!res.ok) return { data: [], error: `HTTP ${res.status}` }
        return { data: await res.json() }
      },
      cacheKey: (request) => `acme:${request.query ?? request.as}`,
    },
  },
}
```

The site then opts in:

```yaml
# site.yml
fetcher:
  transports:
    articles: acme       # as → transport name
    events: default      # reserved name — framework default fetcher
  acme:                   # transport-specific binding config
    baseUrl: https://api.example.com
```

### How the site selects

Selection is a name lookup, nothing more:

1. If `fetcher.transports[request.as]` is set, use that named transport from the primary foundation (or any extension that registers a transport by that name).
2. Otherwise, if `fetcher.transports.default` is set, use that for every other key.
3. Otherwise, the framework default fetcher handles the request.

There is no `match()` predicate, no route-walking, no fallback chain. The site always makes the selection, visible in `site.yml`, auditable.

### The transport contract

A transport is any object with a `resolve` method (and optionally `cacheKey`):

```js
{
  async resolve(request, ctx) {
    // ...
    return { data, error?, meta? }
  },
  cacheKey?: (request) => string,  // optional; see Per-transport knobs below
}
```

**Request** — the normalized fetch config, with an extra `dynamicContext` on a parametric page's record request:

| Field | Type | Description |
| --- | --- | --- |
| `as` | string | Required. The key under `content.data` the result will be stored at. |
| `query` | string | The query the request reads, by name. |
| `path` | string | A compiled file under the site's base — `/data/<query>.json`, or a per-record file. Mutually exclusive with `url`. |
| `url` | string | An external query's address. Mutually exclusive with `path`. |
| `method` / `body` | string / any | An external query's `GET` or `POST`, and the POST's body. |
| `transform` | string | Dot-path into the response (e.g. `data.items`). |
| `record` | object | An external query's `record:` — `{ url?, method?, body?, transform? }` — the request for one record on a parametric page. |
| `detail` | string \| boolean | Set by resolution, never authored: the query has a per-record source — a compiled query's per-record file pattern, or `true`. |
| `whole` | boolean | Set by resolution, never authored: a question to a host's records service asks for whole records — the key's component declares it `whole: true`. Absent, the answer is briefs. |
| `scope` | string | The query's folder branch; its records are the ones placed at or below it. |
| `where` | object | The query's predicate (where-object). |
| `sort` / `limit` | any | The query's order and count. With `scope` and `where` they select **the query's records** — its `limit` included. |
| `narrow` | object | What this fetch takes of the query's records — its own `where`, `sort` and `limit`, applied after the query's: `where` keeps the records that also match, `sort` re-orders them, `limit` takes the first N. On a parametric page's record question to a host's records service it holds `match` instead: the one record the URL names. A key declared `single: true` asks with `limit: 1`. Absent when the fetch takes all of them. A transport that evaluates at the source applies the query first and `narrow` second, so a fetch never gets a record the query does not select. |
| `dynamicContext` | object | Present on a parametric page's record fetch: `{ paramName, paramValue }`. |

**Context** — the framework singletons, handed to the fetcher directly (no `globalThis` reads needed):

| Field | Description |
| --- | --- |
| `website` | The active Website. Read per-site transport config via `ctx.website.config.fetcher`. |
| `page` | The Page scope of the request. `null` for site-level fetches. |
| `block` | The Block triggering the request. `null` for page- and site-level fetches. |
| `signal` | `AbortSignal`. Aborts on block unmount or route change. Pass to `fetch()`. |

**Return** — always the same shape:

```js
{
  data,           // the fetched data. Never undefined — empty is [] or null/{}.
  error?: string, // presence signals failure; may coexist with stale data
  meta?: object,  // fetcher-specific metadata; stored alongside the cache entry
}
```

Throwing is tolerated but not idiomatic — the runtime catches and surfaces `{ data: [], error: String(err) }`. Prefer returning an explicit `error` field.

### Per-transport knobs

Optional fields on a transport object that the dispatcher recognizes:

| Field | Default | Description |
| --- | --- | --- |
| `cacheKey(request)` | `{path, url, as, transform, method?, body?, locale, scope, where, sort, limit, narrow}` stringified | Override when the transport derives response content from fields the default key doesn't cover, or when two requests should intentionally share a key. |
| `prerenderable` | `true` | Set `false` to opt out of build-time (`uniweb build`) execution. The config is skipped at build and fetched at runtime in the browser. Use for transports that need browser-only APIs. |

### Binding config

Under the `fetcher:` block in `site.yml`, a foundation's transport can read its own named sub-block. By convention, use a key matching the transport name:

```yaml
# site.yml
fetcher:
  transports:
    articles: acme
  acme:                        # binding for the 'acme' transport
    baseUrl: https://api.example.com
    sources: { blog: 'posts' }
```

The transport reads its config via `ctx.website.config.fetcher.{transportName}`. The framework does no validation — document the keys your transport reads in the foundation's README.

Values under `fetcher:` are **client-visible** — they ride into the site's HTML or `__DATA__`. The framework does not offer a secret configuration channel. For private credentials, the pattern is same-origin proxying (the site fetches `/api/…`, a deployment-layer proxy attaches the secret server-side). See [Data Sources → Secrets](../development/data-sources.md#secrets).

> **Removed:** the per-site default-fetcher vocabulary — `fetcher.baseUrl`, `headers`, `envelope`, `supports` and `request.*`. The build warns and drops them. A public, keyless endpoint is an external query; a backend that needs a base URL or headers is a transport.

### Composing middleware

`@uniweb/fetchers` ships small middleware primitives that wrap a transport with cross-cutting behavior. The package is middleware-only — for a complete transport, write your own `resolve()` against `fetch()` and compose middleware around it:

```js
import { withAuth } from '@uniweb/fetchers'

const authed = withAuth(myTransport, () => someTokenProvider())
```

### Extensions contributing transports

An extension's `main.js` can export `transports: { … }` too. The dispatcher merges extension transports into a single registry, keyed by name. On a name collision with the primary foundation, the **primary wins** with a dev-mode warning. A malformed or throwing extension transport is logged and skipped — it never blocks the rest of the registry (parallels the `Promise.allSettled` pattern used to load extensions).

This lets a stats-widget extension package its own transport, and sites opt into it by name in `site.yml`:

```yaml
# site.yml
fetcher:
  transports:
    stats: extension-stats        # extension-contributed transport name
```

### When to skip the transports declaration

Most foundations don't need one. Omit `transports:` when:

- The site's data is its own records (the default fetcher reads what the build compiles, or a host's live records).
- The site reads a public JSON API an external query can express — `url`, `method: POST` with a `body`, `transform` — see [Data Sources](../development/data-sources.md).
- Each component calls `fetch()` directly inside `useEffect` (bundled-style foundations).
- A third-party SDK manages transport inside the component.

See [Data Fetching](./data-fetching.md) for the author surface (`fetch:` / `query:` cascade) and [Data Sources](../development/data-sources.md) for the recipes that don't need a custom transport.

---

## Common Variable Patterns

### Spacing System

```js
export const vars = {
  'spacing-xs': { default: '0.25rem', description: 'Extra small spacing' },
  'spacing-sm': { default: '0.5rem', description: 'Small spacing' },
  'spacing-md': { default: '1rem', description: 'Medium spacing' },
  'spacing-lg': { default: '2rem', description: 'Large spacing' },
  'spacing-xl': { default: '4rem', description: 'Extra large spacing' },
}
```

### Visual Style

```js
export const vars = {
  'border-radius-sm': { default: '0.25rem', description: 'Small radius' },
  'border-radius': { default: '0.5rem', description: 'Default radius' },
  'border-radius-lg': { default: '1rem', description: 'Large radius' },
  'shadow-sm': { default: '0 1px 2px rgba(0,0,0,0.05)', description: 'Small shadow' },
  'shadow': { default: '0 4px 6px rgba(0,0,0,0.1)', description: 'Default shadow' },
}
```

### Animation

```js
export const vars = {
  'transition-fast': { default: '150ms', description: 'Fast transitions' },
  'transition-normal': { default: '300ms', description: 'Normal transitions' },
  'transition-slow': { default: '500ms', description: 'Slow transitions' },
}
```

---

## Complete Example

```js
// src/main.js

/**
 * Cross-cutting design tokens that sites can override in theme.yml.
 * Values that belong to a specific component (header height, sidebar width)
 * should be params in that component's meta.js instead.
 */
export const vars = {
  // Spacing
  'section-padding-y': {
    default: 'clamp(4rem, 6vw, 7rem)',
    description: 'Vertical padding for sections (fluid)',
  },
  'container-padding-x': {
    default: '1.5rem',
    description: 'Horizontal padding for containers',
  },

  // Visual
  'radius': {
    default: '0.5rem',
    description: 'Default border radius for cards and buttons',
  },
  'radius-lg': {
    default: '1rem',
    description: 'Large border radius for panels and modals',
  },
  'shadow': {
    default: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
    description: 'Default shadow for elevated elements',
  },
}

export default {
  defaultLayout: 'docs',
}
```

Layout dimensions like header height and sidebar width are layout params, not foundation vars — the layout component owns them:

```js
// src/layouts/docs/meta.js
export default {
  params: {
    headerHeight: { type: 'text', default: '4rem' },
    sidebarWidth: { type: 'text', default: '16rem' },
    maxContentWidth: { type: 'text', default: '80rem' },
  },
}
```

```jsx
// src/layouts/docs/index.jsx
export default function DocsLayout({ header, footer, left, right, body, params }) {
  // No need for fallback values — meta.js defaults are merged into params
  // before the layout component receives them.
  const { headerHeight, sidebarWidth, maxContentWidth } = params

  return (
    <div className="min-h-screen flex flex-col bg-section">
      {header && (
        <div className="sticky top-0 z-50" style={{ height: headerHeight }}>
          {header}
        </div>
      )}

      <div className="flex-1 flex mx-auto w-full" style={{ maxWidth: maxContentWidth }}>
        {left && (
          <aside className="shrink-0 border-r hidden lg:block" style={{ width: sidebarWidth }}>
            <div className="sticky overflow-y-auto" style={{ top: headerHeight, maxHeight: `calc(100vh - ${headerHeight})` }}>
              {left}
            </div>
          </aside>
        )}

        <main className="flex-1 min-w-0">
          {body}
        </main>

        {right && (
          <aside className="shrink-0 border-l hidden xl:block" style={{ width: sidebarWidth }}>
            <div className="sticky overflow-y-auto" style={{ top: headerHeight, maxHeight: `calc(100vh - ${headerHeight})` }}>
              {right}
            </div>
          </aside>
        )}
      </div>

      {footer && <footer>{footer}</footer>}
    </div>
  )
}
```

---

## Runtime Access

Access foundation variables from components:

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

function Component() {
  const theme = useThemeData()

  // Get a foundation variable value
  const radius = theme?.getFoundationVar('radius')

  return <div style={{ borderRadius: radius }}>...</div>
}
```

---

## Best Practices

1. **Use semantic names**: `radius` not `r1` or `size-8`

2. **Only use vars for cross-cutting values**: A header height belongs to the layout component as a param. A border radius used by cards, buttons, and modals belongs as a foundation var. The test: would changing this value need to affect multiple unrelated components simultaneously?

3. **Provide good defaults**: Defaults should work out of the box

4. **Document with descriptions**: The `description` field helps site authors in the visual editor

5. **Register in `styles.css`**: Declare vars in both `main.js` (metadata) and `styles.css` via `@theme inline` (actual CSS)

6. **Consider dark mode**: Vars referencing colors should use theme tokens

7. **Test overrides**: Verify vars work when sites customize them in `theme.yml`

---

## Icon Libraries

Foundations can include icon libraries to enable named icon syntax in content. This allows content authors to use icons without managing SVG files.

### Why Include an Icon Library?

Without an icon library, content authors must:

- Provide SVG files for every icon
- Reference icons by file path: `![check](/icons/check.svg){role=icon}`

With an icon library installed, authors can write:

- `![check](lucide:check)` — Named icon from library
- `![arrow](lucide:arrow-right){size=20 color=blue}` — With size and color

### Installing an Icon Library

**1. Add the package:**

```bash
cd foundation
pnpm add lucide-react
```

**2. Create an icon wrapper component:**

```jsx
// src/components/Icon/index.jsx
import * as LucideIcons from 'lucide-react'

export function Icon({ name, library, size = 24, color, className }) {
  // For lucide library
  if (library === 'lucide' && name) {
    // Convert kebab-case to PascalCase: "arrow-right" → "ArrowRight"
    const pascalName = name
      .split('-')
      .map(part => part.charAt(0).toUpperCase() + part.slice(1))
      .join('')

    const LucideIcon = LucideIcons[pascalName]
    if (LucideIcon) {
      return <LucideIcon size={size} color={color} className={className} />
    }
  }

  // Fallback: render nothing or a placeholder
  return null
}
```

**3. Use in your render component:**

The `@uniweb/kit` Render component handles named icons automatically when your foundation provides an Icon component. If you're using a custom renderer, check for the `library` and `name` attributes:

```jsx
// In your custom renderer
if (node.attrs?.role === 'icon' && node.attrs?.library) {
  const { library, name, size, color } = node.attrs
  return <Icon library={library} name={name} size={size} color={color} />
}
```

### Supported Libraries

The content parser recognizes these icon library prefixes:

| Prefix | Package | Install Command |
| --- | --- | --- |
| `lucide:` | lucide-react | `pnpm add lucide-react` |
| `heroicons:` | @heroicons/react | `pnpm add @heroicons/react` |
| `phosphor:` | @phosphor-icons/react | `pnpm add @phosphor-icons/react` |
| `tabler:` | @tabler/icons-react | `pnpm add @tabler/icons-react` |
| `feather:` | react-feather | `pnpm add react-feather` |

### Example: Multi-Library Support

```jsx
// src/components/Icon/index.jsx
import * as LucideIcons from 'lucide-react'
import * as HeroIcons from '@heroicons/react/24/outline'

function toPascalCase(str) {
  return str
    .split('-')
    .map(part => part.charAt(0).toUpperCase() + part.slice(1))
    .join('')
}

export function Icon({ name, library, size = 24, color, className }) {
  if (!name) return null

  const pascalName = toPascalCase(name)

  // Lucide icons
  if (library === 'lucide') {
    const IconComponent = LucideIcons[pascalName]
    if (IconComponent) {
      return <IconComponent size={size} color={color} className={className} />
    }
  }

  // Heroicons
  if (library === 'heroicons') {
    const IconComponent = HeroIcons[`${pascalName}Icon`]
    if (IconComponent) {
      return <IconComponent width={size} height={size} color={color} className={className} />
    }
  }

  console.warn(`[Icon] Unknown icon: ${library}:${name}`)
  return null
}
```

### Bundle Size Considerations

Icon libraries can be large. Consider these strategies:

**1. Tree-shaking (recommended):**

Most bundlers (Vite, webpack) tree-shake unused icons automatically when you import from the main package.

**2. Individual imports:**

Some libraries support individual imports for smaller bundles:

```jsx
import { Check, ArrowRight, Heart } from 'lucide-react'

const iconMap = { check: Check, 'arrow-right': ArrowRight, heart: Heart }

export function Icon({ name, size, color }) {
  const IconComponent = iconMap[name]
  return IconComponent ? <IconComponent size={size} color={color} /> : null
}
```

This limits which icons are available but produces smaller bundles.

**3. URL-based fallback:**

If named icons aren't found, fall back to URL-based icons:

```jsx
export function Icon({ name, library, url, size = 24, color }) {
  // Try named icon first
  if (library && name) {
    const IconComponent = resolveNamedIcon(library, name)
    if (IconComponent) {
      return <IconComponent size={size} color={color} />
    }
  }

  // Fall back to URL-based icon
  if (url) {
    return <img src={url} width={size} height={size} alt="" />
  }

  return null
}
```

### Content Author Usage

Once your foundation includes an icon library, content authors can use named icons:

```markdown
<!-- In section content -->
![](lucide:check) Feature included
![](lucide:x) Feature not included

<!-- With attributes -->
![success](lucide:check-circle){size=32 color=green}

<!-- In a button -->
[Get Started](lucide:arrow-right) [Get Started](/signup){icon=arrow-right}
```

---

## See Also

- [Site Theming](./site-theming.md) — Site-level theme customization
- [Layout Areas](./layout-areas.md) — Header, footer, and sidebar areas
- [Component Metadata](./component-metadata.md) — Component meta.js schema
- [Kit Reference](./kit-reference.md) — Accessing theme data in components