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

Shada - Jan 19 '22 - - Dev Community

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Strapi’s Admin register page

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.

Strapi Admin Dashboard Welcome Page

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.

Strapi create text field for article collection type

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

Create slug UID field for article

Now, we can create the remaining fields in similar ways.
Once we're done creating our fields, our collection type should look like this:

Overview of article collection type

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.

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

Category relation field

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.

Create new announcements article category

Click Save and then Publish.

We can create more categories in the same way. Here are all our categories for now:

Overview of categories

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.

Creating an article entry

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

Published article entry

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

Overview of project collection type

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:

Editing project category relationships

Click Finish, Save and wait for the server to restart.

Overview of project category collection type

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.

Project category content entries

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:

Create project entry

After providing all the content, click on Save, then click Publish for the changes to go live. Here's our published project:

Overview of published project collection type content

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

Overview of visitor message collection type

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": {
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Modify user permissions for collection types

Now if we send the GET request again, we get our articles! 🚀

successful GET request to Strapi back-end

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

// nuxt.config.js
...
  buildModules: [
    '@nuxtjs/tailwindcss'
  ],
...
Enter fullscreen mode Exit fullscreen mode

After installation, create the configuration file by running:

npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

// nuxt.config.js
...
  css: [
    '~/assets/css/tailwind.css'
  ],
...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then add the plugin to your tailwind.config.js file:

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

Next, create a .env file in your root folder where we’ll define the STRAPI_URL and 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
Enter fullscreen mode Exit fullscreen mode

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,
    })
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'],
      },
    }
    ...
Enter fullscreen mode Exit fullscreen mode

Now that we’ve installed everything we’ll need for the front-end, we can now run our app

    npm run dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

    ...
    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>
Enter fullscreen mode Exit fullscreen mode

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>
    ...
Enter fullscreen mode Exit fullscreen mode

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,

    <!-- 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>
Enter fullscreen mode Exit fullscreen mode

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

http://localhost:1337/api/articles?filters\[slug\][$eq]=my-article&populate=*
Enter fullscreen mode Exit fullscreen mode

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)
      }
    },
    ...
Enter fullscreen mode Exit fullscreen mode

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

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

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>
Enter fullscreen mode Exit fullscreen mode

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/>
Enter fullscreen mode Exit fullscreen mode

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?'
        }
      },
    ...
    }
Enter fullscreen mode Exit fullscreen mode

We then attach a submit event listener to our form:

    <form ref="form" @submit.prevent="submitForm()">
Enter fullscreen mode Exit fullscreen mode

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);
          }
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .