Skip to Content
Astro WidgetValidation

Ahead of the release

npm install @evanion/astro-widget gives you 0.3.0. These pages document main, which has changes that release does not.

Validation

Widgets.astro skips an unknown widget type with a dev-only warning, so a bad CMS save can never break a render. Catch them loudly at build time instead.

This page is the section’s demonstration and it has nothing to click. The thing worth operating here is a rendered Astro page, and an .astro component is compiled against a project rather than in a browser, so the honest version of that control is the storefront’s own build output embedded beside the data that produced it. That is not built. Until it is, every example below is executed — by nx test @evanion/astro-widget, over the real validator — and the reader gets evidence rather than a knob.

validateItems takes the sections, the widget types the registry holds, and a map of the props each type cannot render without. It returns one problem per fault, in order, and throws nothing:

import { validateItems } from '@evanion/astro-widget'; const sections = [ { id: 'header', type: 'listing-header', props: { title: 'Brass: Birmingham' }, }, { id: 'price', type: 'price-box', props: {} }, { id: 'questions', type: 'answer-wall', props: {} }, ]; const required = { 'listing-header': ['title'] }; const problems = validateItems( sections, ['listing-header', 'price-box'], required, ); problems; // -> [{ index: 2, id: 'questions', type: 'answer-wall', message: 'unknown widget type' }]

The answer wall is not a type this registry holds, so the section would have rendered as a gap on the listing page. Run the same call over the payload before the build touches it:

import { validateItems } from '@evanion/astro-widget'; import { registry } from './src/registry'; import page from './src/data/page.json'; const problems = validateItems(page.sections, registry, { 'listing-header': ['title'], }); if (problems.length) { for (const problem of problems) { console.error( `section ${problem.index} (${problem.id}, ${problem.type}): ${problem.message}`, ); } process.exit(1); }

This matters more here than in @evanion/react-widget, because it is the only check a widget’s props get. An .astro component exposes no prop types to infer from, so there is no compile-time half.

What it reports

One WidgetProblem per fault: the item’s index within its own sibling list, its id and type, and the message. What it reports in the widget core is the catalogue — every message, and the payload that produces each one. The nine below are the same nine, run here against this package’s own build.

Six of the nine are structural and fire whatever the registry holds. Five of them show up in one payload:

import { validateItems } from '@evanion/astro-widget'; const saved = [ null, { type: 'listing-header', props: { title: 'Root' } }, { id: 'grid', type: 'game-grid', props: {}, children: 'none' }, { id: 'grid', type: 'game-grid' }, ]; validateItems(saved, ['listing-header', 'game-grid']).map((p) => p.message); // -> ['item is not an object', 'item id is not a string', 'children is not a list', 'duplicate sibling id', 'props is not an object']

The root itself is the ninth, and it reports one problem rather than none: a CMS that wrote an object where the schema said array has broken the page, and a clean run would say it had not.

import { validateItems } from '@evanion/astro-widget'; validateItems({ sections: [] }, ['listing-header']); // -> [{ index: -1, id: '-', type: '-', message: 'items is not a list' }]

A payload with nothing wrong reports nothing, which is the case a build gate runs on every deploy:

import { validateItems } from '@evanion/astro-widget'; const page = [ { id: 'header', type: 'listing-header', props: { title: 'Root' } }, { id: 'grid', type: 'game-grid', props: {}, children: [ { id: 'azul', type: 'listing-header', props: { title: 'Azul' } }, ], }, ]; const required = { 'listing-header': ['title'] }; const problems = validateItems(page, ['listing-header', 'game-grid'], required); problems; // -> []

Required fields

validateItems(sections, registry, { 'listing-header': ['title'], 'game-grid': ['title', 'games'], });

The third argument maps a widget type to the prop names that must carry something. What blank means is the core’s, and the case it catches is the CMS text field an editor opened and left empty:

import { validateItems } from '@evanion/astro-widget'; const blank = [ { id: 'header', type: 'listing-header', props: { title: ' ' } }, ]; validateItems(blank, ['listing-header'], { 'listing-header': ['title'] }); // -> [{ index: 0, id: 'header', type: 'listing-header', message: 'missing field title' }]

Required props are only checked for a widget whose type is in the registry. An unknown type reports once, as an unknown type, rather than once plus one per prop it does not have.

index is scoped to its own level

The validator recurses into children, so a problem in a top-level section and a problem inside one of its children can both report index: 0:

import { validateItems } from '@evanion/astro-widget'; const nested = [ { id: 'grid', type: 'game-grid', props: {}, children: [{ id: 'questions', type: 'answer-wall', props: {} }], }, ]; validateItems(nested, ['game-grid']); // -> [{ index: 0, id: 'questions', type: 'answer-wall', message: 'unknown widget type' }]

id is what tells the two apart. The problem list is flat.

Where to run it

As a script before astro build, so a bad payload fails the build rather than shipping a page with a section missing:

{ "scripts": { "build": "node ./scripts/check-content.mjs && astro build" } }

Or on the CMS webhook, so an editor learns about it at save time rather than at deploy time. Both is better: the webhook gives the fast feedback, the build gate is the one that cannot be skipped.

A WidgetRegistry is Record<string, unknown> here, and only its keys are read, so a CI script that does not want to pull .astro modules into a plain Node process can hand it a bare list of names — validateItems lives in @evanion/widget and imports no framework at all:

import { defineWidgets, validateItems } from '@evanion/astro-widget'; // In a project these are `.astro` modules; the helper reads their keys and // nothing else, so a stand-in is enough to show what it returns. const registry = defineWidgets({ 'listing-header': () => null, 'game-grid': () => null, }); Object.keys(registry); // -> ['listing-header', 'game-grid'] validateItems([], Object.keys(registry)); // -> []

The real registry is the better argument where it is already imported, since the two cannot then disagree about which types exist.

Last updated on