Interact with my blogs by signing in

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

When I started building my SvelteKit project with Strapi CMS, I quickly ran into a frustrating roadblock.

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 3: Context Management

The React version used React Context to pass block and modifier components down. In Svelte 5, I implemented this using Svelte's createContext:


import { createContext } from 'svelte';
export const [getRenderCTX, setRenderCTX] = createContext<{
    blocks: Record<string, any>;
    addMissingBlockType: (type: string) => void;
}>();


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 with text modifiers including ✅ bold, ✅ italic, ✅ <u>underline</u>, ✅ ~~strikethrough~~, and inline code. It also supports:

Headings (h1-h6)

Paragraphs

Links

Lists (ordered and unordered)

Quotes

Code blocks

Images


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 with comprehensive type definitions for all block types.


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 for beautiful, consistent styling.


7. Error Handling

The renderer gracefully handles missing or unsupported block types by logging warnings to the console and continuing to render the rest of the content.


Key Features

✅ Svelte 5 Native - Built with modern runes syntax ($state, $props, $derived) and snippets

✅ Full TypeScript Support - Comprehensive type definitions for all block types

✅ Lightweight - No heavy dependencies, just pure Svelte components

✅ Customizable - Replace any block or modifier component with your own

✅ Complete Coverage - Supports all Strapi block types (headings, paragraphs, lists, quotes, code, images, links)

✅ Text Modifiers - Bold, italic, underline, strikethrough, and inline code

✅ Tailwind Compatible - Works seamlessly with Tailwind CSS v4

✅ Error Resilient - Gracefully handles missing components and continues rendering


Performance Tips

Lazy Loading Images: Consider using Svelte's loading="lazy" for images in a custom image component

Code Syntax Highlighting: Integrate a syntax highlighter like Prism or Shiki in your custom code block component

Caching: Cache your Strapi responses in SvelteKit's load functions


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


If you're building a SvelteKit project with Strapi, I hope sbr-mike saves you the time and frustration I experienced. The package is open-source, MIT licensed, and ready to use in production.


Resources


NPM Package: 

sbr-mike


GitHub Repository: 

github.com/chillingpulsar/sbr


Strapi Documentation: 

docs.strapi.io


Svelte 5 Documentation: 

svelte.dev

Enter your comment here...