Skip to Content
Astro WidgetAPI Reference

These pages document @evanion/astro-widget 0.3.4, the version on npm.

API reference

@evanion/astro-widget publishes one component, Widgets.astro, and re-exports nine names from the widget core, @evanion/widget, which it pins to one exact version. Each re-exported name links to the widget core’s entry for it.

Widgets.astro

Widgets.astro renders a list of items, each through the component its type names in the registry, in the order the list gives them. It is published as .astro source under @evanion/astro-widget/components/Widgets.astro, because only Astro’s own compiler, running in your project, can compile it. Two items from a Baize listing page render through a registry of two widgets:

import { experimental_AstroContainer as AstroContainer } from 'astro/container'; import Widgets from '@evanion/astro-widget/components/Widgets.astro'; import { registry } from '../examples/src/registry'; const items = [ { id: 'header', type: 'listing-header', props: { title: 'Brass: Birmingham', players: '2-4' }, }, { id: 'price', type: 'price-box', props: { price: '649 kr' } }, ]; const container = await AstroContainer.create(); const html = await container.renderToString(Widgets, { props: { items, registry }, }); html; // -> '<header><h1>Brass: Birmingham</h1><p>2-4 players</p></header><p class="price">649 kr</p>'
PropTypeMeaning
itemsAnyWidgetItem[]the items to render
registryWidgetRegistryeach item type mapped to the .astro component for it
ctxRecord<string, unknown>optional; page-level data passed to every widget
chrome{ item?: unknown }optional; item is a component wrapped around every widget

Each widget receives the item’s props spread as its own props, plus id, ctx and children, which are set after the spread and win over a prop of the same name. It never receives meta. An items that is present and is not an array renders nothing and warns in development.

ctx

ctx reaches every widget as a prop of that name, so page-level data such as the store is written once on the page. An Astro component has no render-time context to read from, so ctx is a prop and there is no provider. @evanion/react-widget passes the same prop under the same name. Page data, wrappers and nested items renders a widget that reads it.

chrome

chrome.item wraps every widget. It receives the item’s type, id and meta, never its props, and it has to render <slot />, because an Astro component that renders no slot drops the widget with no error and no warning. @evanion/react-widget accepts a chrome.wrapper around the whole list. astro-widget has none, so put <Widgets> inside the element you want around the list. Page data, wrappers and nested items renders a chrome around a widget.

Nested items

Widgets.astro does not render an item’s children. It passes them to the widget as a prop named children that holds the items as data, because an Astro component takes rendered child content through <slot />. A widget that nests renders its own Widgets over them, through a registry in a module of its own. Page data, wrappers and nested items renders a grid of game cards and says why that registry has its own module.

Unknown types

An item whose type is not an own key of the registry renders nothing, and the rest of the list renders. Unless NODE_ENV is production, console.warn prints ERROR_MESSAGES.UNKNOWN_WIDGET for the item. It prints each distinct message once for the life of the process. Below, the registry holds no answer-wall, so only the price renders:

import { experimental_AstroContainer as AstroContainer } from 'astro/container'; import Widgets from '@evanion/astro-widget/components/Widgets.astro'; import { ERROR_MESSAGES } from '@evanion/astro-widget'; import { registry } from '../examples/src/registry'; const items = [ { id: 'questions', type: 'answer-wall', props: {} }, { id: 'price', type: 'price-box', props: { price: '649 kr' } }, ]; const container = await AstroContainer.create(); const html = await container.renderToString(Widgets, { props: { items, registry }, }); html; // -> '<p class="price">649 kr</p>' ERROR_MESSAGES.UNKNOWN_WIDGET('answer-wall', 'questions'); // -> 'Unknown widget type "answer-wall" for widget ID "questions". Skipping render.'

The lookup uses Object.prototype.hasOwnProperty, so an item typed constructor, toString or __proto__ is an unknown type and never resolves to a function on Object.prototype.

defineWidgets

function defineWidgets<R extends WidgetRegistry>(registry: R): R;
Signature — a shape, not a call

defineWidgets returns the registry it was handed, typed as the literal object passed in, so a code editor completes the type names. Annotating the same object as WidgetRegistry widens its keys to string. The widget core’s entry carries the rest.

import { defineWidgets } from '@evanion/astro-widget'; import ListingHeader from './widgets/ListingHeader.astro'; import PriceBox from './widgets/PriceBox.astro'; export const registry = defineWidgets({ 'listing-header': ListingHeader, 'price-box': PriceBox, });

validateItems

function validateItems( items: unknown, known: KnownWidgetTypes, required?: Record<string, string[]>, ): WidgetProblem[];
Signature — a shape, not a call

