Including Markdown Content in a Vue or Nuxt SPA
Anthony Gore | December 31st, 2018 | 4 min read
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:
---
title: "..."
description: "..."
date: ...
---
# Post body
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
app.use(express.static(__dirname + '/articles'));
// 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
const fs = require("fs");
const fm = require("front-matter");
fs.readdir("articles", files => {
files.foreach(file => {
fs.readFile(`articles/${file}`, data => {
const { url } = fm(data);
// Now you need to add the URL and file path to a file "/manifest.json"
});
});
});
You should end up with a JSON file like this:
[
{ "url": "/2018/12/25/my-blog-post", "file": "/articles/my-blog-post.md" },
{ ... },
]
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
new VueRouter({
routes: [
{ path: '/:year/:month/:day/:title', component: Article }
]
})
As it is, this dynamic path could match almost anything. How do we ensure 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 you 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
function createInstance() {
new Vue(...);
}
axios.$http.get("/manifest.json")
.then(file => {
Vue.prototype.articles = JSON.parse(file);
createInstance();
});
Now, in your Article
component, when the dynamic route is entered, confirm if it's in the URLs provided in the manifest:
Article.vue
export default {
beforeRouteEnter(to) {
next(vm => {
return vm.articles.find(article => article.url === to);
});
}
}
It'd be a good idea to fallback 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
<template>
<div v-html="content">
</template>
import md from "markdown-it";
export default {
data: () => ({
content: null
}),
beforeRouteEnter(to) {...},
created() {
const file = this.articles.find(article => article.url === this.$route.to).file;
this.$http.get(file)
.then({ data } => this.content = md(data));
}
}
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:
created() {
const file = this.articles.find(article => article.url === this.$route.to).file;
this.$http.get(file)
.then({ data } => {
this.templateRender = new Function(data.vue.render)();
this.$options.staticRenderFns = new Function(this.content.vue.staticRenderFns)();
});
}
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:
# My article
Line 1
<advertisement-component/>
Line 2
About Anthony Gore
If you enjoyed this article, show your support by buying me a coffee. You might also enjoy taking one of my online courses!
Click to load comments...