A common challenge one would face when using any CMS is the constant back and forth between developer and content teams. Be it planning and updating content structures in the backend or rearranging content on the frontend of an application, making changes almost always means additional work for both teams. It gets even harder when there is uncertainty around the content structure, how often the content will be updated, and what it will look like. Strapi has an interesting way of making this process more efficient and it comes in the form of a feature called Dynamic Zones.
Dynamic Zones is a new native feature in Strapi that lets teams build reusable content models and minimize the number of changes developers need to make to add new content. It lets your developers build web experiences and have a good night's sleep without worrying about all the content being seamlessly added by the content managers.
In this tutorial, you will build a company website with Nuxt.js that utilizes Strapi Dynamic Zones. You will also learn how to consume data from the Strapi GraphQL API and render that data in a Nuxt application.
Let’s get into what this company website will look like. The website will have four pages - a home page, about page, a team page, and a testimonials page. The pages share a few common elements;, each has a title and description. However, the about page needs a few images and some rich text, while the testimonial page needs a couple of quotes and an image.
The remaining pages follow suit and need different types of content. Besides building a content model, we might need to update content by adding a few customer reviews, team quotes, and even add a video or two.
Traditionally, this would mean creating different content models for each page, with Dynamic Zones you can create a single reusable content model that gives you the flexibility to add content you need on the fly. Visually it would look a little like this.
Now that you understand where Dynamic Zones fit in your application and content workflow, you can get started with building one.
1. Installing Strapi and creating a new project Open your terminal.
yarn create strapi-app api --quickstart
- this creates a new folder called api and builds the admin UI. 2. Create an Administrator user Navigate to http://localhost:1337/admin.
3. Create a Pages Collection Type Navigate to Content-Types Builder under Plugins in the left-hand menu.
These fields will store the title and description of your pages.
You will need images, quotes, and rich text in your Dynamic Zone, so you will build out components for each.
Navigate to Content-Types Builder under Plugins in the left-hand menu.
5. Create a Quote component Navigate to Content-Types Builder under Plugins in the left-hand menu.
6. Create a Rich Text component Navigate to Content-Types Builder under Plugins in the left-hand menu.
Now that you have created your components, you can create a Dynamic Zone.
7. Create a Dynamic Zone Navigate to Content-Types Builder under Plugins in the left-hand menu.
There you go! You have a Dynamic Zone in your Pages collection. It should look like this.
Now that you've built your Pages collection, you can add content.
8. Add content to the Pages Collection Type Navigate to Pages under Collection Types in the left-hand menu.
Title: About Description: What we're about image: Image of Man on a Desk caption: Who we are as a company richText: We're a company specialized in keeping people happy. Our products speak to this goal. Do you want it? We've got it.
Click Save
You can get the content for the teams, testimonials, and home page on this Notion page.
It’s here that you start to see what Dynamic Zones can do. While adding content, whenever you click ‘Add to pageZone’, you can add different components to your page.
Before making requests, you need to make your Pages collection type accessible by tweaking its permissions.
9. Set Roles and Permissions Navigate to Settings then Roles & Permissions**.
You have a collection type, you’ve added some content to it, you’ve set up permissions, let’s send requests to your API? Yes!
10. Send requests to the Collection Types API Navigate to http://localhost:1337/api/pages?populate=* to query your data.
If everything went smoothly, you should get back some JSON data containing the content you just added. For this tutorial, however, you useStrapi's GraphQL API.
To enable it, navigate to ./api
cd api
to change directoriesyarn strapi install graphql
to install the GraphQL plugin or
Navigate to Marketplace under General in the left-hand menu.
When you have the GraphQL plugin up and running, you can test queries in your GraphQL Playground.
That is all for your backend. Now to render all this beautiful content.
1. Installing Nuxt and creating a new project Open your terminal.
npx nuxi init client
- this creates a folder called client that houses our Nuxt frontend. cd client
to change directoriesyarn install
to install your project dependencies yarn dev
to run your app on https://localhost:3000 to see that everything is working well.You should see the brand new Nuxt starter page.
Your application will be built out with a couple of components.
2. Building our Header component Navigate to ./client/components
Create a file called NavBar.vue and paste the following code in it.
1<template>
2 <div class="header">
3 <div class="container">
4 <div class="left">
5 <NuxtLink to="/" class="home-link">
6 <b>Company X </b>
7 </NuxtLink>
8 </div>
9 <nav class="nav right">
10 <NuxtLink class="nav__link" to="/about">About Us</NuxtLink>
11 <NuxtLink class="nav__link" to="/team">Team</NuxtLink>
12 <NuxtLink class="nav__link" to="/testimonials">Testimonials</NuxtLink>
13
14 </nav>
15 </div>
16 </div>
17</template>
18
19<script>
20export default {
21
22};
23</script>
24
25<script setup>
26
27</script>
28
29
30<style scoped>
31.header {
32 position: relative;
33 height: 6rem;
34 z-index: 10;
35}
36.header.sticky {
37 position: fixed;
38 top: 0;
39 left: 0;
40 width: 100%;
41}
42.header > .container {
43 display: flex;
44 align-items: center;
45 justify-content: space-between;
46 height: 100%;
47}
48.home-link {
49 text-decoration: none;
50}
51.logo {
52 height: 4.5rem !important;
53}
54.site-name {
55 font-size: 0.9rem;
56 font-weight: 700;
57 letter-spacing: 0.05em;
58 text-decoration: none;
59 text-transform: uppercase;
60}
61.nav > * {
62 font-size: 0.9rem;
63 font-weight: 600;
64 text-decoration: none;
65 margin-top: 4px;
66 margin-right: 3rem;
67 padding-bottom: 4px;
68 border-bottom: 1px solid;
69 border-color: transparent;
70 transition: border 0.15s;
71}
72.nav > *:last-of-type {
73 margin: 0;
74}
75.nav > *:hover {
76 border-color: inherit;
77}
78.nav > .active {
79 border-color: inherit;
80}
81</style>
This component gives you a navbar to navigate through different pages with the <NuxtLink>
tag.
3. Building our Hero component Navigate to ./client/components
Create a file called Hero.vue and paste the following in it.
1<template>
2 <div class="hero">
3
4 <!-- Displays page Title and Description -->
5
6 <h1 class="hero-title" v-html="content.Title" />
7 <h2 class="hero-subtitle" v-html="content.Description" />
8
9
10 </div>
11</template>
12
13<script setup>
14</script>
15
16<script>
17export default {
18 // content is query data from parent page
19 // title is the page title of parent page
20 props: {
21 content: Object,
22 title: String
23 }
24};
25</script>
26
27<style>
28.hero {
29 text-align: center;
30 width: 480px;
31 max-width: 100%;
32 margin: 0 auto;
33 padding: 4rem 0 8rem 0;
34}
35.hero-title {
36 font-size: 3rem;
37 font-weight: 700;
38 padding: 0;
39 margin: 0 0 2rem 0;
40}
41.hero-title p,
42.hero-subtitle p {
43 margin: 0;
44 padding: 0;
45}
46.hero-subtitle {
47 font-size: 1.15em;
48 font-weight: 400;
49 line-height: 1.68;
50 padding: 0;
51 margin: 0;
52 opacity: 0.6;
53}
54</style>
This component gets the props, content
and title
passed to it from its parent. title
is the name of the page you want to display data for. content
is the query result containing the page content you added earlier. Then you go on to display the title and description.
4. Building our ContentGrid component Navigate to ./client/components
Create a file called ContentGrid.vue and paste the following code in it.
1<template>
2 <div class="hero">
3 <div v-for="zone in content.pageZone" :key="zone.id" class="project">
4 <!-- Display all richText here -->
5 <div class="sub-text" v-if="zone.__typename === 'ComponentPostRichText'">
6 <p>{{ zone.richText }}</p>
7 </div>
8
9 <!-- Displays all Quotes here -->
10 <div v-if="zone.__typename === 'ComponentPostQuote'">
11 <p class="subtitle">{{ zone.quote }}</p>
12 <p class="sub-text">by {{ zone.quoter}}</p>
13 </div>
14
15 <!-- Displays all Images -->
16 <div class="sub-text" v-if="zone.__typename === 'ComponentPostImage'">
17 <img :src="zone.image.data.attributes.url" />
18 <p class="sub-text hero-subtitle">{{ zone.caption }}</p>
19 </div>
20 </div>
21 </div>
22</template>
23
24<script setup>
25</script>
26
27<script>
28export default {
29 // content is query data from parent page
30 // title is the page title of parent page
31 props: {
32 content: Object,
33 title: String
34}
35};
36</script>
37
38<style scoped>
39.hero {
40 text-align: center;
41 width: 650px;
42 max-width: 100%;
43 margin: 0 auto;
44 padding: 1rem 0 8rem 0;
45}
46.sub-text {
47 padding: 0 0 3rem 0;
48}
49.hero-subtitle p {
50 margin: 0;
51 padding: 0;
52}
53.hero-subtitle {
54 font-size: 1em;
55 font-weight: 400;
56 line-height: 1.68;
57 padding: 0;
58 margin: 0;
59 opacity: 0.6;
60}
61.subtitle {
62 font-size: 1.5em;
63 font-weight: 500;
64 line-height: 1.68;
65 padding: 0;
66 margin: 0;
67 opacity: 0.6;
68}
69.project {
70 grid-column: auto / span 2;
71 text-align: center;
72}
73@media (min-width: 920px) {
74 .project {
75 grid-column: auto / span 1;
76 }
77 .project:nth-child(3n + 1) {
78 grid-column: auto / span 2;
79 }
80}
81</style>
This is where you render the content from your Dynamic Zone. Just like Hero.vue, ContentGrid.vue gets props, content
, and title
passed from its parent. title
is the name of the page that you want to display data for. content
is the query result that contains the page content you added earlier. The Dynamic Zone data is stored in contents.pageZone
and you use v-for
to iterate through it.
Each component in the Dynamic Zone has its own type. In the code above, you create a zone in our component to display all the rich text by comparing the __typename
with the type of the rich text component. You get this from exploring our schema in our GraphQL Playground. This lets us display the rich text using zone.richText
.
Similarly, with the other components in your Dynamic Zone, you can use the same approach to create zones that display data that matches their type. You can also do this for any subsequent components you add to the Dynamic Zone that you want to render in your frontend.
5. Building our Footer component Navigate to ./client/components
1<template>
2<div>
3 <footer class="footer">
4 <div class="container">
5 <span /> Copyright © 2022 |<span> Thanks to <a href="https://unsplash.com/@wocintechchat">Christina</a>,<a href="https://unsplash.com/@galina88"> Galina </a>and <a href="https://unsplash.com/@bkotynski">Bethany</a> for the images.</span>
6 </div>
7 </footer>
8</div>
9</template>
10
11<script>
12export default {
13
14};
15</script>
16
17<script setup>
18
19</script>
20
21<style scoped>
22.footer {
23 font-size: 0.8rem;
24 padding: 6rem 0;
25}
26</style>
This component displays some information at the bottom of your page.
6. Adding the Strapi Nuxt module to our application You’re almost ready to query your API and run your application. You will need to install a few packages to make sure GraphQL works in your application.
yarn add --dev @nuxtjs/strapi
to install them.1export default defineNuxtConfig({
2 modules: ['@nuxtjs/strapi'],
3 strapi: {
4
5 url: process.env.STRAPI_URL || 'http://localhost:1337',
6 prefix: '/api',
7 version: 'v4',
8 cookie: {},
9 cookieName: 'strapi_jwt',
10 }
11
12})
We will add data to this file later.
Next up, in client/querys create a file called content.js and paste this in it.
1export const contentQuery = `
2query Pages($Page: String!){
3 pages: pages(filters: { Title: { eq: $Page }}) {
4 data {
5 attributes {
6 Title
7 Description
8 pageZone {
9 __typename
10 ... on ComponentPostImage {
11 caption
12 image {
13 data {
14 attributes {
15 url
16 }
17 }
18 }
19 }
20 ... on ComponentPostQuote {
21 quote
22 quoter
23 }
24 ... on ComponentPostRichText {
25 richText
26 }
27 }
28 }
29 }
30 }
31}`
You can test this query in your GraphQL Playground - it gets page-specific content.
Now that you have your components created, you can piece them together and create your about page. To create a route in Nuxt, you have to add a new file to your page's directory. After this, in your app.vue
file, replace <NuxtWelcome />
with <NuxtPage />
to activate Nuxt Pages.
7. Creating our about page Navigate to ./client/pages.
Create a file called about.vue and paste the following code in it.
1<template>
2 <div class="layout">
3 <NavBar />
4 <Hero :title="title" :content="pages" />
5 <ContentGrid :title="title" :content="pages" />
6 <Footer />
7 </div>
8</template>
9
10<script setup>
11import { contentQuery } from "~/query/content"
12const graphql = useStrapiGraphQL()
13const title = "About"
14const result = await graphql(contentQuery, { "Page": title })
15const pages = result.data.pages.data[0].attributes
16</script>
17
18
19<style>
20* {
21 box-sizing: border-box;
22}
23body {
24 --color-base: rgb(255, 255, 255);
25 --color-base-1: rgb(243, 243, 243);
26 --color-contrast: rgb(0, 0, 0);
27 --color-contrast-1: rgb(43, 43, 43);
28 font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
29 margin: 0;
30 padding: 0;
31 font-size: 16px;
32 background: var(--color-base);
33 color: var(--color-contrast);
34 transition: background 0.5s ease;
35}
36body.dark {
37 --color-base: rgb(0, 0, 0);
38 --color-base-1: rgb(43, 43, 43);
39 --color-contrast: rgb(255, 255, 255);
40 --color-contrast-1: rgb(243, 243, 243);
41}
42h1 {
43 letter-spacing: -0.01em;
44}
45.layout {
46 padding: 0;
47}
48.layout.sticky-header {
49 padding: 0 0 0 0;
50}
51.container {
52 max-width: 1200px;
53 margin: 0 auto;
54 padding: 0 2rem;
55}
56@media (min-width: 860px) {
57 .container {
58 padding: 0 6rem;
59 }
60}
61a {
62 color: inherit;
63}
64img {
65 display: block;
66 margin-left: auto;
67 margin-right: auto;
68 width: 50%;
69}
70.label {
71 display: block;
72 font-weight: 700;
73 margin-bottom: 0.5rem;
74}
75</style>
In this file, you import all your components and make a query to your GraphQL API using the Nuxt module. You then pass the result - pages
and the data property page title - title
as props into our header Hero and ContentGrid components so that they can display page-specific data.
8. Creating other pages
Just like you made your ‘about page’, you can create your home, team, and testimonials pages by pasting the same code and changing the data property title
to the name of the page. The home page is an exception, you need to assign the string “Value” to the data property.
… aaand that’s it. Our application should work now as it does below.
Amazing! You have your application rendering Dynamic Zones now, but it doesn’t end here.
The beautiful thing is that if you make any updates to content in the Dynamic Zone, developers don't need to make changes in the front end. Let's say you want to add a customer review in the form of a quote, all you have to do is make changes to the Testimonials page in the Strapi backend by pressing the plus button and clicking quote. With no changes to the frontend code, the data shows up.
The same thing happens if you want to change the layout and order of data. Strapi lets you rearrange content on a site by tweaking its order in the CMS. Changes reflected with no extra effort from the developers.
Note: When you deploy your application and use it in a Heroku production environment, you will need a third-party provider to deal with uploads - the Strapi documentation has a nice guide on how to use various providers.
That’s it from me today, you can test out the working website or have a look at the GitHub repo. Hopefully, now, you have a better idea of how Dynamic Zones work and how best you can leverage them. It is an extremely powerful feature with limitless possibilities and I can’t wait to see what you build! Till next time!
Please note: Since we initially published this blog post, we released new versions of Strapi and tutorials may be outdated. Sorry for the inconvenience if it's the case. Please help us by reporting it here.
Developer Relations @ Weaviate | Developer Education and Experience | Builder and International Speaker