DEV Community

yev
yev

Posted on • Updated on

Creating a Simple Blog using Vue with Markdown

I'm not a 'natural' frontend developer, learning vue and re-learning Javascript with my Java knowledge came as a challenge. Not to mention all those bundlers, builders, like webpack, gulp, grunt and other weird noises that adds to the overall complexitiy of learning modern web frameworks. But if there's one thing that helped me in learning new technologies that is, to just jump right in and build little projects. Teaching is also a widely accepted technique that will greatly help you in learning. It doesn't mean that you have to gather a huge audience that will listen to you teach something. Sometimes, the audience you need is just yourself. Write what you've learn with the intention of teaching it to your old (or future) stupid self. It's kinda weird and paradoxical I know but just roll with me on this.

With that mind, exactly a year ago, I decided to add a blog section on my personal website just for fun. I just ported my previous portfolio to Vue and I was wondering what's the most simplest way to add blogging functionality to it. I know, some people will probably frown at the idea of using a frontend framework to develop a measly portfolio website in the first place. But if your goal is to really just getting yourself up and running with a framework, I'd argue that using it to build your portfolio website is a good start. It's a simple project that will really familiarize you with the basics and will give you enough motivation to complete it.

So that's what I did. This post tells the story of how I set on adding a blog functionality on my Vue portfolio website. My thought-process, decisions, approach and what-nots.

I know from the start that I want it to be really simple. I know ButterCMS is good and all but as much as possible I don't want to introduce that much complexity, it's just a simple personal website after all. What I really want is as straightforward as hard-coding and commiting a new webpage everytime I write a new post but still simple enough that I wouldn't worry about writing in an HTML markup. That's where markdown comes in. Markdown is a really popular lightweight markup language that just fits the bill.

It's decided then. At the center of it all, what I want to accomplish is just two things:

  • Be able to write my blogs in markdown
  • Vue must be able to display these markdows as a regular html page

Essentially, to accomplish both, I just have to figure out how Vue can interpret markdown. Now, I'm not an expert with webpack, heck when I built my website years ago, I don't even know how to configure webpack myself, I was putting that off in favor of learning whatever framework I was learning at the moment. But being exposed with it for some time, I was able to pick up some concepts and context of what it does. In this case, I know that what I need is a webpack loader that can interpret markdown. Much like how .vue files comes out fine after passing through webpack because of vue-loader.

Vue-Markdown-Loader

Literally the first thing I did after that realization was to google "vue markdown loader". And QingWei-Li's vue-markdown-loader repo comes back as the first result. The documentation is pretty straightforward, I added this code over at my vue.config.js:

module.exports = {
  chainWebpack(config){
    config.module.rule('md')
      .test(/\.md/)
      .use('vue-loader')
      .loader('vue-loader')
      .end()
      .use('vue-markdown-loader')
      .loader('vue-markdown-loader/lib/markdown-compiler')
      .options({
        raw: true
      })
  }
}
Enter fullscreen mode Exit fullscreen mode

Like how vue-loader makes .vue files possible, vue-markdown-loader makes .md work within Vue. In essence, markdowns can now be interpreted as a Vue component. To test this, within my components/ directory, I created the following foo.md:

# This is a test
## Heading 2
_lorem ipsum_ dolor __amet__
Enter fullscreen mode Exit fullscreen mode

Imported it as a component on App.vue and used it within the template.

<template>
  <div id="app">
    <foo />
    ...
  </div>
</template>
<script>
  import Foo from '@/components/foo.md'
  export default {
    components: { Foo },
    ...
  }
</script>
Enter fullscreen mode Exit fullscreen mode

Doing a quick yarn serve and visiting localhost, viola! It works!

foo

At this point, we've verified that our Vue project can now understand and render markdown. We can now write our blogs in .md and just reference them wherever we like. And since it's a component there's nothing stopping us from using it as a route component, say in our router.js:

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import Foo from './components/foo.md'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    { path: '/', name: 'home', component: Home },
    { path: '/foo', name: 'foo', component: Foo }
  ]
})
Enter fullscreen mode Exit fullscreen mode

Now everytime we visit /foo it will render the markdown contents of our foo.md. Pretty neat, right? This works but wouldn't it be better if we could simplify the process of adding a new post a bit? We could create a separate file and put all blog entries in there, and that's the file that we update whenever we have a new blog post — good ol' indirection.

