By Nicky Christensen, on Jun 14, 2019

Build a website using Nuxt and Contentful : a step by step guide

As a frontend developer, working with static site generators and serverless architecture is a joy. 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 and Contentful — including a simple Vuex example.

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

Personally, I run my own website on a Nuxt/Contentful setup with continuous deployment, with GIT repo connected to Netlify. This, combined with Contentful’s webhooks that 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 your next web application, 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 an 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: 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 building 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

1
2
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 aren’t a user yet, you need to create a profile 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 set up 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 Nuxt

When wanting to use Contentful 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:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.

javascript
1
2
3
4
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.

json
1
2
3
4
5
{
  "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.

javascript
1
2
3
4
5
6
7
8
9
10
11
12
// ./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 set up a new content type.

Once the content type is created, we need to add some fields to it. In this example, we’ll set up 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.”

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//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:

javascript
1
2
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? Check out this link: https://nuxtjs.org/api/.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//pages/index.vue
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 {
      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:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<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 goof. 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:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
//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...

javascript
1
2
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.

1
2
3
4
5
6
7
8
9
10
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 look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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 will look something like:

Almost there...

We now have a working website, but it’s still lacking a few things. The navigation is missing whenever we are looking at a page and whenever we are on the front page, 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).

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//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.

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//store/index.js
import 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//store/modules/navigation.js
import {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 of 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//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 and 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 steps

Now that you’ve gotten 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, and that you can start building awesome applications using Nuxt & Contentful.

Nicky Christensen
Arhus, Denmark

Nicky Christensen is a Senior Frontend Developer from Denmark who loves turning complex problems into simple, beautiful and elegant digital solutions. When not coding, you can find him traveling the world, in the gym or behind his laptop writing articles on Medium or on his own website

Community Contributor
Become a contributer