How to Build a Blog with Vue.js and ButterCMS

Posted by Taminoturoko Briggs on November 17, 2022

GSD

If you have tried building a blog by writing server-side code from scratch, you know that it takes a lot of time and effort to complete it. But this isn't the case when using a headless content management system (CMS) like ButterCMS which includes a built-in blog engine that can be used to create and manage blog posts without having to do a single thing.

In this tutorial, we will learn about ButterCMS and how to use it with Vue to build a blog.

Why use ButterCMS?

ButterCMS, being a headless CMS, acts as a repository for storing content which can then be accessed via its content API using native tools. Unlike a traditional CMS which limits the user experience provided to users, a headless CMS like ButterCMS gives us full control over the frontend, allowing us to create custom experiences for users. On top of that, ButterCMS is cloud-based and serverless, which means we don’t have to worry about scaling, security, or maintenance—saving us time to focus on more important problems. 

Another great feature ButterCMS provides that we will be utilizing in this tutorial is its built-in blog engine. It includes a content structure for creating SEO-friendly blog posts and an interface for managing created posts. 

Why use Vue.js?

Vue.js is one of the most popular frontend frameworks for building performant, single-page applications. Its lightweight nature makes it faster than its competitors, like React and Angular. The component-based approach of Vue allows for the creation of reusable pieces of code that can be used anywhere in our app—saving development time. For these reasons, and also its low learning curve and ever-growing ecosystem, Vue is one of the most-recommended frameworks to use in building a frontend.

Vue.js banner cta

Tutorial prerequisites

To follow along with this tutorial, you need the following:

  • Knowledge of Vue3 and Vue Router
  • Node.js v16 or greater installed on your system
  • An active ButterCMS account (New accounts have a free trial period of 30 days.)

Building a blog with Vue.js and ButterCMS

We will start by building the UI for our blog app. After that, we will set up ButterCMS which will hold our blog content that will then be fetched and displayed in our app. Please note that while we will be using the ButterCMS blog engine for this tutorial, you can also create custom blog pages using ButterCMS Page Types. Here is what we will be building in this tutorial:

Blog homepage

Blog post page

Setting up Vue

Here will be using create-vue to scaffold Vite-based projects. Enter the following command in your terminal:

npm init vue@3

If create-vue hasn’t been previously installed, we will be asked to do so. After doing that answer the following question like this:

Create-Vue question answers

With this, a Vue app named vue-blog will be created which includes the Vue Router setup. Next, let’s install dependencies using the following command:

cd vue-blog
npm install

Now, let’s clean our app a little, removing the folders, files, and code not needed for this tutorial. Open the created app in a code editor, head over to the src folder, and remove the following:

  • The assets folder
  • The import of main.css in the main.js file
  • The icons folder and files in the components folder
  • All files in the views folder

Next, modify the App.vue file to the following:

<script setup>
  import { RouterView } from 'vue-router'
</script>

<template>
  <RouterView />
</template>

<style >
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: system-ui, sans-serif;
}

.wrapper {
  width: 95%;
  max-width: 110px;
}
</style>

Also, router/index.js to the following:

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
   
  ]
})

export default router

Creating the blog’s UI

The blog will have a home page where all blog posts will be displayed and can be filtered using a search bar and a detail page for viewing individual posts. Let’s create these pages and the components to be used in them.

For the pages, in src/views create Home.vue and BlogDetail.vue files, and for the components, in src/components create BlogCard.vue and Header.vue files. In BlogCard.vue, we will create the component which will display short information from a blog post in the form of a card that can be clicked to view the full content of the post on the details page, and in Header.vue, we will include the search bar which will be used to filter blog posts.

Here is what the folder structure of the src directory will now look like:

src
 ┣ components
 ┃ ┣ BlogCard.vue
 ┃ ┗ Header.vue
 ┣ router
 ┃ ┗ index.js
 ┣ views
 ┃ ┣ BlogDetail.vue
 ┃ ┗ Home.vue
 ┣ App.vue
 ┗ main.js

Now, for the home page, head over to views/Home.vue and add the following lines of code:

<template>
  <Header @search="handleSearch" />
  <p v-if="!blogPosts.length" class="noBlogPost">No blog post available</p>
  <div v-else class="blogList wrapper">
    <BlogCard />
  </div>
</template>

<script setup>
  import { ref } from "vue";
  import Header from "../components/Header.vue";
  import BlogCard from "../components/BlogCard.vue";

  const blogPosts = ref([]);
  const handleSearch = (searchValue) => {}
<script>