[
  "foo",
  "another-post",
]
Enter fullscreen mode Exit fullscreen mode

We'll have to change the way we register our route components a bit. We'll have to build those routes programmatically and make use of dynamic component registration using dynamic imports:

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import Blogs from './statics/blogs.json'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    { path: '/', name: 'home', component: Home },
    ...Blogs.map(entry => ({
      path: `/${entry}`,
      name: entry,
      component: () => import(`./markdowns/${entry}.md`)
    }))
  ]
})
Enter fullscreen mode Exit fullscreen mode

Notice in the above code, we are assigning the markdown filename to both path and name. And for better structure, we're placing all our markdowns inside it's own directory. In this setup, we'll never have to mess with router.js again whenever we add a new blog post. Now, the only thing left to do is to create actual links that will point to them.

Putting it all together

Using what we've learned, I've put things together and created a working demo on this link. You can also check out the code in this repository. There our some minor changes compared to our previous experiments.

First, let's look at the directory structure:

file structure

Notice that I've created subdirectories 2019 and stories which refers to different sections of the blog. This changes how we structure our routes a little but will greatly improve our overall UI.

Looking at statics/data/blogs.json:

{
  "2019": [
    {
      "id": "vue-markdown-blog",
      "date": "March 10, 2019",
      "title": "Creating a Simple Blog using Vue + Markdown",
      "description": "Simple and neat way to add a blogging feature to add on your website."
    }
  ],
  "stories": [
    {
      "id": "maud-sparrow",
      "date": "April 21, 2018",
      "title": "Maud Sparrow and the Four Wicked Horses",
      "description": "Once upon a time there was a generous girl called Maud Sparrow. She was on the way to see her Albert Connor, when she decided to take a short cut through Spittleton Woods..."
    },
    {
      "id": "nico-borbaki",
      "date": "May 5, 2018",
      "title": "Nefarious Nico Borbaki",
      "description": "Nico Borbaki looked at the enchanted newspaper in his hands and felt conflicted..."
    },
    {
      "id": "jack-butterscotch",
      "date": "June 10, 2018",
      "title": "Jack Butterscotch | The Dragon",
      "description": "In a hole there lived a tender, silver dragon named Jack Butterscotch. Not an enchanted red, stripy hole, filled with flamingos and a cold smell, nor yet a short, hairy, skinny hole with nothing in it to sit down on or to eat: it was a dragon-hole, and that means happiness..."
    },
    {
      "id": "tiny-arrow-wars",
      "date": "July 27, 2018",
      "title": "Galactic Tiny Arrow Wars",
      "description": "A long, long time ago in a tiny, tiny galaxy..."
    },
    {
      "id": "gargoyle-club",
      "date": "August 7, 2018",
      "title": "Club of Gargoyle",
      "description": "Molly Thornhill suspected something was a little off when her creepy daddy tried to club her when she was just six years old. Nevertheless, she lived a relatively normal life among other humans."
    },
    {
      "id": "simon-plumb",
      "date": "September 20, 2018",
      "title": "Simon Plumb and the Two Kind Gerbils",
      "description": "Once upon a time there was a virtuous boy called Simon Plumb. He was on the way to see his Annie Superhalk, when he decided to take a short cut through Thetford Forest..."
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Instead of an array of strings, I've converted it to an object. Each key refers to the blog section which also matches its subdirectory. The object array under each key refers to the actual blog entry. There are bunch of additional properties like date that we'll use in the UI but the important bit is the id which refers to the actual markdown component.

(Also, you can put everything in a js file instead of json. It's a matter of personal preference. Putting it in a js file will probably make your production build much smaller after webpack does it thing. But saving it in a json file under statics will act as a simple REST endpoint where I can issue GET requests from, which is useful if I end up integrating it to some other UI in the future.)

I've implemented all those additional changes to be able to display a UI like this:

home

The last thing we need to do is to adjust router.js. What it does is basically just map those blog sections into route objects. Entries under each section becomes the child of their respective route object. Basically, we'll be able to visit our blog entries using paths in this format: ${section}/${blog entry}, eg: 2019/vue-markdown-blog.

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

import BlogEntries from './statics/data/blogs.json';

const blogRoutes = Object.keys(BlogEntries).map(section => {
  const children = BlogEntries[section].map(child => ({
    path: child.id,
    name: child.id,
    component: () => import(`./markdowns/${section}/${child.id}.md`)
  }))
  return {
    path: `/${section}`,
    name: section,
    component: () => import('./views/Blog.vue'),
    children
  }
})

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    ...blogRoutes
  ]
})
Enter fullscreen mode Exit fullscreen mode

This setup introduces another key improvement to our blog: messing with the styling. Notice that Blog.vue acts as a wrapper layout component of our blog entries. It contains the following code:

<template>
  <div class="blog">
    <router-view />
    <router-link to="/" tag="a" class="back">&laquo; Back</router-link>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Styling

We now have the opportunity to style the rendered markdown however we like. The key here is to target what will eventually be displayed inside <router-view /> using a /deep/ selector. See the following:

<template>
  <div class="blog">
    <router-view />
    <router-link to="/" tag="a" class="back">&laquo; Back</router-link>
  </div>
</template>
<style lang="scss" scoped>
.blog {
  max-width: 50vw;
  margin: 10rem auto;
  /deep/ {
    h1 {
      font-size: 3rem;
      margin-bottom: .2rem;
      color: #42b883;
    }
    h4 {
      margin-bottom: 3rem;
      color: #35495e;
    }
  }
}
</style>
Enter fullscreen mode Exit fullscreen mode

Check out markedstyle.com for more markdown styling ideas. You can even import an external markdown style just don't forget to wrap it properly within a .blog /deep/ selector. ie:

/** external style **/
.blog /deep/ {
 /** paste external markdown style here **/
}
Enter fullscreen mode Exit fullscreen mode

Bit of a warning though, as much as possible one should avoid using /deep/ because it somehow defeats the purpose of scoped and it's been deprecated by Chrome. Vue-loader just still supports it, that's why it works. But I'd argue that this is one those cases where it's useful. We don't want to pollute the global css scope so we'd like to contain styling within Blog.vue's children so we use scoped and /deep/ together. (Although, if someone more knowledgeable has a better solution for this I'll gladly take it.)

