Theme schema
The vocabulary a theme is written in, and the parsers that read it, as an open package.
@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.
It is MIT licensed, and the source is at 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 lists every section, setting and token, read from this package.
Four rules a theme file follows:
- Every setting is optional. A band names its
typeand 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
mediasetting 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 ormailto:. Anything else,javascript:andhttp: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
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:
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:
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:
: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.
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:
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.