WordPress, Vue and Nuxt — an SEO love story
By: Ed O Reilly
Tags: Nuxtjs, SEO, WordPress, Rest Api, Vuejs

We’ll go through the following steps:

Setup a Vue.js (Nuxt) frontend which can display single posts.

Setup a WordPress API that serves post data over a restful endpoint.

Add Yoast SEO Plugin data to that endpoint.

Connect the Vue JS frontend to the API to pull the post and SEO meta data.

This tutorial assumes the following:

An intermediate knowledge of WordPress (PHP) development.

An understanding of Javascript development and some of what goes with that: NPM etc

Light familiarity with Yoast’s SEO Plugin for WordPress.

An interest in using Vue.js.

Setup a Vue.js (Nuxt) frontend which can display single posts.

Choose a directory on your machine and create a new nuxt project like so:

npx create-nuxt-app <project-name>

Follow the steps and configure it however you’d like — I went with NPM over Yarn, no linting and no tests for the purposes of this lil demo.

Run the following to see your site on: http://localhost:3000

npm run dev

And voila — You’ll see your vue-tiful new project running!

The default index page after installing nuxt.

After that you’ll want to navigate to the “pages” directory and create a new folder called “post”. Inside that folder create two files: index.vue and _id.vue.

The index file will be used to disaply the listing page for posts which we won’t be covering in this tutorial and the _id.vue file will be used to display the individual posts.

In the ‘_id.vue’ file add the following code for now:

<template>

<section class="container">

<div>

<logo/>

<h1 class="title">

Static Post Title

</h1>

<span class="content">

Static Post Content

</span>

</div>

</section>

</template> <script> export default { }

</script> <style> .container {

min-height: 100vh;

display: flex;

justify-content: center;

align-items: center;

text-align: center;

} .title {

font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,

'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

display: block;

font-weight: 300;

font-size: 100px;

color: #35495e;

letter-spacing: 1px;

} </style>

Then visit this here: http://localhost:3000/post/1

There we go, a lovely little posts template showing the same static data over and over again no matter what ID is entered into the URL.

Our starting static post page.

Next we will setup a WordPress API that serves post data.

I install WP locally via Docker but it’s totally your call how you wanna do the installation bit. Once you’ve got a site setup we work with the WordPress Rest API to serve the post data — with some additional SEO metadata using Yoast’s SEO Plugin.

WordPress, by default, provides an API endpoint for posts.

You can visit this here: http://your-local-wp-site-url/wp-json/wp/v2/posts/POST_ID

My exact one was: http://devapi.seotest.com:20080/wp-json/wp/v2/posts/1

This will produce a response which looks something like the following:



"id":1,

"date":"2019-01-03T09:54:21",

"date_gmt":"2019-01-03T09:54:21",

"guid":{

"rendered":"http://

},

"modified":"2019-01-03T09:54:21",

"modified_gmt":"2019-01-03T09:54:21",

"slug":"test-post-seo",

"status":"publish",

"type":"post",

"link":"http://

"title":{

"rendered":"My test Post for SEO"

},

