How to Build a Corporate Design Agency Site with NuxtJS and Strapi

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.

Introduction

Most corporate sites have been built using a traditional CMS like WordPressor 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.

Goals

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.

Brief Overview of Traditional and Headless CMSs

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.

What is Strapi

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.

Prerequisites

To follow along in this tutorial, you’ll need a few things:

  • Basic knowledge of JavaScript
  • Basic knowledge of Vue and Nuxt.js
  • Node.js & npm installed, npm comes with Node.js by default now so you can download Node.js from Node.js official site if you haven’t already.

What we’re building

We are going to build a very corporate website, nothing too fancy for an imaginary design agency — Designli. It’ll have a few pages:

  • Home/Landing page
  • About Page
  • Blog page
  • Projects page
  • Project page for each project
  • Contact Us page

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.

Step 1 — Set Up Website back-end With Strapi

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:

cd designli-API
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 adminpage 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.

Step 2 — Create Content Types for various 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:

  • A collection of articles for the website blog and categories
  • A collection of projects containing images, case study text and project categories
  • A collection of user-submitted content from the form on the contact us page

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 text
  • intro: Text, Long text
  • slug: UID, Attached field: title
  • body: Rich Text
  • cover: Media, Single media

Let’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 field
  • articles: Relation, many to many

To 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 Categorycollection 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 announcementsin 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 articlecollection 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 Articlesbutton. 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 text
  • slug: UID, Attached field: title
  • intro: Rich Text
  • body: Rich Text
  • cover: Media, Single media
  • images: Media, Multiple media

Here’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 field
  • description: Text, Long Text
  • cover: Media, Single media
  • project_categories: Relation, many to many

Select 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 Builderpage 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: Email
  • body - the content of the message: Rich Text
  • project_categories - category of the project : JSON

After 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.

Step 3 — Test Strapi back-end API

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

{
"data": null,
"error": {
"status": 403,
"name": "ForbiddenError",
"message": "Forbidden",
"details": {
}
}
}

This is because, by default, Strapi prevents unauthenticated requests from accessing data. To get our data, we will have to set Roles and permissionsfor 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.

Step 4 — Set up front-end with NuxtJS and TailwindCSS

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

npx 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

npm install -D @nuxtjs/tailwindcss tailwindcss@latest postcss@latest autoprefixer@latest

In your nuxt.config.js file, add package to your Nuxt build:

// nuxt.config.js
...
buildModules: [
'@nuxtjs/tailwindcss'
],
...

After installation, create the configuration file by running:

npx 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

@tailwind base;
@tailwind components;
@tailwind utilities;

In your nuxt.config.js file, add the following to define tailwind.cssglobally (included in every page)

// nuxt.config.js
...
css: [
'~/assets/css/tailwind.css'
],
...

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:

// tailwind.config.js
module.exports = {
theme: {
// ...
},
plugins: [
require('@tailwindcss/typography'),
// ...
],
}

Next, create a .env file in your root folder where we’ll define the STRAPI_URLand STRAPI_API_URL