validateItems walks items and their children, and returns every problem it finds. It never throws and never stops at the first problem, so one call reports a whole list. It is the only check an astro-widget item’s props get. Below, answer-wall is not in the list of known types, and the price-box item has no price:

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' }]

Check CMS items before Astro renders them runs it before astro build, and the widget core’s entry carries the parameters.

AnyWidgetItem

interface AnyWidgetItem<Type extends string = string, Props = object> { id: string; type: Type; props: Props; meta?: WidgetMeta; children?: AnyWidgetItem[]; }
Signature — a shape, not a call

AnyWidgetItem is one item as the CMS saved it. Widgets.astro spreads props into the widget, hands meta to the chrome, and passes children to the widget as data. The Baize listing page assigns the JSON its CMS saved to AnyWidgetItem[], so the compiler checks each entry’s id, type and props:

import type { AnyWidgetItem } from '@evanion/astro-widget'; import saved from './brass-birmingham.json'; export const items: AnyWidgetItem[] = saved;

@evanion/react-widget offers WidgetItem<C>, a checked counterpart inferred from its components. astro-widget has none, for the reason under WidgetRegistry. The widget core’s entry carries the fields.

WidgetRegistry

type WidgetRegistry<T = unknown> = Record<string, T>;
Signature — a shape, not a call

WidgetRegistry maps a widget type to the component that renders it. astro-widget leaves T at unknown. An .astro module’s default export is an AstroComponentFactory, and its props live in the file’s frontmatter Props interface, out of the factory type’s reach, so a narrower T would check nothing. The widget core’s entry carries the rest.

WidgetMeta

type WidgetMeta = Record<string, unknown>;
Signature — a shape, not a call

WidgetMeta is the meta an item carries for its chrome, such as a background, a column or a span. Widgets.astro hands it to chrome.item and never to the widget, and a chrome that reads it narrows it itself, as the layout page’s Section.astro does with meta?: { background?: string }. The widget core’s entry carries the rest.

WidgetProblem

interface WidgetProblem { index: number; id: string; type: string; message: string; }
Signature — a shape, not a call

WidgetProblem is one line of the report validateItems returns. index is the item’s position in its own sibling list, and -1 when items is not an array. id and type are the item’s own, or - when that field is not a string. A problem carries no depth, so id tells apart two problems at index: 0 on different levels only when the ids are unique across the tree. The widget core’s entry carries the rest.

KnownWidgetTypes

type KnownWidgetTypes = WidgetRegistry | readonly string[];
Signature — a shape, not a call

KnownWidgetTypes is the set of widget types a list may use: a registry, or a plain list of type names. validateItems reads only a registry’s keys, so both report the same problems. A script run under plain Node, which cannot import .astro modules, passes the names. The example passes both:

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']); // -> []

The widget core’s entry carries the rest.

ERROR_MESSAGES

ERROR_MESSAGES holds the text Widgets.astro and @evanion/react-widget warn with, so a stale type reads the same way whether React or Astro drew the page. Widgets.astro uses three of the five keys:

KeyWidgets.astro uses it
UNKNOWN_WIDGETto warn that an item’s type is not an own key of the registry
MALFORMED_ITEMSto warn that items is present and is not an array
UNKNOWNin the UNKNOWN_WIDGET warning, for a type or id that is not a string

Only @evanion/react-widget warns with MALFORMED_ITEM and MALFORMED_CHILDREN. The Unknown types example prints UNKNOWN_WIDGET, and the widget core’s entry lists all five.

VALIDATION_MESSAGES

VALIDATION_MESSAGES holds every message validateItems reports, so a caller sorts a report without matching on prose. MISSING_FIELD is a function of the field name, and the other eight keys are strings. The widget core’s entry carries the rest.

KeyMessage
NOT_A_LISTitems is not a list
NOT_AN_OBJECTitem is not an object
INVALID_IDitem id is not a string
INVALID_TYPEitem type is not a string
UNKNOWN_TYPEunknown widget type
INVALID_PROPSprops is not an object
INVALID_CHILDRENchildren is not a list
DUPLICATE_IDduplicate sibling id
MISSING_FIELDmissing field <field>

A CMS webhook splits one report by message, unknown types for the developers and blank fields for the editor:

import { , } from '@evanion/astro-widget'; const = [ { : 'header', : 'listing-header', : { : '' } }, { : 'questions', : 'answer-wall', : {} }, ]; const = (, ['listing-header'], { 'listing-header': ['title'], }); const = .( () => . === ., ); const = .( () => . === .('title'), ); .(() => .); // -> ['questions'] .(() => .); // -> ['header']

What validateItems reports gives the list that produces each message.

Last updated on