{{announcement.body}}
{{announcement.title}}

Including Markdown Content in a Vue or Nuxt SPA

DZone 's Guide to

Including Markdown Content in a Vue or Nuxt SPA

We explore why markdown can be such a great choice for developers when they're creating a single page app with Vue.js or Nuxt.

· Web Dev Zone ·
Free Resource

Developers love to show off a solution they've come up with to solve a tricky problem (heck, I'm doing it right now). For that reason, you'll probably create a developer blog at some point in your career to showcase your favorite hacks.

And, as a developer, you'll no doubt irrationally build your blog from scratch rather than use a pre-made solution, because that's just what we do!

Markdown is a really handy format for writing developer blog posts, as it makes it easy to include code blocks and other kinds of formatting without the verbosity of writing HTML.

If you are going to build a markdown-based developer blog, a Vue (or Nuxt) single-page app would be an excellent choice as we'll see in a moment.

Including Markdown Files

Including markdown files in a Vue SPA is actually a bit tricky. The biggest challenge is that each markdown file should be a "page" of your SPA. This means Vue Router needs to be aware of them, but since they're ever-changing content, you don't want to hardcode their URLs in the app code.

For the rest of the article, I'll be outlining an app architecture that deals with this.

Meta Info With Frontmatter

Often you'll want to include meta information about a post in the markdown file. For example, what is the banner image to be used, the meta description, the URL, the tags, etc.

I recommend using "frontmatter" for your markdown files, whereby the meta info as added as YAML data at the top of the file, like this:

Plain Text




x


 
1
---
2
title:  "..."
3
description: "..."
4
date: ...
5
---
6
 
          
7
# Post body
8
 
          
9
Using markdown.



We'll need frontmatter in this architecture to ensure that we can derive a URL from each new markdown file.

Serve Your Markdown Files

Make sure your markdown files are in a directory that is being statically served.

server.js

JavaScript




xxxxxxxxxx
1


 
1
app.use(express.static(__dirname + '/articles'));
2
 
          
3
// e.g. /articles/my-blog-post.md



In a more sophisticated setup, we'd use Webpack to bundle the markdown, but I don't want to complete the key idea so we'll continue with this less efficient solution for now.

Generate a Manifest File

You should now generate a manifest file that contains each article's URL and path on the server.

Firstly, you'll need to decide on a set URL structure for each post e.g. /:year/:month/:day/:title. Make sure this is derivable from the post by adding the required data to your frontmatter.

Now, create an executable script that will run during your build process. The script will iterate all your markdown files and generate a list of URLs and file paths in a JSON array which can then used by Vue Router.

Here's some pseudo code so you can see how it should work. Note that the frontmatter can be extracted using the front-matter NPM module.

generateManifest.js

JavaScript




xxxxxxxxxx
1
11


 
1
const fs = require("fs");
2
const fm = require("front-matter");
3
 
          
4
fs.readdir("articles", files => {
5
  files.foreach(file => {
6
    fs.readFile(`articles/${file}`, data => {
7
      const { url } = fm(data);
8
      // Now you need to add the URL and file path to a file "/manifest.json"
9
    });
10
  });
11
});



You should end up with a JSON file like this:

JSON




xxxxxxxxxx
1


 
1
[
2
  { "url": "/2018/12/25/my-blog-post", "file": "/articles/my-blog-post.md" },
3
  { ... },
4
]



Note that the generated manifest should also be statically served, as, in the next step, the SPA will grab it with AJAX and use it to dynamically add the routes.

Dynamic Routes

Be sure to set up Vue Router to include a dynamic path that matches your article's URL structure. This route will load a page component that will, in the next step, display your markdown:

router.js

JavaScript




xxxxxxxxxx
1


 
1
new VueRouter({
2
  routes: [
3
    { path: '/:year/:month/:day/:title', component: Article }
4
  ]
5
})



As it is, this dynamic path could match almost anything. How do we ensure that the provided URL actually matches an article? Let's grab the manifest, and before we attempt to load an article, ensure the URL provided is in the manifest.

In the created hook of your Vue instance, use AJAX and fetch this manifest file. The manifest data should be available to any component that needs it, so you can add it to your global bus or Vuex store if you're using one, or just tack it onto the Vue prototype:

app.js

JavaScript




xxxxxxxxxx
1


 
1
function createInstance() {
2
  new Vue(...);
3
}
4
 
          
5
axios.$http.get("/manifest.json")
6
  .then(file => {
7
    Vue.prototype.articles = JSON.parse(file);
8
    createInstance();
9
  });



Now, in your Article component, when the dynamic route is entered, confirm if it's in the URLs provided in the manifest:

Article.vue

JavaScript




xxxxxxxxxx
1


 
1
export default {
2
  beforeRouteEnter(to) {
3
    next(vm => {
4
      return vm.articles.find(article => article.url === to);
5
    });
6
  }  
7
}



It'd be a good idea to fall back to a 404 page if beforeRouteEnter returns false.

Loading the Markdown

Okay, so now the SPA is recognizing the correct URLs corresponding to your markdown content. Now's the time to get the actual page content loaded.

One easy way to do this is to use AJAX to load the content and parse it using a library like "markdown-it". The output will be HTML which can be appended to an element in your template using the v-html directive.

Article.vue

JavaScript




xxxxxxxxxx
1
15


 
1
<template>
2
  <div v-html="content">
3
</template>
4
import md from "markdown-it";
5
export default {
6
  data: () => ({
7
    content: null
8
  }),
9
  beforeRouteEnter(to) {...},
10
  created() {
11
    const file = this.articles.find(article => article.url === this.$route.to).file;
12
    this.$http.get(file)
13
      .then({ data } => this.content = md(data));
14
  }
15
}


Server-Side Rendering

The big downside to this architecture is that the user has to wait for not one but two AJAX calls to resolve before viewing an article. Eww.

If you're going to use this approach, you really must use server-side rendering or prerendering.

The easiest way, in my opinion, is to use Nuxt. That's how I did it with this site.

Also, using Nuxt's asyncData method makes it very easy to load in the manifest, and using the verify method of each page component you can tell the app whether the article exists or not.

Plus, you can easily execute your generate manifest script as part of Nuxt's build process.

Bonus: Inserting Vue Components in the Content

A downside to using markdown for content is that you can't include dynamic content, i.e. there's nothing like "slots" in your markdown content.

There is a way to achieve, that, though!

Using the awesome frontmatter-markdown-loader, you can get Webpack to turn your markdown files into Vue render functions during the build process.

You can then load these render functions using AJAX instead of the markdown file:

JavaScript




xxxxxxxxxx
1


 
1
created() {
2
  const file = this.articles.find(article => article.url === this.$route.to).file;
3
  this.$http.get(file)
4
    .then({ data } => {
5
      this.templateRender = new Function(data.vue.render)();
6
      this.$options.staticRenderFns = new Function(this.content.vue.staticRenderFns)();
7
    });
8
}
9
 
          



This means you can include Vue components in your markdown and they will work! For example, on the Vue.js Developers blog, I insert an advertisement inside an article by adding a component like this:

JSX
xxxxxxxxxx
1
 
1
# My article
2
3
Line 1
4
5
<advertisement-component/>
6
7
Line 2
Topics:
nuxt js ,markdown ,vue.js ,front-end development ,web dev

Published at DZone with permission of Anthony Gore , DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}