Build a website using Nuxt & Contentful — A step by step guide

Nicky Christensen
May 31 · 11 min read

As a frontend-developer, working with static-site-generators and serverless architecture is a joy, and with it, we can create very powerful and amazing applications which we can also server-side render

This article aims to give you a step by step guide for building a very basic website using Nuxtjs + Contentful — Including a simple Vuex example.

Find the full GIT repo here:
https://github.com/nickycdk/nuxt-contentful-example

Personally, my own website is running on a Nuxt/Contentful setup with Continuous deployment by connecting my GIT repo with Netlify, this combined with Contentful’s webhooks to rebuild your site automatically when publishing new content is just…. Awesome

What is Nuxt

If you’ve built Vue applications before, you’ve probably heard about NuxtJS, which is the equivalent to what NextJS is for React.

Nuxt is a framework that builds on top of Vue that simplifies the development of universal or single page Vue apps, which is great if you are building a website and want to make sure it can get indexed by Google.

Derick Sozo has written a great piece on why you should choose Nuxt for for next webapplication which can be found here: https://medium.com/vue-mastery/10-reasons-to-use-nuxt-js-for-your-next-web-application-522397c9366b

What is Contentful:

Contentful is known as a headless CMS system, which means it is a API-First content-management-system from where you can create, manage and distribute content to any platform or device.

Learn much more about Contentful on their own website right here: https://www.contentful.com/


In this article you’ll learn how to build a very simple nuxt website that pulls data from Contentful. Once you’ve gotten a grasp of both and how you can use these two together, you can really start build powerful and amazing applications.


Nuxt Setup

Before we can start building, we need to install Nuxt. We’ll do that by using the VueCLI. If you haven’t installed this on your system before, you need to install it by using the terminal:

npm install -g vue-cli

Now you can use the VueCLI to setup a Nuxt project.

vue init nuxt/starter nuxt-contentful-demo

Follow the instructions and give the project a name, description and author.

When done, cd into the folder of you project and run

npm install
npm run dev

Beautiful 😊 We’re now one step closer and we have a foundation to build upon.

Contentful Setup

Go to Contentful and login with your username and password.
If you haven’t already got a user, you need to create one to be able to use Contentful. They have a free plan which you can use.

Once logged in, first thing we need to do is setup a new space for our website.

When you’re in Contentful, click on “Create space”

When creating a new space, we’ll need to fill in a few details.
Choose the free plan and give your space a name and confirm the creation.

Integrate Contentful into the Nuxt

When wanting to use Contentful in in Nuxt projects, we need to install the javascript SDK. We can do this by running the following command:

npm install --save contentful

When done installing, we can go to our project in our IDE, and create a new file under “plugins”. This will be basically be the file that are telling Nuxt to use contentful as a plugin, which enables us to easily fetch our data from Contentful.

Go ahead a create a new file:

const contentful = require('contentful');

// use default environment config for convenience
// these will be set via `env` property in nuxt.config.js

const
config = {
space: process.env.CTF_SPACE_ID,
accessToken: process.env.CTF_CDA_ACCESS_TOKEN
};


// export `createClient` to use it in page components
module.exports = {
createClient () {
return contentful.createClient(config)
}
}

As you might have noticed we are referencing some environment variables that we haven’t created yet.

const config = {
space: process.env.CTF_SPACE_ID,
accessToken: process.env.CTF_CDA_ACCESS_TOKEN
};

For this to work, we will need to create a new file “contentful.json”, which we’ll place in our root directory. This file will need to hold some configuration.

{
"CTF_SPACE_ID": "YOURSPACEID",
"CTF_CDA_ACCESS_TOKEN": "YOURACCESSTOKEN",
"CTF_ENVIRONMENT": "master"
}

You can find these settings by navigating to > Settings > Api Keys in the Contentful dashboard

When done, save the file and go to the nuxt.config.js

We need to require the newly created config file and add a bit of code to our nuxt.config.js file

// ./nuxt.config.js
const config = require('./.contentful.json')

module.exports = {
// ...
env: {
CTF_SPACE_ID: config.CTF_SPACE_ID,
CTF_CDA_ACCESS_TOKEN: config.CTF_CDA_ACCESS_TOKEN,
CTF_ENVIRONMENT: config.CTF_ENVIRONMENT
}
// ...
}

The env property is a way to define values that will be available when using process.env when the site is run in a node.js context.

Now that we’ve gotten all the basics setup for using Contentful, next step is to create some content, we can pull into our nuxt application.

Build content in Contentful

Before we can fetch content into the application, we need to create a content-type and some content. Start off by navigating to the tab: Content Model and setup a new content-type

Once the content-type is created, we need to add some fields to it.
In this example, we’ll setup a very basic model with the following fields:

