Rendering Dynamic Blocks Using SvelteKit
Having used Directus as a Headless CMS to create individual blocks that can be re-used on different pages, let's integrate them into our Svelte website.
Before You Start
You will need:
- A Directus project with the collections defined in our Create Reusable Blocks with Many-to-Any Relationships tutorial.
- Your public policy should have read permission on the
pages
, x, y collections. - Fundamental understanding of Svelte concepts.
Set Permissions
After creating reusable blocks with Many-to-Any relationships from the tutorual, you need to make the created collections readable by the public. To do this go to Settings -> Access Policies -> Public and add read permissions to all the collections created in the previous tutorial.
Configure Cors
You also need to make sure to configure CORS. Update your docker-compose.yml file as follows:
CORS_ENABLED: "true"
CORS_ORIGIN: "http://localhost:5173"
CORS_CREDENTIALS: "true"
Set Up Your SvelteKit Project
Initialize Your Project
To start building, you need to install SvelteKit and Directus sdk. Run this command to install SvelteKit:
npx sv create dynamic_blocks
When prompted, select SvelteKit minimal as the template. Do not add type-checking, as this tutorial is implemented in JavaScript. Your output should look like this:
Welcome to the Svelte CLI! (v0.6.16)
│
◇ Which template would you like?
│ SvelteKit minimal
│
◇ Add type checking with Typescript?
│ No
│
◆ Project created
│
◇ What would you like to add to your project? (use arrow keys / space bar)
│ none
│
◇ Which package manager do you want to install dependencies with?
│ npm
│
◆ Successfully installed dependencies
│
◇ Project next steps ─────────────────────────────────────────────────────╮
│ │
│ 1: cd dynamic_blocks │
│ 2: git init && git add -A && git commit -m "Initial commit" (optional) │
│ 3: npm run dev -- --open
Afterward, cd
into your project directory and install the Directus SDK by running this command:
npm install @directus/sdk
You need to initialize Directus SDK in your project. Create a file called directus.js
inside the ./src/lib
directory. Add the following code:
// src/lib/directus.js
import { createDirectus, rest } from '@directus/sdk';
const directus = createDirectus('http://localhost:8055/').with(rest());
export default directus;
Create Frontend Components
Going by the structure of our reusable blocks, let's create a single component for each individual collection.
Hero Component
Create a ./src/lib/components/Hero.svelte
file. Add the following code:
<!--src/lib/components/Hero.svelte-->
<script>
export let data;
</script>
<section class="hero">
<div class="text">
<h1>{data.headline}</h1>
<p>{@html data.content.replace(/<\/?p>/g, '')}</p>
<div class="buttons">
{#each data.buttons as button}
<a href={button.href} class="btn {button.variant}">{button.label}</a>
{/each}
</div>
</div>
</section>
<style>
.hero {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2rem;
background: #f5f5f5;
}
.text {
max-width: 50%;
}
.buttons a {
margin-right: 10px;
padding: 10px 20px;
border-radius: 5px;
text-decoration: none;
}
.primary {
background: blue;
color: white;
}
</style>
The code above is a hero section that dynamically displays a headline, content, buttons, and an optional image based on the data prop it receives.
Rich Text Component
Create a src/lib/components/RichText.svelte
file. Add the following code:
<!--src/lib/components/RichText.svelte-->
<script>
export let data;
</script>
<section class="rich-text">
<h2>{data.headline}</h2>
<div class="content">{@html data.content.replace(/<\/?p>/g, '')}</div>
</section>
<style>
.rich-text {
padding: 2rem;
background: white;
}
.content {
font-size: 1rem;
line-height: 1.5;
}
</style>
Card Group Component
Create a src/lib/components/CardGroup.svelte
file. Add the following code:
<!--- src/lib/components/CardGroup.svelte-->
<script>
export let data;
</script>
<section class="card-group">
<h2>{data.headline}</h2>
<p>{@html data.content.replace(/<\/?p>/g, '')}</p>
<div class="cards">
{#each data.cards as card}
<div class="card">
<p>{card.content}</p>
</div>
{/each}
</div>
</section>
<style>
.card-group {
padding: 2rem;
}
.cards {
display: flex;
gap: 1rem;
}
.card {
padding: 1rem;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
Page Component
Create a fetchPage.js
file inside the ./src/lib
directory to use the Directus client to fetch pages. Add the foloowing code:
// src/lib/fetchPage.js
import directus from './directus';
import { readItems } from '@directus/sdk';
export async function fetchPage(slug) {
try {
const response = await directus.request(
readItems('pages', {
filter: { slug: { _eq: slug } },
fields: [
'*',
{
blocks: [
'*',
{
item: ['*']
}
]
}
],
limit: 1
})
);
if (!response || response.length === 0) {
console.warn(`No page found for slug: ${slug}`);
return null;
}
let pageData = response[0];
pageData.blocks = Array.isArray(pageData.blocks) ? pageData.blocks : [];
console.log("Fetched page data:", pageData);
console.log("Blocks Data:", pageData.blocks);
return pageData;
} catch (error) {
console.error("Error fetching page:", error);
return null;
}
}
The code above fetches a page from Directus by looking up its slug, retrieves its content and blocks, ensures valid data formatting, and handles errors gracefully.
Dynamically Fetch Page Data
You need a dynamic route to help import your page builder components, call your pages
collection via the API, and add a filter rule to match the requested page’s `slug. '
Create a src/routes/[slug]/+page.svelte
file. Add the following code:
<!--src/routes/[slug]/+page.svelte-->
<script>
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { fetchPage } from '$lib/fetchPage'
import PageBuilder from '$lib/pageBuilder.svelte';
import { get } from 'svelte/store';
let pageData = null;
async function loadPage() {
const slug = get(page).params.slug;
pageData = await fetchPage(slug);
}
onMount(loadPage);
</script>
{#if pageData}
<PageBuilder blocks={pageData.blocks} />
{:else}
<p>Loading...</p>
{/if}
Map Blocks to Components
Create a src/lib/pageBuilder.svelte
file. Add the following code:
<!--- src/lib/pageBuilder.svelte-->
<script>
import Hero from '$lib/components/Hero.svelte';
import RichText from '$lib/components/RichText.svelte';
import CardGroup from '$lib/components/CardGroup.svelte';
export let blocks = [];
const blockMap = {
block_hero: Hero,
block_richtext: RichText,
block_cardgroup: CardGroup
};
</script>
{#if Array.isArray(blocks) && blocks.length > 0}
{#each blocks as block (block.id)}
{#if block?.collection && blockMap[block.collection]}
<svelte:component
this={blockMap[block.collection]}
data={(typeof block.item === 'object') ? block.item : {}} />
{:else}
<p>Unknown block type: <strong>{block.collection}</strong></p>
{/if}
{/each}
{:else}
<p>No blocks found.</p>
{/if}
The code above maps all the possible page.pages_blocks.collection
names to your page block components.
It also loops through the page.blocks
array and passes the correct data (props) that each page_builder component needs to render properly.
Test the Application
To test the project, run this command:
npm run dev
In Directus, create a page and add some blocks to it.
Visit http://your-wesite-url/your-slug
to see the result. For this example, it's going to be localhost:5173/rabbit
.
Summary
In this post, you learned how to create a page builder in Directus and use it to display dynamic components in a Svelte application.
Dynamic blocks enhance user experience. It can be used in various sections of your website, and Directus makes it easy to implement.