Rendering Dynamic Blocks Using Nuxt
Having used Directus as a Headless CMS to create individual blocks that can be re-used on different pages, let's integrate them into your Nuxt 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 Nuxt concepts.
Set Up Your Directus Project
Set Permissions
Continuing on from the Create Reusable Blocks with Many-to-Any Relationships tutorial requires that we 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 may need set your content security policy to allow access to your Nuxt project. For example if you are self-hosting, or in development, and using Docker, then this is achieved by updating your docker-compose.yml
file as follows:
environment:
CONTENT_SECURITY_POLICY_DIRECTIVES__FRAME_SRC: your-website-url
Replace
your-website-url
with your Nuxt URL and the port. eg. If your Nuxt URL ishttp://localhost:3000
, replaceyour-website-url
withlocalhost:3000
.
Set Up Your Nuxt Project
Initialize Your Project
Create a new Nuxt project using Nuxi:
npx nuxi@latest init directus-blocks
cd directus-blocks
Inside our Nuxt project install the Directus SDK package by running:
npm install @directus/sdk
Remove <NuxtWelcome />
from app.vue
and replace with <NuxtPage />
so this it looks like as follows:
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtPage />
</div>
</template>
Create a pages
folder in the root of your project.
Configure Nuxt
Configure Nuxt so that it is able to communicate with the (external) Directus API.
Create a .env
file with the Directus URL:
API_URL="http://0.0.0.0:8055"
Add a type definition for our new environment variable by creating an env.d.ts
file with the following content:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Depending on your project configuration and if you are in development or production you may need to configure a Nuxt proxy to allow access between your Nuxt project and Directus in your nuxt.config.ts
:
routeRules: {
"/directus/**": { proxy: `${import.meta.env.API_URL}/**` },
},
This will allow your Nuxt project to access directus via your Nuxt URL, eg. http://localhost:3000/directus
Define a Directus Schema
Typescript needs to know what the structure of our Directus data will be. To achieve this create a directus.d.ts
file in the root of our project which defines our schema:
/// <reference types="@directus/extensions/api.d.ts" />
interface DirectusSchema {
pages: Page[];
}
interface Block {
id: string;
collection: string;
item: Hero | RichText | CardGroup;
}
interface Page {
title: string;
slug: string;
blocks: Block[];
}
interface Button {
label: string;
href: string;
variant: string;
}
interface Card {
id: number;
image: string;
content: string;
}
interface Post {
id: number;
title: string;
content: string;
}
interface Hero {
headline: string;
content: string;
buttons: Button[];
}
interface RichText {
headline: string;
content: string;
}
interface CardGroup {
headline: string;
content: string;
group_type: string;
posts: Post[];
cards: Card[];
}
interface Card {
image: string;
content: string;
}
Configure the Directus SDK
Create a new plugin file in the plugins
directory called directus.ts
:
import {
createDirectus,
rest,
readItems,
} from "@directus/sdk";
const directus = createDirectus<DirectusSchema>(
"http://your-website-url/directus",
).with(rest());
export default defineNuxtPlugin(() => {
return {
provide: { directus, readItems },
};
});
Replace
your-website-url
with your application's url and port.
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 new file /components/Hero.vue
with the following content:
<script setup lang="ts">
defineProps<{
id: string,
headline: string,
content: string,
buttons?: Button[]
image: string,
}>()
</script>
<template>
<h2>{{ headline }}</h2>
<div v-html="content"></div>
<div v-if="buttons">
<button v-for="button in buttons" :key="button.label" :href="button.href" class="button"
:class="button.variant">{{ button.label }}</button>
</div>
</template>
<style scoped>
.button {
background-color: grey;
color: white;
}
.button.primary {
background-color: green;
color: white;
}
.button.outline {
background-color: transparent;
border: 1px solid black;
color: black;
}
</style>
In this code defineProps
informs the component what data will be passed into it. The remainder of the code is the display template for the component. Note the use of v-html
to render the content
prop as HTML otherwise Nuxt would render the field as plain text.
Some basic CSS styling is included to differentiate a primary button from a standard one (you will recall out button has a variant
field that can be set to primary
or default
or outline
).
Rich Text Component
Create a new file /components/RichText.vue
with the following content:
<script setup lang="ts">
defineProps<{
id: string,
headline: string,
content: string
}>()
</script>
<template>
<h2>{{ headline }}</h2>
<div v-html="content"></div>
</template>
Card Group Component
Create a new file /components/CardGroup.vue
with the following content:
<script setup lang="ts">
defineProps<{
id: string,
headline: string,
content: string,
group_type: string,
posts: Post[],
cards: Card[]
}>()
</script>
<template>
<h2>{{ headline }}</h2>
<div v-html="content"></div>
<div v-if="group_type === 'posts'" v-for="post in posts" :key="post.id">
<h2>{{ post.title }}</h2>
<div v-html="post.content"></div>
</div>
<div v-if="group_type === 'custom'" v-for="card in cards" :key="card.id">
<h2>{{ card.image }}</h2>
<p>{{ card.content }}</p>
</div>
</template>
Page Component
Create a new file /components/Page.vue
and add the following:
<script setup lang="ts">
import Hero from './Hero.vue'
import RichText from './RichText.vue'
import CardGroup from './CardGroup.vue'
defineProps<{
title: String,
blocks: Block[]
}>()
const blockToComponent = (collectionName: string) => {
switch (collectionName) {
case 'block_hero':
return Hero
case 'block_richtext':
return RichText
case 'block_cardgroup':
return CardGroup
default:
return 'div'
}
}
</script>
<template>
<Head>
<title>{{ title }}</title>
</Head>
<h1>{{ title }}</h1>
<div v-for="block in blocks" :key="block.id">
<component :is="blockToComponent(block.collection)" v-bind="block.item"></component>
</div>
</template>
This component is the key to displaying our blocks. The blockToComponent
function maps the Directus Collection name (or block) to an actual Nuxt/Vue component which is then passed to Vue's dynamic component element with all its properties via the v-bind
attribute.
Dynamically Fetch Page Data
With all the components created let's create a Nuxt page that will display the Page itself. Create a new file /pages/[slug].vue
and add the following:
<script setup lang="ts">
const { $directus, $readItems } = useNuxtApp()
const route = useRoute()
const page: Ref<Page | null> = ref(null)
const { data, error } = await useAsyncData('post', async () => {
const slugParam = Array.isArray(route.params.slug) ? route.params.slug[0] : route.params.slug
return $directus.request($readItems('pages', {
filter: {
slug: { _eq: slugParam },
},
fields: ['title', 'slug', 'blocks.id', 'blocks.collection', 'blocks.item.*'],
limit: 1
}))
})
if (error.value || data.value === null || data.value.length === 0) {
console.error(error)
throw createError({
statusCode: 404,
statusMessage: "Page not found"
})
}
page.value = data.value[0]
</script>
<template>
<Page v-if="page" :title="page.title" :blocks="page.blocks"></Page>
<div v-else>Loading...</div>
</template>
This page uses the Directus plugin created earlier to fetch the requested page based on the slug
parameter. The Page
component is used to render the page by providing it with the title
and blocks
properties coming from the Directus SDK response.
Now we are ready to test the application. In Directus create a page and add some blocks to it.
In the example above the slug used is becoming-a-productive-rabbit
so we can use that to visit our page at http://your-wesite-url/becoming-a-productive-rabbit.
Summary
In this post, you learned to create a page builder in Directus and use it to display dynamic components in a Nuxt application.
The code for this tutorial is available on GitHub.