Headless WordPress With Vue and Nuxt.js

Will Johnston Avatar

·

If you have not already read our post on Headless WordPress With Next.js I encourage you to go through it if you are interested in learning how to create a Headless WordPress site with React Next.js. This post is going to cover a similar topic, but with Vue and Nuxt.js instead.

What Is Nuxt.js?

Nuxt.js (or simply Nuxt) is a server-side framework for writing frontend applications using Vue. Vue is a frontend framework similar to Angular or React… kinda. There are plenty of comparison articles on React versus Vue, and most will point out that Vue is more a framework while React is a library. Let’s not get into that for this post. For this post, we will assume Vue and React offer essentially the same thing for frontend developers and work with their respective Nuxt and Next server counterparts.

You can think of Nuxt as Next but for Vue instead of React. So if you are looking to write a Headless WordPress site and want to use Vue, Nuxt may be an excellent option for you to use as your server-side framework. It supports Server-Side Rendering and Static Site Generation similar to Next and is generally an opinionated framework that helps you build a frontend website. Before we dive into Nuxt let’s first discuss how to setup WordPress for headless.

Setting Up WordPress For Headless

If you haven’t already, check out our post about setting up WordPress for headless. For this post, you will need to install and configure the following plugins (outlined in the post linked above):

  1. WPGraphQL
  2. Faust.js

Make sure you follow the instructions in the post for configuring Faust.js. In the Faust.js settings (found in Settings > Faust), set Front-end site URL to http://localhost:3000/. This will eventually be the URL that serves where our frontend site.

Creating The Nuxt Site

Now that you have your WordPress site hooked up, let’s dive into creating our Nuxt app. We will be following the create-nuxt-app installation instructions from the Nuxt documentation for the most part, with some configuration. You can start by opening a terminal to where you want to create your app and running the following:

$ npx create-nuxt-app <project-name>
Code language: HTML, XML (xml)

For this project, I will use the name nuxt-headless-wp, but you can use whatever name you want. create-nuxt-app is going to ask you some configuration questions, such as:

create-nuxt-app v3.5.2
✨  Generating Nuxt.js project in nuxt-headless-wp
? Project name: nuxt-headless-wp
? Programming language: TypeScript
? Package manager: Npm
? UI framework: Bootstrap Vue
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git
Code language: HTML, XML (xml)

After your Nuxt app is generated you will get instructions on how to run your site, which should look something like the following:

$ cd nuxt-headless-wp
$ npm run dev

If you don’t get any errors, your site should be running on http://localhost:3000/. We’re done…

Not! Now is when things get interesting.

Building Pages For The Blog

We’re going to keep with a simple UI using only Bootstrap CSS classes. Let’s first scaffold out our HTML with some dummy data and style it a little before adding in data from our WordPress CMS. We will want the index page to show a list of posts and let you navigate to an individual post. Let’s start with the index page.

Showing A List Of Posts

In pages/index.vue, remove all of the styles in the <style></style> tags. We won’t be needing this. Next, let’s add some dummy data in the TS area of our index.vue. Add the following script:

import Vue from 'vue'

export default Vue.extend({
  computed: {
    posts() {
      return [
        {
          title: 'Test Post',
          slug: 'test-post',
          content: '<p>This is a test post</p>',
          excerpt: '<p>This is a test post</p>',
        },
      ]
    },
    pageInfo() {
      return {
        endCursor: '',
        hasNextPage: false,
        hasPreviousPage: false,
        startCursor: '',
      }
    },
  },
})
Code language: JavaScript (javascript)

The code above sets up two computed properties with default values for a list of posts and page information. We are using computed properties because later, we will populate these with values from a Vuex store.

Now that we have some sample data let’s build out our list of posts. Use the following template:

<template>
  <div class="container">
    <div>
      <h1 class="text-center">Nuxt Headless WordPress Demo</h1>
      <div class="container py-4 posts">
        <div class="row">
          <post-card v-for="post in posts" :key="post.slug" :post="post" />
          <div class="d-flex w-100">
            <div class="ml-auto" v-if="pageInfo.hasNextPage">
              <NuxtLink :to="{ query: { after: pageInfo.endCursor } }"
                ><span>Next Page</span></NuxtLink
              >
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
Code language: HTML, XML (xml)

This code is going to iterate through our posts computed property and render a post-card for each post. It will also show a link to view the next page of posts if there is one. You might be wondering what post-card is. We need to create our post-card component so that we can show our posts as cards. Add a components/PostCard.vue and put the following code in it:

