The quest for the perfect blogging system

With Vue.js and Markdown

I’ve been looking around and have yet to find what would be the perfect system for the next iteration of this blog.
Nonetheless, I’ve found something very close to it.
It’s called Gridsome.

I’ve enjoyed using Hugo for blogging, but I’d rather be able to leverage my skills in Vue.js to have a better and more interactive blogging experience, than fighting with a templating language I hardly use.

This tutorial will guide you on how to setup a new Gridsome project that allows you to write your templates in Pug, Sass and have a simple Vuetify theme ready for your content.

Writing a blog with Vue.js, Markdown and GraphQL by my side is just a dream come true.

Prerequisites

Install Node.js and Yarn

Install Gridsome

yarn global add @gridsome/cli

A new project

Now, let’s generate a new project:

gridsome create my-site

Start the dev server:

cd my-site
gridsome develop

Visit http://localhost:8080 in your browser and you should see:

My site

Notice that there is also another URL in the terminal.
Visit http://localhost:8080/___explore and you’ll see:

GraphQL playground

Now, copy this inside the left pane and run the query:

query {
  allPage {
    id
    type
    title
    slug
    path
    component
    pageQuery {
      type
      content
      options
    }
    content
  }
}

On the right hand, you’ll see something like:

{
  "data": {
    "allPage": [
      {
        "id": "2",
        "type": "page",
        "title": "be19209865d1080b24679f9f34f803f3",
        "slug": "index",
        "path": "/",
        "component": "~/pages/Index.vue",
        "content": null
      },
      {
        "id": "1",
        "type": "page",
        "title": "f965356798034b782b9ae752a852689d",
        "slug": "about",
        "path": "/about",
        "component": "~/pages/About.vue",
        "content": null
      }
    ]
  }
}

So as we can see, Gridsome comes with two pages in the form of two components:

Which you can visit on:

Initial configuration

Change the dev server command

The Vue CLI 3.0 project changed the development server command from yarn dev to yarn serve.
Nuxt.js is still using yarn dev.
Gridsome uses yarn develop.

I find it frustrating to mistype it everytime.
I like consistency, so the first thing I did was to edit my package.json file and change:

   "private": true,
   "scripts": {
     "build": "gridsome build",
-    "develop": "gridsome develop",
+    "serve": "gridsome develop",
     "explore": "gridsome explore"
   },
   "dependencies": {
     "gridsome": "^0.3.0"
   }
}

So now I can start my development server with:

yarn serve

Just like in any other Vue.js app I usually work with. Awesome!

Add Pug and Sass support

Install these project dependencies:

yarn add pug pug-plain-loader node-sass sass-loader --dev

Modify the gridsome.config.js file to look like this:

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('pug')
      .test(/\.pug$/)
      .use('pug-plain-loader')
        .loader('pug-plain-loader')
  }
}

Now you can turn src/pages/About.vue from this:

<template>
  <Layout>
    <h1>About us</h1>
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
  </Layout>
</template>

<script>
export default {}
</script>

Into this:

<template lang="pug">
layout
  h1 About us
  p Lorem ipsum dolor sit amet, consectetur adipisicing elit.
</template>


<script>
export default {}
</script>


<style lang="sass" scoped>
h1
  color: red
</style>

Visit http://localhost:8080/about and you’ll see:

Pug and Sass

Neat, right?

Add Markdown support

Install these project dependencies:

yarn add @gridsome/source-filesystem @gridsome/transformer-remark --dev

Modify gridsome.config.js to:

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('pug')
      .test(/\.pug$/)
      .use('pug-plain-loader')
        .loader('pug-plain-loader')
  },
  plugins: [
    {
      use: '@gridsome/source-filesystem',
      options: {
        path: 'blog/**/*.md',
        typeName: 'Post',
        route: '/blog/:slug'
      }
    }
  ]
}

This will look for any .md files in a blog directory located in your app’s root.

Now let’s create a template for the Post type.

Add src/templates/Post.vue with this content:

<template lang="pug">
layout
  div(v-html="$page.post.content")
</template>


<page-query>
query Post ($path: String!) {
  post: post (path: $path) {
    title
    content
  }
}
</page-query>


<script>
export default {
  metaInfo () {
    return {
      title: this.$page.post.title
    }
  }
}
</script>

Then, add a blog/first-post.md with this content:

---
slug: first-post
title: This is my first post!
---

# This is some first level title

* Bullet point #1
* Bullet point #2
* Bullet point #3

## This is a second level title

> This is a blockquote

If you don’t specify the slug in the frontmatter it’ll be automatically extracted from the title.

Visit http://localhost:8080/blog/first-post

First post

Curious about how the GraphQL query for your posts looks like?
Copy this in the playground:

query {
  allPost {
    totalCount
    edges {
      node {
        id
        title
        slug
        path
        content
        excerpt
        date
        headings {
          depth
          value
          anchor
        }
        timeToRead
      }
    }
  }
}

It’ll give back:

