The Problem: No Native Strapi Support for Svelte
When I started building my SvelteKit project with Strapi CMS, I quickly ran into a frustrating roadblock. Strapi's rich text editor outputs content in a structured JSON format (blocks), but the official `@strapi/blocks-react-renderer` package only works with React. As a fresh Svelte 5 and SvelteKit user, I found myself without a proper solution to render Strapi's rich content.
The options were limited:
- Write custom rendering logic for every single block type (tedious and error-prone)
- Use a different CMS (not ideal when Strapi fits perfectly otherwise)
- Create a Svelte port of the React renderer (challenging but worthwhile)
I chose option 3, and **sbr-mike** (Svelte Blocks Renderer) was born.
The Journey: Porting from React to Svelte 5
Porting the React blocks renderer to Svelte 5 came with its own set of challenges and learning experiences:
Challenge 1: Understanding the Block Structure
Strapi's rich text content comes as an array of block objects:
[
{
type: 'heading',
level: 1,
children: [{ type: 'text', text: 'My Title', bold: true }]
},
{
type: 'paragraph',
children: [
{ type: 'text', text: 'This is ' },
{ type: 'text', text: 'bold text', bold: true },
{ type: 'text', text: ' and ' },
{ type: 'text', text: 'italic text', italic: true }
]
}
];
Each block can contain nested children with various text modifiers (bold, italic, underline, strikethrough, inline code).
Challenge 2: Adapting to Svelte 5's New Syntax
Svelte 5 introduced runes (`$state`, `$props`, `$derived`) and snippets, which required rethinking how components receive and render children:
**React approach:**
function Paragraph({ children }) {
return <p>{children}</p>;
}
**Svelte 5 approach with snippets:**
<script lang="ts">
let { children }: { children: Snippet } = $props();
</script>
<p>{@render children()}</p>
Challenge 2: Adapting to Svelte 5's New Syntax
The React version used React Context to pass block and modifier components down. In Svelte 5, I implemented this using Svelte's `createContext`:
<script lang="ts" module>
import { createContext } from 'svelte';
export const [getRenderCTX, setRenderCTX] = createContext<{
blocks: Record<string, any>;
addMissingBlockType: (type: string) => void;
}>();
</script>
The Solution: sbr-mike
After overcoming these challenges, I successfully created a lightweight, fully-typed Svelte 5 blocks renderer that works seamlessly with Strapi.
Installation
bun add sbr-mike
// or
npm i sbr-mike
// or
pnpm add sbr-mike
// or
yarn add sbr-mike
Basic Usage
1. Simple Content Rendering
The most basic use case - rendering Strapi content in your SvelteKit page:
<script lang="ts">
import { BlocksRenderer } from 'sbr-mike';
import type { BlocksContent } from 'sbr-mike';
// This would typically come from your Strapi API
const content: BlocksContent = [
{
type: 'heading',
level: 1,
children: [{ type: 'text', text: 'Welcome to My Blog' }]
},
{
type: 'paragraph',
children: [
{ type: 'text', text: 'This is a ' },
{ type: 'text', text: 'simple example', bold: true },
{ type: 'text', text: ' of the blocks renderer.' }
]
}
];
</script>
<BlocksRenderer {content} />
2. Real-World Example: Fetching from Strapi
Here's how you'd use it with actual Strapi data in SvelteKit:
// src/routes/blog/[slug]/+page.ts
export async function load({ params, fetch }) {
const response = await fetch(
`https://your-strapi-api.com/api/articles?filters[slug][$eq]=${params.slug}&populate=*`
);
const data = await response.json();
return {
article: data.data[0]
};
}
// src/routes/blog/[slug]/+page.svelte
<script lang="ts">
import { BlocksRenderer } from 'sbr-mike';
import type { BlocksContent } from 'sbr-mike';
let { data } = $props();
const content: BlocksContent = data.article.attributes.content;
</script>
<article class="prose lg:prose-xl">
<h1>{data.article.attributes.title}</h1>
<BlocksRenderer {content} />
</article>
3. All Supported Block Types
The renderer supports all common Strapi block types:
<script lang="ts">
import { BlocksRenderer } from 'sbr-mike';
import type { BlocksContent } from 'sbr-mike';
const richContent: BlocksContent = [
// Headings (h1-h6)
{
type: 'heading',
level: 2,
children: [{ type: 'text', text: 'Features' }]
},
// Paragraphs with text modifiers
{
type: 'paragraph',
children: [
{ type: 'text', text: 'Text with ' },
{ type: 'text', text: 'bold', bold: true },
{ type: 'text', text: ', ' },
{ type: 'text', text: 'italic', italic: true },
{ type: 'text', text: ', ' },
{ type: 'text', text: 'underline', underline: true },
{ type: 'text', text: ', ' },
{ type: 'text', text: 'strikethrough', strikethrough: true },
{ type: 'text', text: ', and ' },
{ type: 'text', text: 'inline code', code: true }
]
},
// Links
{
type: 'paragraph',
children: [
{ type: 'text', text: 'Check out ' },
{
type: 'link',
url: 'https://github.com/chillingpulsar/sbr',
children: [{ type: 'text', text: 'the GitHub repo' }]
}
]
},
// Lists (ordered and unordered)
{
type: 'list',
format: 'unordered',
children: [
{
type: 'list-item',
children: [{ type: 'text', text: 'First item' }]
},
{
type: 'list-item',
children: [{ type: 'text', text: 'Second item' }]
},
{
type: 'list-item',
children: [{ type: 'text', text: 'Third item' }]
}
]
},
// Quotes
{
type: 'quote',
children: [
{
type: 'text',
text: 'This is a blockquote. Perfect for highlighting important information.'
}
]
},
// Code blocks
{
type: 'code',
children: [
{ type: 'text', text: 'const greeting = "Hello, Svelte 5!";\nconsole.log(greeting);' }
]
},
// Images
{
type: 'image',
image: {
name: 'example.jpg',
alternativeText: 'Example image',
url: 'https://example.com/image.jpg',
caption: 'An example image',
width: 1200,
height: 800,
hash: 'example_hash',
ext: '.jpg',
mime: 'image/jpeg',
size: 245.6,
provider: 'local',
createdAt: '2025-01-01T00:00:00.000Z',
updatedAt: '2025-01-01T00:00:00.000Z'
},
children: [{ type: 'text', text: '' }]
}
];
</script>
<BlocksRenderer content={richContent} />
4. Customizing Components
One of the most powerful features is the ability to replace default components with your own:
<script lang="ts">
import { BlocksRenderer } from 'sbr-mike';
import type { BlocksContent } from 'sbr-mike';
// Your custom components
import CustomHeading from './CustomHeading.svelte';
import CustomParagraph from './CustomParagraph.svelte';
import CustomBold from './CustomBold.svelte';
const content: BlocksContent = [
/* your content */
];
// Override default block components
const customBlocks = {
heading: CustomHeading,
paragraph: CustomParagraph
};
// Override default modifier components
const customModifiers = {
bold: CustomBold
};
</script>
<BlocksRenderer {content} blocks={customBlocks} modifiers={customModifiers} />
5. TypeScript Support
The package is fully typed, making it easy to work with in TypeScript projects:
import type {
BlocksContent,
RootNode,
TextInlineNode,
ParagraphBlockNode,
HeadingBlockNode,
ListBlockNode,
ImageBlockNode,
LinkInlineNode,
Modifier
} from 'sbr-mike';
// Type-safe content creation
const content: BlocksContent = [
{
type: 'heading',
level: 1, // TypeScript will ensure this is 1-6
children: [{ type: 'text', text: 'Hello' }]
}
];
// Type for a single block
const paragraph: ParagraphBlockNode = {
type: 'paragraph',
children: [{ type: 'text', text: 'This is typed!' }]
};
6. Working with Tailwind
The renderer works perfectly with Tailwind CSS. The default components use minimal styling, so you can easily wrap the renderer in a container with Tailwind's typography plugin:
<script lang="ts">
import { BlocksRenderer } from 'sbr-mike';
let { data } = $props();
</script>
<article
class="prose prose-lg prose-slate prose-headings:text-primary
prose-headings:font-bold prose-a:text-blue-600
prose-a:no-underline hover:prose-a:underline prose-code:text-pink-600
prose-code:bg-slate-100 prose-code:px-1 prose-code:rounded prose-pre:bg-slate-900
prose-pre:text-white max-w-none"
>
<BlocksRenderer content={data.article.content} />
</article>
Conclusion
Porting the Strapi blocks renderer from React to Svelte 5 was a challenging but rewarding experience. It not only solved my immediate problem but also gave me deep insights into:
- Svelte 5's new runes and snippet system
- How to properly structure reusable component libraries
- The differences and similarities between React and Svelte architectures
- TypeScript best practices for component libraries
Resources
NPM Package : sbr-mike
GitHub Repository : github.com/chillingpulsar/sbr
Strapi Documentation: docs.strapi.io
Svelte 5 Documentation: svelte.dev
Have questions or suggestions? Feel free to open an issue on GitHub or contribute to the project! -Mikey
Connect with me!