// .env
STRAPI_URL=http://localhost:1337
STRAPI_API_URL=http://localhost:1337/api
`STRAPI_API_URL` will be used to fetch data from Strapi and,
`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

// store/index.js
export const state = () => ({
apiUrl: process.env.STRAPI_API_URL,
url: process.env.STRAPI_URL,
})

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,

// nuxt.config.js
...
{
modules: [
'@nuxtjs/markdownit'
],
markdownit: {
runtime: true, // Support `$md()`
preset: 'default',
linkify: true,
breaks: true,
use: ['markdown-it-div', 'markdown-it-attrs'],
},
}
...

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:

designli
├─ assets/
│ ├─ css/
│ │ ├─ main.css
│ │ └─ tailwind.css
│ └─ img/
├─ components/
│ ├─ ArticleCard.vue
│ ├─ NuxtLogo.vue
│ ├─ ProjectCard.vue
│ ├─ ServiceCard.vue
│ ├─ SiteFooter.vue
│ ├─ SiteHeader.vue
│ └─ SiteNav.vue
├─ layouts/
│ └─ default.vue
├─ pages/
│ ├─ About/
│ │ └─ index.vue
│ ├─ Blog/
│ │ ├─ _slug.vue
│ │ └─ index.vue
│ ├─ Projects/
│ │ ├─ _slug.vue
│ │ └─ index.vue
│ ├─ Contact.vue
│ └─ index.vue
├─ static/
├─ store/
│ ├─ README.md
│ └─ index.js
├─ jsconfig.json
├─ .gitignore
├─ .prettierrc
├─ README.md
├─ nuxt.config.js
├─ package-lock.json
├─ package.json
└─ 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.

Step 5 — Fetch content in Nuxt homepage

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 the services, 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

<!-- pages/index.vue -->
...
<script>
export default {
// use destructuring to get the $strapi instance from context object
async asyncData({ $strapi }) {
try {
// fetch data from strapi
const services = await (
await fetch(`${store.state.apiUrl}/project-categories?populate=*`)
).json()
const projects = await (
await fetch(`${store.state.apiUrl}/projects?populate=*`)
).json()
const articles = await (
await fetch(`${store.state.apiUrl}/articles?populate=*`)
).json()

// make the fetched data available in the page
// also, return the .data property of the entities where
// the data we need is stored
return {
projects: projects.data,
articles: articles.data,
services: services.data,
}
} catch (error) {
console.log(error)
}
},
}
</script>

We will pass in this data as props to our components later on.

Step 6 — Displaying our data

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:

<script>
export default {
props: ['article'],
computed: {
// computed property to obtain new absolute image URL
coverImageUrl(){
const url = this.$store.state.url
const imagePath = this.article.cover.data.attributes.formats.medium.url
return url + imagePath
}
}
}
</script>
<!-- components/ArticleCard -->
<template>
<li class="article md:grid gap-6 grid-cols-7 items-center mb-6 md:mb-0">
<div class="img-cont h-full overflow-hidden rounded-xl col-start-1 col-end-3">
<!-- fetch media from strapi using the STRAPI_URL + relative image URL -->
<img :src="coverImageUrl" alt="">
</div>
<header class=" col-start-3 col-end-8">
<h1 class="font-bold text-xl mb-2">{{article.title}}</h1>
<p class="mb-2">{{article.intro}}</p>
<!-- link to dynamic page based on the `slug` value -->
<nuxt-link :to="`/blog/${article.slug}`">
<button class="cta w-max">Read more</button>
</nuxt-link>
</header>
</li>
</template>
<script>
export default {
props: ['article'],
computed: {

// computed property to obtain new absolute image URL
coverImageUrl(){
const url = this.$store.state.url
const imagePath = this.article.cover.data.attributes.formats.medium.url
return url + imagePath
}
}
}
</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.

<!-- components/ServiceCard -->
<template>
<li class="service rounded-xl shadow-lg">
<header>
<div class="img-cont h-36 overflow-hidden rounded-xl">
<img v-if="coverImageUrl" :src="coverImageUrl" alt="" />
</div>
<div class="text-wrapper p-4">
<h3 class="font-bold text-xl mb-2">{{service.name}}</h3>
<p class="mb-2">
{{service.description}}
</p>
</div>
</header>
</li>
</template>
<script>
export default {
props: ['service'],
computed: {
coverImageUrl(){
const url = this.$store.state.url
const imagePath = this.service.cover.data.attributes.formats.medium.url
return url + imagePath
}
}
}
</script>
<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_categoriesproperty and return an array of the name value. Let’s use a computed property for this

...
computed: {
...
projectCategories(){
return this.project.project_categories.data.map(
x=>x.attributes["name"]
).toString()
}
}
<!-- components/ArticleCard -->
<template>
<li class="project grid gap-4 md:gap-8 md:grid-cols-7 items-center mb-8 md:mb-12">
<header style="height: min-content;" class="md:grid md:col-start-5 md:col-end-8">
<h1 class="text-xl md:text-3xl font-bold">{{project.title}}</h1>
<p>{{project.intro}}</p>
<!-- map through the project categories and convert the array to string -->
<!-- to display categories seperated by commas -->
<p class="text-gray-600 text-sm mb-2">{{ projectCategories }}</p>
<nuxt-link :to="`/projects/${project.slug}`">
<button class="cta w-max">View Project</button>
</nuxt-link>
</header>
<div
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">
<img v-if="coverImageUrl" :src="coverImageUrl" alt="">
</div>
</li>
</template>
<script>
export default {
props: ['project'],
computed: {
coverImageUrl(){
const url = this.$store.state.url
const imagePath = this.project.cover.data.attributes.formats.medium.url
return url + imagePath
},
projectCategories(){
return this.project.project_categories.data.map(
x=>x.attributes["name"]
).toString()
}
}
}
</script>
<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.

<!-- pages/index.vue -->
...
<section class="site-section services-section">
<div class="wrapper m-auto py-12 max-w-6xl">
<header class="relative grid md:grid-cols-3 gap-6 z-10 text-center"> ... </header>
<ul class="services grid md:grid-cols-3 gap-6 transform md:-translate-y-20" >
<!-- service card component -->
<service-card
v-for="service in services"
:key="service.id"
:service="service.attributes"
/>
</ul>
</div>
</section>
<section class="site-section projects-section">
<div class="wrapper py-12 m-auto max-w-4xl">
<header class="text-center mb-6"> ... </header>
<ul v-if="projects" class="projects">
<!-- project card component -->
<project-card
v-for="project in projects"
:key="project.id"
:project="project.attributes"
/>
</ul>
<div class="action-cont text-center mt-12">
<nuxt-link to="/projects">
<button class="cta">View more</button>
</nuxt-link>
</div>
</div>
</section>
<section class="site-section blog-section">
<div class=" wrapper py-12 md:grid gap-8 grid-cols-7 items-center m-auto max-w-6xl">
<header style="height: min-content" class="md:grid col-start-1 col-end-3 mb-8">
...
</header>
<ul v-if="articles" class="articles md:grid gap-6 col-start-3 col-end-8">
<!-- article card component -->
<article-card
v-for="article in articles"
:key="article.id"
:article="article.attributes"
/>
</ul>
</div>
</section>
...

Here’s an example of the data being displayed with the ServiceCardcomponent

Sweet!

We can also display all this data in a page. For example, for the Projectspage — pages/Projects/index.vue,

<!-- pages/Projects/index.vue -->
<template>
<main>
<header class="px-4 mb-12">
<div class="wrapper mt-28 m-auto max-w-6xl">
<h1 class="hero-text">Our Projects</h1>
<p>See what we've been up to</p>
</div>
</header>
<ul class="m-auto px-4 max-w-5xl mb-12">
<project-card v-for="project in projects" :key="project.id" :project="project.attributes" />
</ul>
</main>
</template>
<script>
export default {
async asyncData({ store }) {
try {
// fetch all projects and populate their data
const { data } = await (
await fetch(`${store.state.apiUrl}/projects?populate=*`)
).json()
return { projects: data }
} catch (error) {
console.log(error)
}
},
}
</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:

Step 7 — Fetching and displaying content in Individual pages

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 articlefrom 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:

http://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

...
// use destructuring to get the context.params and context.store
async asyncData({ params, store }) {
try {
// fetch data by slug using Strapi query filters
const { data } = await (
await fetch(
`${store.state.apiUrl}/articles?filters\[slug\][$eq]=${params.slug}&populate=*`
)
).json()
return { article: data[0].attributes }
} catch (error) {
console.log(error)
}
},
...

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 .proseclasses

<article class="prose prose-xl m-auto w-full">
...
<div v-html="$md.render(article.body)" class="body"></div>
</aticle>
...

The pages/Blog/_slug.vue code would look like:

<!-- pages/Projects/_slug.vue -->
<template>
<main>
<div v-if="article">
<header class="">
<div class="cover img-cont h-full max-h-96">
<img v-if="coverImageUrl" class="rounded-b-2xl" :src="coverImageUrl" alt="" />
</div>
</header>
<div class="cont relative bg-gray-50 p-12 z-10 m-auto max-w-6xl rounded-2xl">
<article class="prose prose-xl m-auto w-full">
<span style="margin-bottom: 1rem" class=" uppercase text-sm font-thin text-gray-600">from the team</span>
<h1 class="hero-text mt-4">{{ article.title }}</h1>
<p>{{ article.intro }}</p>
<p class="text-gray-600 text-sm mb-2"><span class="font-extrabold">Categories: </span> {{ articleCategories }}</p>

<!-- use markdownit to render the markdown text to html -->
<div v-html="$md.render(article.body)" class="body"></div>
</article>
</div>
</div>
<div v-else class="h-screen flex items-center justify-center text-center">
<header class="">
<h1 class="hero-text">Oops..</h1>
<p>That article doesnt exist</p>
</header>
</div>
</main>
</template>
<script>
export default {
async asyncData({ params, store }) {
try {
// fetch data by slug using Strapi query filters
const { data } = await (
await fetch(
`${store.state.apiUrl}/articles?filters\[slug\][$eq]=${params.slug}&populate=*`
)
).json()
return { article: data[0].attributes }
} catch (error) {
console.log(error)
}
},
computed: {
coverImageUrl() {
const url = this.$store.state.url
const imagePath = this.article.cover.data.attributes.formats.medium.url
return url + imagePath
},
articleCategories() {
return this.article.categories.data
.map((x) => x.attributes['name'])
.toString()
},
},
}
</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.

Step 8 — Sending content 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:

<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:

...  
export default {
data(){
return{
success: false,
name: 'Miracle',
company: 'Miracleio',
email: 'mio@mio.co',
services: ['branding'],
message: 'What\'s up yo?'
}
},
...
}

We then attach a submit event listener to our form:

<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)

...  
export default {
data(){
return{
success: false,
name: 'Miracle',
email: 'mio@mio.co',
services: ['branding'],
message: 'What\'s up yo?'
}
},
methods: {
async submitForm(){
const data = {
name: this.name,
email: this.email,
project_categories: this.services,
body: this.message
}
try {
// send a POST request to create a new entry
const msgs = await fetch(`${this.$store.state.apiUrl}/visior-messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({data})
})
if(msgs) this.success = true
} catch (error) {
console.log(error);
}
}
}
}

Now if we fill the form and submit it, a new item gets added to our Visitor messages collection.

Conclusion

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:

  • Articles
  • Categories (for articles)
  • Projects
  • Project categories (also services)
  • Visitor messages

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:

  • Home/Index page — Using components to fetch data in different sections
  • Blog — fetching content from articles collection
  • Projects — fetching content from projects collection
  • Services — fetching content from Project categories collection
  • Contact — Using a form to send data to the Visitor messages collection

As 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.

Resources & further reading

Here are some resources that might help you going forward

Link to code repository — https://github.com/miracleonyenma/designli-agency-site

The open source Headless CMS Front-End Developers love.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Strapi

Strapi

The open source Headless CMS Front-End Developers love.

More from Medium

Build a News Aggregator App using Strapi and Nuxtjs

How and Why to Create a Progressive Web App

How to build a Tailwind CSS timeline component with Flowbite

Writing Chrome Extensions Using Svelte-Kit and Manifest v3