Creating a blog with Headless CMS + Vue.js

In this short tutorial, we will learn how to use the popular Vue.js (The Progressive JavaScript Framework) with the Webiny Headless CMS.

All of the code shown in this tutorial is also hosted in our GitHub repository.



1. A Webiny Project

First of all, make sure you have a working Webiny project set up.

When setting up a new project, currently, there are two project templates you can choose from: full and cms. Both include the Headless CMS app by default.

2. Content Delivery API URL

The Headless CMS app exposes data via the Content Delivery API, which is a simple GraphQL API that dynamically updates its schema on content model changes that you make. Once you've deployed your API stack (using the yarn webiny deploy api command), you should be able to find the Content Delivery API URL in the console output:

Headless CMS API URLs

3. Content Delivery API Access Token

In order to access the data via the Content Delivery API, we'll need a valid Access Token. These can be created via the Access Tokens form, which you can reach via the main menu:

Headless CMS API Access Tokens

Create a new token and make sure to copy the actual token string. We'll need it soon.

Headless CMS API Access Tokens

Creating our first content model

Now that we have all of the prerequisites out of the way, it's time to create our first content model. Let's open the Models section of the Headless CMS app.

The Models View

Let's create a new content model named Blog Post. Click on the "plus" icon in the lower right corner of the screen and in the dialog that's about to be shown, enter the following:

New Content Mode Dialog

For the content model group, we'll use the Ungrouped, which is the default group that comes out of the box with every Headless CMS app installation.

Content model groups give you a way to organize the content models inside the main menu, allowing you to build logical sections for your content editors. You can click here to learn more.

Once we've submitted the form in the dialog, we should be redirected to the Content Model Editor. Let's add three fields: title as a Text, description as a Long Text, and image as a Files field. They will match every fact's title, description and image, respectively.

Fact Model

Save the changes by clicking on the Save button in the top right corner of the screen.

Now it's time to create the actual content. Proceed by clicking on the View content button, located on the left side of the Save button.

You can also reach the content area by clicking on the newly added Fact item in the main menu:

Fact Model - Main Menu

Managing Content

As mentioned, navigate to Headless CMS > Ungrouped > Fact and create couple of facts. Feel free to unleash your creativity. ๐Ÿ˜‰

Fact Form

Once you feel happy with the facts, you can save the changes by clicking the Save button, located at the bottom of the form.

The next and final step is to publish the blog post, which will make it actually visible in the Content Delivery API. To do that, click on the Publish icon, found at the right side in the form header.

Now that we've covered the basics of creating content models and managing content, we can move on to the Vue.js part of this tutorial.

Creating a new Vue.js app

We can create a new Vue.js app by running this command.

npx @vue/cli create <project-name>

For this example we'll use coffeeshop as the project name, and the default preset with it.

Creating a New Vue.js App

Ideally, you should create your Vue.js project in a folder outside of the Webiny project.

Now that we have a new Vue.js app ready to go, let's see what it takes to make a simple page that renders a list of all interesting facts about coffee that we've just created.

Fetching Facts

We're going to start off by installing few NPM packages:

These will help us with fetching the actual content from the Content Delivery API.

In your Vue.js project root, run the following command:

yarn add vue-apollo graphql apollo-client apollo-link-http apollo-cache-inmemory apollo-link-context graphql-tag

Once we have these ready, we can jump to the code. The following snippet shows the code located in the apolloClient.js file:

import { ApolloClient } from "apollo-client";
import { createHttpLink } from "apollo-link-http";
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from "apollo-cache-inmemory";
// Your Content Delivery API URL.
// Your Content Delivery API Access Token.
// HTTP connection to the API
const httpLink = createHttpLink({
// You should use an absolute URL here
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
// return the headers to the context so httpLink can read them
return {
headers: {
authorization: accessToken ? `${accessToken}` : "",
// Cache implementation
const cache = new InMemoryCache();
// Create the apollo client
const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
export default apolloClient;

Now we will add this apollo Client to our Vue app so that we can fetch the content from the Content Delivery API.

Let's head over to main.js file:

import Vue from 'vue';
import VueApollo from "vue-apollo";
import Vue from 'vue';
import VueApollo from "vue-apollo";
import "./assets/styles.css";
import App from './App.vue';
// import the `apolloClient` that we just created
import apolloClient from './apolloClient';
Vue.config.productionTip = false;
// add `VueApollo`
// create a provider with the `apolloClient`
const apolloProvider = new VueApollo({
defaultClient: apolloClient
new Vue({
render: h => h(App),

Finally let's render some mark up ๐Ÿ™‚

Head over to App.vue file:

<div id="app" class="min-h-screen bg-gray-900 text-gray-100">
<Nav />
<div class="max-w-3xl mx-auto px-4">
<!-- some markup code removed for brevity -->
<!-- Article list -->
<div class="flex mt-16">
<!-- Show loading message while query is loading -->
<p v-if="$apollo.queries.listFacts.loading" class="text-green-600">Loading facts ...</p>
<!-- Show error message if there is an error -->
<p v-if="$apollo.queries.listFacts.error" class="text-red-600">Oops! Something went wrong!!</p>
<div class="flex flex-col">
<!-- Loop over facts returned by query -->
<div class="mb-16" v-for="fact in listFacts &&" v-bind:key="">
<h3 class="mb-4 text-lg text-gray-200 uppercase">
{{ fact.title }}
<img :src="fact.image" alt="" class="mb-4"/>
<p class="mb-4 text-lg text-gray-300">
{{ fact.description }}
import gql from "graphql-tag";
import Nav from "./components/Nav";
export default {
name: 'App',
components: {
data() {
// save `listFacts` returned from GraphQL query
return {
listFacts: []
apollo: {
// The actual GraphQL query
listFacts: gql`
query listFacts {
listFacts {
data {

Previewing the page

Let's run yarn serve in our Vue.js project directory so we can see our page in action:

Vue.js Blog


Congratulations! ๐ŸŽ‰

We've successfully created a simple page that displays a list of all created facts, powered by Webiny Headless CMS and Vue.js.

The same can also be achieved with other popular tools, like Gatsby. To learn more, click here.

Last updated on by John Bampton