In this tutorial we’ll learn the benefits of a Headless CMS and create a corporate design agency site with Strapi as our headless CMS back-end and NuxtJS as our frontend.
Most corporate sites have been built using a traditional CMS like WordPress or Drupal. These CMSs can be seen as “monolithic” as the front-end and back-end are packed into one system. Headless CMSs like Strapi allow you to decouple the two and give you the freedom to choose however you want to build your front-end. Creating a site with pages for blogs, projects, case studies, and other content requires not only the database but also a system to easily create and manage it. Strapi handles all of that for you.
At the end of this tutorial, we would have created a complete design agency site with all the functionality like fetching data, displaying content, and routing on the front-end (built with NuxtJS) and content managed in the back-end with Strapi. We’ll learn the benefits of a headless CMS and its real-world application in building corporate sites with any front-end of choice.
CMS is short for Content Management System. A CMS allows users to manage, modify and publish content on their websites without having to know or write code for all the functionality.
For a long time organizations have been using Traditional CMS options such as WordPress or Drupal to build their websites. Traditional CMSs are monolithic in the sense that the front-end and back-end can't run separately, they are coupled together. This limits your choice of the front-end technology to the one provided by the CMS and makes you dependent on themes provided by CMS creators or the community for customization. Although there are some advantages to using a traditional CMS, especially for some organizations that want a site ready in a short period of time without much effort. However, for modern sites and applications, the benefits of a Headless CMS far outweigh that of a traditional CMS.
What is a Headless CMS anyway? A Headless CMS is simply one where the front-end and back-end are separated from each other. This means that we can build our front-end on any stack or framework, host it anywhere and access our content in the CMS via APIs.
Headless CMSs are gaining a lot of popularity as they allow developers to deliver content to their audience using front-end technologies of their choice.
We know what a Headless CMS is, let's talk about one - Strapi. Strapi is a world-leading JavaScript open-source headless CMS. Strapi makes it very easy to build custom APIs either REST APIs or GraphQL that can be consumed by any client or front-end framework of choice.
Now that we know that Strapi gives us the superpower of choice, we'll see how we can easily build a corporate website using Strapi and a front-end framework of our choice - Nuxt.js.
To follow along in this tutorial, you'll need a few things:
We are going to build a very corporate website, nothing too fancy for an imaginary design agency - Designli. It'll have a few pages:
To build this site, we need to first set up Strapi. We’ll create the collection types for the various content that will be provided for each page. For example, an article collection type for the blog, and projects collection type for the projects page.
Then, we'll build the UI using Nuxt. We'll fetch the data we need for each page from our Strapi API and display them on the site.
You can find the source code for the finished frontend here on GitHub
Alright. Let's get started.
Now the fun stuff. Strapi is pretty easy to get started with. You can take a look at Strapi’s installation guide for more info on how to get started.
We'll be using the quickstart flag which creates the project in the quick-start mode which uses the default SQLite database for the project.
In your terminal, install Strapi with the following command:
npx create-strapi-app@latest designli-API --quickstart
Once Strapi has successfully been installed, the Strapi app starts automatically by default and opens up your browser to http://localhost:1337/admin/auth/register-admin
. If this doesn’t happen for some reason, run:
1 cd designli-API
2 npm run develop
This builds Strapi and automatically opens up your browser to http://localhost:1337/admin/auth/register-admin
. This shiny new Strapi v4 page contains a registration form to create an admin
account.
You'll use the admin account to create and manage collections and content.
Once the admin account has been created, you'll be taken to the admin page at http://localhost:1337/admin/
. This is where we'll create our collection types and content.
Now that we've created our Strapi app, let's add some content.
We'll now create content types for the content of our collections on our design agency website. Content types define the structure of our data and we can set our desired fields which are meant to contain (e.g. text, numbers, media, etc.). The collections we'll need to create for our website will include:
Let's start by creating the content types.
Create Article Collection Content-Type To create a content type for our collections, we can click on the Create your first Content-Type button on the welcome page. You can also navigate to Content-Types Builder page by clicking on the link right under PLUGINS in the sidebar, then, on the Content-Type builder page, click on Create new collection type.
A Create a collection type modal will appear where we'll create our Content-Type and fields. In the Configurations, we'll enter in the display name of our Content-Type - article. We're using the singular article as the display name since Strapi is going to automatically use the plural version of the display name - articles for the collection later on.
Click on continue to proceed to add fields. There are a number of field types available here The field names and types for our article are:
title
: Text, Short textintro
: Text, Long textslug
: UID, Attached field: titlebody
: Rich Textcover
: Media, Single mediaLet's create the Title field. In the collection types menu, select Text. This opens a new modal form where you can enter the Name and select the type of text. We'll choose Short Text.
Then click on the Add another field button to proceed to the Slug, Body and Cover fields according to the name, and type specified in the list above.
Remember, select title as the attached field when creating the slug field. This will allow Strapi to dynamically generate the slug value based on the title. For example, in the content builder, if we set the article name to say “My first blog post”, the slug field will dynamically be updated to “my-first-blog-post”.
Now, we can create the remaining fields in similar ways. Once we're done creating our fields, our collection type should look like this:
Great! Now click on Save and the server will restart to save the changes. Once saved, we can go to the content manager page to access our newly created collection. In the Content Manager page, under the COLLECTION TYPES menu in the sidebar. Select the article collection type.
Here, we can create new articles and add some content. Before we do that though, we need to create a Categories collection type.
Create Categories collection type Strapi also makes it easy to create relationships between collection types. In the articles, for instance, we want each article to be under one or multiple categories like Announcements, Design, Tech, Development, Tips, etc. We also want each category to have multiple articles. That's a typical Many-to-many relationship.
To create a new collection we follow similar steps as before, navigate to Content-Types Builder > Create new collection type. In the modal, set the display name as category
and click on Continue.
Now we can create new field types. The field names and types for the categories collection are:
name
: Text, Short text, then, under advanced settings > select Required field and Unique fieldarticles
: Relation, many to manyTo create the name field, choose the Text field type, set the Name as name
. Select Required field and Unique field under advanced settings.
Once you’re done, click on Add another field to add the Relation field.
To add the Relation field, select Article from the drop-down menu on the right. This will automatically set the field name as categories. Choose the many-to-many relationship and here's what the relation field settings look like:
Once the name
and the articles
fields have been created, save the collection type. We can now create new categories.
Add new Categories
Navigate to the content manager page and click on the Category collection type in the sidebar. Then click on the Add New entry button to create a new entry. Enter the name of the category, which is announcements
in this case.
Click Save and then Publish.
We can create more categories in the same way. Here are all our categories for now:
Add a new Article To add a new article, on the content manager page, select the article collection type and click on the Add new entry button. This will open a page where we can add content to each field we created for the article collection. Let's create a new article.
Here, we have the Title, the Body with some markdown, the Cover image which we uploaded into our media library or assets from either our device or a URL and the Slug which is the Unique ID (UID) for our article.
We can also select a category for our article, in the menu on the right. Here, we chose the announcements category. Once you've provided all the content, click on Save. Our new article has now been saved as a draft. Now click Publish for the changes to be live. Here's our published article
Great! We can create even more articles by clicking on the Add New Articles button. Let's create our next collection, Projects.
Create Projects Collection Content-Type Now that we've been able to create the Articles collection type, we can follow the steps to create the Projects collection type.
On the Content-Type Builder page, click on Create new collection type. Then, in the modal, set the display name as project
then click continue. Now, we have to select the fields for our collection. The fields and types for the project’s collection would be:
title
: Text, Short textslug
: UID, Attached field: titleintro
: Rich Textbody
: Rich Textcover
: Media, Single mediaimages
: Media, Multiple mediaHere's what our collection type should look like:
Before we continue to create new projects, let’s create a categories collection type for our projects,
Create Project Categories collection type
Navigate to the Content-Type Builder page and click on Create new collection type. Set the display name as - Project Category The field names and types for the categories collection are:
name
: Text, Short text, then under advanced settings > select Required field and Unique fielddescription
: Text, Long Textcover
: Media, Single mediaproject_categories
: Relation, many to manySelect Project from the drop-down menu. This will set the field name as project_categories. Choose the many-to-many relationship and here's what the relation field settings look like:
Click Finish, Save and wait for the server to restart.
Add new Project Categories Let's add new project categories like Branding, Graphics, UI/UX, etc. We’ll navigate to the Content Manager page and select project category under COLLECTION TYPES.
Since we’re now familiar with how to add entries to a collection type, we'll add, save and publish entries for: Branding, Graphics, UI/UX, etc. by following the steps in the previous Categories collection type. We should have something like this.
Great! Now let's add a new project.
Add a new Project We can access our newly created Projects collection on the content manager page as projects under the COLLECTION TYPES menu in the sidebar. To add a new project, on the Content Manager page, click on the Add New Entry button. Now we can provide our project content. Here's what mine looks like:
After providing all the content, click on Save, then click Publish for the changes to go live. Here's our published project:
Create a Collection of User-submitted project details The last collection we have to create now is for user-submitted content. So far we've been dealing with data created within Strapi, now we're going to work with data that will be created by visitors to our site and how they'll be saved to Strapi.
First, we create the collection type. Navigate to the Content-Types Builder page and click on Create new collection type.
Set the display name to visitor message
. The field names and types for the categories collection would be:
name
- visitor name: Text, Short text.email
- visitor email: Emailbody
- the content of the message: Rich Textproject_categories
- category of the project : JSONAfter creating the fields, it should look like this:
Unlike the previously created collections, this will be updated from the frontend by visitors on the site. So we have to edit some permissions in order for this to work. To be able to create new items in a collection we have to update the permissions on our Strapi Roles and Permissions settings. Navigate to Settings > Roles (under *"*USERS & PERMISSIONS PLUGIN ") > Public. Now under Permissions, click on the create checkbox to allow it***.
Now we can send post requests and create new items for the Visitor messages collection.
So far we've been able to create the collection types and some content for our website back-end with Strapi. Now, we'll see how we can interact with our content using Strapi's API.
To do that we'll use an API tester like Postman or Talend API Tester which I use in my browser.
Let's send a request to Strapi to get our articles. To do that, we'll send a GET request to http://localhost:1337/api/articles/
.
With the new Strapi v4 update, we'll have to add the api/
route in order to access the API.
However, if we send the request at this point, this is the response we'll get
1{
2 "data": null,
3 "error": {
4 "status": 403,
5 "name": "ForbiddenError",
6 "message": "Forbidden",
7 "details": {
8 }
9 }
10}
This is because, by default, Strapi prevents unauthenticated requests from accessing data. To get our data, we will have to set Roles and permissions for each collection type for the Public role which is the "Default role given to the unauthenticated user."
Navigate to Settings > Roles *(under "*USERS & PERMISSIONS PLUGIN ").
Between Authenticated and Public roles, select *Public*.
Now under *Permissions, choose all allowed actions for each collection type which are count, find, and findone
. Click on save***.
Now if we send the GET request again, we get our articles! 🚀
Now that our API is working, we can build our front-end.
NuxtJS is a front-end framework for VueJS that provides server-side rendering capabilities. We’ll be using Nuxt to build the frontend of our corporate website. With Nuxt, we’ll be able to communicate and fetch data such as blog posts from the Strapi back-end and display for visitors. We’ll be using Nuxt v2 in this project as the current v3 is in beta and not yet production ready.
We’ll also be using tailwind for styling the application. TailwindCSS is a utility-first CSS framework that provides us with classes to style our applications without having to write a lot of custom CSS.
Before we get our hands dirty setting up a new project, I’d like to mention that the source code for frontend is available on Github. You can clone the project from GitHub and follow the instructions on the README.md
to install. Then, you can skip ahead to the part where you create your .env
files and setup your environment variables.
If you’re following along the set up and installation, you can also get the source code from Github and paste into the designated files as you build along. That being said, let’s go!
Install Nuxt To get started, in a different directory, run
1npx create-nuxt-app designli
This asks us a set of questions before installing Nuxt. Here are the options I chose for the project.
Install and setup TailwindCSS and Tailwind First, install TailwindCSS for Nuxt. You can find the installation guide of TailwindCSS for Nuxt here. Basically, run the following command to install
1npm install -D @nuxtjs/tailwindcss tailwindcss@latest postcss@latest autoprefixer@latest
In your nuxt.config.js
file, add package to your Nuxt build:
1// nuxt.config.js
2...
3 buildModules: [
4 '@nuxtjs/tailwindcss'
5 ],
6...
After installation, create the configuration file by running:
1npx tailwindcss init
This will create a tailwind.config.js
file at the root of your project. Follow the instructions to remove unused styles in production.
Create a new CSS file /assets/css/tailwind.css
and add the following
1@tailwind base;
2@tailwind components;
3@tailwind utilities;
In your nuxt.config.js
file, add the following to define tailwind.css
globally (included in every page)
1// nuxt.config.js
2...
3 css: [
4 '~/assets/css/tailwind.css'
5 ],
6...
Install Tailwind Typography plugin
The Typography plugin is according to the docs is "a plugin that provides a set of prose
classes you can use to add beautiful typographic defaults to any vanilla HTML you don't control (like HTML rendered from Markdown, or pulled from a CMS)".
You can find more about the plugin and installation guide and even a demo on the Typography plugin docs. Installation is pretty straightforward.
Install the plugin from npm:
npm install @tailwindcss/typography
Then add the plugin to your tailwind.config.js
file:
1 // tailwind.config.js
2 module.exports = {
3 theme: {
4 // ...
5 },
6 plugins: [
7 require('@tailwindcss/typography'),
8 // ...
9 ],
10 }
Next, create a .env
file in your root folder where we’ll define the STRAPI_URL
and STRAPI_API_URL
1 // .env
2 STRAPI_URL=http://localhost:1337
3 STRAPI_API_URL=http://localhost:1337/api
4
5`STRAPI_API_URL` will be used to fetch data from Strapi and,
6`STRAPI_URL` will be used to fetch media from Strapi
Then, create a new file store/index.js
where we will store the variable and make it globally accessible
1 // store/index.js
2 export const state = () => ({
3 apiUrl: process.env.STRAPI_API_URL,
4 url: process.env.STRAPI_URL,
5 })
Great! Now we can access the API URL using $store.state.url
in our Nuxt app.
Install @nuxtjs/markdownit module
One more module we need to install is the [@nuxtjs/markdownit](https://www.npmjs.com/package/@nuxtjs/markdownit)
which will parse the mardown text from the Rich Text fields.
npm i @nuxtjs/markdownit markdown-it-attrs markdown-it-div
Then in nuxt.config.js
,
1 // nuxt.config.js
2 ...
3 {
4 modules: [
5 '@nuxtjs/markdownit'
6 ],
7 markdownit: {
8 runtime: true, // Support `$md()`
9 preset: 'default',
10 linkify: true,
11 breaks: true,
12 use: ['markdown-it-div', 'markdown-it-attrs'],
13 },
14 }
15 ...
Now that we’ve installed everything we’ll need for the front-end, we can now run our app
npm run dev
Front-end project source code
Going forward, I’ll highlight the key features of the front-end where we interact with and use content from Strapi. The source code for the completed front-end can be found on GitHub.
To follow along, clone the project from GitHub to access the source files.
You can also follow the instructions on the README.md
to install and run the project.
Once downloaded, you can set up your Strapi back-end server, run it and then start up your front-end.
Here’s what the frontend should look like when we run npm run dev
in the frontend folder
Here’s what the directory structure looks like:
1 designli
2 ├─ assets/
3 │ ├─ css/
4 │ │ ├─ main.css
5 │ │ └─ tailwind.css
6 │ └─ img/
7 ├─ components/
8 │ ├─ ArticleCard.vue
9 │ ├─ NuxtLogo.vue
10 │ ├─ ProjectCard.vue
11 │ ├─ ServiceCard.vue
12 │ ├─ SiteFooter.vue
13 │ ├─ SiteHeader.vue
14 │ └─ SiteNav.vue
15 ├─ layouts/
16 │ └─ default.vue
17 ├─ pages/
18 │ ├─ About/
19 │ │ └─ index.vue
20 │ ├─ Blog/
21 │ │ ├─ _slug.vue
22 │ │ └─ index.vue
23 │ ├─ Projects/
24 │ │ ├─ _slug.vue
25 │ │ └─ index.vue
26 │ ├─ Contact.vue
27 │ └─ index.vue
28 ├─ static/
29 ├─ store/
30 │ ├─ README.md
31 │ └─ index.js
32 ├─ jsconfig.json
33 ├─ .gitignore
34 ├─ .prettierrc
35 ├─ README.md
36 ├─ nuxt.config.js
37 ├─ package-lock.json
38 ├─ package.json
39 └─ tailwind.config.js
From the above structure, the pages
directory contains our pages in their respective folders e.g. Blog page - Blog/index.vue
.
The <page name>/_slug.vue
files are dynamic pages that will render content for an individual entity.
Let’s display our Project Categories (services), Projects, and Articles on the home page. We can fetch them from our Strapi API.
First, make sure the Strapi server is running. Go to the Strapi directory and run npm run develop
.
Now in our pages/index.vue
, we’ll use the AsyncData hook which is only available for pages and doesn’t have access to this
inside the hook. Instead, it receives the context as its argument.
Here, we’ll use the fetch
API to fetch data for projects
, articles
and services
Some heads up…
We’ll be using the
project-categories
collection for theservices
, Also, in order to obtain data like images and relations, we have to specify the fields to populate in our query. To do that we’ll include the?populate=*
query parameter to the URL . More info can be found in the V4 docs
1 <!-- pages/index.vue -->
2 ...
3 <script>
4 export default {
5 // use destructuring to get the $strapi instance from context object
6 async asyncData({ $strapi }) {
7 try {
8 // fetch data from strapi
9 const services = await (
10 await fetch(`${store.state.apiUrl}/project-categories?populate=*`)
11 ).json()
12 const projects = await (
13 await fetch(`${store.state.apiUrl}/projects?populate=*`)
14 ).json()
15 const articles = await (
16 await fetch(`${store.state.apiUrl}/articles?populate=*`)
17 ).json()
18
19 // make the fetched data available in the page
20 // also, return the .data property of the entities where
21 // the data we need is stored
22 return {
23 projects: projects.data,
24 articles: articles.data,
25 services: services.data,
26 }
27 } catch (error) {
28 console.log(error)
29 }
30 },
31 }
32 </script>
We will pass in this data as props
to our components later on.
We have three main components that display our content - ArticleCard
, ServiceCard
and ProjectCard
.
The ArticleCard component
In this component we obtain the data passed down through props. Then display the Title, Intro and Cover.
To get the cover images, we combine the Strapi URL (STRAPI_URL
) in $store.state.url
to the relative URL (/uploads/medium_<image_name>.jpg
) gotten from article.cover.formats.medium.url
.
The src
value should now look something like this when combined: http://localhost:1337/uploads/medium_<image_name>.jpg
.
To obtain this new URL, we’ll use a computed property:
1 <script>
2 export default {
3 props: ['article'],
4 computed: {
5 // computed property to obtain new absolute image URL
6 coverImageUrl(){
7 const url = this.$store.state.url
8 const imagePath = this.article.cover.data.attributes.formats.medium.url
9 return url + imagePath
10 }
11 }
12 }
13 </script>
14
15 <!-- components/ArticleCard -->
16 <template>
17 <li class="article md:grid gap-6 grid-cols-7 items-center mb-6 md:mb-0">
18 <div class="img-cont h-full overflow-hidden rounded-xl col-start-1 col-end-3">
19 <!-- fetch media from strapi using the STRAPI_URL + relative image URL -->
20 <img :src="coverImageUrl" alt="">
21 </div>
22 <header class=" col-start-3 col-end-8">
23 <h1 class="font-bold text-xl mb-2">{{article.title}}</h1>
24 <p class="mb-2">{{article.intro}}</p>
25 <!-- link to dynamic page based on the `slug` value -->
26 <nuxt-link :to="`/blog/${article.slug}`">
27 <button class="cta w-max">Read more</button>
28 </nuxt-link>
29 </header>
30 </li>
31 </template>
32 <script>
33 export default {
34 props: ['article'],
35 computed: {
36
37 // computed property to obtain new absolute image URL
38 coverImageUrl(){
39 const url = this.$store.state.url
40 const imagePath = this.article.cover.data.attributes.formats.medium.url
41 return url + imagePath
42 }
43 }
44 }
45 </script>
The ServiceCard component In this component, data is obtained through props. We then display the Name and Description. the image is obtained similarly to the last component.
1 <!-- components/ServiceCard -->
2 <template>
3 <li class="service rounded-xl shadow-lg">
4 <header>
5 <div class="img-cont h-36 overflow-hidden rounded-xl">
6 <img v-if="coverImageUrl" :src="coverImageUrl" alt="" />
7 </div>
8 <div class="text-wrapper p-4">
9 <h3 class="font-bold text-xl mb-2">{{service.name}}</h3>
10 <p class="mb-2">
11 {{service.description}}
12 </p>
13 </div>
14 </header>
15 </li>
16 </template>
17 <script>
18 export default {
19 props: ['service'],
20 computed: {
21 coverImageUrl(){
22 const url = this.$store.state.url
23 const imagePath = this.service.cover.data.attributes.formats.medium.url
24 return url + imagePath
25 }
26 }
27 }
28 </script>
29 <style scoped> ... </style>
The ProjectCard component
In this component, to display the project categories of the project in a comma separated string, we map through the project_categories
property and return an array of the name value.
Let’s use a computed property for this
1 ...
2 computed: {
3 ...
4 projectCategories(){
5 return this.project.project_categories.data.map(
6 x=>x.attributes["name"]
7 ).toString()
8 }
9 }
10
11
12 <!-- components/ArticleCard -->
13 <template>
14 <li class="project grid gap-4 md:gap-8 md:grid-cols-7 items-center mb-8 md:mb-12">
15 <header style="height: min-content;" class="md:grid md:col-start-5 md:col-end-8">
16 <h1 class="text-xl md:text-3xl font-bold">{{project.title}}</h1>
17 <p>{{project.intro}}</p>
18 <!-- map through the project categories and convert the array to string -->
19 <!-- to display categories seperated by commas -->
20 <p class="text-gray-600 text-sm mb-2">{{ projectCategories }}</p>
21 <nuxt-link :to="`/projects/${project.slug}`">
22 <button class="cta w-max">View Project</button>
23 </nuxt-link>
24 </header>
25 <div
26 class="img-cont rounded-xl h-full max-h-40 md:max-h-72 row-start-1 md:col-start-1 md:col-end-5 overflow-hidden">
27 <img v-if="coverImageUrl" :src="coverImageUrl" alt="">
28 </div>
29 </li>
30 </template>
31 <script>
32 export default {
33 props: ['project'],
34 computed: {
35 coverImageUrl(){
36 const url = this.$store.state.url
37 const imagePath = this.project.cover.data.attributes.formats.medium.url
38 return url + imagePath
39 },
40 projectCategories(){
41 return this.project.project_categories.data.map(
42 x=>x.attributes["name"]
43 ).toString()
44 }
45 }
46 }
47 </script>
48 <style scoped> ... </style>
Next, to display the data from these components, we’ll import our components into pages/index.vue
component. We’ll loop through the data using v-for
to render the component for each item in the data array and pass its respective props.
1 <!-- pages/index.vue -->
2 ...
3 <section class="site-section services-section">
4 <div class="wrapper m-auto py-12 max-w-6xl">
5 <header class="relative grid md:grid-cols-3 gap-6 z-10 text-center"> ... </header>
6 <ul class="services grid md:grid-cols-3 gap-6 transform md:-translate-y-20" >
7 <!-- service card component -->
8 <service-card
9 v-for="service in services"
10 :key="service.id"
11 :service="service.attributes"
12 />
13 </ul>
14 </div>
15 </section>
16 <section class="site-section projects-section">
17 <div class="wrapper py-12 m-auto max-w-4xl">
18 <header class="text-center mb-6"> ... </header>
19 <ul v-if="projects" class="projects">
20 <!-- project card component -->
21 <project-card
22 v-for="project in projects"
23 :key="project.id"
24 :project="project.attributes"
25 />
26 </ul>
27 <div class="action-cont text-center mt-12">
28 <nuxt-link to="/projects">
29 <button class="cta">View more</button>
30 </nuxt-link>
31 </div>
32 </div>
33 </section>
34 <section class="site-section blog-section">
35 <div class=" wrapper py-12 md:grid gap-8 grid-cols-7 items-center m-auto max-w-6xl">
36 <header style="height: min-content" class="md:grid col-start-1 col-end-3 mb-8">
37 ...
38 </header>
39 <ul v-if="articles" class="articles md:grid gap-6 col-start-3 col-end-8">
40 <!-- article card component -->
41 <article-card
42 v-for="article in articles"
43 :key="article.id"
44 :article="article.attributes"
45 />
46 </ul>
47 </div>
48 </section>
49 ...
Here’s an example of the data being displayed with the ServiceCard
component
Sweet!
We can also display all this data in a page. For example, for the Projects page - pages/Projects/index.vue
,
1 <!-- pages/Projects/index.vue -->
2 <template>
3 <main>
4 <header class="px-4 mb-12">
5 <div class="wrapper mt-28 m-auto max-w-6xl">
6 <h1 class="hero-text">Our Projects</h1>
7 <p>See what we've been up to</p>
8 </div>
9 </header>
10 <ul class="m-auto px-4 max-w-5xl mb-12">
11 <project-card v-for="project in projects" :key="project.id" :project="project.attributes" />
12 </ul>
13 </main>
14 </template>
15 <script>
16 export default {
17 async asyncData({ store }) {
18 try {
19 // fetch all projects and populate their data
20 const { data } = await (
21 await fetch(`${store.state.apiUrl}/projects?populate=*`)
22 ).json()
23 return { projects: data }
24 } catch (error) {
25 console.log(error)
26 }
27 },
28 }
29 </script>
Since this is a page, we can use the asyncData
hook to fetch project data using $strapi
. We then pass the data as props to each component.
Here’s what the project page looks like:
So far we’ve been fetching collections as a whole and not individual items of the collection. Strapi allows us to fetch a single collection item by its id or parameters. Here are available endpoints from the Strapi docs
To display the content of individual items of our collections e.g an article from Articles, we can create and set up dynamic pages in Nuxt. In the pages/Blog/
directory, we have a _slug.vue
file. This will be the template for each of our articles.
Fetch content using parameters
We’ll fetch our data using the asyncData()
hook.
We’ll use the Slug property of the article collection item to fetch the data.
In asyncData()
we can get the access to the value of the URL in the address bar using context
with params.slug
To do this, we have to use query parameter Filters. For example, in order to fetch data of an article with a slug
of "``my-article``"
, we’ll have to use this route:
1http://localhost:1337/api/articles?filters\[slug\][$eq]=my-article&populate=*
Notice the filters
parameter with the square brackets []
. The first bracket tells Strapi what field it should run the query against, the second bracket holds the operator which defines the relationship i.e $eq
- equal to
, $lt
- less than
etc.
You can explore more operators and what they do here
1 ...
2 // use destructuring to get the context.params and context.store
3 async asyncData({ params, store }) {
4 try {
5 // fetch data by slug using Strapi query filters
6 const { data } = await (
7 await fetch(
8 `${store.state.apiUrl}/articles?filters\[slug\][$eq]=${params.slug}&populate=*`
9 )
10 ).json()
11 return { article: data[0].attributes }
12 } catch (error) {
13 console.log(error)
14 }
15 },
16 ...
Rendering markdown with @nuxtjs/markdownit
After getting our project data, we can now display it in our <template>
. Remember that we also have a Body field in our Project Collection. This Body field holds data in markdown format. To render it to valid HTML, we will use the global $md
instance provided by @nuxtjs/markdownit
which we installed and set up previously.
We will then style the rendered html using the Tailwind Typography .prose
classes
1 <article class="prose prose-xl m-auto w-full">
2 ...
3 <div v-html="$md.render(article.body)" class="body"></div>
4 </aticle>
5 ...
The pages/Blog/_slug.vue
code would look like:
1 <!-- pages/Projects/_slug.vue -->
2 <template>
3 <main>
4 <div v-if="article">
5 <header class="">
6 <div class="cover img-cont h-full max-h-96">
7 <img v-if="coverImageUrl" class="rounded-b-2xl" :src="coverImageUrl" alt="" />
8 </div>
9 </header>
10 <div class="cont relative bg-gray-50 p-12 z-10 m-auto max-w-6xl rounded-2xl">
11 <article class="prose prose-xl m-auto w-full">
12 <span style="margin-bottom: 1rem" class=" uppercase text-sm font-thin text-gray-600">from the team</span>
13 <h1 class="hero-text mt-4">{{ article.title }}</h1>
14 <p>{{ article.intro }}</p>
15 <p class="text-gray-600 text-sm mb-2"><span class="font-extrabold">Categories: </span> {{ articleCategories }}</p>
16
17 <!-- use markdownit to render the markdown text to html -->
18 <div v-html="$md.render(article.body)" class="body"></div>
19 </article>
20 </div>
21 </div>
22 <div v-else class="h-screen flex items-center justify-center text-center">
23 <header class="">
24 <h1 class="hero-text">Oops..</h1>
25 <p>That article doesnt exist</p>
26 </header>
27 </div>
28 </main>
29 </template>
30 <script>
31 export default {
32 async asyncData({ params, store }) {
33 try {
34 // fetch data by slug using Strapi query filters
35 const { data } = await (
36 await fetch(
37 `${store.state.apiUrl}/articles?filters\[slug\][$eq]=${params.slug}&populate=*`
38 )
39 ).json()
40 return { article: data[0].attributes }
41 } catch (error) {
42 console.log(error)
43 }
44 },
45 computed: {
46 coverImageUrl() {
47 const url = this.$store.state.url
48 const imagePath = this.article.cover.data.attributes.formats.medium.url
49 return url + imagePath
50 },
51 articleCategories() {
52 return this.article.categories.data
53 .map((x) => x.attributes['name'])
54 .toString()
55 },
56 },
57 }
58 </script>
And here’s a screenshot of the output:
We can also do the same thing for project pages, here’s the code for the project pages on GitHub. That’s about it for displaying content. Next, we’ll see how we can send data to Strapi.
n the Contact Us page - [pages/Contact.vue](https://github.com/miracleonyenma/designli-agency-site/blob/master/pages/Contact.vue)
, we have a form where we get the data with two-way binding: v-model
like so:
1 <input type="text" id="name" v-model="name" value="Miracleio" required/>
We’ll do this for each input field so that we have a data property for each input value with some defaults if we like:
1 ...
2 export default {
3 data(){
4 return{
5 success: false,
6 name: 'Miracle',
7 company: 'Miracleio',
8 email: 'mio@mio.co',
9 services: ['branding'],
10 message: 'What\'s up yo?'
11 }
12 },
13 ...
14 }
We then attach a submit event listener to our form:
1 <form ref="form" @submit.prevent="submitForm()">
The submitForm()
method takes the data and sends it to Strapi using the create
method. Which takes the entity or collection name as the first argument and the the data as the second - $strapi.create('visitor-messages', data)
1 ...
2 export default {
3 data(){
4 return{
5 success: false,
6 name: 'Miracle',
7 email: 'mio@mio.co',
8 services: ['branding'],
9 message: 'What\'s up yo?'
10 }
11 },
12 methods: {
13 async submitForm(){
14 const data = {
15 name: this.name,
16 email: this.email,
17 project_categories: this.services,
18 body: this.message
19 }
20 try {
21 // send a POST request to create a new entry
22 const msgs = await fetch(`${this.$store.state.apiUrl}/visior-messages`, {
23 method: 'POST',
24 headers: {
25 'Content-Type': 'application/json'
26 },
27 body: JSON.stringify({data})
28 })
29 if(msgs) this.success = true
30 } catch (error) {
31 console.log(error);
32 }
33 }
34 }
35 }
Now if we fill the form and submit it, a new item gets added to our Visitor messages collection.
So far we’ve seen how we can create and manage content for our website with Strapi and how to access the content from the front-end. We created a few collection types:
In order to get the content of these collections, we also had to modify the roles and permissions of the public or unauthenticated user.
For the frontend, We built it with NuxtJS, made use of a few packages like markdown-it
for example to work with the Rich Text content type.
The following pages were built:
articles
collectionprojects
collectionProject categories
collectionVisitor messages
collectionAs mentioned earlier, you can get the entire source code for the front-end from the GitHub repo. We can use any technology stack of our choice to interact with a Headless CMS so that we can build modern and flexible applications.
Here are some resources that might help you going forward
Link to code repository - https://github.com/miracleonyenma/designli-agency-site