Back

How to build a CMS-powered blog with Nuxt

How to build a CMS-powered blog with Nuxt

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.

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:

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

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:

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.

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. 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 options; more on that in the Nuxt docs.

// 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.. In the plugins/ directory, create a new butter.js file which will automatically be registered as a plugin:

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

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.

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

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

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

  • Click on the Upload Media button at the top-right corner of the modal to upload an image:

  • Now, we can upload an image from our file manager or drag and drop a file onto the box.

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

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.

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.

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

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.

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

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

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

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>

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:

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:

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:

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. We’ll be using the 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:

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:

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.

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.