@k11k/better-blocks-astro-renderer
v0.7.0
Published
Astro renderer for Strapi v5 Blocks content with full Better Blocks plugin support — colors, tables, to-do lists, media embeds, alignment, and more. Native Astro components, zero client-side JavaScript.
Maintainers
Readme
Table of Contents
- Why?
- Compatibility
- Installation
- Usage
- Supported Blocks
- Supported Modifiers
- Custom Renderers
- TypeScript
- Contributing
- Support this project
- License
Why?
The official Strapi blocks renderers are built for React. If your site is built with Astro, you can render Strapi blocks through the @astrojs/react integration — but that pulls React into your build for what is purely presentational content.
This package is a native Astro renderer. It renders Strapi v5 Blocks content — including every feature the Better Blocks plugin adds (color marks, text alignment, to-do lists, tables, media embeds, and more) — using plain .astro components. The output is static HTML with zero client-side JavaScript, and math is rendered to a string on the server (see Math (KaTeX)).
It is a drop-in renderer that handles all Better Blocks features out of the box — no configuration needed.
Compatibility
| Strapi Version | Renderer Version | Astro Version | | -------------- | ---------------- | ------------- | | v5.x | v0.x | ≥ 4 |
Installation
# Using yarn
yarn add @k11k/better-blocks-astro-renderer
# Using npm
npm install @k11k/better-blocks-astro-rendererPeer dependencies: astro >= 4
Usage
---
import { BlocksRenderer } from '@k11k/better-blocks-astro-renderer';
const { blocks } = Astro.props;
---
<BlocksRenderer content={blocks} />That's it. All Better Blocks features — colors, tables, to-do lists, media embeds, alignment, and more — work automatically, and the component renders to static HTML (no hydration, no client directive).
A typical page that fetches from Strapi:
---
import { BlocksRenderer, type BlocksContent } from '@k11k/better-blocks-astro-renderer';
// Import the KaTeX stylesheet once (e.g. in a shared layout) so math displays correctly.
import 'katex/dist/katex.min.css';
const res = await fetch('https://your-strapi.example.com/api/articles?status=published');
const { data } = await res.json();
---
{
data.map((article: { content: BlocksContent }) => (
<article>
<BlocksRenderer content={article.content} />
</article>
))
}Math (KaTeX)
Math nodes are rendered with KaTeX — inline math becomes a <span class="katex-inline"> and block math a <div class="katex-block">. Rendering happens via katex.renderToString on the server, so it works during SSR and static builds with no client-side hydration step.
KaTeX needs its stylesheet to display correctly. Import it once in your app (for example in a shared layout):
---
import 'katex/dist/katex.min.css';
---katex ships as a dependency of this package, so the stylesheet resolves without a separate install. If KaTeX fails to parse a formula, the renderer falls back to the raw LaTeX source instead of crashing.
Diagrams (Mermaid)
Mermaid diagram blocks ({ type: 'diagram', format: 'mermaid' }) are pre-rendered to inline SVG on the server using beautiful-mermaid — a pure-Node renderer that needs no headless browser (no Puppeteer, no Chromium download). Like math, rendering happens during SSR and static builds with zero client-side JavaScript and no hydration step.
Supported diagram types — flowchart, sequence, state, class, ER, and xychart — render to a <div class="mermaid-diagram"> wrapping the generated SVG. Diagram types beautiful-mermaid does not implement yet (gantt, pie, mindmap, gitGraph, …) and any source that fails to parse fall back gracefully to the raw definition in a <pre class="mermaid-source">, so content is never lost.
beautiful-mermaid ships as a dependency of this package, so no extra install or stylesheet is required.
Diagram colors
Diagrams render in color by default, with a palette that mirrors mermaid.js's familiar look (lavender node fills, purple borders, dark edges). Pass diagramTheme to pick a built-in palette (github-light, github-dark, dracula, nord, tokyo-night, catppuccin-mocha, solarized-light, …) or a custom color object ({ bg, fg, line, accent, muted, surface, border }):
---
import { BlocksRenderer } from '@k11k/better-blocks-astro-renderer';
const { blocks } = Astro.props;
---
<!-- built-in theme -->
<BlocksRenderer content={blocks} diagramTheme="github-dark" />
<!-- or a custom palette -->
<BlocksRenderer content={blocks} diagramTheme={{ bg: '#fff', fg: '#1f2328', accent: '#8250df' }} />
beautiful-mermaidderives a clean, single-accent palette from these colors — it is intentionally minimal, not a 1:1 clone of mermaid.js's multi-color default theme. To take full control of the markup (e.g. to render with the real mermaid.js on the client), override thediagramblock viablocks.diagram.
Callouts (Admonitions)
Block-level callout nodes render GitHub-style alerts in five variants — note, tip, important, warning, and caution. Each renders as an <aside role="note"> with a colored left border, a title row (icon + label), and the nested block children (paragraphs, lists, links, etc.). If a title is set on the node it is used; otherwise the localized variant label is shown.
Colors come from a small scoped <style> that ships with the component (still zero client-side JavaScript), and the default palette adapts to dark mode automatically via @media (prefers-color-scheme: dark). The accent for each variant is driven by a --bb-callout-accent custom property on the .bb-callout-{variant} element, so you can retheme colors from your own CSS without replacing the markup:
/* Recolor a single variant, or override per color scheme */
.bb-callout-note {
--bb-callout-accent: #2563eb;
}To replace the markup entirely, override the callout block. It receives variant and title; the nested children arrive via <slot />:
---
import { BlocksRenderer } from '@k11k/better-blocks-astro-renderer';
import MyCallout from '../components/MyCallout.astro';
const { blocks } = Astro.props;
---
<BlocksRenderer content={blocks} blocks={{ callout: MyCallout }} />Details / Summary (Collapsible)
Block-level details nodes render a native, keyboard-accessible <details> / <summary> disclosure with zero client-side JavaScript — the open/closed state is handled entirely by the browser. The summary field is the plain-text label, the optional defaultOpen boolean maps to the HTML open attribute (honored on initial render so screen readers get the correct state), and children are block-level content (paragraphs, lists, tables, images, and nested details) rendered after the summary. The default markup carries stable bb-details and bb-details-summary classes.
A small scoped <style> ships with the component (still zero client-side JavaScript): a GitHub-inspired card with a rotating disclosure marker. Retheme it from your own CSS via the --bb-details-* custom properties (--bb-details-border, --bb-details-bg, --bb-details-summary-bg, --bb-details-marker) without replacing the markup:
.bb-details {
--bb-details-border: #c8c8c8;
--bb-details-summary-bg: #eee;
}To replace the markup entirely, override the details block. It receives summary and defaultOpen; the nested children arrive via <slot />:
---
import { BlocksRenderer } from '@k11k/better-blocks-astro-renderer';
import MyDetails from '../components/MyDetails.astro';
const { blocks } = Astro.props;
---
<BlocksRenderer content={blocks} blocks={{ details: MyDetails }} />Buttons (CTA & File Download)
Block-level button nodes render a WordPress-style call-to-action as a single, accessible <a> (or a styled <span> when no target is set). Two modes are driven by buttonType:
- Link (
buttonType: 'link') →<a href={link.url} target rel aria-label>for a normal CTA.rel="noopener noreferrer"is honored when present (the editor adds it automatically fortarget="_blank"). - File (
buttonType: 'file') → a download link (<a href={file.url} download={file.name}>) for a Media Library asset, optionally prefixed with a file-type icon (showFileIcon) and suffixed with a human-readable size (showFileSize, e.g.(5 MB)).
Download vs. preview
By default a file button force-downloads the asset. The native download attribute only works same-origin, so for cross-origin assets (Strapi/CDN) browsers ignore it and open renderable files (PDF, video, images) inline. To fix that, download-mode buttons are tagged data-bb-download and a tiny scoped <script> (the renderer's only client-side JavaScript) fetches the asset as a blob and saves it from a same-origin object URL. This is progressive enhancement: without JS the anchor still works via its href + download attributes, and a CORS-blocked fetch falls back to native navigation.
Set filePreview: true to instead open the file in a new tab (target="_blank" rel="noopener noreferrer", no download) so users can preview it before saving — this path is fully zero-JS.
The optional style object is applied as inline CSS (backgroundColor, textColor, borderRadius, fontSize, fontWeight, padding, border), and alignment (left / center / right) wraps the button in a text-aligned .bb-button-wrapper (none renders it inline with no wrapper). A cssClass is appended to the default bb-button class for theming.
Because inline styles can't express :hover, the hoverBackgroundColor / hoverTextColor are exposed as --bb-button-hover-bg / --bb-button-hover-color custom properties, and a scoped <style> wires up the hover transition and a visible keyboard focus ring by default — no extra CSS required.
To replace the markup entirely, override the button block. It receives label, buttonType, alignment, link, file, showFileSize, showFileIcon, filePreview, style, and cssClass as props:
---
import { BlocksRenderer } from '@k11k/better-blocks-astro-renderer';
import MyButton from '../components/MyButton.astro';
const { blocks } = Astro.props;
---
<BlocksRenderer content={blocks} blocks={{ button: MyButton }} />Supported Blocks
| Block | Default element | Source |
| ------------------------------- | -------------------- | --------------------------- |
| paragraph | <p> | Strapi core |
| heading (1–6) | <h1>–<h6> | Strapi core |
| list (ordered/unordered/todo) | <ol> / <ul> | Strapi core + Better Blocks |
| list-item | <li> | Strapi core |
| link | <a> | Strapi core |
| quote | <blockquote> | Strapi core |
| code | <pre><code> | Strapi core |
| image | <figure><img> | Strapi core |
| horizontal-line | <hr> | Better Blocks |
| table | <table> | Better Blocks |
| media-embed | <iframe> (16:9) | Better Blocks |
| math (inline/block) | <span> / <div> | Better Blocks |
| diagram (mermaid) | <div> (inline SVG) | Better Blocks |
| callout (admonition) | <aside> | Better Blocks |
| details (collapsible) | <details> | Better Blocks |
| button (CTA / file download) | <a> / <span> | Better Blocks |
Block properties
| Property | Applies to | Description |
| -------------- | ------------------------- | --------------------------------------------------------- |
| textAlign | paragraph, heading, quote | Text alignment (left, center, right, justify) |
| lineHeight | paragraph, heading, quote | CSS line-height value (e.g. 1.5, 2.0) |
| indent | paragraph, heading, quote | Block indentation level (marginLeft: N * 2rem) |
| indentLevel | list | Cycling list-style-type per nesting depth |
| format | list | ordered, unordered, or todo |
| checked | list-item (in todo lists) | Checkbox state (true/false) |
| target | link | _blank for new-tab links |
| rel | link | noopener noreferrer for new-tab links |
| caption | image | Text displayed below the image |
| imageAlign | image | Image alignment (left, center, right) |
| url | media-embed | Embed URL (YouTube/Vimeo iframe src) |
| originalUrl | media-embed | Original user-provided URL |
| format | math | inline (<span>) or block (<div>) |
| value | math | LaTeX source rendered with KaTeX |
| format | diagram | mermaid (the only supported diagram format) |
| value | diagram | Mermaid source, pre-rendered to SVG on the server |
| summary | details | Plain-text label for the <summary> |
| defaultOpen | details | Open on initial render (HTML open attribute) |
| buttonType | button | link (CTA) or file (Media Library download) |
| label | button | Visible button text |
| alignment | button | left, center, right, or none (inline) |
| link | button | { url, target?, rel?, ariaLabel? } (link mode) |
| file | button | { url, name, size?, ext?, mime? } (file mode) |
| showFileIcon | button | Prefix a file-type icon (file mode) |
| showFileSize | button | Suffix a human-readable size, e.g. (5 MB) |
| filePreview | button | true opens the file in a new tab instead of downloading |
| style | button | Inline CSS + hover* colors via custom properties |
| cssClass | button | Extra class appended to bb-button |
Supported Modifiers
| Modifier | Default element | Source |
| ----------------- | --------------------------------- | ------------- |
| bold | <strong> | Strapi core |
| italic | <em> | Strapi core |
| underline | <span> | Strapi core |
| strikethrough | <del> | Strapi core |
| code | <code> | Strapi core |
| uppercase | <span style="text-transform"> | Better Blocks |
| superscript | <sup> | Better Blocks |
| subscript | <sub> | Better Blocks |
| color | <span style="color"> | Better Blocks |
| backgroundColor | <span style="background-color"> | Better Blocks |
| fontFamily | <span style="font-family"> | Better Blocks |
| fontSize | <span style="font-size"> | Better Blocks |
Custom Renderers
Override any block type or text modifier with your own Astro component. Pass a map of type → component via the blocks and modifiers props. Each custom component receives its props through Astro.props and its inner content through the default <slot />.
Custom block renderers
---
import { BlocksRenderer } from '@k11k/better-blocks-astro-renderer';
import MyParagraph from '../components/MyParagraph.astro';
import MyImage from '../components/MyImage.astro';
import MyTable from '../components/MyTable.astro';
const { blocks } = Astro.props;
---
<BlocksRenderer
content={blocks}
blocks={{
paragraph: MyParagraph,
image: MyImage,
table: MyTable,
}}
/>---
// src/components/MyImage.astro
const { image, caption, imageAlign } = Astro.props;
---
<figure style={{ textAlign: imageAlign }}>
<img src={image.url} alt={image.alternativeText || ''} loading="lazy" />
{caption && <figcaption>{caption}</figcaption>}
</figure>The props each custom block component receives:
| Block | Props (plus <slot /> for children where applicable) |
| ---------------------------------------------------------- | ------------------------------------------------------------- |
| paragraph | { style?} |
| heading | { level: 1–6; style? } |
| list | { format: 'ordered' \| 'unordered' \| 'todo'; indentLevel } |
| list-item | { checked? } |
| link | { url; target?; rel? } |
| quote | { style? } |
| code | { plainText } (also via <slot />) |
| image | { image; caption?; imageAlign? } (no slot) |
| horizontal-line | none |
| table / table-row / table-cell / table-header-cell | children via <slot /> |
| media-embed | { url; originalUrl? } (no slot) |
| math | { formula; inline } (no slot) — bring your own math engine |
| diagram | { code; format } (no slot) — bring your own diagram engine |
Custom modifier renderers
---
import { BlocksRenderer } from '@k11k/better-blocks-astro-renderer';
import Highlight from '../components/Highlight.astro';
const { blocks } = Astro.props;
---
<BlocksRenderer content={blocks} modifiers={{ backgroundColor: Highlight }} />---
// src/components/Highlight.astro
const { backgroundColor } = Astro.props;
---
<mark style={{ backgroundColor }}><slot /></mark>The color/size/font modifiers receive a value prop (color, backgroundColor, fontFamily, fontSize); the rest receive only their <slot />.
TypeScript
All types are exported:
import type {
BlocksContent,
BlocksRendererProps,
BlockNode,
TextNode,
LinkNode,
ListNode,
ListItemNode,
ParagraphNode,
HeadingNode,
QuoteNode,
CodeNode,
ImageNode,
HorizontalLineNode,
TableNode,
TableRowNode,
TableCellNode,
TableHeaderCellNode,
MediaEmbedNode,
MathNode,
DiagramNode,
TextAlign,
CustomBlocksConfig,
CustomModifiersConfig,
} from '@k11k/better-blocks-astro-renderer';Contributing
Contributions are welcome! The easiest way to get started is with Docker:
# Clone the repository
git clone https://github.com/k11k-labs/better-blocks-astro-renderer.git
cd better-blocks-astro-renderer
# Start the playground with Docker
cd playground
docker compose upThis will start a Strapi v5 instance with the Better Blocks plugin and an Astro app that renders the content — all pre-configured with a showcase article.
- Strapi admin: http://localhost:1337/admin (login:
[email protected]/admin12#) - Astro app: http://localhost:4321
Development workflow
- Edit the
.astrocomponents insrc/ - The Astro app picks up the change automatically — there is no build step
Without Docker
# Install dependencies (no build step — the renderer ships .astro source)
yarn install
# Start Strapi
cd playground/strapi && cp .env.example .env && npm install && npm run dev
# Start the Astro app (in another terminal)
cd playground/astro-app && npm install && npm run devRunning tests
yarn test # Run tests (Astro container API + Vitest)
yarn test:ts # Type check (astro check)
yarn lint # Check formattingCommunity & Support
- GitHub Issues — Bug reports and feature requests
Related
- @k11k/better-blocks-react-renderer — React renderer with the same Better Blocks support
- @k11k/strapi-plugin-better-blocks — Strapi plugin that extends the Blocks editor with colors, tables, to-do lists, media embeds, and more
Support this project
This package is built and maintained in my free time, and it's free for everyone. If it has saved you time on a project, you can help keep it caffeinated and actively developed:
Every coffee goes toward fixing bugs, reviewing PRs, writing docs, and shipping the features you ask for. Thank you! ☕
