DEV Community

Cover image for Easy Headless Wordpress with Nuxt & Netlify part II
David Y Soards
David Y Soards

Posted on • Updated on

Easy Headless Wordpress with Nuxt & Netlify part II

Part 2 - Nuxt & Tailwind

Part 1 deals with setting up Wordpress as a Headless CMS.

Part 3 covers deploying with Netlify and adding a build hook to our CMS.

Now that the JSON API endpoints are setup, the data from our Wordpress posts and media files can be queried, manipulated and rendered to static HTML files using Vue and Nuxt.

Create Nuxt App

Start a brand new nuxt project from the command line with

npx create-nuxt-app wp-nuxt

For purposes of this demo use the following settings:

? Project name: wp-nuxt
? Programming language: JavaScript
? Package manager: Npm
? UI framework: Tailwind CSS
? Nuxt.js modules: Axios
? Linting tools: ESLint, Prettier
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
Enter fullscreen mode Exit fullscreen mode

With this configuration, and if you are using VS Code, I recommend placing the following in your workspaces's .vscode/settings.json to avoid conflicts between prettier, eslint, and Vetur and to correctly enable auto formatting of code on save.

settings.json

{
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[css]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.validate": [
    "javascript",
    "vue"
  ],
  "vetur.validation.template": false,
  "css.validate": false,
}
Enter fullscreen mode Exit fullscreen mode

