Skip to main content

Building a Svelte 5 Blocks Renderer for Strapi: A Journey from React to Svelte

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:

  1. Write custom rendering logic for every single block type (tedious and error-prone)
  2. Use a different CMS (not ideal when Strapi fits perfectly otherwise)
  3. 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!

Github link

Facebook link