Create a blog with NuxtJS and Netlify CMS - Part 2

Updated by Tom Wells on

Part 2 of this series focuses on the internals with Nuxt and Netlify CMS. Utilising Netlify CMS, we can have a fully-featured blog without the need for a database or server. If you haven't already read part 1, go check it out!

Again, if you'd prefer to skip this tutorial, feel free to clone the repository from Github instead.

Automating your Netlify CMS posts

One problem I faced while creating this blog was having the homepage generate the posts created in the content/blog folder. I needed to loop through the files in the folder at build time (using nuxt generate) and create a JSON array with the list of posts available. So, here was my solution!

First of all, install the following dependencies:

yarn add front-matter-markdown frontmatter-markdown-loader

or

npm i front-matter-markdown frontmatter-markdown-loader

Then create index.js and blogs.json files in the content folder with the following content:

/content/index.js
const fs = require('fs')
const parseMarkdown = require('front-matter-markdown')

/**
 * getFiles - Get list of files in directory
 * @param {string} dir
 * @returns {Array} Array of objects
 */
const getFiles = (dir) => {
  const files = fs.readdirSync(dir)
  let filelist = []

  files.forEach((file) => {
    const markdownFile = fs.readFileSync(`content/blog/${file}`, 'utf-8')
    const fileContents = parseMarkdown(markdownFile)
    const date = fileContents.date
    const slug = file
      .split('.')
      .slice(0, -1)
      .join('.')

    const obj = { date, slug }

    filelist.push(obj)
  })
  return filelist
}

/**
 * Write blogs json file
 */
const writeBlogs = async () => {
  const fileArray = await getFiles('content/blog/')

  // Order array by date (default asc)
  const sortedArray = await fileArray.sort((a, b) => {
    return a.date.getTime() - b.date.getTime()
  })

  // Reverse array and write to JSON
  const reversedArray = await sortedArray.reverse()
  const jsonContent = await JSON.stringify(reversedArray)

  fs.writeFile('content/blogs.json', jsonContent, (err) => {
    if (err) throw new Error(err)
  })
}

writeBlogs()

This file uses the fs module to read the files in content/blog and writes the filename and date to the blogs.json file as an array of objects. The array is then reversed to ensure the posts are in the correct order. NOTE: you’ll need to have a published datetime field in your Netlify CMS config.yml for this to work correctly.

/content/blogs.json
[]

For now, blogs.json is empty and will populate itself when you’ve written a post. If you’ve already written a sample post, running node content/index.js in your terminal will run the script and you should see the blogs.json file populate with a post!

Finally, to ensure this script runs when you generate your blog, you need to make the following modification to your package.json:

package.json
{
  "scripts": {
    "generate": "node content/index.js && nuxt generate"
  }
}

Now, whenever you generate your blog with yarn generate, the JSON file will contain all of your posts!

Creating your homepage

Now you’ve got your blog creating your array of posts; it’s time to set up the rest of your nuxt project to generate your posts and display them on your homepage.

To start, let’s ensure nuxt generates your blog posts. First of all, you'll need to import your JSON file and loop through the posts:

nuxt.config.js
import blogs from './content/blogs.json'

export default {
  generate: {
    routes: [].concat(blogs.map((blog) => `/blog/${blog.slug}`))
  },

  build: {
    extend(config, ctx) {
      // Add this to your build config
      config.module.rules.push({
        test: /\.md$/,
        loader: 'frontmatter-markdown-loader',
        options: {
          vue: true
        }
      })
    }
  }
}

You’ll notice we’ve also updated the build configuration to include the frontmatter-markdown-loader. This is added so that your markdown files can be parsed in your Vue components.

Speaking of which, it’s now time to create the homepage (pages/index.vue)! I’m not going to show you all components, only how you can use the markdown files to display your posts on the homepage:

/pages/index.vue
import blogs from '~/content/blogs.json'

export default {
  async asyncData({ app }) {
    async function awaitImport(blog) {
      const wholeMD = await import(`~/content/blog/${blog.slug}.md`)
      return {
        attributes: wholeMD.attributes,
        link: blog.slug
      }
    }

    const blogList = await Promise.all(
      blogs.map((blog) => awaitImport(blog))
    ).then((res) => {
      return {
        blogList: res
      }
    })

    return blogList
  }
}

If you’ve read Marina’s post, you’ll recognise some of this code. In this case it’s a bit simpler as we’re not creating blog posts in different languages. If that’s something you’re looking to do, check out Marina’s post and make the necessary edits.

The code above gives you access to an array of objects containing the post attributes (title, author etc) and the link. From here, you could add the following to your template block to list your posts:

/pages/index.vue
<template>
  <section>
    <PostCard
      v-for="(blog, index) in blogList"
      :key="index"
      :post-info="blog"
    />
  </section>
</template>

The PostCard component will then have the postInfo prop to help you display your posts. This prop will contain your slug and post attributes as defined above.

Displaying your posts

Finally, we need to create a page for your blog posts (pages/blog/_slug.vue). This is quite a simple component:

/pages/blog/_slug.vue
export default {
  async asyncData({ params }) {
    const post = await import(`~/content/blog/${params.slug}.md`)
    const attr = post.attributes
    const slug = params.slug

    const {
      author,
      authorlink,
      date,
      summary,
      thumbnail,
      title,
      type,
      update
    } = attr

    const dateOptions = {
      weekday: 'long',
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    }

    const publishedDate = new Date(date)
    const updatedDate = new Date(update)
    const published = publishedDate.toLocaleDateString('en-GB', dateOptions)
    const updated = updatedDate.toLocaleDateString('en-GB', dateOptions)

    return {
      title,
      author,
      authorlink,
      date,
      update,
      published,
      updated,
      type,
      thumbnail,
      summary,
      slug,
      html: post.html
    }
  }
}

our list of attributes may differ from the above, depending on the fields set in your Netlify CMS config.yml file. In short, the above is take the slug from the router, fetching the markdown and extracting the attributes and post html for displaying. This is achieved by using the frontmatter-markdown-loader package we added and configured earlier.

From here, you can easily add this content to your template:

/pages/blog/_slug.vue
<template>
  <section class="post">
    <h1>{{ title }}</h1>
    <img v-lazy="thumbnail" class="thumbnail" :alt="title" />
    <div class="post-content" v-html="html"></div>
  </section>
</template>

Of course, this is a very basic example and you can add all the attributes in whatever way you want. But this will work!

Final thoughts

This has been a very long post but I hope it’s been useful for you. However, there are many improvements that could be made, such as:

  • Create several JSON files for paginated posts.
  • Loop through all directories for other collections (such as portfolio, events etc).
  • Potentially move away from markdown for the posts and instead use JSON.

If you have any questions, contact me on Twitter @Pipinghot. I’ll be happy to help!