# Theme schema

> The vocabulary a theme is written in, and the parsers that read it, as an open package.

Source: https://storeware.ai/docs/themes/theme-schema

`@storeware/theme-schema` is the vocabulary a Storeware theme is written in: the sections a page is built from, the settings each one takes, the tokens a shop may restyle, and the parsers that read all three. The platform parses every theme with it, and so does the CLI's [`theme validate`](https://storeware.ai/docs/themes#the-loop).

It is MIT licensed, and the source is at [github.com/storeware-ai/theme-schema](https://github.com/storeware-ai/theme-schema). `zod` is its only dependency.

## What a theme can hold

Every value in a theme is one of three things: a name from a table the package owns, a number inside a declared range, or a string that has passed an allow list. There is no expression syntax, no partials, no loops, and no `html` or `css` setting. A theme cannot make a request, read data or run.

| Module | What it holds |
| --- | --- |
| `sections` | Every section type, the pages it may sit on, its settings and its blocks. |
| `layout` | A page as stored, and the parsers that turn a stored template into sections. |
| `tokens` | The tokens a shop may set, the check each value passes, and the CSS they become. |
| `presets` | The named looks a section may be drawn in. |
| `css-values` | `isColour`, `isLength` and `isFontStack`: what may go in a CSS value. |

The [theme reference](https://storeware.ai/docs/themes/reference) lists every section, setting and token, read from this package.

Four rules a theme file follows:

- **Every setting is optional.** A band names its `type` and whatever settings it wants. What it leaves out falls back to the default.
- **References are handles.** `"collection": "winter"` names a collection by the handle in the shop's own URLs. A handle that resolves to nothing renders no band rather than an error.
- **Pictures stay with the shop.** A `media` setting holds the id of a picture in one shop's own media, which means nothing to another shop. A theme meant to travel carries no pictures.
- **A link is a path, an anchor, an `https://` address or `mailto:`.** Anything else, `javascript:` and `http:` included, is refused.

`richtext` is the one kind that carries markup, and the package bounds its length but does not sanitise it. A renderer must sanitise rich text before it puts it on a page.

## Validating a theme

```ts
import { readFile } from 'node:fs/promises';
import { layoutInput, themeCss, themeTokensInput } from '@storeware/theme-schema';

const file = JSON.parse(await readFile('theme.json', 'utf8'));

const tokens = themeTokensInput.parse(file.tokens);
const templates = layoutInput.parse(file.templates);
```

`themeTokensInput` refuses a token nobody may set and a value the allow list does not recognise, and says which:

```text
accent: red; } body { display: none is not a value this can accept.
shadow: shadow is not a token a shop can set.
```

`layoutInput` takes one optional array per page type. A band whose type this release does not build is an error that names it:

```text
home.0: carousel_3d is not a section this platform builds.
```

`themeCss(tokens)` turns the tokens into custom properties on `:root`, and leaves out anything that failed its check:

```css
:root {
  --sw-bg: #fbfaf7;
  --sw-fg: #17181a;
  --sw-accent: oklch(0.52 0.11 244);
  --sw-accent-fg: #ffffff;
  --sw-radius: 0.5rem;
  --sw-font-sans: Archivo, system-ui, sans-serif;
}
```

## Strict on the way in, lenient on the way out

The package keeps two kinds of parser, for two jobs.

`layoutInput` and `themeTokensInput` are for a file somebody is importing. They refuse the whole thing and say why, because there is a person there to fix it.

`parseTemplate` and `sectionsFor` are for a stored layout on its way to a shopper. They never throw. A band they cannot read is dropped on its own, and the rest of the page stands.

```ts
import { parseTemplate, sectionsFor } from '@storeware/theme-schema';

parseTemplate(stored.home, 'home'); // unreadable bands left out, no fallback
sectionsFor(stored, 'home'); // the same, then the page type's default if nothing is left
```

| What is wrong | What happens |
| --- | --- |
| An unknown section type | The band is dropped. The page stands. |
| A `select` value it cannot read | The setting falls back, and the band renders in its default look. |
| A media id or a link it cannot read | The band is dropped. |

If you implement the format yourself, copy this split. The failure a shopper sees should never be worse than the failure a merchant caused.

## The compatibility promise

A section type is never renamed or removed. A setting is never removed, and its kind never changes. A bound only rises, a minimum only falls, a block maximum only rises, a choice is never withdrawn, and a page is never taken off a section's list. The parsers pass through settings they do not know, so a layout written by a newer release survives an older one.

The vocabulary may grow and widen. It never shrinks or narrows. Stored layouts are not rewritten when the package changes, so a rename would quietly drop a band from every shop that used it.

The promised shape is published as `schema.snapshot.json`, and `compatibilityBreaks` compares the current release against any snapshot you pinned:

```ts
import { readFile } from 'node:fs/promises';
import { compatibilityBreaks, type SchemaSnapshot } from '@storeware/theme-schema/compatibility';

const pinned = JSON.parse(await readFile('theme-schema.pinned.json', 'utf8')) as SchemaSnapshot;
const breaks = compatibilityBreaks(pinned);
```

An empty array means nothing you relied on was taken away. Each break names where it is and the promise it breaks. Additions never appear in it.

`@storeware/theme-schema/schema.snapshot.json` is the current release's shape: copy it to start.

## What is promised, and what is only exported

The promise covers exactly what the snapshot records: the section types, the pages each may sit on, the name, kind, bounds and choices of every setting, the block type and maximum of every section that takes blocks, `PAGE_TYPES`, `FIELD_KINDS`, `MAX_HEADING`, `MAX_BODY` and `MAX_SECTIONS_PER_PAGE`, and the parsers passing unknown settings through.

Everything else the entry point exports is pure and yours to use, with no promise to keep it. That includes `dataPlanFor`, which gathers what a page's bands need into one plan, `sectionsJsonLd`, `defaultTemplateFor` and the `blueprint*` helpers. Which preset values a section offers is promised. The layout each preset is drawn with is not.

To draw what a theme describes, see the [theme kit](https://storeware.ai/docs/themes/theme-kit).