Next up, we’ll need a few pages. Go ahead and create some pages based on the content-type created. You can do this in the “Content” tab.

Awesome — Now we have some content created and we’re ready to do some more work in our code.

Creating the navigation

Our website needs a navigation so our users can navigate between pages. In the “Components” folder, create a new component called “Navigation”

// Navigation.vue<template>
<div class="navigation">
<nav>
<ul role="menu">
<li v-for="(navItem, index) in pages" :key="index">
<nuxt-link :to="'/' + navItem.fields.slug.trim()" role="menuitem">{{navItem.fields.navTitle}}</nuxt-link>
</li>
</ul>
</nav>
</div>
</template>

<script>
export default {
name: 'Navigation',
props: {
pages: {
type: Array, // We expect an array of pages that we need for our navigation
required: true
}
}
}
</script>

Next, go to the folder “pages” and open up the index.vue file.
In this file we’ll need our navigation component to be included, so we can add it to the file.

//pages/index.vue<template>
<section class="container">
<Navigation :navItems="pages" />
</section>
</template>

<script>
import Navigation from '../components/Navigation';
export default {
components: {
Navigation
}
}
</script>

As you might notice, in the Navigation.vue, we are passing a prop called navItems with some data that doesn’t exist yet. To pass the pages down to the navigation component, we first need to fetch the data from Contentful

Fetch data from Contentful

First thing we need to do is, import the client from the contentful plugin, we created earlier in the plugins directory:

Add the following to the code:

import { createClient } from '../plugins/contentful';
const contentfulClient = createClient();

Next, we need to make use of the asyncData method. This allows us to fetch and render the data server-side. In this we’ll fetch all pages created in Contentful.

Interested in learning more about asyncData: https://nuxtjs.org/api/

//pages/index.vueasyncData ({env}) {
return Promise.all([
// fetch all blog posts sorted by creation date
contentfulClient.getEntries({
'content_type': 'page',
order: '-sys.createdAt'
})
]).then(([pages]) => {
// return data that should be available
// in the template
return {
navItems: pages.items
}
}).catch(console.error)
}

What happens is, we start off by fetching all content created with the content-type of page ordered by date of creation.

When we have the data, we assign it to the property pages, which is also the prop data passed to our navigation component:

Your index.vue file should now resemble something like this:

<template>
<section class="container">
<Navigation />
<div class="container__content">
<h1>Please select a page you wish to view</h1>
<p>This is a website for demo purposes of using Nuxt & Contentful together</p>
</div>
</section>
</template>
<script>
import Navigation from '../components/Navigation';
import {createClient} from '../plugins/contentful';
const contentfulClient = createClient();
export default {
components: {
Navigation
},
asyncData ({env}) {
return Promise.all([
// fetch all blog posts sorted by creation date
contentfulClient.getEntries({
'content_type': 'page',
order: '-sys.createdAt'
})
]).then(([pages]) => {
// return data that should be available
// in the template
return {
pages: pages.items
}
}).catch(console.error)
}
}
</script>

And your page something like this:

Wauw

Just had to include a meme for this 😊
Ok, maybe not the prettiest thing, but again, style and design as you see fit :)

Anyway…… so far, so good… Now, what happens if you try clicking one of the navigation items? Argh, you get an error page ☹

Let’s fix this.
The problem is Nuxt will automatically look for a component with the name same as the child route, which means if you have a url like: /about — Nuxt will look for an about.vue component or folder structure like /about/index.vue

To fix this, we need to create a new component in “pages/_id” — Call this index.vue
This will tell Nuxt to use this component for all child routes, ex: /about

We can easily test it out and see if it works by adding some hardcoded html:

//pages/_id/index.vue<template>
<div class="page-component">
<p>This is the page component</p>
</div>
</template>

<script>
export default {
name: 'index'
}
</script>

Now when clicking one of the links in our navigation, you should see something like this:

Next step is to fetch the contents of the current page. Again, we’ll import the {createClient} from our contentful plugin and assign it to a variable

import {createClient} from '../../plugins/contentful';
const contentfulClient = createClient();

and again, we need to use the asyncData method, but this time, we’ll get the data by matching the slug and the params from the url:

asyncData ({ env, params }) {
return contentfulClient.getEntries({
'content_type': page,
'fields.slug': params.id // the magic happens here
}).then(page => {
return {
page: page.items[0]
}
}).catch(console.error)
}

Now we have access to the data in the property: page. You can now create your template HTML and style it as you like — the full component should now look something like this:

// pages/_id/index.vue<template>
<div class="page-component">
<a @click="$router.go(-1)">Go back to overview</a>
<hr />
<h1>{{page.fields.heading}}</h1>
<img :src="page.fields.image.fields.file.url" :alt="page.fields.heading" v-if="page.fields.image" />
<p>
{{page.fields.content}}
</p>
</div>
</template>

