Static Nuxt + Wordpress API

Ok, so we’ve been using Nuxt for a while and just decided to change our workflow a little. We went from hosting the websites on a node server so we didnt have to generate and always have the latest content on the page to a fully generated version of our newest websites.

This has some complications, like…

  • When the page didn’t exist in render, the website doesn’t load.
  • When the page did exist, but the content is older, you see the content from when the page was generated when you directly go to the url.

But we fixed that!

A little bit about the setup;
We use a fully Nuxt frontend with a backed which is a wordpress installation where we just use the Wordpress Api. The frontend is running on Netlify while the backend (Wordpress) is at the moment at a cheap shared hosting package. Both will be running in production on a dedicated server, but actually wouldn’t be necessary.

Setup Axios

In your nuxt.config.js define the path of your api server (wordpress)

    axios: {
        retry: { retries: 3 },
        baseURL: https://silvandiepen.site/wp-json
    },

Pages

So we build all pages up in Wordpress, with custom fields and sections and everything, but about that I will make another post. The pages are all available by default on yourdomain.com/wp-json/wp/v2/pages in our case we have it running on api.yourdomain.com and we’ve made another endpoint just for the pages, to make it easy to include all the information and fields we need. Also to have fallbacks for pages which don’t exist (to a 404 page) and when there is no page given (to home).

The endpoint in Wordpress

Just include this file to your functions.php and there you go.

This is the endpoint for pages

<?php
function get_pages_categories($ID)
{
	$cats = array();
	foreach (wp_get_post_categories($ID) as $cat) {
		$cats[] = get_category($cat)->slug;
	}
	return $cats;
}

function get_pages_tags($ID)
{
	$tags = array();
	foreach (wp_get_post_tags($ID) as $tag) {
		$tags[] = $tag->name;
	}
	return $tags;
}


function get_pages_all()
{
	$pages = array();
	$i = 0;
	foreach (get_pages(array('numberpages' => 999)) as $post) {
		if ($post->post_status === "publish") {
			$pages[$i] = (object)array();
			$pages[$i]->ID = $post->ID;
			$pages[$i]->title = $post->post_title;
			$pages[$i]->name = $post->post_name;
			$pages[$i]->slug = $post->post_name;
			$pages[$i]->uri = get_page_uri($post->ID);
			$pages[$i]->content = wpautop($post->post_content);
			$pages[$i]->excerpt = wpautop($post->post_excerpt);
			$pages[$i]->date = $post->post_date;
			$pages[$i]->date_modified = $post->post_modified;
			$pages[$i]->title = $post->post_title;
			$pages[$i]->status = $post->post_status;
			$pages[$i]->categories = get_pages_categories($post->ID);
			$pages[$i]->tags = get_pages_tags($post->ID);
			$pages[$i]->fields = get_fields($post->ID);
			$pages[$i]->featured_image = get_the_post_thumbnail_url($post->ID);
			$i++;
		}
	}
	return $pages;
}

function get_pages_tag()
{
	$pages = array();
	$i = 0;
	foreach (get_pages(array(
		'tag' => $_GET['tag'],
		'post_status' => 'publish'
	)) as $post) {
		if ($post->post_status === "publish") {
			$pages[$i] = (object)array();
			$pages[$i]->ID = $post->ID;
			$pages[$i]->title = $post->post_title;
			$pages[$i]->name = $post->post_name;
			$pages[$i]->slug = $post->post_name;
			$pages[$i]->content = wpautop($post->post_content);
			$pages[$i]->excerpt = wpautop($post->post_excerpt);
			$pages[$i]->date = $post->post_date;
			$pages[$i]->date_modified = $post->post_modified;
			$pages[$i]->title = $post->post_title;
			$pages[$i]->status = $post->post_status;
			$pages[$i]->categories = get_pages_categories($post->ID);
			$pages[$i]->tags = get_pages_tags($post->ID);
			$pages[$i]->fields = get_fields($post->ID);
			$pages[$i]->featured_image = get_the_post_thumbnail_url($post->ID);
			$i++;
		}
	}
	return $pages;
}
function get_page_by_slug($slug = null)
{
	$page = array();
	if (gettype($slug) === 'object') {
		$slug = $_GET['slug'];
	}
	$post = get_page_by_path($slug);
	if ($post && $post->post_status === "publish") {
		$page = get_page_fields($post);
		// Get recurring fields; 
		foreach($page['fields']['layout_content'] as $key => $value){
			if($value['acf_fc_layout'] == 'repeating-block'){
				$page['fields']['layout_content'][$key]['layout_content']->fields = get_fields($value['layout_content']->ID);
			}
		}		
	} else {
		$page404 = get_page_by_path('404-page');
		if ($page404->ID) {
			$page = get_page_fields($page404);
		} else {
			$page['type'] = '404';
		}
	}
	return $page;
}