And that's it! A simple blogging feature for your website without using any other 3rd-party service. Just plain and simple vue.

Further Improvements

If you want to take it a step further, you can add page metas using all those additional fields inside blog entries, date, title, description, etc. This would play nicely if you've implemented some sort of social sharing as well. You can check out my website to see this in action: www.josephharveyangeles.com

Useful Links

Top comments (32)

Collapse
 
peter profile image
Peter Kim Frank

Great article and I love the writing style throughout.

Not to mention all those bundlers, builders, like webpack, gulp, grunt and other weird noises that adds to the overall complexitiy of learning modern web frameworks.

😆

Collapse
 
matt123miller profile image
Matt Miller

I know I'm late to the party but thank you so much for this! This is exactly what I was looking to implement and you've given me a skeleton to follow and adapt. This is the last hurdle to me actually deploying my site for real instead of procrastinating for the last year. My own site is using Nuxt but it shouldn't be that difficult to incorporate :) Here it is for reference matt123miller-site.herokuapp.com/

Collapse
 
vycoder profile image
yev

That's cool! I never really thought that this could really help help in any way!

Collapse
 
jamesdordoy profile image
James Dordoy

Thanks for this, using your implementation i created a simple SPA with markup rendering for code examples. Can be viewed here: jamesdordoy.github.io/laravel-vue-...

I made use of this component for markdown styling also github.com/metachris/vue-highlightjs. You can just wrap the markdown components and github styling will be applied.

Great article, thanks for making it.

Collapse
 
misterlinux profile image
Misterlinux • Edited

Hello, I was trying the blog and I wanted to
add some different data from a different.js file into the
.vue file but I get the
"[Vue warn]: Property or method " " is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option or for class-based components, by initializing the property."

do I need to create a new file or change the basic structure of a Vue instance?

Collapse
 
vycoder profile image
yev

Hi, I'm not sure I follow. Can you paste a code snippet?

Collapse
 
misterlinux profile image
Misterlinux

Hello, sorry for the late response, after studying Vue I resolved the problem and I created a simple project in which I wanted to integrate the blog structure, here the link:

github.com/Misterlinux/quasi-blog

I will use the route structure of the route.js file into my project but I would like to separate and design different sections and not have them all in the same column.

Collapse
 
malgamves profile image
Daniel Madalitso Phiri

Hey awesome post! Did you have a routing problem when you deployed your site?

Collapse
 
vycoder profile image
yev

Hey, thanks! Nothing comes to mind. What sort of routing problem are you referring? I did encounter routing problems whenever I construct the route objects wrong. But I can't remember something deployment-specific.

Collapse
 
malgamves profile image
Daniel Madalitso Phiri

I had to setup the _redirects file to let me open up the blog from a main page. For some reason, when I click to open a markdown file using router.push my console tells me the file doesn't exist so I'm guessing Vue did not add it to dist after running npm build. Even though I did add it to the public and src folders to test it

Thread Thread
 
vycoder profile image
yev

Ahh I see. I'm not entirely sure if I understand what happened in your case correctly. But my first guess is that it's really just a consequence of routing in SPA when not using hash mode. I don't particularly like this mode that's why my demo repo was set to history mode due to habit.

More info about this at vue-router docs: Server Configurations

Collapse
 
tobiassn profile image
Tobias SN • Edited

This is certainly different to how I would do it. You might wanna consider checking out Vuepress. But this is certainly a good approach for those who don’t like using packages with too many dependencies, or just want full control.

Collapse
 
vycoder profile image
yev • Edited

Thanks for the response. You're right! I know and have tried Vuepress before and it's really cool. But this article is intended for those people who wants to build a really simple and easy blog without any third-party libraries in Vue. I might not have stressed that enough with the article.

Collapse
 
anandbaraik profile image
Anand-Baraik

What tech stacks did you use for the portfolio. It looks awesome.

Collapse
 
vycoder profile image
yev

Thanks! I'm not a very frontend-y guy probably way overkill. But it's just quasar and vue and some other stuff. It's on github if you're curious and want to look around: github.com/josephharveyangeles/por...

Collapse
 
anandbaraik profile image
Anand-Baraik

Thank you, I'll definitely look into it for ideas.

Collapse
 
prashantnirgun profile image
Prashant Nirgun

Thanks I was frustrated with Mediawiki updates and I try to move it to some other domain it get failed. I was searching for .md file CMS. My search will end over here. I can style and add tags functionality and it will be done for me.

Collapse
 
dimensi0n profile image
Erwan ROUSSEL

Nice post 💪

Collapse
 
vycoder profile image
yev

Thanks! Glad you appreciate it!

Collapse
 
andreasvirkus profile image
ajv • Edited

Are you considering moving your current setup to Vuepress (version 1 is now out), since it has various plugins like sitemap & rss feed generation, PWA-capability baked in, etc?

Great write-up btw!

Collapse
 
vycoder profile image
yev

Hi, thanks for the response!

I haven't checked Vuepress again but maybe in the future I will, if things started difficult to manage. But right now, my website is pretty simple and straightforward, I still doesn't feel the need to add more stuff. I don't have that much audience anyway (aside from myself lol). While PWA and rss feed generation are cool and something I had considered before, I figured it's better to focus on at least putting out consistent blog entries first.

Collapse
 
josiasaurel profile image
Josias Aurel

Thanks for this great article.

Collapse
 
rojadesign profile image
Rush

A little bit late, but I really appreciate your effort on this. I used your approach and I'm very happy about the results. Thank you!

Collapse
 
rattanakchea profile image
Rattanak Chea

Well written. Is it possible to create pagination with this setup?

Collapse
 
vycoder profile image
yev • Edited

Hi, It is possible. Simplest way I can think of is to just name your markdowns like: blog-post-2.md and just add some indicator on blogs.json that a post has more than 1 page so you could display necessary buttons alongside back. Basically, you render blog-post.md but pressing and will just concatenate the page number.

Although, if things start to get more demanding. Might want to invest on using ButterCMS and the like.

Collapse
 
mgh87 profile image
Martin Huter

Really nice blog you created there!

Collapse
 
vycoder profile image
yev

Thanks!

Collapse
 
manavsmo profile image
manavpanchal

Great post. I will be implementing many of these tips mentioned. Thanks for the share. VueJS Development