Skip to Content
Astro WidgetGetting Started

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.

Getting started

The registry

// src/registry.ts import { defineWidgets } from '@evanion/astro-widget'; import ListingHeader from './widgets/ListingHeader.astro'; import GameGrid from './widgets/GameGrid.astro'; export const registry = defineWidgets({ 'listing-header': ListingHeader, 'game-grid': GameGrid, });

defineWidgets comes from @evanion/widget and returns the object unchanged; the core’s entry is where it is written down. That declaration is what gives an editor the key union it completes on.

Only the keys are read, so the helper is shown here over stand-ins for the two .astro modules — what it returns is the object it was handed:

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

Rendering

Every .astro block on this page carries a mark saying nothing executed it, and that is the package rather than the budget: an .astro module is compiled by Astro’s own compiler against a project, there is no runtime for one in a browser, and no bundler in scope produces one. The half of this package that is plain TypeScript — the registry, the item shape, the validator — runs under nx test @evanion/astro-widget and the pages cite those runs.

--- import Widgets from '@evanion/astro-widget/components/Widgets.astro'; import { registry } from '../registry'; import page from '../data/page.json'; --- <Widgets items={page.sections} registry={registry} />
Not executed — nothing here runs it
PropMeaning
itemsthe widgets to render
registrywidget type to component
ctxpage-level data, passed to every widget as a ctx prop
chrome{ item } — a component rendered around every widget

An items that is not an array renders nothing rather than throwing.

The data shape

An item is id, type, props, and optional meta and children. Getting started in the widget core teaches the four fields and the core’s reference carries the interface. The same item an @evanion/react-widget consumer writes, which is what makes one payload render through both.

What this renderer does with it: a CMS that writes

{ "id": "header", "type": "listing-header", "props": { "title": "Brass: Birmingham", "mechanism": "Economic" } }

renders <ListingHeader title="Brass: Birmingham" mechanism="Economic" id="header" ctx={ctx} />.

props is spread. id reaches the widget as well as the chrome, because a widget often wants it as an anchor target. meta does not: where a widget sits is not something the widget should know.

ctx

<Widgets items={sections} registry={registry} ctx={{ site: Astro.site, locale }} />
Not executed — nothing here runs it

Every widget receives ctx as a prop. A prop rather than a provider: an Astro component runs once, top down, and has no render-time context to read from.

This is the counterpart to @evanion/react-widget’s ctx, under the same name, so a widget ported between the two reads the same field.

Chrome

Wrap every widget without each widget reimplementing section markup:

<Widgets items={items} registry={registry} chrome={{ item: Section }} />
Not executed — nothing here runs it

Section receives the item’s type, id and meta — never its props, which are the widget’s own business — and it must render <slot />:

--- // Section.astro interface Props { type: string; id: string; meta?: { background?: string }; } const { type, id, meta } = Astro.props; --- <section id={id} data-widget-type={type} class:list={['widget', meta?.background && `bg-${meta.background}`]} > <slot /> </section>
Not executed — nothing here runs it

An Astro component that does not render its slot discards the content passed to it, so a chrome missing <slot /> makes every widget vanish from the page — with nothing thrown and nothing logged. That is the first thing to check when a page comes out empty.

meta is the placement channel: grid column, background, CMS edit affordances. It is handed to the chrome and never spread into the widget.

There is no region wrapper. The container is whatever element you put <Widgets> inside, and one the library supplied would be markup you did not ask for. @evanion/react-widget has a chrome.wrapper because a React region has to return one element; an Astro fragment does not.

Nesting

The renderer does not recurse. children on an item is forwarded to its widget as ordinary prop data, nothing more — Astro projects child content through <slot />, never through a children prop, so there is nothing for a renderer to pass nested items into.

A widget that wants nested sections renders them itself:

--- // GameGrid.astro import Widgets from '@evanion/astro-widget/components/Widgets.astro'; import { registry } from '../registry'; const { children = [], title } = Astro.props; --- <section> <h2>{title}</h2> <Widgets items={children} registry={registry} /> </section>
Not executed — nothing here runs it

The registry has to be imported again, because the nested <Widgets> is a fresh call with its own props. Importing it from one module — as src/registry.ts above — keeps that from becoming a second registry that drifts.

Unknown types are skipped

An item whose type is not a key of the registry renders nothing and draws a dev-only console.warn naming the type and the id. Nothing throws: a gap in build-time validation must not take a page down mid-render.

Run validateItems at build time, or a bad CMS save is a missing section with only a line in a log to say so.

Registry lookup uses Object.prototype.hasOwnProperty, so an item typed constructor, toString or __proto__ is skipped like any other unknown type rather than resolving to a function off Object.prototype and reaching Astro as a component. Items are CMS data, so any string is reachable.

Last updated on