This article was originally posted on veritystothard.com.
Checkout the project repository here, and the Netlify deployment here.
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, 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.
To start the project, run yarn run dev
Set up a Contentful space
Create an account or login to Contentful and create a space for your project using the blog template:
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.
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 by running yarn add dotenv
, and then require it at the top of your nuxt.config.js
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:
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:
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.
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.
Now we can use this data to create a preview tile for each post returned:
<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/
.
<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 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 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
.
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:
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-markdown>{{content.fields.body}}</vue-markdown>
Deploying to Netlify
Set up an account or login to Netlify 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.
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.
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:
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
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.
Top comments (5)
looks really nice ! I was about to do some stuff like this
I think asyncData can be used without async/await.
Also in you post you can set the language in 'code' markdown tag, so the code will be displayed with some colors :D
Hey Thomas, I just revisited my code and you do need async/await on the asyncData method or the data caught in the stories variable below is a pending promise, causing the page to fail.
let blogPosts = await contentful.getEntries({ content_type: "blogPost" });
:)
yes ! I meant like this !
i'm pretty sure it would do the exact same thing :)
I use it that way with axios module.
Ah gotcha! Thanks for the tip π
Oh awesome, thank you! I'll have a look and ammend this later today :)