function get_page_fields($post)
{
	$page = array();
	if ($post) {
		$page['id'] = $post->ID;
		$page['title'] = $post->post_title;
		$page['name'] = $post->post_name;
		$page['content'] = wpautop($post->post_content);
		$page['excerpt'] = wpautop($post->post_excerpt);
		$page['date'] = $post->post_date;
		$page['date_modified'] = $post->post_modified;
		$page['title'] = $post->post_title;
		$page['status'] = $post->post_status;
		$page['categories'] = get_pages_categories($post->ID);
		$page['tags'] = get_pages_tags($post->ID);
		$page['fields'] = get_fields($post->ID);
		$page['featured_image'] = get_the_post_thumbnail_url($post->ID);
	}
	return $page;
}

add_action('rest_api_init', function () {

	register_rest_route('pages', '/all', array(
		'methods' => 'GET',
		'callback' => 'get_pages_all',
	));

	register_rest_route('pages', '/page', array(
		'methods' => 'GET',
		'callback' => 'get_page_by_slug'
	));
	register_rest_route('pages', '/tag', array(
		'methods' => 'GET',
		'callback' => 'get_post_by_slug',
	));
});

Fetching and storing the page in Vuex

In the frontend we use Axios to get all the posts, when a page is requested we let Vuex fetch it and store it. In this way when the page is requested a second time in a sessions, you will just get served the stored version. Of course we also make sure that the page won’t be too old, so we check when the page is been stored. We save the pages under their own slug

import Vue from 'vue';

const storeCache = 300; // 300 in sec is 5 min
Date.time = function() {
	const now = new Date();
	return now.getTime() / 1000;
};

export const state = () => ({
	pages: {}
});

export const mutations = {
	setPage(state, page) {
		page.lastFetched = Date.time();

		if (page.slug) {
			if (page.slug.indexOf('/') > -1) {
				Vue.set(state.pages, page.uri.replace('/', ''), page);
			}
		} else {
			Vue.set(state.pages, page.name, page);
		}
	}
};

export const actions = {
	async fetchPage({ state, commit }, page) {
		let slug;
		if (page !== undefined) {
			slug = page.replace('/', '');
		} else {
			slug = 'home';
			page = 'home';
		}
		if (!state.pages[slug] || state.pages[slug].lastFetched + storeCache < Date.time()) {
			const content = await this.$axios.$get(`/pages/page?slug=${page}`);

			commit('setPage', content);
		}
	}
};

The pages in Nuxt

index.vue

First there is the index.vue, this is the main file, but because we want to do exactly the same in this page as in all other pages we just include the _.vue here as component and make use of that file.

<template>
	<MainLayout></MainLayout>
</template>

<script>
import MainLayout from './_.vue';
export default {
	components: { MainLayout },
	async asyncData({ store, params }) {
		await store.dispatch('pages/fetchPage', params.pathMatch);
	}
};
</script>

<style></style>

_.vue

A file starting with an underscore can be used in Nuxt as a wildcard. So we have this _.vue file to get all the pages.

In this file, we do a call to Vuex to request the page and serve it. This page will do all the logic to serve the page from wordpress.
From top to bottom:

  • Computed: We get the path of the page, strip all slashes (like we did in vuex too) and request this page.
  • asyncData: Dispatch a request to Vuex to get the page (for generation).
  • created: Dispatch a request to Vuex to get the page (for live), this makes sure that when the page is loaded it will still get the page from the server so it will always have the newest content.

