Build a Vue.js SPA on Top of Headless WordPress

In a rush? Skip to technical tutorial or live demo

I can’t believe I’m about to write about WordPress again.

I’m only (half) kidding, as the last time I played around the WP REST API, I thought it was actually pretty dope!

It was a real breath of fresh air to see a mammoth of the web industry opening up to modern trends and new paradigms.

So I wondered: how fun would it be to use WordPress as a headless CMS with Vue.js—my beloved frontend framework?

The answer? A whole d*mn lot of fun, as it turns out. In this post, I want to go further in-depth with that stack.

In the technical tutorial, I'll cover the following steps:

  1. Creating models with custom fields in WordPress

  2. Building a custom endpoint for the WP REST API

  3. Setting up a Vue.js single-page application

  4. Hosting the app

First, let’s define “using WordPress as a headless CMS.”

Full on JAMstack stuff here! <3

Using WordPress as a headless backend

Monolithic CMS vs Headless CMS [source]

What does it mean?

Before diving deeper into WordPress as a specific example, let’s make sure we’re on the same page as to what headless CMS means.

Until a few years ago, we were used to traditional CMSs working a certain way. They were managing everything. By that I mean the backend (data/content management, roles & permissions, admin tasks) to the frontend (in most cases PHP-generated views) and everything in between.

However, there’s a significant shift happening thanks to the rise of modern frontend development. In the vast ecosystem that has spawned around it, there’s simply no place for old, monolithic CMSs. Static site generators, API-centric micro-services & serverless architectures are here to stay.

It doesn’t mean that something like WordPress should go away though. It only means that it shouldn’t manage all parts of a website anymore. Why’s that? Because there are tools that are way better at accomplishing certain tasks than others.

So, why not use WordPress only for what it’s really good at? That’s exactly what the WP JSON REST API has enabled since the release of WordPress 4.7. It allows for the content injected in a WordPress backend to be consumed into frontend apps, no matter what technology is involved.

That’s a game changer.

You can find a more detailed WP REST API explanation in our first post on the subject.

Why would you use that?

I’m pretty sure that if you’re a developer with a minimal amount of WordPress experience, you already see the world of new opportunities this brings.

The main downsides of WordPress (which devs have been complaining about for years, by the way) was the lack of development freedom and the hassle that was template customization. But by using WordPress only as a backend, these concerns are thrown out the window and at a high-velocity, no less:

→ Freedom is given back to developers to use the tools they love, on a modular level. This allows for projects to scale only when needed and, incidentally, results in better performance.

→ Dealing with WP templating is over. Plug it into the frontend of your liking and don’t worry about overriding PHP-based templates. It’s way easier to build kick-ass UIs from scratch that can then consume WordPress data.

The following demo will highlight these benefits. Vue.js is the frontend framework that I’ll use to build my application on top of WordPress.

Why Use Vue.js with WordPress?

Tying it with JS frameworks allows us to use WordPress in applications where it would have given you a hard time in the past. Think multi-device apps, IoT, progressive web apps and other modern practices.

Other than the fact that I already used React for my first WP headless demo, there are many reasons why I would suggest Vue.js to anyone looking for the right frontend framework.

The main things you’ll hear people gush about concerning Vue is its modularity, speed, and high-performance. I would say that these are all absolutely true. And you can rest assured that I’m not just bullsh*tting you here as we, at Snipcart, are even using it to power the newest version of our shopping cart.

We expand more on why it was the right choice for us in this blog post.

Also, the tools built with this up-and-coming tech are getting more mature, very quickly.

Vue.js options

Just like the JavaScript ecosystem in general, Vue’s community is expanding by the minute. There are now tools based on it for almost any kind of frontend projects. Here are the main ones you should keep an eye on:

  • Nuxt: A compelling framework for static site generation, single page app (SPA) development and server-side rendered projects. We used it to build a static site on top of a headless CMS in this tutorial.

  • Gridsome: The Vue.js equivalent of Gatsby. Powered by GraphQL to fetch data from any sources, including headless CMSs. It can generate Progressive Web Applications (PWA).

  • VuePress: A minimalist Vue-powered static site generator, perfect to craft documentation & small blogs.

Is SEO an issue?

One thing that’s great about using WordPress the traditional way is that you don’t have to worry too much about SEO; it’s mostly optimized right out of the gate. So, I would understand the reluctance to use JS frameworks when we hear that they’re not great SEO-wise.

But let’s set the record straight, once and for all. You can actually have great SEO results with Vue.js if you handle it correctly. Server-side rendering with Nuxt.js is one way to go, but you could also use a simple pre-rendering service, as we did right here.

Then, better performances and UX will help you actually improve your SEO results!

Tutorial: Headless WordPress tied to a Vue.js SPA

Context

Our team is expanding in size, but also in cities where some of our members live. So let’s pretend it’s becoming harder to remember where everyone’s at as we scale. I’ll build an app to declare marker points on an interactive map and make sure we keep track of everyone’s whereabouts.

I'll show you how to craft a responsive Vue SPA to do precisely that. The backend part highly resembles my post addressing React with WordPress, but I’ll consume it in a totally different way with the Vue.js app.

Prerequisites

  • A running WordPress instance

  • Basic Vue knowledge

  • A free MapBox account

1. Creating models with custom fields in WordPress

Let's jump right into the WordPress admin.

I'll use the ACF (Advanced Custom Fields) plugin to build our custom entities for the demo.

This plugin allows you to add custom fields to native WordPress entities such as posts. It's thoroughly tested, stable, and gives us a jump-start when adding custom data to pages.

You can easily install it using the Plugins tab inside the WordPress dashboard. Here's what you should be looking for:

Declare these custom fields by clicking on the new custom fields section in the left panel.

For this use case, you’ll need four attributes: two coordinates, a name and an image. Here's what it looks like in the dashboard:

Now that you can add custom data to your WordPress posts let's use them to create markers to show where the team members are living. Here's the list of newly created posts:

2. Building a custom endpoint

Now that you have your data, you need to open it to the public to be consumed through the JSON REST API.

To do so, build a custom endpoint. Open your WordPress folder and open the functions.php file. That's where you'll register your new endpoint.

Add the following lines at the end of the file:

function  markers_endpoint( $request_data ) {
    $args = array(
        'post_type' => 'post',
        'posts_per_page'=>-1, 
        'numberposts'=>-1
    );
    
    $posts = get_posts($args);
    foreach ($posts as $key => $post) {
        $posts[$key]->acf = get_fields($post->ID);
    }
    return  $posts;
}
    
add_action( 'rest_api_init', function () {
    register_rest_route( 'markers/v1', '/post/', array(
        'methods' => 'GET',
        'callback' => 'markers_endpoint'
    ));
});

The add_action method creates the custom endpoint and registering it through the rest_api_init hook. Once you call the endpoint, the callback will be executed, which adds your custom fields under the acf key.

Now that the mapping works appropriately, feel free to hit your new REST endpoint at: /wordpress/wp-json/markers/v1/post.

Here's the structure you should see after a GET request, notice the new ACF field:

[
    {
        "ID": 19,
        "post_author": "1",
        "post_date": "2019-03-14 15:48:40",
        "post_date_gmt": "2019-03-14 19:48:40",
        "post_content": "",
        "post_title": "Chuck",
        "post_excerpt": "",
        "post_status": "publish",
        "comment_status": "open",
        "ping_status": "open",
        "post_password": "",
        "post_name": "chuck",
        "to_ping": "",
        "pinged": "",
        "post_modified": "2019-03-14 15:48:41",
        "post_modified_gmt": "2019-03-14 19:48:41",
        "post_content_filtered": "",
        "post_parent": 0,
        "guid": "http:\/\/localhost\/wordpress\/?p=19",
        "menu_order": 0,
        "post_type": "post",
        "post_mime_type": "",
        "comment_count": "0",
        "filter": "raw",
        "acf": {
            "Name": "Chuck",
            "image": "http:\/\/localhost\/wordpress\/wp-content\/uploads\/2019\/03\/charles-snipcart.png",
            "longitude": "49.8951",
            "Latitude": "97.1384"
        }
    }, ...
]

3. Setting up the Vue SPA

Let's consume that freshly baked data to make it useful.

To scaffold your project, use the neat Vue CLI. If you don't already have it, you can install it easily with npm install -g @vue/cli.

Then, create your project: vue create markers.

The first thing you’ll do is fetch the data from the API directly in your App component. To do so, sim

ply add the following declaration to the component:

 <template>
  <div id="app">
        
    <div class="badge-container">
      <div v-for="badge in markers" :key="badge.name">
        <Badge :name="badge.name" :image="badge.image" />
      </div>
    </div>
    <Map v-if="markers.length > 0" :markers="markers" />
        
  </div>
</template>
<script>
import Badge from './components/Badge.vue'
import Map from './components/Map.vue'
export default {
  name: 'app',
  data(){
    return {
      markers: []
    }
  },
  components: {
    Badge,
    Map
  },
  mounted(){
    fetch('https://wordpress-vue.herokuapp.com/index.php/wp-json/markers/v1/post')
      .then((r) => r.json())
      .then((res) => this.markers = res.map(x => x.acf));
  }
}

If you're not familiar with the mounted Vue component lifecycle hook, I strongly invite you to check the diagram that explains it here.

Now that you have the data, let's make a first component (in the /components folder), called badge.

Declare it this way:

<template>
  <div class="badge" :id="name">
    <img :src=img width=500/>
    <h2>{{ name }}</h2>
  </div>
</template>
    
<script>
export default {
  name: 'Badge',
  props: {
    Name: String,
    Image: String
  }
}
</script>
    
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
    
</style>

Let's hop back into the App component and import the new component with import Badge from './components/Badge.vue'.

Register it directly in your component object with:

components: {
    Badge
  }

Now, let's craft a new component: Map.vue.

Here, I’ll be using Mapbox to render the map. They have a nice free tier, neat and thorough documentation… I can’t ask for anything more. Once you’ve created an account, you’ll be able to access the access token directly on the dashboard homepage: https://account.mapbox.com/.

First, add Mapbox's JS and CSS directly in your index.html file:

<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.1/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.1/mapbox-gl.css' rel='stylesheet' />

Then, define your component with the following lines:

<template>
  <div>
      <div id='map' class='map'> </div>
  </div>
</template>
    
<script>
    
export default {
  name: 'Map',
  mounted(){
    
    window.mapboxgl.accessToken = "{YOUR_MAPBOX_ACCESS_TOKEN}";    
    
    var map = new window.mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/dark-v9',
        center: [-81.4608, 48.4758], 
        zoom: 4
    });
    
    map.on('load', (() => {
    
        this.markers.forEach(function(marker) {
            var el = document.createElement('div');
            el.className = 'marker';
    
            new window.mapboxgl.Marker(el)
                .setLngLat([parseFloat(marker.latitude), parseFloat(marker.longitude)])
                .addTo(map);
        });
    
        this.markers.forEach((x) => {
            document.getElementById(x.name)
                .addEventListener('click', () => {
                    map.flyTo({
                        center: [parseFloat(x.latitude), parseFloat(x.longitude)],
                        zoom: 9
                    })
                ;}
            )
        })
    
    }).bind(this));
  },
  props: {
   markers: Array
  }
}
</script>
    
<style>
.marker {
  background-image: url('./../assets/mapbox-icon.png');
  background-size: cover;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  cursor: pointer;
}
</style>

As you can see, the only props you need to pass is markers. This is an array containing all our markers information. You need to bind yourself on the map.on 'load' so that you don't try to add anything to the map until it's ready to receive information.

Then, craft your little markers (using the background image defined in the CSS) and add them to the map.

The last thing you need to do is add some event listeners so that the map moves to the proper marker when someone clicks on a badge.

That's it; it should be enough for now. You can add or remove markers directly in your WordPress backend, and the app will load them accordingly. Go on and try it!

4. Hosting the demo

To make this public we have to host it somewhere. To do this cheaply, you’ll have to host your database and WordPress instance on different ecosystems. For my instance, I’ll use Heroku, but since the free tier only supports an ephemeral filesystem, I’ll also have to host the database on another server.

Now, hosting a MySQL database can be done a thousand ways, so I’ll leave this part up to you and focus on hosting the WordPress instance and the Vue app solely.

Here’s how to do it with Heroku:

Hop in their dashboard and create a new project.

Once that's done, add their repository as a remote to your project. You can see how to do this directly in the dashboard if you don't already know how.

Then, change your database connection strings. It can be done really easily in the wp-config.php file with the following attributes:

define( 'DB_NAME', '{YOUR_DB_NAME}' );
define( 'DB_USER', '{YOUR_DB_USER}' );
define( 'DB_PASSWORD', '{YOUR_DB_PASSWORD}' );
define( 'DB_HOST', '{YOUR_DB_HOST}' );

Since you likely started the development on your localhost, you’ll probably have to change your site URL since it won't be local anymore. There are many ways to do so, so I’ll let you choose the path you prefer directly in the official docs.

Hooray, you only need to host your Vue app, and it’ll be good to go!

I’ll use Netlify's free tier here, which means it’ll push the demo to a public repo and hook it a Netlify project after.

So, once you’ve pushed the code, hop in your Netlify's dashboard and use the following configuration for the site:

Tada!

Simply refactor your fetch method so that it points to your new Heroku instance (and not your localhost) and there you go.

The app will fetch the data from your Heroku instance, which itself is connected to your remote database. It will parse the data and send it back to the client so it can render the markers on the map.

Live demo & GitHub repo

See the live demo here

See GitHub repo here

Closing thoughts

I find working with WordPress as a headless CMS more enjoyable each time I try it! Vue.js always implies a wonderful development experience, so I had a lot of fun building this demo.

Mapbox is quite impressive as well. It was my first time playing with a Maps API, and theirs is excellent—so was their docs. Always cool trying something new. :)

I must have spent around 2 to 3 hours doing the whole thing!

You could push the integration to define more complicated entities, which would take a little longer, but the whole idea would stay the same! This way we could render much more than markers and elevate our map to a richer experience. Are you up to it?


If you've enjoyed this post, please take a second to share it on Twitter. Got comments, questions? Hit the section below!

About the author

Maxime Laboissonniere
Developer

Max was the first dev hire for Snipcart back in 2016. Since then, he has stood out by his curiosity towards new technologies. He’s the one that introduced the team to Vue.js, for instance. In his 4 years experience as a developer, he’s mastered JavaScript and its ecosystem, as well as C#. These days, he likes to explore Elixir, Clojure, ELM, RxJS, and data science--when he doesn’t have his nose in a book.

Follow him on Twitter.

Build an E-Commerce Site with Wyam, a .NET Static Content Generator

Read next from Maxime
View more

36 000+ geeks are getting our monthly newsletter: join them!