{
  "data": {
    "allPost": {
      "totalCount": 1,
      "edges": [
        {
          "node": {
            "id": "9f237d8cca1d091837cd0a93f2ab7e41",
            "title": "This is my first post!",
            "slug": "first-post",
            "path": "/blog/first-post",
            "content": "<h1 id=\"this-is-some-first-level-title\"><a href=\"#this-is-some-first-level-title\" aria-hidden=\"true\"><span class=\"icon icon-link\"></span></a>This is some first level title</h1>\n<ul>\n<li>Bullet point #1</li>\n<li>Bullet point #2</li>\n<li>Bullet point #3</li>\n</ul>\n<h2 id=\"this-is-a-second-level-title\"><a href=\"#this-is-a-second-level-title\" aria-hidden=\"true\"><span class=\"icon icon-link\"></span></a>This is a second level title</h2>\n<blockquote>\n<p>This is a blockquote</p>\n</blockquote>\n",
            "excerpt": "",
            "date": "2018-11-15T19:32:38-06:00",
            "headings": [
              {
                "depth": 1,
                "value": "This is some first level title",
                "anchor": ""
              },
              {
                "depth": 2,
                "value": "This is a second level title",
                "anchor": ""
              }
            ],
            "timeToRead": 1
          }
        }
      ]
    }
  }
}

Pretty cool!

Add Vuetify support

Let’s add Vuetify so we can write our layouts and templates using this awesome library components.

yarn add vuetify
yarn add webpack-node-externals --dev

Add a gridsome.server.js file with this content:

const nodeExternals = require('webpack-node-externals')

module.exports = function (api) {
  api.chainWebpack((config, { isServer }) => {
    if (isServer) {
      config.externals([
        nodeExternals({
          whitelist: [/^vuetify/]
        })
      ])
    }
  })
}

Change main.js from this:

import DefaultLayout from '~/layouts/Default.vue'

export default function (Vue) {
  Vue.component('Layout', DefaultLayout)
}

To this:

import Vuetify from 'vuetify'
import DefaultLayout from '~/layouts/Default.vue'

import 'vuetify/dist/vuetify.min.css'

export default function (Vue) {
  Vue.use(Vuetify)
  Vue.component('Layout', DefaultLayout)
}

Then, change src/layouts/Default.vue from this:

<template>
  <div class="layout">
    <header class="header">
      <strong>
        <g-link :to="{ name: 'home' }">Gridsome</g-link>
      </strong>
      <nav class="nav">
        <g-link class="nav__link" :to="{ name: 'home' }">Home</g-link>
        <g-link class="nav__link" :to="{ name: 'about' }">About</g-link>
      </nav>
    </header>
    <slot/>
  </div>
</template>

<style>
body {
  font-family: -apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  margin:0;
  padding:0;
  line-height: 1.5;
}

.layout {
  max-width: 600px;
  margin: 0 auto;
  padding-left: 20px;
  padding-right: 20px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  height: 80px;
}

.nav__link {
  margin-left: 20px;
}
</style>

To this:

<template lang="pug">
v-app
  v-toolbar
    v-toolbar-title Gridsome
    v-spacer
    v-toolbar-items
      v-btn(flat exact :to="{ name: 'home' }") Home
      v-btn(flat :to="{ name: 'about' }") About

  v-container
    slot
</template>

Visit http://localhost:8080 and you’ll see:

Vuetify

Beautiful!

Blogging

Let’s add a list of our blog posts to the home page.

Change src/pages/Index.vue to:

<template lang="pug">
layout
  v-img(alt="Example image" :src="imgUrl" width="135")
  h1 Hello, world!
  p Lorem ipsum dolor sit amet, consectetur adipisicing elit.

  .title.mb-3 Total posts: {{ totalCount }}

  v-list(two-line)
    v-list-tile(v-for="(post, index) in posts" :key="index" @click="onClick(post)")
      v-list-tile-content
        v-list-tile-title {{ post.node.title }}
        v-list-tile-sub-title {{ post.node.date }}
</template>


<page-query>
query {
  allPost {
    totalCount
    edges {
      node {
        id
        title
        slug
        path
        date
      }
    }
  }
}
</page-query>


<script>
export default {
  data () {
    return {
      imgUrl: require('@/favicon.png')
    }
  },
  computed: {
    posts () {
      return this.$page.allPost.edges
    },
    totalCount () {
      return this.$page.allPost.totalCount
    }
  },
  methods: {
    onClick (post) {
      this.$router.push({ path: post.node.path })
    }
  }
}
</script>

And with this, we have a very basic, but functional blogging system!

Blogging system

Troubleshooting

If you ever encounter this error when trying to start your development server:

$ yarn serve
yarn run v1.12.3
warning ../../package.json: No license field
$ gridsome develop
internal/modules/cjs/loader.js:736
  return process.dlopen(module, path.toNamespacedPath(filename));
                 ^

Error: libvips-cpp.so.42: cannot open shared object file: No such file or directory
    at Object.Module._extensions..node (internal/modules/cjs/loader.js:736:18)
    at Module.load (internal/modules/cjs/loader.js:605:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:544:12)
    at Function.Module._load (internal/modules/cjs/loader.js:536:3)
    at Module.require (internal/modules/cjs/loader.js:643:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at Object.<anonymous> (/home/yomero/tmp/my-site/node_modules/sharp/lib/constructor.js:10:15)
    at Module._compile (internal/modules/cjs/loader.js:707:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:718:10)
    at Module.load (internal/modules/cjs/loader.js:605:32)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Just rm -rf node_modules then yarn install and everything should be fine again.


— lt

Feedback & comments

Get in touch on Twitter

Or by good ol' email at adriandcs@gmail.com