Native Astro renderer for Strapi v5 Blocks content — supports all standard blocks plus Better Blocks features: color, highlight, text alignment, nested lists, to-do lists, tables, media embeds, image captions, and more. Zero client-side JavaScript.
- Why?
- Compatibility
- Installation
- Usage
- Supported Blocks
- Supported Modifiers
- Custom Renderers
- TypeScript
- Contributing
- Support this project
- License
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.
| Strapi Version | Renderer Version | Astro Version |
|---|---|---|
| v5.x | v0.x | ≥ 4 |
# Using yarn
yarn add @k11k/better-blocks-astro-renderer
# Using npm
npm install @k11k/better-blocks-astro-rendererPeer dependencies: astro >= 4
---
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 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.
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.
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.
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 }} />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 }} />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)).
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 }} />| 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 |
| 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 |
| 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 |
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 />.
---
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 |
---
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 />.
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';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:
admin@example.com/admin12#) - Astro app: http://localhost:4321
- Edit the
.astrocomponents insrc/ - The Astro app picks up the change automatically — there is no build step
# 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 devyarn test # Run tests (Astro container API + Vitest)
yarn test:ts # Type check (astro check)
yarn lint # Check formatting- GitHub Issues — Bug reports and feature requests
- @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
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! ☕