In the template we use a component called ‘BuildSections’ because the pages have been build up completely using sections (made with Custom fields) this component will generate all the sections.

<template>
	<BuildSections
		v-if="page && page.fields && page.fields.layout_content"
		:sections="page.fields.layout_content"
	></BuildSections>
</template>

<script>
import BuildSections from '~/components/build-sections.vue';
export default {
	components: {
		BuildSections
	},
	head() {
		return {
			title: this.pageTitle,
			titleTemplate: '%s - Website title'
		};
	},
	computed: {
		pageTitle() {
			if (this.page && this.page.title && this.page.title.rendered) {
				return this.page.title || this.page.title.rendered;
			} else if (this.page && this.page.title) {
				return this.page.title;
			} else {
				return '';
			}
		},
		page() {
			let route;
			if (this.$route.params.pathMatch) {
				route = this.$route.params.pathMatch.replace('/','');
			} else {
				route = 'home';
			}
			return this.$store.state.pages.pages[route];
		}
	},
	async asyncData({ store, params, error }) {
		await store.dispatch('pages/fetchPage', params.pathMatch).catch((e) => {
			error(e);
		});
	},
	created() {
		this.$store.dispatch('pages/fetchPage', this.$route.params.pathMatch).catch(() => {
			this.$nuxt.error({ statusCode: 404, message: 'Page not found' });
		});
	}
};
</script>

Build Sections

The build sections handles all the components and layouts there are. It gets loads the components when needed and in this way builds the whole page. It also handles repeated blocks, which is a template which can take other pages from another post type.

<template>
	<main class="page">
		<template v-for="(section, idx) in sections">
			<section
				v-if="section.acf_fc_layout !== 'repeating-block'"
				:key="idx"
				:class="section.acf_fc_layout"
				:index="idx"
			>
				<component
					:is="section.acf_fc_layout"
					:key="idx"
					:index="idx"
					:content="section"
				/>
			</section>
			<template v-else-if="section.layout_content.fields">
				<template v-for="(subSection, index) in section.layout_content.fields.layout_content">
					<section :key="`${index}sub`" :class="subSection.acf_fc_layout">
						<component
							:is="subSection.acf_fc_layout"
							:index="index"
							:content="subSection"
						/>
					</section>
				</template>
			</template>
		</template>
	</main>
</template>

<script>
export default {
	components: {
		OneColumn: () => import('~/components/section/one-column.vue'),
		TwoColumn: () => import('~/components/section/two-column.vue'),
		CenterColumn: () => import('~/components/section/center-column.vue'),
		BasicContent: () => import('~/components/section/basic-content.vue'),
	},
	props: {
		sections: {
			type: Array,
			default: () => []
		}
	}
};
</script>

A layout component

In this case just the basic-content.vue, The main class on the section will be already set on in build-sections. It handless all the content it gets through the prop and in this way just shows the data.

<template>
	<div class="basic-content__wrapper">
		<div class="row center">
			<div class="column small-full medium-two-third large-10">
				<div class="basic-header__content">
					<h3 v-if="basicTitle">{{ basicTitle }}</h3>
					<h4 v-if="basicSubtitle">{{ basicSubtitle}}</h4>
					<template v-if="basicContent">{{ basicContent }}</template>
				</div>
			</div>
		</div>
	</div>
</template>

<script>
export default {
	components: {},
	props: {
		content: {
			type: [String, Object],
			default: () => {}
		}
	},
	computed: {
		basicTitle() {
			return this.$props.content.block_title || null;
		},
		basicSubtitle() {
			return this.$props.content.block_subtitle || null;
		},
		basicContent() {
			if (typeof this.$props.content === 'string') {
				return this.$props.content;
			}
			return this.$props.content.block_content || null;
		}
	},
	created() {}
};
</script>

<style lang="css">
.basic-content {
	//styling here
}
</style>