How to Build a Vue Front End for a Headless CMS
In this guide, we’ll learn how to build a modern blog website using Vue.js and GraphCMS, a headless CMS platform.
If you’re looking to start a quick blog today, my recommendation is to go straight to WordPress.
But what if you’re a media powerhouse and you want to deliver your content as fast as possible to multiple devices? You’ll probably also need to integrate your content with ads and other third-party services. Well, you could do that with WordPress, but you’ll come across a few problems with that platform.
- You’ll need to install a plugin to implement additional features. The more plugins you install, the slower your website becomes.
- PHP is quite slow compared to most JavaScript web frameworks. From a developer’s perspective, it’s much easier and faster to implement custom features on a JavaScript-powered front end.
JavaScript offers superior performance to PHP in browser loading tests. In addition, modern JavaScript and its ecosystem provides a far more pleasant development experience when it comes to building new web experiences fast.
Want to learn Vue.js from the ground up? This article is an extract from our Premium library. Get an entire collection of Vue books covering fundamentals, projects, tips and tools & more with SitePoint Premium. Join now for just $9/month.
So there’s been a growth of headless CMS solutions — which are simply back ends for managing content. With this approach, developers can focus on building fast and interactive front ends using a JavaScript framework of their choice. Customizing a JavaScript-powered front end is much easier than making changes on a WordPress site.
GraphCMS differs from most Headless CMS platforms in that, instead of delivering content via REST, it does so via GraphQL. This new technology is superior to REST, as it allows us to construct queries that touch on data belonging to multiple models in a single request.
Consider the following model schema:
Post
- id: Number
- title: String
- content : String
- comments : array of Comments
Comment
- id: Number
- name: String
- message: String
The above models have a one(Post)-to-many(Comments) relationship. Let’s see how we can fetch a single Post record attached with all linked Comment records.
If the data is in a relational database, you have to construct either one inefficient SLQ statement, or two SQL statements for fetching the data cleanly. If the data is stored in a NoSQL database, you can use a modern ORM like Vuex ORM to fetch the data easily for you, like this:
const post = Post.query()
.with('comments')
.find(1);
Quite simple! You can easily pass this data via REST to the intended client. But here’s the problem: whenever the data requirement changes at the client end, you’ll be forced to go back to your back-end code to either update your existing API endpoint, or create a new one that provides the required data set. This back and forth process is tiring and repetitive.
What if, at the client level, you could just ask for the data you need and the back end will provide it for you, without you doing extra work? Well, that’s what GraphQL is for.
Prerequisites
Before we begin, I’d like to note that this is a guide for intermediate to advanced users. I won’t be going over the basics, but rather will show you how to quickly build a Vue.js blog using GraphCMS as the back end. You’ll need to be proficient in the following areas:
- ES6 and ES7 JavaScript
- Vue.js (using CLI version 3)
- GraphQL
That’s all you need to know to get started with this tutorial. Also, a background in using REST will be great, as I’ll be referencing this a lot. If you’d like a refresher, this article might help: “REST 2.0 Is Here and Its Name Is GraphQL”.
About the Project
We’ll build a very simple blog application with a basic comment system. Below are the links you can visit to check out the completed project:
Please note that a READ-ONLY token has been used in the demo and consequently the comments system won’t work. You’ll need to supply your OPEN permission token and endpoint as per the instructions in this tutorial for it to work.
Create GraphCMS Project Database
Head over to the GraphCMS website and click the “Start Building for Free” button. You’ll be taken to their signup page.
Sign up using your preferred method. Once you’ve completed the account authentication and verification process, you should be able to access the main dashboard.
In the above example, I’ve already created a project called “BlogDB”. Go ahead and create a new one, and call it whatever you want. After you’ve entered the name, you can leave the rest of the fields in their defaults. Click Create and you’ll be taken to their project plan.
For the purposes of this tutorial, select the free Developer plan then click Continue. You’ll be taken to the project’s dashboard, which looks something like this:
Go to the Schema tab. We’re going to create the following models, each with the following fields:
Category
- name: Single line text, required, unique
Post
- slug: Single line text, required, unique
- title: Single line text, required, unique
- content: Multi line text
Comment
- name: Single line text, required
- message: Multi line text, required
Use the Create Model button to create models. On the right side, you should find a hidden panel for Fields, which is activated by clicking the Fields button. Drag the appropriate field type onto the model’s panel. You will be presented with a form to fill in your field’s attributes. Do note at the bottom there’s a pink button labeled Advanced. Clicking it will expand the panel to give you more field attributes you can enable.
Next, you’ll need to add the relationship between models as follows:
- Post > Categories (many-to-many)
- Post > Comments (one-to-many)
Use the Reference field to define this relationship. You can add this field to any side; GraphCMS will automatically create the opposite relation field in the referenced model. When you’ve completed defining the models, you should have something like this:
You’ve now completed the first part. Let’s now provide some data to our models.
GraphQL Data Migration
To add content to your models, you can simply click the Content tab in the project dashboard where you can create new records for each of your models. However, if you find this to be a slow method, you’ll be happy to know that I’ve created a GraphCMS migration tool that copies data from CSV files and uploads them to your GraphCMS database. You can find the project here in this GitHub repository. To start using the project, simply download it into your workspace like this:
git clone git@github.com:sitepoint-editors/graphcsms-data-migration.git
cd graphcsms-data-migration
npm install
Next, you’ll need to grab your GraphCMS project’s API endpoint and token from the dashboard’s Settings page. You’ll need to create a new token. For the permission level, use OPEN, as this will allow the tool to perform READ and WRITE operations on your GraphCMS database. Create a file called .env
and put it at the root of the project:
ENDPOINT=<Put api endpoint here>
TOKEN=<Put token with OPEN permission here>
Next, you may need to populate the CSV files in the data folder with your own. Here’s some sample data that has been used:
// Categories.csv
name
Featured
Food
Fashion
Beauty
// Posts.csv
title,slug,content,categories
Food Post 1,food-post-1,Breeze through Thanksgiving by making this Instant Pot orange cranberry sauce,Food|Featured
Food Post 2,food-post-2,This is my second food post,Food
Food Post 3,food-post-3,This is my last and final food post,Food
Fashion Post 1,fashion-post-1,This is truly my very first fashion post,Fashion|Featured
Fashion Post 2,fashion-post-2,This is my second fashion post,Fashion
Fashion Post 3,fashion-post-3,This is my last and final fashion post,Fashion
Beauty Post 1,Beauty-post-1,This is truly my very first Beauty post,Beauty|Featured
Beauty Post 2,Beauty-post-2,This is my second beauty post,Beauty
You can change the content if you want. Make sure not to touch the top row, as otherwise you’ll change the field names. Please note, for the column categories
, I’ve used the pipe |
character as a delimiter.
To upload the CSV data to your GraphCMS database, execute the following commands in this order:
npm run categories
npm run posts
Each script will print out records that have uploaded successfully. The reason we uploaded categories
first is so that the posts
records can link successfully to existing category
records.
If you want to clean out your database, you can run the following command:
npm run reset
This script will delete all your model’s contents. You’ll get a report indicating how many records were deleted for each model.
I hope you find the tool handy. Go back to the dashboard to confirm that data for the Posts
and Categories
have successfully been uploaded.
With the back end taken care of, let’s start building our front-end blog interface.
Building the Blog’s Front End Using Vue.js
As mentioned earlier, we are going to build a very simple blog application powered by a GraphCMS database back end. Launch a terminal and navigate to your workspace.
If you haven’t got Vue CLI installed, do that now:
npm install -g @vue/cli
Then create a new project:
vue create vue-graphcms
Choose to manually select features, then select the following options:
- Features: Babel, Router
- Router History Mode: Y
- ESLint with error prevention only
- Lint on save
- Config file placement: Dedicated Config Files
- Save preset: your choice
Once the project creation process is complete, change into the project directory and install the following dependencies:
npm install bootstrap-vue axios
To set up Bootstrap-Vue
in our project, simply open src/main.js
and add the following code:
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
Vue.config.productionTip = false;
Vue.use(BootstrapVue);
Next, we need to start laying down our project structure. In the src/components
folder, delete the existing files and create these new ones:
CommentForm.vue
CommentList.vue
Post.vue
PostList.vue
In the src/views
folder, delete About.vue
and create a new file called PostView.vue
. As seen from the demo, we’ll have several category pages each displaying a list of posts filtered by category. Technically, there will only be one page that will display a different list of posts based on an active route name. The PostList
component will filter posts based on the current route.
Let’s first set up the routes. Open src/router.js
and replace the existing code with this:
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Post from "./views/PostView.vue";
Vue.use(Router);
export default new Router({
mode: "history",
base: process.env.BASE_URL,
linkActiveClass: "active",
routes: [
{
path: "/",
name: "Featured",
component: Home
},
{
path: "/food",
name: "Food",
component: Home
},
{
path: "/fashion",
name: "Fashion",
component: Home
},
{
path: "/beauty",
name: "Beauty",
component: Home
},
{
path: "/post/:slug",
name: "Post",
component: Post
}
]
});
Now that we have our routes, let’s set up our navigation menu. Open src/App.vue
and replace the existing code with this:
<template>
<div id="app">
<b-navbar toggleable="md" type="dark" variant="info">
<b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
<b-navbar-brand href="#">GraphCMS Vue</b-navbar-brand>
<b-collapse is-nav id="nav_collapse">
<b-navbar-nav>
<router-link class="nav-link" to="/" exact>Home</router-link>
<router-link class="nav-link" to="/food">Food</router-link>
<router-link class="nav-link" to="/fashion">Fashion</router-link>
<router-link class="nav-link" to="/beauty">Beauty</router-link>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<b-container>
<router-view/>
</b-container>
</div>
</template>
This will add a nav bar to the top of our site with links to our different categories.
Save the file and update the following files accordingly:
src/views/Home.vue
<template>
<div class="home">
<PostList />
</div>
</template>
<script>
import PostList from "@/components/PostList.vue";
export default {
name: "home",
components: {
PostList
}
};
</script>
src/components/PostList.vue
<template>
<section class="post-list">
<h1>{{ category }} Articles</h1>
<hr/>
<p>Put list of posts here!</p>
</section>
</template>
<script>
export default {
name: "PostList",
data() {
return {
category: ""
};
},
created() {
this.category = this.$route.name;
},
watch: {
$route() {
this.category = this.$route.name;
}
}
};
</script>
Notice that, in the PostList
component, we’re using a custom watcher to update our category
data property, based on our current URL.
Now we’re ready to perform a quick test to confirm the routes are working. Spin up the Vue.js server using the command npm run serve
. Open a browser at localhost:8080 and test each navigation link. The category
property should output the same value we defined in route name’s attribute.
Pulling in Data From GraphCMS
Now that we have our routing code working, let’s see how we can pull information from our GraphCMS back end. At the root of your project, create an env.local
file and populate it with values for the following fields:
VUE_APP_ENDPOINT=
VUE_APP_TOKEN=
Do note that Vue.js single-page applications only load custom environment variables starting with VUE_APP
. You can find the API endpoint and token from your GraphCMS dashboard settings page. For the token, make sure to create one with OPEN permission, as that will allow both READ and WRITE operations. Next, create the file src/graphcms.js
and copy the following code:
import axios from "axios";
export const ENDPOINT = process.env.VUE_APP_ENDPOINT;
const TOKEN = process.env.VUE_APP_TOKEN;
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${TOKEN}`
};
export const apiClient = axios.create({
headers
});
export const POSTS_BY_CATEGORY_QUERY = `
query PostsByCategory($category: String!){
category(where: {
name: $category
}
){
name,
posts {
id
slug
title
content
categories {
name
}
}
}
}
`;
export const POST_BY_SLUG_QUERY = `
query PostBySlug($slug: String!){
post(where: {
slug: $slug
})
{
id
title
content
categories {
name
}
comments {
name
message
}
}
}
`;
export const CREATE_COMMENT_MUTATION = `
mutation CreateComment($post: PostWhereUniqueInput!, $name: String!, $message: String!){
createComment(data: {
name: $name,
message: $message,
post: {
connect: $post
},
status: PUBLISHED
})
{
id
name
message
}
}
`;
This helper file we just created provides two main functions:
- It creates an instance of axios that’s configured to perform authorized requests to your GraphCMS back end.
- It contains GraphQL queries and mutations used in this project. These are responsible for fetching posts (either by category or by slug), as well as for creating new comments. If you’d like to find out more about GraphQL queries and mutations, please consult the GraphQL docs.
You can also use the API explorer in your project dashboard to test out these queries and mutations. To do this, copy the query or mutation from the code above and paste it into the top window of the API explorer. Enter any query variables in the window below that, then hit the Play button. You should see the results in a new pane on the right.
Here’s a query example:
Here’s a mutation example:
Displaying the Data in a Template
Now, let’s create our HTML template in our src/components/PostList.vue
that will display a list of posts in a neat way. We’ll also add the axios code that will pull in posts
data from our GraphCMS database:
<template>
<section class="post-list">
<h1>{{ category }} Articles</h1>
<hr/>
<b-row v-if="loading">
<b-col class="text-center">
<div class="lds-dual-ring"></div>
</b-col>
</b-row>
<div v-if="!loading" >
<b-card tag="article" v-for="post in posts" :key="post.id" :title="post.title" :sub-title="post.categories.map(cat => cat.name).toString()">
<p class="card-text">
{{ post.content }}
</p>
<router-link class="btn btn-primary" :to="'post/' + post.slug">
Read Post
</router-link>
</b-card>
</div>
</section>
</template>
<script>
import { ENDPOINT, apiClient, POSTS_BY_CATEGORY_QUERY } from "../graphcms.js";
export default {
name: "PostList",
data() {
return {
category: "",
loading: false,
posts: []
};
},
methods: {
async fetchPosts() {
try {
this.loading = true;
const response = await apiClient.post(ENDPOINT, {
query: POSTS_BY_CATEGORY_QUERY,
variables: {
category: this.category
}
});
const body = await response.data.data;
this.posts = await body.category.posts;
this.loading = false;
} catch (error) {
console.log(error);
}
}
},
created() {
this.category = this.$route.name;
this.fetchPosts();
},
watch: {
$route() {
this.category = this.$route.name;
this.posts = [];
this.fetchPosts();
}
}
};
</script>
<style>
h1{
margin-top: 25px !important;
}
.lds-dual-ring {
display: inline-block;
width: 64px;
height: 64px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 46px;
height: 46px;
margin: 1px;
border-radius: 50%;
border: 5px solid #ccc;
border-color: #ccc transparent #ccc transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
Let’s quickly go through the code’s main features:
- Loading. When a request is made, a loading spinner is displayed to indicate to the user there’s something in progress. When the request is fulfilled, the loading spinner is replaced with a list of posts.
- Query. In order to get a list of posts by category, I found it easier to query for the category, then use the category-to-posts relationship to get access to the filtered posts.
- Created. When the page is loaded for the first time, the
fetchPosts()
function is called from within thecreated
lifecycle hook. - Watch. When the route URL changes, the
fetchPosts()
function is called each time.
After making those changes, you should now have the following view:
Displaying an Individual Post
Make sure the top main navigation is working as expected. Let’s now work on the Post
component. It will have its own fetchPost()
function, where it will query by slug
. If you’re wondering where the slug
parameter is coming from, let me remind you of this bit of code we put in router.js
:
//...
{
path: '/post/:slug',
name: 'Post',
component: Post
},
//...
This states that anything that comes after /post/
in the URL is available to us in the component as this.$route.params.slug
.
The post
component is a parent of the CommentForm
and CommentList
components. The comments
data will be passed as props to the CommentList
component from the Posts record. Let’s insert code for src/components/CommentList.vue
now:
<template>
<section class="comment-list">
<hr/>
<h4 class="text-muted">Comments</h4>
<b-card v-for="comment in comments" :title="comment.name" title-tag="h5" :key="comment.id">
<p class="card-text text-muted">{{ comment.message }} </p>
</b-card>
<p v-if="comments.length === 0" class="text-center text-muted">No comments posted yet!</p>
</section>
</template>
<script>
export default {
name: "CommentsList",
props: ["comments"]
};
</script>
Unless you’ve manually entered comments via the GraphCMS dashboard, don’t expect to see any results just yet. Let’s add code to src/components/CommentForm.vue
that will enable users to add comments to a blog post:
<template>
<section class="comment-form">
<h4 class="text-muted">Comment Form</h4>
<b-form @submit.prevent="onSubmit">
<b-form-group label="Name">
<b-form-input id="input-name" type="text" v-model="name" placeholder="Enter your name" required></b-form-input>
</b-form-group>
<b-form-group label="Message">
<b-form-textarea id="input-message" v-model="message" placeholder="Enter your comment" :rows="3" :max-rows="6" required>
</b-form-textarea>
</b-form-group>
<b-button type="submit" variant="primary">Submit</b-button>
</b-form>
</section>
</template>
<script>
import { apiClient, ENDPOINT, CREATE_COMMENT_MUTATION } from "../graphcms.js";
export default {
name: "CommentForm",
props: ["post"],
data() {
return {
name: "",
message: ""
};
},
methods: {
async onSubmit() {
const formattedComment = {
name: this.name,
message: this.message,
post: {
id: this.post.id
}
};
try {
const response = await apiClient.post(ENDPOINT, {
query: CREATE_COMMENT_MUTATION,
variables: formattedComment
});
const body = await response.data.data;
const newComment = body.createComment;
this.post.comments.push(newComment);
this.name = "";
this.message = "";
} catch (error) {
console.log(error);
}
}
}
};
</script>
<style>
.comment-form {
margin-top: 35px;
}
</style>
We now have a basic comment form capable of submitting a new comment to our GraphQL back-end system. Once the new comment is saved, we’ll take the returned object and add it to the post.comments
array. This should trigger the CommentList
component to display the newly added Comment
.
Let’s now build the src/components/Post.vue
component:
<template>
<section class="post">
<b-row v-if="loading">
<b-col>
<div class="lds-dual-ring text-center"></div>
</b-col>
</b-row>
<b-row v-if="!loading">
<b-col>
<h1>{{post.title}}</h1>
<h4 class="text-muted">{{post.categories.map(cat => cat.name).toString()}}</h4>
<hr>
<p>{{ post.content }}</p>
</b-col>
</b-row>
<!-- List of comments -->
<b-row v-if="!loading">
<b-col>
<CommentList :comments="post.comments" />
</b-col>
</b-row>
<!-- Comment form -->
<b-row v-if="!loading">
<b-col>
<CommentForm :post="post" />
</b-col>
</b-row>
</section>
</template>
<script>
import { ENDPOINT, apiClient, POST_BY_SLUG_QUERY } from "../graphcms.js";
import CommentList from "@/components/CommentList";
import CommentForm from "@/components/CommentForm";
export default {
name: "Post",
components: {
CommentList,
CommentForm
},
data() {
return {
loading: false,
slug: "",
post: {}
};
},
methods: {
async fetchPost() {
try {
this.loading = true;
const response = await apiClient.post(ENDPOINT, {
query: POST_BY_SLUG_QUERY,
variables: {
slug: this.slug
}
});
const body = await response.data.data;
this.post = body.post;
this.loading = false;
} catch (error) {
console.log(error);
}
}
},
created() {
this.slug = this.$route.params.slug;
this.fetchPost();
}
};
</script>
Finally, here’s the code for src/views/PostView.vue
to tie everything together:
<template>
<div class="post-view">
<Post/>
</div>
</template>
<script>
import Post from "@/components/Post.vue";
export default {
name: "PostView",
components: {
Post
}
};
</script>
You should now have the following view for Posts. Take note of the :slug
at the end of the URL localhost:8080/post/fashion-post-1
:
In the above example, I’ve added a couple of comments to test out the new feature. Make sure you do the same on yours.
Summary
I hope you’ve seen how easy it is to build a blog website using Vue.js and GraphQL. If you’d been using plain PHP and MySQL, you’d have written much more code. Even with a PHP framework, you still would have written more code for a simple blog application.
For the sake of this tutorial, I had to keep things as simple as possible. You may note that this blog project is far from even meeting a minimalistic blog setup. There are several things we haven’t tackled, such as error handling, form validation and caching. For the last bit, I recommend Apollo Client, as it has mechanisms for caching GraphQL query results. Then of course there needs to be an author model, and a proper comments system that supports authentication and message approval.
If you’re up to it, please go ahead and take this simple Vue.js GraphCMS blog even further.