<script>
import {createClient} from '../../plugins/contentful';
const contentfulClient = createClient();

export default {
name: 'index',
asyncData ({ env, params }) {
return contentfulClient.getEntries({
'content_type': 'page',
'fields.slug': params.id
}).then(page => {
return {
page: page.items[0]
}
}).catch(console.error)
}
}
</script>

And your page looking something like:

Almost there…

We now got a working website, but it’s still lacking a few things. The navigation is missing whenever we are looking at a page + whenever we are at the frontpage, we have no control on the order of our navigation items. Not really the best UX pattern. Let’s fix this as well.

Move the navigation component out of our “pages/index”, and place it in “layouts/default” and remove the props passed to our navigation component (also remember to remove it inside the actual component)

//layout/default.vue<template>
<div>
<Navigation></Navigation>
<nuxt/>
</div>
</template>
<script>
import Navigation from '../components/Navigation';

export default {
components: {
Navigation
}
}
</script>

Unfortunately, Nuxt doesn’t allow us to make use of the asyncData method in the layout, if we try, we’ll get an error when fetching content from Contentful
More about that here: https://github.com/nuxt/nuxt.js/issues/1740

Go Vuex

In this case, we’ll use make use of Vuex so solve our problem. Go to the store folder and create an index.js file.

//store/index.jsimport Vuex from 'vuex';
import navigation from './modules/navigation';

const createStore = () => {
return new Vuex.Store({
modules: {
navigation: { ...navigation, namespaced: true }
},
strict: false
})
};

export default createStore

As you can see, we’re importing a file called navigation that doesn’t exist yet, so we’ll need to create this and do a little work:

//store/modules/navigation.jsimport {createClient} from '../../plugins/contentful';
const contentfulClient = createClient();
export const state = {
navItems: null
};

export const mutations = {
setMenuItems(state, data) {
state.navItems = data;
}
};

export const actions = {
getPageItems({commit}) {
contentfulClient.getEntries({
'content_type': page,
order: '-sys.createdAt'
}).then((page) => {
if(page) {
const navItems = page.items;
commit('setMenuItems', navItems);
}
}).catch((err) => {
console.log("error", err);
});
}
};

export default {
state,
mutations,
actions,
};

Note: Remember to remove all asyncData from the pages/index.vue, as we have no need to this anymore

We are creating an action that commits our navigation items. When committing, we’ll mutate the state, and save basically save the navigation items in the state.

Next up, we’ll need to refactor our navigation component to get data from the store.

//components/Navigation.vue<template>
<div class="navigation">
<nav>
<ul role="menu">
<li v-for="(navItem, index) in navItems" :key="index">
<nuxt-link :to="'/' + navItem.fields.slug.trim()" role="menuitem">{{navItem.fields.navTitle}}</nuxt-link>
</li>
</ul>
</nav>
</div>
</template>

<script>
import {mapState} from 'vuex';
export default {
name: 'Navigation',
computed: {
...mapState('navigation', [
'navItems'
])
},
mounted() {
this.$store.dispatch("navigation/getPageItems");
}
}
</script>

Now, when checking, our navigation should be fully functional again and when going to a page, our navigation will be visible.

For controlling the order of the navigation items, you could add an order field in your content-type. It’s also possible to create a separate content-type for navigation items, create a global “container”, and make use of the reference field in Contentful

There are a ton of different ways you could do this, and it all depends on how you choose to structure and manage your data in Contentful, as there are no specific guidelines on how to structure content.

You’re at the finish line

You’ve now built your first very basic with Nuxt & Contentful, and although this is a very basic example, I hope you get the idea of how you can use these to platforms to create very powerful and awesome applications.

Find the full repo here:
https://github.com/nickycdk/nuxt-contentful-example

Next step:

Now that you’ve gotten a hold of the basics, I urge you to try building a real website using what you’ve learned.

In the future, I’ll try to cover topics like

  • How you connect and deploy your static site to Netlify
  • How to generate a sitemap using Nuxt
  • Generate files from dynamic routes
  • Rendering markdown from your content fields in contentful
  • Rendering content from a WYSIWYG field in contentful
  • And much more….

I hope you enjoyed the article and found it useful, so you can start building awesome applications using Nuxt & Contentful

If you’d like to catch up with me sometime, follow me on Twitter | LinkedIn| Facebook or simply visit my portfolio website (That is however in Danish)

Vue.js Developers

Helping web professionals up their skill and knowledge of Vue.js

Nicky Christensen

Written by

Head Of Development @ C-LOG (c-log.io) — VueJS Fanboy — In love with most frontend related stuf — nickychristensen.dk / https://twitter.com/nickycdk

Vue.js Developers

Helping web professionals up their skill and knowledge of Vue.js

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade