Build a blog with Nuxt, Contentful, Netlify and Tailwind
Tags: javascript, vue, nuxt, contentful
Published: 2019-04-30 11:54:13

---
title: Build a blog with Nuxt, Contentful, Netlify and Tailwind
published: true
description: An end-to-end guide on building a lightening fast blog.
tags: #javascript, #vue, #nuxt, #contentful
canonical_url: http://www.veritystothard.com/blog/build-blog-with-nuxt-contentful-netlify-tailwind
---
*This article was originally posted on [veritystothard.com](http://www.veritystothard.com/blog/build-blog-with-nuxt-contentful-netlify-tailwind).*

Checkout the project repository [here](https://github.com/VStothard/nuxt-contentful-starter), and the Netlify deployment [here](https://nuxt-contentful-starter.netlify.com "nuxt-contentful-starter-blog").

Note: This post assumes some experience with JavaScript frameworks, it was written using the Nuxt version 2.4.0

### Create a Nuxt app

Firstly, we need to create a repository for your project, we will be using Github. This is optional, but we will use this repo later to deploy to [Netlify](https://www.netlify.com/), so make sure your provider is supported.

Once you have created and cloned your repo:

- Create Nuxt app in your current directory: `yarn create nuxt-app` 
- *Or*, create in a sub directory: `yarn create nuxt-app <my-project->`

On running one of the above, will we be guided through the setup process. For reference, these are my selections for this project:

- server framework: none
- features to install: none
- UI framework: Tailwind
- Test framework: None
- Rendering mode: Universal
- Package manager: Yarn

For more info more info on Nuxt installation, check out their [docs](https://nuxtjs.org/guide/installation/ "Nuxt Installation").

To start the project, run `yarn run dev`

### Set up a Contentful space

Create an account or login to [Contentful](https://www.contentful.com/) and create a space for your project using the blog template:

![Screen Shot 2019-04-28 at 6.45.51 pm](//images.ctfassets.net/8i5c5iwa1tzc/9uVM8oPdqGGWsWWe5O23b/4c176448291ce310755a286a3e7a0368/Screen_Shot_2019-04-28_at_6.45.51_pm.png)

Have a look around and you will see Contentful automatically has create some dummy content for us.

In order to use access our content, we will need to add Contentful to our project and set up our environment variables for use in our api calls. To install, run `yarn add contentful`.

Create a .env file at the root of the project and add your details. You can find your space details in settings > api keys > Example space token 1. You will need the 'Space ID' and 'Content Delivery API - access token'

```    
    CONTENTFUL_SPACE_ID=[SPACE_ID]
    CONTENTFUL_ACCESS_TOKEN=[ACCESS_TOKEN]
```

In your `nuxt.config.js` file, map your environment variables into the env object.

```javascript
    export default {
      ...
      env: {
        spaceId: process.env.CONTENTFUL_SPACE_ID,
        accessToken: process.env.CONTENTFUL_ACCESS_TOKEN
      },
      ...
    }
```

In order to access our env variables throughout the project, we will need to install [dotenv](https://www.npmjs.com/package/dotenv) by running `yarn add dotenv`, and then require it at the top of your `nuxt.config.js`

```javascript
    require('dotenv').config()
    
    export default {
    ...
    }
```

Now we have all our variables set up, lets create a Nuxt plugin in the `/plugins/` folder to handle the creation of the client and make it globally accessible. We will name the plugin `contentful.js`, make our environment variables accessible in a config object, and then init and export the client:

```javascript
    const contentful = require('contentful')
    
    const config = {
        space: process.env.spaceId,
        accessToken: process.env.accessToken
    }
    
    const client = contentful.createClient(config)
    
    export default client
```

You will then need to add the plugin to `nuxt.config.js`  and restart your project to make it usable:

```javascript
    export default {
    	...
    	plugins: [
    	  '~/plugins/contentful.js'
    	]
    	...
    }
```

### Getting entries with AsyncData

Async data allows you to pre-render data on the page so the first load of your site is lightning fast, you can read up on it [here](https://nuxtjs.org/guide/async-data/).

First, we will set up some post preview tiles on the home page. In `pages/index.vue` we will create an asynchronous function that gets all entries of type 'blogPost' and prints them to the page

*Note: async data will only work on the page level, not in components.*

You should see that your entry data is printed to the page in JSON. 

![Screen Shot 2019-04-29 at 5.29.17 pm](//images.ctfassets.net/8i5c5iwa1tzc/4R0k2NVq1EgWnGMs10ksIe/aec4ba957c45cb0ae7af940671f81574/Screen_Shot_2019-04-29_at_5.29.17_pm.png)

Now we can use this data to create a preview tile for each post returned:

```vue
    <template>
      <div>
        <div v-for="(post, i) in blogPosts" :key="i">
          <nuxt-link :to="{ name: `blog-slug`, params: { slug: post.fields.slug }}">
            <div v-if="post.fields.heroImage" class="w-full h-64 bg-cover bg-center" :style="`background-image: url('https:${post.fields.heroImage.fields.file.url}')`"></div>
            <p v-if="post.fields.publishDate">{{post.fields.publishDate}}</p>
            <h2 v-if="post.fields.title">{{post.fields.title}}</h2>
            <p v-if="post.fields.description">{{post.fields.description}}</p>
            <p >
              <span v-for="(tag, i) in post.fields.tags" :key="i">
                <template v-if="i < 2">#{{tag}} </template>
              </span>
            </p>
          </nuxt-link>
        </div>
      </div>
    </template>
    
    <script>
    import contentful from "~/plugins/contentful.js";
    
    export default {
        async asyncData(context) {
            let blogPosts = await contentful.getEntries({ content_type: "blogPost" });
    
            return {
               blogPosts: blogPosts.items
            }
        }
    };
    </script>
```

### Dynamic pages

Now, we need our preview tiles to link somewhere when we click on them, so lets create a dynamic blog page that uses the parameters passed in the `<nuxt-link>` to populate the page with the desired blog post.

In the pages folder, create a folder named `blog`, containing a file named `_slug.vue`

Our dynamic blog post page (`_slug.vue`) will use an asyncData function to return the entry that has type 'blogPost' *and* a slug field that matches the slug in the URL, e.g. `/static-sites-are-great/`. 

```vue
    <template>
      <div>
        <nuxt-link to="/">back to latest posts</nuxt-link>
        <div v-if="content.fields.heroImage" class="w-full h-64 bg-cover bg-center" :style="`background-image: url('${content.fields.heroImage.fields.file.url}')`"></div>
        <p v-if="content.fields.publishDate">{{content.fields.publishDate}}</p>
        <h2 v-if="content.fields.title">{{content.fields.title}}</h2>
        <vue-markdown>{{content.fields.body}}</vue-markdown>
        <p>
          <span v-for="(tag, i) in content.fields.tags" :key="i">
            <template v-if="i < 2">#{{tag}} </template>
          </span>
        </p>
      </div>
    </template>
    <script>
    import contentful from "~/plugins/contentful.js";
    
    export default {
      async asyncData({ env, params }) {
        return await contentful
          .getEntries({
            content_type: "blogPost",
            "fields.slug": params.slug
          })
          .then(entries => {
            return {
              content: entries.items[0]
            };
          })
          .catch(console.error);
      }
    };
    </script>
```

You may notice the body content in your blog post looks a little funky, this is because it the data is returned in [markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet "Markdown cheat sheet") and needs to be parsed before it can be rendered on the page as HTML. To handle this, we need to install a markdown parser such as [vue-markdown](https://www.npmjs.com/package/vue-markdown) by running `yarn add vue-markdown`.

We need this module to be accessible globally, so we will create another plugin file to import the module and register the `vue-markdown` component. We will name this plugin `vueMarkdown.js`.

```javascript    
    import VueMarkdown from 'vue-markdown';
    
    Vue.component('VueMarkdown', VueMarkdown)
```

Don't forget to add to the plugin list in nuxt.config.js and restart your project:

```javascript
    export default {
    	...
    	plugins: [
    	  '~/plugins/contentful.js',
    	  '~/plugins/vueMarkdown.js'
    	],
    	...
    }
```

Now we can wrap the post body in the component and see it is converted into HTML:

```vue
    <vue-markdown>{{content.fields.body}}</vue-markdown>
```

### Deploying to Netlify

Set up an account or login to [Netlify](https://www.netlify.com/) and follow their instructions for setting up your deployment. 

Your build settings should be:

- Repository: [your-repository-url]
- Base directory: Not set
- Build command: nuxt generate
- Publish directory: dist
- Deploy log visibility: Logs are public

In the environment section (Site settings > Build & Deploy > Environment), you will need to add your environment variables, the same as you have them in your `.env` file.

![Screen Shot 2019-04-30 at 8.29.03 am](//images.ctfassets.net/8i5c5iwa1tzc/5Okk51q9ye989TE7dhMBZ6/c6c5a8d3374ed141c9d5263bd32988fa/Screen_Shot_2019-04-30_at_8.29.03_am.png)

Through the magic of Netlify, your project should continuously deploy on push to master 🙌

### Set up dynamic route generation

If you visit your Netlify URL and click around, your site should be working as intended, but you may notice that the dynamic blog pages we create show a page not found error when you refresh the page. This is because when Netlify ran the `nuxt generate` command, nuxt looked in the config for a `generate: {}` object to determine which routes it needed to create, and found none. 

We need to go back to our project and specify that we would like a route generated for every entry in our Contentful space of type `blogPost`.

In `nuxt.config.js` we need to import Contentful and set up our client. You may notice this is a duplication of the code we have in our `contentful.js` plugin. In this context, we are not able to use the plugin as the environment variables we set up in our `nuxt.config.js` are not accessible until *after* the config itself has finished parsing. We therefore need to create the client at the top of the file to give us access to Contentful before the config finishes parsing. 


```javascript
    const contentful = require('contentful')
    const config = {
        space: process.env.CONTENTFUL_SPACE_ID,
        accessToken: process.env.CONTENTFUL_ACCESS_TOKEN
    }
    const client = contentful.createClient(config)
    
    export default {
    	...
    }
```

Next we will create an async function to get the slug of each our entries and push them to an array of routes:

```javascript
    generate: {
        routes: async function () {
          const entries = await client.getEntries({ content_type: "blogPost" });
          const routes = []
          entries.items.forEach(item => {
            routes.push(`blog/${item.fields.slug}`)
          })
          return routes
        }
      }
```

To test your function, run `yarn generate` , you should see your routes logged in the terminal

![Screen Shot 2019-04-29 at 4.53.37 pm](//images.ctfassets.net/8i5c5iwa1tzc/sOkcnDgYVk7e4OWVoQqdU/6fcd606d4c0e1e166f938dbadb9f09be/Screen_Shot_2019-04-29_at_4.53.37_pm.png)

Finally, commit and push your changes to your production branch, and check that the routes work as expected on your Netlify site. 

### Styling with Tailwind 💅🏻

Now we have the functionality set up, we can use tailwind to style up our blog, check out the finished project on GitHub to see how I styled everything.

[https://github.com/VStothard/nuxt-contentful-starter](https://github.com/VStothard/nuxt-contentful-starter)