"content":{

"rendered":"<p>My test post main body content – ain’t that nice!</p>

",

"protected":false

} :1,:"2019-01-03T09:54:21",:"2019-01-03T09:54:21",:{:"http:// devapi.seotest.com :20080/?p=1"},:"2019-01-03T09:54:21",:"2019-01-03T09:54:21",:"test-post-seo",:"publish",:"post",:"http:// devapi.seotest.com :20080/test-post-seo/",:{:"My test Post for SEO"},:{:" My test post main body content – ain’t that nice!

",:false ........

There you go! You have an API endpoint setup which returns post data and you didn’t even have to write any code!

Add Yoast SEO data to those posts.

The next part is also pretty straight forward. We need to add Yoast’s SEO Meta data to this response.

First things first — visit your plugins page and install the Yoast SEO plugin.

I’m only going to focus on the Title and Meta Description to keep this post from getting too long but the rest of the fields follow a very similar pattern.

Add the following code to your functions.php:

<?php add_action( 'rest_api_init', 'slug_register_yoast_seo_meta' ); function slug_register_yoast_seo_meta() {

register_rest_field( 'post',

'_yoast_wpseo_title',

array(

'get_callback' => 'get_seo_meta_field',

'update_callback' => null,

'schema' => null,

)

);

register_rest_field( 'post',

'_yoast_wpseo_metadesc',

array(

'get_callback' => 'get_seo_meta_field',

'update_callback' => null,

'schema' => null,

)

);

} function get_seo_meta_field( $object, $field_name, $request ) {

return get_post_meta( $object[ 'id' ], $field_name, true );

}

What this does is the following:

Hooks into the ‘rest_api_init’ function to register two custom fields. Using ‘rest_api_init’ instead of ‘init’ so that this code only runs on API requests and not regular post requests.

Specifies the callback function for fetching the two fields as ‘get_seo_meta_field’ which simply takes the object ID (the post in this case) and the field name (either ‘_yoast_wpseo_title’ or ‘_yoast_wpseo_metadesc’) and returns the value of that post meta field.

Wonderful! Now we have a posts API endpoint that returns Yoast SEO Meta data! Woop Woop!

You can find the full description for how to add additional meta fields to endpoints here: https://v2.wp-api.org/extending/modifying

Connect the Vue JS app to pull post data

This is the final step — we need to tell the Vue Frontend to fetch the Post Data for each post from the WordPress API. We do this as follows:

We’re going to use Axios for this so in your _id.vue file add the following in the <script> tags.

<script>

import axios from 'axios'



asyncData ({ params }) {

return axios.get(`http://

.then(response => {

return { post: response.data }

})

.catch((error) => {

return { error: error }

})

},

data () {

return {

post: {},

error: []

}

}

}

</script> export default {asyncData ({ params }) {return axios.get(`http:// devapi.seotest.com :20080/test-post-seo /wp-json/wp/v2/posts/${params.id}` .then(response => {return { post: response.data }}).catch((error) => {return { error: error }})},data () {return {post: {},error: []

What’s going on here?

We’re importing Axios into the file.

We’re using Nuxt’s ‘asyncData’ method to perform our Axios request.

We’re using a dynamic ID parameter so that we can pull different posts by visiting different URLs.

We’re adding the returned post data into a ‘post’ object that we’ve created.

We can now display a dynamic title and dynamic content for our post like so:

<template>

<section class="container">

<div>

<h1 class="title">

{{post.title.rendered}}

</h1>

<span v-html="post.content.rendered"></span>

</div>

</section>

</template>

Please note the:

v-html=”post.content.rendered”

This formats the HTML markup returned nicely and we should see something like the following:

Woop! We’re getting there!

Of course the content itself will depend on what you put in the post you created on the WordPress side and the ID in the URL bar will depend on the ID of that post.

Next we’ll want to include the SEO data as follows:

<script>

import axios from 'axios' export default {

head () {

return {

title: this.post._yoast_wpseo_title,

meta: [

{ hid: 'description', id: 'description', name: 'description', content: this.post._yoast_wpseo_metadesc }

]

}

},

asyncData ({ params }) {

Full code for the file below:

<template>

<section class="container">

<div>

<h1 class="title">

{{post.title.rendered}}

</h1>

<span v-html="post.content.rendered"></span>

</div>

</section>

</template> <script>

import axios from 'axios'

head () {

return {

title: this.post._yoast_wpseo_title,

meta: [

{ hid: 'description', id: 'description', name: 'description', content: this.post._yoast_wpseo_metadesc }

]

}

},

asyncData ({ params }) {

return axios.get(`

.then(response => {

return { post: response.data }

})

.catch((error) => {

return { error: error }

})

},

data () {

return {

post: {},

error: []

}

}

}

</script> export default {head () {return {title: this.post._yoast_wpseo_title,meta: [{ hid: 'description', id: 'description', name: 'description', content: this.post._yoast_wpseo_metadesc }},asyncData ({ params }) {return axios.get(` http://devapi.go2africa.com:20080/wp-json/wp/v2/posts/${params.id}` .then(response => {return { post: response.data }}).catch((error) => {return { error: error }})},data () {return {post: {},error: [] <style> .container {

min-height: 100vh;

display: flex;

justify-content: center;

align-items: center;

text-align: center;

} .title {

font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,

'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

display: block;

font-weight: 300;

font-size: 100px;

color: #35495e;

letter-spacing: 1px;

} .subtitle {

font-weight: 300;

font-size: 42px;

color: #526488;

word-spacing: 5px;

padding-bottom: 15px;

} .links {

padding-top: 15px;

}

</style>

This has been done the simplest possible way — just to illustrate the point of how to tie it all together. I mention below how we’ve actually done quite a bit more to get this working in production.

Wrapping up

There you go, now you have a Vue.js frontend with SEO provided by Yoast’s SEO plugin from a WordPress backend!

Here’s a screenshot of our example:

You can see the SEO Title and Meta Description pulling through in the TAB and the Console!

Please note

In the project referenced in the previous post we went quite a lot further and added Metadata for all taxonomies and schema.org metadata for images and videos etc.

We also used Mixins to be DRY and avoid repeating code on every different content type.

The above is actually quite important if it’s a production site but it’s tough to get all of that into one post concisely so I thought I’d explain the main mechanisms here and can cover doing this with Mixins and schema.org etc in a follow up post if there’s sufficient interest.

What next?

Feel free to reach out if you have any trouble getting this to run — I might have missed a step.

Let me know in the comments below if you’d be interested in a follow up post or more details on this setup.

Follow me or leave a comment if you’d be keen on hearing more!