<style scoped>
.blogList {{
  display: grid;
  margin-top: 20px;
  place-content: center;
  grid-template-columns: repeat(auto-fill, 350px);
  gap: 15px;
}

.noBlogPost {
  margin-top: 20px;
  text-align: center;
}
</style>

And for the details page, add the following line of code in views/BlogDetail.vue:

<template>
  <div></div>
</template>

<script setup>
</script>

<style>
.blogDetail {
  width: 95%;
  max-width: 900px;
  margin: 50px auto;
}

.blogDetail__head span:nth-child(2):after {
  content: '.';
  margin-left: 5px;
  vertical-align: top;
}

.blogDetail__head span {
  margin-left: 5px;
  color: rgb(128, 128, 128);
}

.blogDetail > h1 {
  margin-bottom: 15px;
}

.blogDetail > img {
  margin: 15px 0;
}

.blogDetail img{
  width: 100%;
}

.blogDetail .blogDetail__profileImg {
  width: 30px;
  height: 30px;
  border-radius: 100%;
  vertical-align: middle;
}

.blogDetail__body p {
  color: rgb(62, 60, 60);
  font-size: 18px;
}

.blogDetail__body h2, h3{
  margin: 20px 0 5px;
}

.blogDetail__body img, video {
  margin: 5px 0;
}
</style>

Now, let’s make these pages accessible by defining their routes. Head over to src/router/index.js and modify the router variable to the following:

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: "/", component: Home },
    { path: "/detail/:slug", component: BlogDetail },
  ],
});

Now, let’s create the components. Head over to components/BlogCard.vue and add the following line of code:

<template>
  <div class="card" @click="$router.push(`/detail/${slug}`)">
    <img :src="image" />
    <div class="card__details">
      <span class="mgn">{{ date }}</span>
      <h3>{{ title }}</h3>
      <p class="mgn">{{ summary }}</p>
      <div class="card__author">
        <img :src="authorImage" />
        <span>{{ authorName }}</span>
      </div>
    </div>
  </div>
</template>

<script setup>
  defineProps([
    "slug",
    "image",
    "date",
    "title",
    "summary",
    "authorImage",
    "authorName",
  ]);
</script>

<style scoped>
.mgn {
  margin-bottom: 15px;
  display: block;
}
.card {
  max-width: 350px;
  cursor: pointer;
}
.card:hover {
  box-shadow: 0 2px 6px gainsboro;
}

.card > img {
  object-fit: cover;
  width: 100%;
  height: 200px;
}

.card__details {
  padding: 10px;
}

.card__details span:first-child {
  color: gray;
}

.card__details p {
  font-size: 16px;
  color: rgb(63 63 66);
}

.card__author img {
  object-fit: cover;
  width: 50px;
  height: 50px;
  border-radius: 100%;
  vertical-align: middle;
}

.card__author span {
  margin-left: 10px;
}
</style>

Next, add the following lines of code in components/Header.vue: 

<template>
  <header>
    <div class="wrapper">
      <h2>Vue Blog</h2>
      <form @submit.prevent="this.$emit('search', searchValue)">
        <input
          v-model="searchValue"
          placeholder="Search blogs"
        />
        <button>Search</button>
      </form>
    </div>
  </header>
</template>

<script setup>
 import {ref} from 'vue'
 const searchValue = ref('')
</script>

<style scoped>
header {
  border-bottom: 1px solid gainsboro;
}

header .wrapper {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 50px;
}

form input,button {
  height: 30px;
  padding: 0 10px;
}
</style>

With this, when we open our app in the browser, we will see the home page:

Blog home page with no posts

And we can navigate to the details page by appending the URL with /detail/<slug>. Right now, the home page displays no blogs and the details page is empty. Let’s work on that.

Vue.js banner cta

Setting up ButterCMS

To get started, navigate to the ButterCMS website and set up a new user account if you do not have one already. After logging in, we will be directed to the user dashboard where we can navigate to the blog engine by clicking on the Blog posts icon in the sidebar.

Select Blog Post tab from the side menu in ButterCMS dashboard

We will see the following page:

Select "New Post" to create a new blog post via the ButterCMS blog engine.

Here, an example post has already been created for us which we will be working with in this tutorial. But If you wish to create a new blog post, click on the New Post button at the top-right. On the page that opens, write your blog content with the WYSIWYG editor then fill out the rest of the inputs and click on the Publish button at the top-right of the page. Below is a gif demonstrating how to work with the WYSIWYG editor:

Using the WYSIWYG editor

Now, let’s get our read API token which will enable us to work with ButterCMS in our app. To do this, hover over your image at the top on the sidebar and click on Settings in the dropdown that appears and we will be taken to the following page where we will see the token:

Read API token

Copy the token and store it in a .env file in our app which should look like this:

VITE_READ_API_TOKEN = <your-read-api-token-here>

Working with ButterCMS in our application 

Now that everything has been set up, we are down to consuming our content created with the ButterCMS blog engine. For this, we can either use the content API or ButterCMS SDK, both of which function as stated in the API Reference. For this tutorial, we will be using the content API.

We will start by displaying all our blog posts on the home page. Head over to src/views/Home.vue and add the following code after blogPosts state:

fetch(`https://api.buttercms.com/v2/posts/?exclude_body=true&auth_token=${import.meta.env.VITE_READ_API_TOKEN}`)
  .then((res) => res.json())
  .then((data) => {
    blogPosts.value = data.data
 })

The above code will fetch all blog posts from our CMS and store them in the blogPosts state. In the URL passed to fetch, since we have set the exclude_body query parameter to true, the return posts will not include a body.

Now, to display our post, modify <BlogCard /> in the template to the following:

<BlogCard
  v-for="post in blogPosts"
  :slug="post.slug"
  :image="post.featured_image"
  :date="new Date(post.created).toISOString().split('T')[0]"
  :title="post.title"
  :summary="post.summary"
  :authorImage="post.author.profile_image"
  :authorName="post.author.first_name + post.author.last_name"
/>

With this, when we open our app in the browser, we should see the following:

Blog homepage search bar in right corner

Next, let’s add the search functionality where text entered in the search bar will be used to filter posts by their title. Here is what we will be creating:

Using the blog search function

For this, we will need to create another state that will hold the filtered post and be used to display the UI rather than directly filtering the fetched posts in the blogPosts state. This way, no post will be lost even after filtering.

First, let's render our UI from another state which will also hold the filtered results. 

Add the following import in the script tag of the Home.vue file:

import {watch} from 'vue'

Next, add the following lines of code after the blogPosts state:

const filtered = ref([])

watch(blogPosts, (newData) => {
  filtered.value = newData
})

Next, modify the rendered BlogCard component to loop through the filtered state rather than the blogPosts state:

<BlogCard
   v-for="post in filtered"
   //...
 />

Now, to start filtering, modify the handleSearch function which, while creating the UI, we have passed to the search event emitted when the search form is submitted.

const handleSearch = (searchValue) => {
  const filtered = blogPosts.value.filter((post) => (
    post.title.toLowerCase().includes(searchValue.toLowerCase())
  ))
  filtered.value = result
 }

The above code filters the blog posts with titles that include the searched text passed as an argument and then sets the results in the filtered state. With this, when we enter text in the search bar in our app and click on the Search button, the appropriate posts will be displayed.

Finally, let’s work on the details page where we will view the full content of an individual post. For this, we will be using the slug attached to the URL when a post is clicked to fetch a single post from our CMS.

Head over to src/views/BlogDetail and modify the script tag to the following:

<script setup>
import { ref } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
const slug = route.params.slug;
const blogPost = ref({});

fetch(
  `https://api.buttercms.com/v2/posts/${slug}/?auth_token=${
    import.meta.env.VITE_READ_API_TOKEN
  }`
)
  .then((res) => res.json())
  .then((data) => {
    blogPost.value = data.data;
  });
</script>

In the above code, we get the slug from the page’s URL then use it to fetch the corresponding blog post and set the returned response in the blogPost state.

Now, let’s display the content of the post. Modify the template tag to the following:

<template>
  <div class="blogDetail">
    <h1>{{blogPost.title}}</h1>
    <div class="blogDetail__head">
      <img :src="blogPost.author.profile_image" class="blogDetail__profileImg" />
      <span>{{blogPost.author.first_name + ' ' + blogPost.author.last_name}}</span>
      <span>{{new Date(blogPost.created).toISOString().split('T')[0]}}</span>
    </div>
    <img :src="blogPost.featured_image"/>
    <div class="blogDetail__body" v-html="blogPost.body"></div>
  </div>
</template>

With this when we click on the example post in our app, we will be taken to the details page which will look like this:

Blog post page

Final thoughts

The ButterCMS blog engine is a game changer for building and adding blogs to your application. With it, we can literally set up a blog in a few hours rather than taking several days to write lengthy code to set up a backend which we'd have to maintain for the foreseeable future. After reading this tutorial, we encourage you to try Butter's blog engine out for yourself. Let us know how it goes in the comments below!

Make sure you receive the freshest Vue.js tutorials and Butter product updates.
    
Taminoturoko Briggs

Taminoturoko Briggs is a software developer and technical writer with sound knowledge of different web technologies. His core languages include JavaScript and Python.

ButterCMS is the #1 rated Headless CMS

Related articles

Don’t miss a single post

Get our latest articles, stay updated!