How to build a CMS-powered blog with Nuxt
Tags: javascript, nuxt
Published: 2022-04-11 21:49:13

by [Uma Victor](https://blog.openreplay.com/authors/uma-victor)

So you just built a new website and need a blog on your new site. How do you do this without spinning up a server? We will look at how to add a blog to our new site powered by just APIs that we can call. Check out what the final project looks like [here](https://butter-cms-blog.netlify.app/).

There are a couple of ways we can add a blog to a site. One way is to use a **Headless CMS**, which will power the blog. In this tutorial, we’ll be looking at what a Headless CMS is, the options available for us to use, and the front-end framework that will be best for our blog.

## What is a Headless CMS

Many of us have heard of or come across the term “Headless CMS”. For those who aren’t familiar with the term, it simply refers to a CMS (Content Management System) where the front-end and back-end are decoupled, unlike in a traditional CMS. Unlike a traditional CMS where the front-end can’t be separated from the back-end like with WordPress or Drupal, a headless CMS provides only the back-end functionality and allows you to run your front-end and deploy it wherever and connect to the CMS using APIs. A Headless CMS offers developers much more benefits than a traditional CMS in many ways. One significant advantage is that you can use APIs to connect your front-end to your CMS, which allows for faster development and freedom to use any front-end framework of choice, which is good for DX and UX.

Now that we have an idea of what a Headless CMS is, we’ll look into how we can build a blog powered by a popular, easy-to-setup Headless CMS — ButterCMS.


## Prerequisites

To follow along, we need to have a few things in place:

- [ButterCMS](https://buttercms.com/join/) account
- Text editor: VSCode, for example
- Prior [Vue](https://vuejs.org/) or Nuxt knowledge
- A recent [**Node.js**](https://nodejs.org/en/) version (v14 or v16 - preferably): At the time of writing, I’m using v16.13.0. You can read [this article to learn how to update your Node version](https://phoenixnap.com/kb/update-node-js-version).


## What we’ll build

We’ll be building a blog with Nuxt for our front-end and ButterCMS as our back-end. Nuxt (or Nuxt.js) is a front-end framework for Vue (or Vue.js) that provides server-side rendering capabilities and features that will also enhance SEO and page load time for our blog.


## Set up Nuxt

Before we jump into setting up Butter, let’s create the front-end we will connect Butter to. We’ll be using Nuxt 3, which is still in beta at the time of writing but offers a lot of new features and allows us to use Vue 3’s composition API. To create a new Nuxt 3 project in your terminal, run:

```
    npx nuxi init butter-blog
```

This creates a new `butter-blog/` directory, which you can navigate to. Install the dependencies and run the app:

```
    cd butter-blog
    yarn install #or npm install
    yarn dev #or npm run dev
```

Let’s also install [WindiCSS](https://windicss.org/), a Tailwind alternative that works well with Nuxt.


```
    yarn add nuxt-windicss -D
```

Once the installation is complete, within your `nuxt.config.js` add the following:

```javascript
import { defineNuxtConfig } from "nuxt3";

export default defineNuxtConfig({
  buildModules: ["nuxt-windicss"],
});
```

Next, let’s add a header and a home page to our Nuxt app. Go to the `./app.vue` file and enter the following lines of code:

```
<template>
  <div>
    <header class="sticky top-0 p-4 bg-white">
      <div
        class="wrapper flex items-center justify-between w-full max-w-5xl m-auto"
      >
        <figure>
          <NuxtLink to="/">
            <h1>Butter Blog</h1>
          </NuxtLink>
        </figure>
        <nav>
          <ul>
            <li>
              <NuxtLink to="/blog">Blog</NuxtLink>
            </li>
          </ul>
        </nav>
      </div>
    </header>
    <!-- render pages based on route -->
    <NuxtPage />
  </div>
</template>

```

Create a new file, `pages/index.vue`:  the home page of the Nuxt app.

```
<!-- pages/index.vue -->
<template>
  <main>
    <header
      class="flex flex-col gap-4 items-center justify-center w-full h-screen bg-gray-900 text-gray-100"
    >
      <h1 class="font-black text-5xl">Hello!</h1>
      <p class="text-xl">
        Go to the
        <nuxt-link class="underline" to="/blog"
          >blog</nuxt-link
        >
      </p>
    </header>
  </main>
</template>

```

We should have something like this:

![Home page](https://blog.openreplay.com/static/af29b9a9eb994c94109193c1c4ab786f/68382/image01.png)


Great! We can proceed with setting up Butter.

## Set up Butter

To get started, sign up and log in to your Butter account to get your Butter API token. You can follow the [Butter Quickstart setup page for Nuxt](https://buttercms.com/docs/api-client/nuxtjs#ExistingApp).

**Install the Butter SDK**

```
    yarn add buttercms 
    #or npm install buttercms --save
```

**Add API token to Nuxt Runtime configurations**
You can copy your API token from the [settings page](https://buttercms.com/settings/). The API token will be used to initialize our Butter SDK. We can expose config and environment variables to the rest of our app by defining the runtime configuration in our `nuxt.config.js` file using the [publicRuntimeConfig](https://v3.nuxtjs.org/docs/directory-structure/nuxt.config#publicruntimeconfig) options; more on that in the [Nuxt docs](https://v3.nuxtjs.org/docs/usage/runtime-config/).

```javascript
// nuxt.config.ts
import { defineNuxtConfig } from "nuxt3";
export default defineNuxtConfig({
  buildModules: ["nuxt-windicss"],
  publicRuntimeConfig: {
    API_TOKEN: process.env.BUTTER_API_TOKEN,
  },
});
```

Next, create a .env file to add the token.

```
//.env
BUTTER_API_TOKEN=<API TOKEN HERE>
```

Great! Now we can access our API token in our Nuxt app.

**Create a helper for the Butter SDK**
To have global access to the Butter SDK in our Nuxt app, we’ll create a [**plugin and configure it as a helper.**](https://v3.nuxtjs.org/docs/directory-structure/plugins/#automatically-providing-helpers). In the `plugins/` directory, create a new **butter.js** file which will automatically be registered as a plugin:

```javascript
// plugins/butter.js

import { defineNuxtPlugin } from "#app";
import Butter from "buttercms";

export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig();

  return {
    provide: {
      // initialise butter with private runtime config
      butter: Butter(config.API_TOKEN),
    },
  };
});
```

In the code above, we import the Butter SDK and get the API token from our private runtime config using `useRuntimeConfig()`.

**Fetch content from Butter**
Now, it’ll be a good idea to try this out. Butter has an example blog post to get started with, and we’ll be using that to try things out. On your Butter dashboard, navigate to the [blog posts page](https://buttercms.com/blog_home/).

![Butter dashboard](https://blog.openreplay.com/static/7cbcefb33a161ddba91854c419e59ab9/68382/image02.png)


Click on the post and open the API Explorer, which allows us to see which endpoint to access for the post and an example response.

![API Explorer](https://blog.openreplay.com/static/613d4f045e60abc711fd4344565c2704/68382/image03.png)


Here’s what it looks like with the API explorer.

![API Explorer](https://blog.openreplay.com/static/202453ed110d55416a77fc4aec2a6e4d/68382/image04.png)


We can use the Javascript SDK code for fetching this post in our Nuxt app. Let’s set up a quick example to see how this works. Create a new dynamic page `pages/blog/[slug].vue`

```
//pages/blog/[slug].vue

<template>
  <header>
    <h1>{{ post.title }}</h1>
    <p>{{ post.summary }}</p>
  </header>
</template>

<script setup lang="ts">

const { $butter } = useNuxtApp();
const { data: post } = await useAsyncData(
  "posts",
  async () => {
    try {
      let data = await $butter.post
        .retrieve("example-post")
        .then((res) => res.data.data)
        .catch((err) => console.log(err.response));
      return data;
    } catch (error) {
      console.log(error);
      return [];
    }
  }
);
</script>

```

In the code above, we’re using `useAsyncData`, which Nuxt provides to fetch or get asynchronous data into our app; more on data fetching in [Nuxt in the docs.](https://v3.nuxtjs.org/docs/usage/data-fetching#useasyncdata). Within the `useAsyncData` block, we’re using the global `$butter` helper we created earlier to retrieve the example post as shown in the **API Explorer.** Now, if we navigate to `http://localhost:3000/blog/<any-slug>`, we’ll see this:


![An example post](https://blog.openreplay.com/static/050960788459a9785d9c2469f87713d4/68382/image05.png)


Great! Now that we’ve set up Butter SDK in our Nuxt app, let’s go back to the Butter dashboard to start creating, organizing, and publishing content.

## Create a new blog post

It’s pretty easy to create a new blog post using ButterCMS. It provides you with a preconfigured blog post template with all the fields you need to get an SEO-optimized blog up and running quickly. Navigate to ***Blog Posts > New*** to create a new blog post. Let’s create our first blog post.

**Enter Title and WYSIWYG fields**
Enter content into the **Title** and **WYSIWYG**(What You See Is What You Get) fields.

![]./images/image06.png)


Butter’s WYSIWYG editor offers rich features, so feel free to play around and explore.
Next, Butter also offers the ability to enter metadata and SEO optimization fields. We’ll look at this now:

**Enter Metadata fields**
Metadata fields allow you to enter helpful information about the blog post. Let’s look at some of the fields:

- **Author:** With Butter, we can add Authors and their corresponding bio information ********by navigating to the*** [*Users*](https://buttercms.com/team/people/)[*](https://buttercms.com/team/people/) [page.](https://buttercms.com/team/people/) You can select the Author for our Blog Post from the Author dropdown. It’s set to our user account by default.
- **Publish Date:** This is the date the post is published, and we can change it from the default date.
- **Categories & Tags:** These are comma-separated values that can be used to organize and filter blog posts
- **Summary:** This can be a short description for the blog post.
- **Featured Image:** We can also add a featured image to the blog post using Butter’s Media Library. 
- To add a featured image, click on the **featured media box** that opens up the Media library:
![Media library](https://blog.openreplay.com/static/7eca0a45df7df1028008e1926deabd36/68382/image07.png)

- Click on the **Upload Media** button at the top-right corner of the modal to upload an image:
![Uploading an image](https://blog.openreplay.com/static/80b97b17e112acffbc778719e60c3d5e/68382/image08.png)

- Now, we can upload an image from our file manager or drag and drop a file onto the box.
![Uploading an image](https://blog.openreplay.com/static/ef1b28cf0b907bca8926b0ff359c8d1b/68382/image09.png)

- Once we’ve uploaded the image, we can insert it by clicking on the **Insert Media** button at the top.
- **Featured Image Alt Text:** Here’s a field we can use to enter the alt text for the featured image.

Here’s what our metadata section looks like now:

![Metadata section, updated](https://blog.openreplay.com/static/87748e02d0b993b9a5951ddb049970ac/68382/image10.png)


**Enter SEO fields:**
With Butter, we can also optimize our SEO for each blog post by providing SEO fields such as title and description. Butter also has a handy search engine preview which helps keep the text within the character range.

![Entering SEO fields](https://blog.openreplay.com/static/3a3c046625d2f872623267a545366d83/68382/image11.png)


**View the post in the API explorer**
As we’ve seen earlier, the API explorer allows us to see the Butter SDK in action and the expected response data. This way, we can see the data we’re expecting and know how to work with it. Click on the **Save draft** button to save the post.

![Saving a post](https://blog.openreplay.com/static/3c4ef7f4d08451c98365453a2d9f13cf/68382/image12.png)


Click on the **three dots (…)** button and select **API Explorer** in the dropdown.

![Opening API Explorer](https://blog.openreplay.com/static/45d40ef16a70649d0a005cc6150e9b88/68382/image13.png)


Now that we’ve seen what the data looks like, we can close the API explorer and publish the post by clicking on the **Publish** button.

**Update blog post URL slug**
We might need to update the default auto-generated slug value for our post for some reason. To do that, open the post from the **Blog Posts** page and select the blog post. There’s a new **URL Slug** field in the SEO section where we can update the slug and click on the **Update** button to save the changes.

![Updating the blog](https://blog.openreplay.com/static/c3235e3e9af955de5088658a889fc3bb/68382/image14.png)


Next, we’ll create a few more posts and head over to Nuxt to display our posts on the front end.

## Open Source Session Replay
_Debugging a web application in production may be challenging and time-consuming. [OpenReplay](https://github.com/openreplay/openreplay) is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder. OpenReplay is the only open-source alternative currently available._

![replayer.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1647097041609/Qi6sMOaaB.png)

_Happy debugging, for modern frontend teams - [Start monitoring your web app for free](https://github.com/openreplay/openreplay)._


## Display posts in the Nuxt front end

First, we’ll be displaying all the blog posts from Butter. Let’s create a new page `pages/index.vue`. In the `<script>`, we’ll import our `$butter` helper function, pass in pagination options and fetch the first ten pages. Also, since we have categories and tags for our posts, we’ll be fetching all the categories to create a `<select>` input to filter all the posts by categories. We can also do the same for tags, but we’ll focus on categories for now.

```
<!-- pages/index.vue -->

<script setup>
const { $butter } = useNuxtApp();
const posts = ref([]);
const categories = ref([]);

posts.value = await $butter.post
  .list({
    page: 1,
    page_size: 10,
  })
  .then((res) => res.data.data)
  .catch((err) => {
    console.log("ERR ==>", err.response);
    return [];
  });
categories.value = await $butter.category
  .list()
  .then((res) => {
    return res.data.data;
  })
  .catch((err) => {
    console.log("ERR ==>", err.response);
    return [];
  });
</script>

```

In the above code, we’re setting up the `posts` and `categories` variables as reactive variables using `ref()`. Then we set the values by calling `$butter.post.list()` and `$butter.category.list()` and assigning the fetched data to `posts.value` and `categories.value` respectively. Next, in the template, we’ll create a simple UI to list the posts. We’ll use a `v-for= “”  “post in posts “” ` to loop through all the posts and render a `<article-card>` list item with the post data.

**Create** `article-card` **component**
Create a new `component/articleCard.vue` component

```
<!-- component/articleCard.vue -->
<template>
  <li class="post">
    <nuxt-link
      :to="`/blog/${post.slug}`"
      class="flex flex-col md:flex-row gap-4 w-full bg-white p-4"
    >
      <div class="img-cont w-full md:w-40 h-32">
        <img
          class="object-cover w-full h-full"
          :src="post.featured_image"
          :alt="post.featured_image_alt"
        />
      </div>
      <div
        class="wrapper flex flex-col justify-between gap-4"
      >
        <header>
          <h1 class="font-bold text-2xl">
            {{ post.title }}
          </h1>
          <p>{{ post.summary }}</p>
        </header>
        <ul class="categories flex gap-2">
          <li
            v-for="category in post.categories"
            class="category bg-gray-100 text-gray-800 p-1 px-2"
          >
            {{ category.name }}
          </li>
        </ul>
        <footer>
          <hr class="my-4" />
          <ul class="flex gap-4 text-gray-600">
            <li class="author">
              {{
                `${post.author.first_name} ${post.author.last_name}`
              }}
            </li>
            <li class="published">
              {{ new Date(post.published).toDateString() }}
            </li>
          </ul>
        </footer>
      </div>
    </nuxt-link>
  </li>
</template>
<script setup>
defineProps(["post"]);
</script>

```

Cool, so here you can see that we’re passing props using the `defineProps()` [macro](https://vuejs.org/guide/components/props.html#props-declaration) and rendering the data in the template. Let’s see this in action.

```
<!-- pages/index.vue -->

<template>
  <main class="min-h-screen">
    <header
      class="page-header flex items-center justify-center h-56 bg-gray-900 text-gray-50 text-center"
    >
      <div class="wrapper max-w-5xl m-auto">
        <h1 class="font-black text-5xl">Posts</h1>
        <p>View all posts</p>
      </div>
    </header>
    <section class="p-4">
      <div class="wrapper max-w-5xl m-auto">
        <form action="" class="filter-form relative p-4">
          <div class="wrapper">
            <div class="form-control">
              <select
                name="category"
                id="category"
                class="bg-gray-50 p-4 shadow"
              >
                <option value="">All Categories</option>
                <option
                  v-for="category in categories"
                  :key="category.slug"
                  :value="category.slug"
                >
                  {{ category.name }}
                </option>
              </select>
            </div>
          </div>
        </form>
        <ul class="posts flex flex-col gap-4">
          <article-card
            v-for="post in posts"
            :key="post.id"
            :post="post"
          />
        </ul>
      </div>
    </section>
  </main>
</template>

```

Here’s what it should look like:

![The end result](https://blog.openreplay.com/static/53f75a2ded12fa138157ebb00c1c0f48/68382/image15.png)


Next, we’ll work on filter functionality to filter the posts by category.

## Filtering posts by categories

There are two ways we can go about filtering our blog posts. We can filter it on the client-side when we’ve fetched all the articles on the blog page using a computed property, or we can create dynamic pages and fetch the categories from Butter with the posts data included.

**1. Use a computed property**
In `pages/index.vue`, we’ll use a computed method to filter the posts and return them to a reactive `filterdPosts` variable.

```
<!-- pages/index.vue -->

<script>
// ...

const activeCategory = ref("");
const filteredPosts = computed(() => {
  if (
    activeCategory.value &&
    activeCategory.value !== "all"
  ) {
    let filteredPosts = posts.value.filter((post) => {
      return post.categories.some(
        (cat) => cat.slug === activeCategory.value
      );
    });
    console.log(filteredPosts);
    return filteredPosts;
  } else {
    return posts.value;
  }
});
</script>
```

In the code above, we have a reactive `activeCategory` variable which will be used to set the category to filter posts within the `filteredPosts` function. In the `filteredPosts` function, we assign it to a `computed` method which checks if `activeCategory` has a value and is not equal to `“all”`. Then returns the posts that contain that category. With the `else` block, if `activeCategory` is `”  “all ” ` or some other value, it’ll return the original `posts` data. We can implement this in the `<template>` by modifying the `<select>` element.

```
<!-- pages/index.vue -->
<template>
  ...
  <select
    name="category"
    id="category"
    class="bg-gray-50 p-4 shadow"
  >
    <option
      value="all"
      @click="() => (activeCategory = `all`)"
    >
      All Categories
    </option>
    <option
      v-for="category in categories"
      :key="category.slug"
      :value="category.slug"
      @click="() => (activeCategory = category.slug)"
    >
      {{ category.name }}
    </option>
  </select>
  ...
</template>
```

This sets the `activeCategory` value to the `category.slug` of the selected `<option>`. We can render posts using the computed `filteredPosts` variable instead.

```
<!-- pages/index.vue -->

<template>
  ...
  <article-card
    v-for="post in filteredPosts"
    :key="post.id"
    :post="post"
  />
  ...
</template>
```

![Categories](https://blog.openreplay.com/2b2dae79d3f400d09ef363e3b6a7c95f/image16.gif)


**2. Use dynamic pages and the Butter** `**include**` **query parameter**
Since we can fetch all categories using `$butter.category.list()`, we can go a step further and create dynamic pages for each category. If we want to do the same for other metadata like **Authors** and **Tags**, we can easily do so. First, we will create a new category page, `pages/blog/category/index.vue`, listing all categories.

```
<!-- pages/blog/category/index.vue -->

<template>
  <main class="min-h-screen">
    <header
      class="page-header flex items-center justify-center h-56 bg-gray-900 text-gray-50 text-center"
    >
      <div class="wrapper max-w-5xl m-auto">
        <h1 class="font-black text-5xl">Categories</h1>
        <p>View all categories</p>
      </div>
    </header>
    <section class="p-4 py-12">
      <div class="wrapper max-w-5xl m-auto">
        <ul
          class="posts flex flex-row justify-center gap-8"
        >
          <li
            v-for="category in categories"
            :key="category.slug"
            class="text-2xl p-4 bg-gray-100 text-gray-800 hover:bg-gray-800 hover:text-gray-100"
          >
            <NuxtLink
              :to="`/blog/category/${category.slug}`"
              >{{ category.name }}</NuxtLink
            >
          </li>
        </ul>
      </div>
    </section>
  </main>
</template>
<script setup>
const { $butter } = useNuxtApp();
const categories = ref([]);
categories.value = await $butter.category
  .list()
  .then((res) => {
    return res.data.data;
  })
  .catch((err) => {
    console.log("ERR ==>", err.response);
    return [];
  });
</script>
```

We should have something like this:

![Changes done](https://blog.openreplay.com/static/a8bb4f13ad505837ddfd64932289bc12/68382/image17.png)


Great! We can create a dynamic page that will render posts from each category based on the slug. Create a new file `pages/blog/category/[slug].vue`

```
<!-- pages/blog/category/[slug].vue -->

<template>
  <main class="min-h-screen">
    <header
      class="page-header flex items-center justify-center h-56 bg-gray-900 text-gray-50 text-center"
    >
      <div class="wrapper max-w-5xl m-auto">
        <h1 class="font-black text-5xl">
          {{ postsByCategories.name }}
        </h1>
        <p>View all posts</p>
      </div>
    </header>
    <section class="p-4">
      <div class="wrapper max-w-5xl m-auto">
        <ul class="posts flex flex-col gap-4">
          <article-card
            v-for="post in postsByCategories.recent_posts"
            :key="post.id"
            :post="post"
          />
        </ul>
      </div>
    </section>
  </main>
</template>
<script setup>
import ArticleCard from "~~/components/articleCard.vue";
const route = useRoute();
const { $butter } = useNuxtApp();
const postsByCategories = ref([]);
const slug = route.params.slug;
postsByCategories.value = await $butter.category
  .retrieve(slug, {
    include: "recent_posts",
  })
  .then((res) => {
    return res.data.data;
  })
  .catch((err) => {
    console.log("ERR ==>", err.response);
    return [];
  });
</script>
```

In the code above, we’re retrieving the posts using the `$butter.category.retrieve()` method. We must add the `include: “recent_posts”` parameter to return posts in that category. If we navigate to say `http://localhost:3000/blog/category/article`, we should have something like this:

![Navigating to an article](https://blog.openreplay.com/static/e27fee671684e48b10cdb16f06127497/68382/image18.png)


Beautiful! A little extra thing we can do is to change the current category select to a dropdown button in the blog home page for faster navigation. Let’s create a new component `components/categoriesDropDown.vue`

```
<template>
  <div class="categories z-10">
    <div class="relative group w-42">
      <div
        class="flex items-center cursor-pointer border group-hover:border-grey-light py-1 px-2"
      >
        <p>All categories</p>
        <svg
          class="fill-current h-4 w-4"
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 20 20"
        >
          <path
            d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
          />
        </svg>
      </div>
      <div
        class="items-center absolute border border-t-0 p-1 bg-white p-2 invisible group-hover:visible w-full"
      >
        <nuxt-link
          v-for="category in categories"
          :key="category.slug"
          :to="`/blog/category/${category.slug}`"
          class="block hover:bg-gray-100 p-1 cursor-pointer"
        >
          {{ category.name }}
        </nuxt-link>
      </div>
    </div>
  </div>
</template>
<script setup>
defineProps(["categories"]);
</script>
```

This component takes in `categories` as props and renders a list of `<NuxtLink>`’s to the corresponding pages. In `pages/blog/index.vue` we can replace the current `<select>`

```
<!-- pages/blog/index.vue -->
<template>
  // ...

  <section class="p-4">
    <div class="wrapper max-w-5xl m-auto">
      <!-- replace select form with categories dropdown -->
      <categories-drop-down :categories="categories" />
      <ul class="posts flex flex-col gap-4">
        // ...
      </ul>

      // ...
    </div>
  </section>
</template>

<script>
import CategoriesDropDown from "~~/components/categoriesDropDown.vue";
// ...
</script>
```

We should have something like this now:

![How it works](https://blog.openreplay.com/38058c3adfdbb4e5fd539a84cfdb124b/image19.gif)


Alright, currently our site sucks at SEO. That’s because we’ve not yet implemented some of the awesome SEO features that Nuxt provides. Let’s fix that.

## Optimize for SEO with Nuxt Meta Tags

We can use Nuxt Meta Tags to customize meta tags for our blog. There are multiple ways we can use Meta tags in Nuxt. You can learn more about them in the [Nuxt docs](https://v3.nuxtjs.org/docs/usage/meta-tags#meta-tags). We’ll be using the [Meta Components](https://v3.nuxtjs.org/docs/usage/meta-tags#meta-components) in this tutorial since it is very quick and easy. et’s add meta tags to each of our pages so far.
**In app.vue**
We’ll add it to the top of the page, within the `<template />`.

```
<!-- app.vue -->

<template>
  <div>
    <Head>
      <Title> My CMS powered blog </Title>
      <Meta
        name="description"
        content="Welcome to my CMS powered blog! Enjoy!"
      />
    </Head>
    // ...
  </div>
</template>
```


See it in action:

![In action](https://blog.openreplay.com/static/268b9af934554b81f7f2fc10195ec28c/68382/image20.png)


Awesome. 
Let’s add some dynamic meta tags to our blog post page.
**In** `**pages/blog/[slug].vue**`
Since this is a dynamic page, we will pass the data in from `post` data fetched from Butter. Here are a few tags important tags we can add:

```
<!-- pages/blog/[slug].vue -->

<template>
  <main>
    <Head>
      <Title> {{ post.title }} </Title>
      <Meta name="description" :content="post.summary" />
      <Meta property="og:title" :content="post.title" />
      <Meta name="twitter:title" :content="post.title" />
      <Meta
        property="og:description"
        :content="post.summary"
      />
      <Meta
        name="twitter:description"
        :content="post.summary"
      />
      <Meta
        property="og:image"
        :content="post.featured_image"
      />
      <Meta
        property="twitter:image"
        :content="post.featured_image"
      />
    </Head>
    ...
  </main>
</template>
```

Here’s what it should look like:

![Result](https://blog.openreplay.com/static/594e96fac6a516526c96ac862083f834/68382/image21.png)


Awesome! Now we can add meta tags to the rest of our pages to really boost our SEO. You can check out the GitHub code [here](https://github.com/uma-victor1/butter-cms-blog).

## Conclusion

So we’ve managed to build a CMS-powered blog with ButterCMS and NuxtJS. ButterCMS has tons of other features, and we just covered the blog post feature. Butter also allows you to build pages and collections as content types. You can always access them on your dashboard and see how to use them using the **API Explorer**. Nuxt also allows us to build our blog with powerful and useful features like dynamic routing, meta tags, and more. Feel free to explore and try out more of the features Butter offers and how you can make them work hand-in-hand with NuxtJS.