@stelstone/astro-blocks
v0.14.0
Published
Built-in block renderer components for Stelstone sites built with Astro.
Readme
@stelstone/astro-blocks
Built-in block renderer components for Stelstone sites built with Astro. Ships all 12 standard block types out of the box. Override any block or add custom types via the components prop — no configuration files.
Install
npm i @stelstone/astro-blocksUsage
---
import BlockRenderer from "@stelstone/astro-blocks";
const { entry } = Astro.props;
---
<BlockRenderer blocks={entry.data.blocks} />blocks is the array from your Astro content collection entry — whatever entry.data.blocks contains after the CMS writes it.
Built-in block types
| Type | Renders as | Key properties |
|---------------|-------------------------------------|---------------------------------------------------------------|
| heading | <h2>–<h4> | text (required), level (2/3/4) |
| text | Rich HTML fragment | content (required, HTML) |
| html | Raw HTML fragment | content (required, HTML) |
| image | <figure> + <img> | src (required), alt, width |
| text-image | Two-column text + image | content (required), image (required), imageDescription, reversed |
| blockquote | <blockquote> | content (required), cite |
| list | <ul> or <ol> | items (required, JSON array), ordered |
| button | <a> styled as button | text (required), href (required), style |
| divider | <hr> | (none) |
| spacer | Empty <div> with height | size (tall/short) |
| video-embed | Embedded YouTube or Mux player | src (required), provider (youtube/mux) |
| columns | CSS Grid column layout | columnCount (2/3/4), width1…width4, children |
related-content is not built in
Resolving a relation needs the site's content store, which a renderer library cannot reach, so this block renders nothing until the site passes a component for it — and the warning names the type. The relation itself lives in the page's meta, which a component does not receive (it gets { properties, children, components }, not the entry), so resolve it before rendering and pass the entries in as a property. The Relations page has the whole setup, including the order to do it in.
Overriding built-in blocks
Pass a components map to replace any built-in renderer. Your component receives the same { properties, children?, components? } props.
---
import BlockRenderer from "@stelstone/astro-blocks";
import CdnImage from "./components/CdnImage.astro";
// CdnImage replaces the default <img> renderer with CDN-optimised output.
// All other block types use their built-in renderers.
---
<BlockRenderer
blocks={entry.data.blocks}
components={{ image: CdnImage }}
/>Override component interface:
---
// components/CdnImage.astro
interface Props {
properties: Record<string, unknown>;
components?: Record<string, unknown>; // forwarded for recursion
}
const { properties: p } = Astro.props;
const src = String(p.src ?? "");
const alt = String(p.alt ?? "");
---
{src && <figure><img src={src} alt={alt} loading="lazy" /></figure>}Adding custom block types
Define the block schema in cms.config.mjs (so the editor shows it in the block palette), then pass your renderer via components:
// cms.config.mjs
blocks: {
hero: {
label: "Hero",
icon: "fa-star",
properties: {
heading: { type: "text", label: "Heading", required: true },
image: { type: "image", label: "Background" },
},
defaults: { heading: "Welcome" },
},
},---
// src/components/blocks/hero.astro
interface Props {
properties: Record<string, unknown>;
}
const { properties: p } = Astro.props;
---
<section class="hero" style={`background-image:url(${p.image})`}>
<h1>{p.heading}</h1>
</section>---
// src/pages/[slug].astro
import BlockRenderer from "@stelstone/astro-blocks";
import HeroBlock from "../components/blocks/hero.astro";
---
<BlockRenderer
blocks={entry.data.blocks}
components={{ hero: HeroBlock }}
/>Custom blocks sit alongside built-ins — both appear in the editor palette and are rendered transparently.
Spacing
A block that sets spacingTop or spacing is wrapped in
.block-spacing[data-spacing-top][data-spacing]; a block that sets neither
gets no wrapper at all. The steps read --block-space-sm / -md / -lg from
your CSS, falling back to 0.5rem / 1.5rem / 3rem so the token means
something before the design defines them.
:root {
--block-space-sm: 0.75rem;
--block-space-md: 2rem;
--block-space-lg: 4rem;
}The wrapper clears the block's own margin only on the side being set, so setting a top space leaves the design's bottom rhythm alone.
Replacing this renderer with your own? Spread spacingAttrs() rather than
reading the tokens yourself — the editor shows those fields whatever you render
with, so a renderer that ignores them leaves the author with controls that do
nothing:
---
import { spacingAttrs } from "@stelstone/blocks";
const spacing = spacingAttrs(block.properties);
---
{spacing ? <div {...spacing}><MyBlock {...props} /></div> : <MyBlock {...props} />}Project images are built
An image or text-image whose value points at a file the project holds under
src/ (/src/assets/photo.jpg, with or without the leading slash) is rendered
with Astro's <Image>: resized to several widths, re-encoded, and given a
srcset, a sizes and its intrinsic dimensions. A CDN URL, a public/ path,
an SVG, or a path naming a file the project does not have renders the plain
<img> it always did.
Replacing the image component and want the same distinction? localImageEntry
and imageWidths are exported from @stelstone/blocks — see
docs/media.md.
Image fit
An image or text-image block with a fit carries it on its frame as
data-fit="cover|contain", plus data-focus for a cover's kept edge and
data-ratio for a lone image's shape; an image with an align carries
data-align="left|center|right". A block with none of these gets no
attribute.
The built-in CSS lays the frame out as a column and lets the picture grow or
shrink with its box — starting from its natural height, so with nothing to
fill (a lone image, or a column that stacked on a phone) the image looks as
it always did. A fit fills a stretched column only when the image is the
column's only block; beside other blocks it would push them out.
Replacing the image component? Spread imageFitAttrs() for the same reason
as spacingAttrs() above, and keep the frame's CSS:
---
import { imageFitAttrs } from "@stelstone/blocks";
const fit = imageFitAttrs(properties);
---
<figure {...(fit ?? {})}><img ... /></figure>Nested blocks (columns)
The columns block renders child block arrays recursively. Custom block types passed via components are available inside columns too — BlockRenderer forwards the map automatically.
Column widths arrive as a custom property rather than an inline
grid-template-columns:
<div class="block-columns cols-2" style="--block-columns-template: 1fr 2fr">That distinction matters. An inline grid-template-columns would outrank the
stacking rule, and a 1/3 + 2/3 block would stay side by side on a phone. If
you restyle the grid, keep the media query able to win:
@media (max-width: 640px) {
.block-columns { grid-template-columns: 1fr; }
}<BlockRenderer
blocks={entry.data.blocks}
components={{ hero: HeroBlock, image: CdnImage }}
/>
<!-- hero and CdnImage work inside column children too -->Props reference
<BlockRenderer>
| Prop | Type | Default | Description |
|--------------|-----------------------------------|---------|------------------------------------------|
| blocks | Block[] | required | Array from entry.data.blocks |
| components | Record<string, AstroComponent> | {} | Override or extend built-in block types |
| nested | boolean | false | Set by columns for recursive renders |
Block shape
interface Block {
id: string;
type: string; // matches a built-in or components key
properties: Record<string, unknown>; // editor-set values
children?: Block[][]; // columns only
}