Skip to Content
Astro WidgetValidation

Check CMS items before Astro renders them

@evanion/astro-widget 0.3.0 and its patch releases up to 0.3.4, the version on npm. Values on this page were produced by running the source at 2ee3e7e, a later commit than the 0.3.0 tag, and are not re-executed.

validateItems checks the items a CMS saved against the widget types you render. By running this check as part of your build process, you can stop astro build on any save that would otherwise publish a page with missing or broken items.

The Problem: The “Silent Gap” in Production

Widgets.astro follows a “fail-soft” strategy: if it encounters a widget type it doesn’t recognize, it simply skips that item and renders the rest of the region.

While this prevents the entire page from crashing, it results in a “silent gap” in your production content. A content editor might accidentally use a stale widget type or misspell a type name, and you wouldn’t know the page is broken until a user reports it.

Solution: A Build-Time Quality Gate

You can use validateItems to scan your CMS payload before the build process starts. This turns a silent runtime failure into a loud build-time error.

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

Each problem returned is a WidgetProblem object, containing the item’s index, id, type, and a message. In the example above, answer-wall is an unknown type, and the price-box item is missing its required price prop.

Integrating into the Build Process

To ensure no broken content ever ships, create a validation script (e.g., scripts/check-content.ts) and call it as part of your build command:

import { validateItems } from '@evanion/astro-widget'; import { items } from '../src/data/brass-birmingham'; const known = ['listing-header', 'price-box']; const required = { 'listing-header': ['title'], 'price-box': ['price'] }; const report = validateItems(items, known, required).map( (problem) => `item ${problem.index} (${problem.id}, ${problem.type}): ${problem.message}`, ); if (report.length > 0) throw new Error(report.join('\n'));

Update your package.json to run this check before astro build:

npx tsx scripts/check-content.ts && astro build

If validateItems finds any problems, the script will throw an error and exit with a non-zero code, stopping the build immediately.

Enforcing Required Props

Since .astro components do not have the same compile-time prop inference as React, validateItems is the only way to ensure that an item carries the data it needs to render.

By passing a required map, you can specify which props must be present and not blank (where “blank” means undefined, null, or a string of only whitespace).

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

validateItems only checks required props for items whose type is already known. If a type is unknown, it is reported once as an UNKNOWN_TYPE fault, and its props are ignored.

Registry vs. List of Names

validateItems can accept either a full component registry or a simple list of type names as the known argument.

  • List of Names: Ideal for Node scripts (like a CI build) where you cannot import .astro components.
  • Registry: Ideal for code running within the Astro/Vite pipeline (like a page’s frontmatter), as it ensures the validation and the rendering use a single source of truth for supported types.
import { validateItems } from '@evanion/astro-widget'; import { items } from '../examples/src/data/brass-birmingham'; import { registry } from '../examples/src/registry'; Object.keys(registry); // -> ['listing-header', 'price-box'] validateItems(items, registry); // -> [] validateItems(items, ['listing-header', 'price-box']); // -> []

Next Steps: Page Layout

Your build now stops on a missing type or a blank required prop. Page data, wrappers and nested items shows how to provide page-level data to your widgets, wrap them in shared markup, and create nested layouts.

Last updated on