<template>
  <article class="col-12 py-5">
    <div class="card shadow">
      <div class="card-body">
        <h5 class="card-title">
          <NuxtLink :to="{ name: 'slug', params: { slug: post.slug } }">{{
            post.title
          }}</NuxtLink>
        </h5>
        <!-- eslint-disable-next-line vue/no-v-html -->
        <div class="card-text" v-html="post.excerpt"></div>
      </div>
    </div>
  </article>
</template>

<script lang="ts">
import { PropsDefinition } from 'vue/types/options'
import { Post } from '~/store/posts'

export default {
  props: ['post'] as PropsDefinition<{ post: Post }>,
}
</script>
Code language: HTML, XML (xml)

The PostCard component expects a post prop and will render the post title and excerpt in a Bootstrap card. It will also link to what will eventually be our single post page. Before you continue, run your app and make sure you see the test post.

Showing A Single Post

Now that we have our list of posts displaying let’s add a single post page. If you look at the PostCard.vue component, you will see that we link to a slug page that takes in a slug parameter. Create a pages/_slug.vue that will receive the slug URL parameter and use it to display a post. Put the following code into the _slug.vue page:

<template>
  <div class="container">
    <h1 class="text-center">{{ post.title }}</h1>
    <!-- eslint-disable-next-line vue/no-v-html -->
    <article class="py-5" v-html="post.content"></article>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  computed: {
    post() {
      return {
        title: 'Test Post',
        slug: 'test-post',
        content: '<p>This is a test post</p>',
        excerpt: '<p>This is a test post</p>',
      }
    },
  },
})
</script>

Code language: HTML, XML (xml)

When you run your app, you will be able to navigate from the list of posts to a single post. Now we have to actually populate our list of posts with data from our WordPress site.

Building The Posts Store

To get data from WordPress, we need a store, but we need to install some dependencies. Run the following commands from the root directory of your project:

$ npm i apollo-boost graphql isomorphic-fetch vue-apollo
$ npm i -D @types/isomorphic-fetch

This will install the dependencies we need to call our WordPress GraphQL API and retrieve our lists of posts and individual posts. The next thing we need to do is set up a store for our posts. Add a store/posts.ts file. This file is going to have a number of things:

  1. TypeScript interfaces for our data
  2. GQL query strings
  3. Apollo client setup
  4. State definition
  5. Mutation methods
  6. Action methods

Let’s step through this 1-by-1. We’re going to need to import the following dependencies at the top of our posts.ts file:

import { ActionContext } from 'vuex'
import ApolloClient, { gql } from 'apollo-boost'
import fetch from 'isomorphic-fetch'
Code language: JavaScript (javascript)

Next, let’s create our TypeScript interfaces, so we know the shape of the data we’re trying to fetch. In your posts.ts file put the following interfaces:

export interface Post {
  id: string
  slug: string
  title: string
  content: string
  excerpt: string
  uri: string
  status: string
}

export interface PageInfo {
  endCursor: string
  hasNextPage: boolean
  hasPreviousPage: boolean
  startCursor: string
}

export interface PostsState {
  nodes: Post[]
  pageInfo: PageInfo
  post: Post
}
Code language: CSS (css)

Now, let’s use the gql library to format our GQL queries. Earlier, we installed the WPGraphQL plugin for WordPress, so we need to use its schema. Put the following queries in your posts.ts file:

const postsQuery = gql`
  fragment pageInfoData on WPPageInfo {
    endCursor
    hasNextPage
    hasPreviousPage
    startCursor
  }
  fragment listPostData on Post {
    id
    slug
    title
    content
    excerpt
    uri
    status
  }
  query GetPosts(
    $where: RootQueryToPostConnectionWhereArgs
    $after: String
    $before: String
    $first: Int
    $last: Int
  ) {
    posts(
      where: $where
      after: $after
      before: $before
      first: $first
      last: $last
    ) {
      pageInfo {
        ...pageInfoData
      }
      nodes {
        ...listPostData
      }
    }
  }
`

const postQuery = gql`
  fragment postData on Post {
    id
    slug
    title
    content
    uri
    status
  }
  fragment pageData on Page {
    id
    slug
    title
    content
    uri
    status
  }
  query GetContentNode($id: ID!) {
    contentNode(id: $id, idType: URI) {
      ... on Post {
        ...postData
      }
      ... on Page {
        ...pageData
      }
    }
  }
`
Code language: PHP (php)