Nuxt gives you access to Vuex (Vue's state management library) out-of-the-box. Navigate to the store/ directory and create a new file index.js. Most of our data fetching and manipulation will take place in this file.

store/index.js

export const state = () => ({
  events: [],
});

export const getters = {};

export const mutations = {};

export const actions = {};
Enter fullscreen mode Exit fullscreen mode

Custom Fields

Before we can query the data we need to generate it in Wordpress. Add a few of the new custom post types we created in Part 1 and add some ACF fields to them. To do that, go to Custom Fields -> Field Groups -> Add New in the Wordpress dashboard. If you're new to ACF the documentation is actually pretty good.

Add Field Group

For this demo, create a new Field Group named Events and set the Location to "Show this Field Group if - Post Type is equal to Event".

show field group if

Add 4 Required fields with the following settings:

Label: Speaker
Name: speaker
Type: Text

Label: Start Time
Name: start_time
Type Date Time Picker

Label: End Time
Name: end_time
Type: Date Time Picker

Label: Image
Name: image
Type: Image
Return Format: Image Array
Enter fullscreen mode Exit fullscreen mode

ACF field groups

Add several Events and fill in the required fields as well as add some text to the default content area.

Navigate to http://headless.local/wp-json/wp/v2/events?page=1&per_page=100&_embed=1 and you should see your data being returned, including an acf object with keys that match the Name you entered in your custom fields.

Fetching Data

Back in your Nuxt repo in the Vuex store add a mutation for updating the events array, and an async action for fetching the events data.

store/index.js

export const mutations = {
  SET_EVENTS: (state, events) => {
    state.events = events;
  },
};

export const actions = {
  async getEvents({ state, commit }) {
    // if events is already set, stop
    if (state.events.length) return;
    try {
      let events = await this.$axios.$get(`/wp-json/wp/v2/events?page=1&per_page=100&_embed=1`);
      // filter out unnecessary data
      events = events.map(({ id, slug, title, content, acf }) => ({
        id,
        slug,
        title,
        content,
        acf,
      }));
      commit('SET_EVENTS', events);
    } catch (err) {
      console.error('getEvents', err);
    }
  },
};

Enter fullscreen mode Exit fullscreen mode

The @nuxtjs/axios module that was installed when we ran create-nuxt-app gives us access to this.$axios.

Using $get gives immediate access to the data and doesn't require the usual .then(res => res.data) at the end of the call, which is a pretty cool feature IMO.

Before this will work as is though, we have to add our baseURL to the axios object in the nuxt config file.

nuxt.config.js

axios: {
  baseURL: 'http://headless.local',
},
Enter fullscreen mode Exit fullscreen mode

Now we call the action in the created hook of a component.

index.vue

<script>
import { mapState, mapActions } from 'vuex';
export default {
  computed: {
    ...mapState(['events']),
  },

  created() {
    this.getEvents();
  },

  methods: {
    ...mapActions(['getEvents']),
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

Alternatively, you could access the Vuex state and actions with this.$store.state.events and this.$store.dispatch('getEvents'), but I prefer to use the Vuex map helpers because it looks cleaner and shows in one place all of the global state and actions that are being used in a particular component.

Run Server Side

In order to make sure our fetch request runs on the server when we are generating our static HTML, we can add a Nuxt plugin. Create a file called data.server.js inside the plugins/ directory.

plugins/data.server.js

export default async ({ store }) => {
  await store.dispatch('getEvents');
};
Enter fullscreen mode Exit fullscreen mode

And add the plugin to your nuxt config.

nuxt.config.js

plugins: ['~/plugins/data.server.js'],
Enter fullscreen mode Exit fullscreen mode

Render to the page

Now we can use the data in the component's template.

index.vue

<template>
  <div class="max-w-screen-lg mx-auto p-10">
    <div v-for="(event, index) in events" :key="event.id">
      <div :key="index" class="lg:flex lg:max-w-screen-lg pb-8 lg:pb-16">
        <div class="lg:w-1/4">
          <img
            v-if="event.acf.image"
            :src="event.acf.image.sizes.large"
            :alt="event.acf.image.alt"
            class="w-64 h-64 object-cover mb-4 lg:mb-0"
          />
        </div>
        <div class="lg:w-3/4 lg:pl-8">
          <h4 class="text-xl lg:text-3xl font-normal leading-tight">
            {{ event.title.rendered }}
          </h4>
          <h3 class="lg:text-2xl font-bold mb-2">
            {{ event.acf.speaker }}
          </h3>
          <time class="text-sm lg:text-lg font-mono block mb-2">
            {{ event.acf.start_time }} - {{ event.acf.end_time }}
          </time>
          <p class="mb-4" v-html="event.content.rendered"></p>
          <nuxt-link :to="`/events/${event.slug}`" class="btn-sm lg:btn btn-green mb-2 mr-2">
            Event Info
          </nuxt-link>
        </div>
      </div>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Here I'm using utility classes from the Tailwind CSS framework that we also installed when we ran create-nuxt-app. If you'd like to learn more about Tailwind, the docs for it are some of the best I've ever used.

If you've followed along until this point you should have something that resembles this:

events web page

But what if we need to show the events in order by date. For that we can use a getter, which I think of as a computed property for Vuex state.

store/index.js

export const getters = {
  sortedEvents: (state) => {
    return state.events.slice().sort((a, b) => new Date(a.acf.start_time) - new Date(b.acf.start_time));
  },
};
Enter fullscreen mode Exit fullscreen mode

Because the sort method mutates the original array, unlike map, filter, or reduce, I'm first using the slice method with no arguments to create a shallow copy and then sorting the copy.

Now add the following to your component:

index.vue

- import { mapState, mapActions } from 'vuex';
+ import { mapState, mapGetters, mapActions } from 'vuex';
export default {
  computed: {
    ...mapState(['events']),
+    ...mapGetters(['sortedEvents']),
  },
  created() {
    this.getEvents();
  },
  methods: {
    ...mapActions(['getEvents']),
  },
};
Enter fullscreen mode Exit fullscreen mode

And in the template:

- <div v-for="(event, index) in events" :key="event.id">

+ <div v-for="(event, index) in sortedEvents" :key="event.id">
Enter fullscreen mode Exit fullscreen mode

For a little more control over the format of our start and end times, install the date-fns nuxt module with npm i @nuxtjs/date-fns.

Then add @nuxtjs/date-fns to the build modules in your nuxt config, and import the methods you will be using. Being able to import only the functions you require is a huge performance advantage of date-fns over something like moment.js. This example only requires 1 method - format. For more info on date-fns check out the docs.

nuxt.config.js

buildModules: [
  '@nuxtjs/tailwindcss',
+  '@nuxtjs/date-fns',
],
dateFns: {
  methods: ['format'],
},
Enter fullscreen mode Exit fullscreen mode

Now we can use $dateFns methods directly in our templates like so:

index.vue

- {{ event.acf.start_time }} - {{ event.acf.end_time }}
+ {{ $dateFns.format(new Date(event.acf.start_time), 'E h') }} - {{ $dateFns.format(new Date(event.acf.end_time), 'haaaaa') }}
Enter fullscreen mode Exit fullscreen mode

Our Vue JS page rendered with content from the Wordpress JSON API is looking pretty good!

In Part 3 we will deploy our Nuxt app to Netlify and add a build hook so we can rebuild our site anytime new content is published.

Thanks for reading! Take a look at the source code for midwestdesignweek.com. 👀

If all of this setup is too much, or maybe you're just in a hurry, Netlify was a great repo made for just this purpose that you could use as a starting point. It was co-written by Vue Core Team member Sarah Drasner, and even has a companion article explaining its inner workings on Smashing Magazine.

This article and repo were extremely helpful for me when I was getting started.

GitHub logo netlify-labs / headless-wp-nuxt

🏔 Headless WordPress JAMstack Template

Top comments (4)

Collapse
 
mathieucattan profile image
mathieucattan

hello,
i m following your great tuto and trying to use acf.
I have a issue with ACF. I dont know how to deal with datas from several acf field groups and pages. How can i get the datas and display on templates ?

thanks !

Collapse
 
faigabaszada profile image
Faig-Abaszada • Edited

great great great article , only one full tutorial about this topic , I was very excited all the way you explain. thank you thousand times you saved us)

Collapse
 
ninjasoards profile image
David Y Soards

You are very welcome! I'm glad it was helpful. I couldn't find a whole lot on this topic either, which is why I decided to gather what I did find up here. I guess you figured out your .image of undefined issue?

Collapse
 
faigabaszada profile image
Faig-Abaszada

Yeah I solved the problem:) I can't thank you enough)