Parsing Markdown Front Matter on Nuxt.js

Extracting front matter from markdown posts using the frontmatter-markdown-loader package on Nuxt.js with ease.

Published on April 9, 2020

Parsing front matter can be frustrating, especially if you want to use Nuxt.js as a JAMstack, there are many options available, from official nuxt modules to custom plugins and loaders, the list goes on and on, but luckily for you I’ve got you covered.

After doing some research and testing I’ve found the frontmatter-markdown-loader package to work best with Nuxt. Now a side note, if you want to render markdown data coming from an API you should probably use the official @nuxtjs/markdownit module and get the attributes from the database. This package will also take care of rendering the content for you using markdown-it under the hood.

A good example for using this package would be a statically generated blog (SSR), where you may (probably) want to extract metadata from each post in order to display it dynamically, which is why this is the topic of choice to be covered in this post.

But enough talk, let’s get to work, add the module to your devDependencies section in your package.json using:

yarn add -D frontmatter-markdown-loader

Inside your nuxt configuration file, register the loader under the extend() function so any markdown file can be processed by the front matter module. In this article I’ll be using the module’s “render functions” option to parse down any vue components inside a markdown file using a custom render function which will implement later in this post.

// nuxt.config.js

import * as path from 'path'

export default {
  extend(config, ctx) {
    config.module.rules.push({
      test: /\.md$/,
      loader: 'frontmatter-markdown-loader',
      // load files within the 'posts' directory
      include: path.resolve(__dirname, 'posts'),
      options: {
        mode: ['vue-render-functions'],
        vue: {
          // this handles the css class used to
          // style your post wrapper
          root: 'content'
        }
      }
    })
  }
}

Note: There are two options for rendering vue components inside a markdown file built within this module, vue-component and vue-render-functions which is now deprecated. I chose to use the older method because the newer method didn’t work quite as well as the older.

Create a new folder at the root of your project called posts to store all of your posts/articles.

Inside that folder, create a new markdown file. You may now use the --- block to house all your custom metadata in yaml format such as the example below. This data should be at the very top of your file.

 # posts/your-post.md

---
title: Your Custom Title
description: Your custom description.
image: path/to/image.jpg
---

...

Within the components folder, create a new component called DynamicMarkdown.vue, this file will be responsible for rendering your post content as well as any vue components found inside the markdown file (if any).

This file doesn’t have a <template> tag and only renders data passed to it via props.

<!-- components/DynamicMarkdown.vue -->

<script>
export default {
  props: {
    renderFunc: {
      type: String,
      default: ''
    },
    staticRenderFuncs: {
      type: String,
      default: ''
    }
  },
  created() {
    // eval is dangerous but since we are using SSR
    // it shouldn't be a problem
    // eslint-disable-next-line
    this.templateRender = eval(this.renderFunc)
    // eslint-disable-next-line
    this.$options.staticRenderFns = eval(this.staticRenderFuncs)
  },
  render(createElement) {
    return this.templateRender ? this.templateRender() : createElement('div')
  }
}
</script>

Great, we must now create a new file called _slug.vue inside pages/blog (You will need to create the blog directory first though). This is the file where you should define how your individual post page will look like, a _ prefix indicates a dynamic route.

<!-- pages/blog/_slug.vue -->

<template>
</template>

<script>
// import our custom renderer
import DynamicMarkdown from '~/components/DynamicMarkdown.vue'

export default {
  components: {
    DynamicMarkdown
  },
  async asyncData({ params }) {
    // import a post based on the url param
    const post = await import(`~/posts/${params.slug}.md`)

    return {
      // this is your post name based on the url params
      slug: params.slug,
      // post metadata such as title, description and etc.
      attributes: post.default.attributes,
      // stringify render functions in order to be eval'd
      renderFunc: `(${post.vue.render})`,
      staticRenderFuncs: `[${post.vue.staticRenderFns}]`
    }
  }
}
</script>

Edit the <template> section to contain anything you may want to be displayed in the post page. The DynamicMarkdown tag is where your content coming from the markdown file will be rendered to. Remember that you can style that wrapper using the content class defined earlier in the loader’s configuration.

<!-- pages/blog/_slug.vue -->

<template>
  <article class="post">
    <header>
      <h1 class="title" v-text="attributes.title" />
      <p class="subtitle" v-text="attributes.description" />
    </header>
    <DynamicMarkdown
      :render-func="renderFunc"
      :static-render-funcs="staticRenderFuncs"
    />
  </article>
</template>

Now, if you run the generate command you will likely find Nuxt reporting a routing error, that’s because yarn generate won’t dynamically create routes for your posts unless you specify the routes yourself, in order to overcome this problem we must implement our own function which returns all the paths to be generated so our posts can be found.

Update your nuxt configuration file so it looks something like this:

// nuxt.config.js

import fs from 'fs'
import * as path from 'path'

const getPaths = () =>
  fs
    // walk through posts folder
    .readdirSync(path.resolve(__dirname, 'posts'))
    // return only the files with 'md' ext
    .filter((filename) => path.extname(filename) === '.md')
    // map each result to its corresponded url
    .map((filename) => `/blog/${path.parse(filename).name}`)

And finally, below the extend() section reference our custom helper function to the routes option:

// nuxt.config.js

export default {
  extend(config, ctx) {...},
  generate: {
    routes: getPaths()
  }
}

Now whenever you build your files to production using the generate command all your paths will be mapped properly.

Using Vue Components Inside Markdown Files

All you have to do is import and register the component you want to use inside the DynamicMarkdown.vue component.

<!-- components/DynamicMarkdown.vue -->

<script>
import MyComponent from '~/components/MyComponent.vue'

export default {
  components: {
    MyComponent
  }
}
</script>
<!-- posts/my-post.md -->

An example post with my custom component below:

<MyComponent />

And that’s it, you have made it to the end. Fire up your (development || production) environment and see the results by navigating to a post’s url.

Thanks for reading!

how-todevnuxtmarkdown

Marlos Pomin
Full Stack Developer & Retoucher based in Brazil, also a casual pentester.