Use the first query to get a list of posts and the second an individual post. Note that the fields in our query match the fields in the TypeScript interfaces.

Next, let’s set up our Apollo client. I am going to use a demo WordPress site, but you can use your own:

const client = new ApolloClient({
  uri: `https://headlessfw.wpengine.com/graphql`,
  fetch,
})
Code language: JavaScript (javascript)

Now we need to define our state:

export function state(): PostsState {
  return {
    nodes: [],
    pageInfo: {
      endCursor: '',
      hasNextPage: false,
      hasPreviousPage: false,
      startCursor: '',
    },
    post: {
      id: '',
      slug: '',
      title: '',
      content: '',
      excerpt: '',
      uri: '',
      status: '',
    },
  }
}
Code language: JavaScript (javascript)

The last piece of this puzzle is to create the functions that will fetch our data and provide it to the view. Create the following mutations:

export const mutations = {
  setPosts(state: PostsState, posts: any[]) {
    state.nodes = posts
  },
  setPost(state: PostsState, post: any) {
    state.post = post
  },
  setPageInfo(state: PostsState, pageInfo: any) {
    state.pageInfo = pageInfo
  },
}
Code language: JavaScript (javascript)

These mutations will let us set a state and provide a list of posts, page information, and an individual post. Next, create the following actions:


export const actions = {
  async getPosts(
    { commit }: ActionContext<PostsState, PostsState>,
    variables: any
  ) {
    const result = await client.query({
      query: postsQuery,
      variables,
    })

    const { nodes, pageInfo } = result.data?.posts

    commit('setPosts', nodes)
    commit('setPageInfo', pageInfo)
  },
  async getPost(
    { commit }: ActionContext<PostsState, PostsState>,
    slug: string
  ) {
    const result = await client.query({
      query: postQuery,
      variables: {
        id: slug,
      },
    })

    const post = result.data?.contentNode
    commit('setPost', post)
  },
}

Code language: JavaScript (javascript)

We have two actions: getting the list of posts and the other for getting an individual post. Both use our GQL queries and the Apollo client to make requests to our WordPress site for data, and then they commit the data to the state by calling the mutations. Now we have our store, the last piece we need is to wire it up to our views.

Wiring Up The Data

To wire up the state from our store to our view, we will need to modify our index.vue and _slug.vue pages. Modify the script in the index.vue page to look like the following:

import Vue from 'vue'
import { PostsState } from '~/store/posts'

const pageCount = 5

export default Vue.extend({
  computed: {
    posts() {
      return (this.$store.state.posts as PostsState).nodes
    },
    pageInfo() {
      return (this.$store.state.posts as PostsState).pageInfo
    },
  },
  watch: {
    async $route() {
      await this.$nuxt.refresh()
      window.scrollTo(0, 0)
    },
  },
  async asyncData({ store, query }) {
    await store.dispatch('posts/getPosts', {
      after: query.after,
      before: query.before,
      first: query.before ? undefined : pageCount,
      last: query.before ? pageCount : undefined,
    })
  },
})
Code language: JavaScript (javascript)

This accomplishes a few goals. First, it changes our computed properties from being hardcoded to using the posts state values. Second, it sets up a $route watcher that will refresh our data when the route changes and scroll to the top of the page. This is used for pagination (note we are using a pageCount of 10). The next page will create a query string with the appropriate parameters and refresh the data when you go to the next page. The last thing this code does is implement the asyncData hook to dispatch the posts/getPosts action, retrieve the list of posts, and then set them on the state. Now let’s modify the _slug.vue to have the following script:

import Vue from 'vue'

export default Vue.extend({
  computed: {
    post() {
      return this.$store.state.posts.post
    },
  },
  async asyncData({ store, params }) {
    await store.dispatch('posts/getPost', params.slug)
  },
})
Code language: JavaScript (javascript)

This is much more straightforward. All we need to do in the _slug.vue page is dispatch the action to get a single post.

There you have it; you have successfully set up a Headless WordPress blog using Vue and Nuxt! Now when you run your site, you should see a paginated list of posts. If you have more than 10 posts, you will be able to page through them. Click on the title of a post, that title will take you to the individual page for that post, where you see all of the content.

When you are still struggling to get your site setup, check out the code for this post. If you haven’t already and are interested, be sure to check out our post on Headless WordPress Using NextJS.