# How I Built this Blog on VuePress

I previously wrote about why I started building this site on VuePress. As mentioned, it will end up being much more than just a blog.

But we have to start somewhere, so I figured I'd walk you through each step while I attempt to turn VuePress into a personal knowledge management system.

The first requirement was to be able to create public articles (aka a blog) with a static site generator.

✅ Done.

Let's explore how I did it (and explain why I made certain decisions along the way).

Table of Contents

# Install VuePress

VuePress (opens new window) was designed to support the desire to prop up technical documentation quickly and easily. It does this very well. Thankfully, the community that sprouted up around it started extending it and we now have more capabilities than what it was originally intended for.

As of this writing, VuePress is on v1.5.0, and following the Getting Started (opens new window) page is the way to go.

NOTE: This article will likely become outdated at some point, but this is what I did today. The intention here is not to create evergreen content, but to explain what I did and why I did it.

Without further ado, jump in and install VuePress. I did so globally using yarn.

# install globally
yarn global add vuepress

# create the project folder
mkdir vuepress-starter && cd vuepress-starter

# create a markdown file
echo '# Hello VuePress' > README.md

# build the site
vuepress build

I skipped the vuepress dev line the documentation mentions so I can explain it a bit.

While, vuepress build does build the file structure and site here, I'm not going to assume you are running a webserver. You may already have one that works well.

I use Laravel Valet (opens new window) as my web server, which I love. But I quickly found that using vuepress dev to run the webserver has a few advantages:

  1. Using Valet, I spent time setting up a symbolic link to the internal directory structure of VuePress to show the domain and ensure all the relative assets load properly. Meh. 👎
  2. vuepress dev gives you a "hot reloading" site out of the box while we're writing. I don't get that benefit unless I spend a bunch more time building it into webpack. Nah. 👎

To flip this to a positive frame of reference, if we use vuepress dev we get hot reloading while we're writing and a preset local domain where assets don't need to be configured to show properly. It just works beautifully.

That said, let's not run vuepress dev quite yet. We've got a few other things to setup.

# Add plugin-blog to package.json

I'll readily acknowledge here that there is a shortcut to the blog setup if you just include the theme-blog (opens new window) package here. I did try that, but really hated that the theme departed from the default theme's minimalism and cleanliness. So, I opted not to remove that theme and just use the plugin-blog (opens new window) package for it's feature set.

Simply running yarn add -D @vuepress/plugin-blog at the root changes package.json to something like this:

  "devDependencies": {
    "@vuepress/plugin-blog": "^1.9.2",
    "vuepress": "^1.5.0"
  "scripts": {
    "dev": "vuepress dev site",
    "build": "vuepress build site"

While I was messing with the theme-blog plugin I added the scripts object to my package.json as well, which allowed me to use yarn dev and yarn build instead of vuepress dev and vuepress build commands and separated the blog from the rest of the site. So, I ended up adding a /site directory off the root and I kinda like it like that.

I note this /site directory for you because we build a bit more inside that directory to be able to hold the article files and assets later.

# Edit the /site/.vuepress/config.js file

Early on I struggled a little to nail down the format of this file because config.js is actually a plugin and some of the resources and tutorials out there were using an outdated convention.

That said, here's what I ended up with after a little editing ....

module.exports = {
    title: 'Nate Ritter',
    description: 'Nate Ritter is a CTO and technical entrepreneur.',

    plugins: [
        ['@vuepress/blog', {
            directories: [{
                id: 'article',
                dirname: '_articles',
                itemPermalink: '/articles/:slug',
                pagination: {
                    lengthPerPage: 2,
            sitemap: {
                hostname: 'https://nateritter.com'

    themeConfig: {
        author: 'Nate Ritter',
        nav: [
            { text: 'Home', link: '/' },
            { text: 'Articles', link: '/articles/' },

Let's go through each part as there are some decisions and changes I explicitly made here.

title: 'Nate Ritter',
description: 'Nate Ritter is a CTO and technical entrepreneur.',

These lines set a default HTML meta title and description tag. They are helpful to set for search engine optimizaiton reasons.

plugins: [
    ['@vuepress/blog', {
        directories: [{
            id: 'article',
            dirname: '_articles',
            itemPermalink: '/articles/:slug',
            pagination: {
                lengthPerPage: 2,
        sitemap: {
            hostname: 'https://nateritter.com'

This section allows us to configure some of the features in the blog plugin.

I decided to change the post directory name to article here because I'm actively trying to reduce the technical jargon I use. This may sound funny since blogs have been around since 1994. But people to this day say they are writing a "blog" when they mean they are writing a "post" for their "blog" (a collection of "posts").

Ugh. Simply writing that sentance makes me tired.

For simplicity I decided to just use a word that everyone understands properly.

To ensure the convention of the file structure and links are upheld, we need to of course modify the other mentions of the plural word as well.

I am also not the biggest fan of the inherent push to sort writings by the date they are published. Other than a couple articles like this one, I'd like to focus my attention on evergreen, long-lasting ideas. Therefore I should optimize for the content, not the published date.

Also, I can't imagine I'll be writing the exact same article title even years from now. So there should be no conflict with slugs.

That's why you see itemPermalink: '/articles/:slug', instead of itemPermalink: '/posts/:year/:month/:day/:slug',

Finally, again for SEO purposes I'd like to have a sitemap auto-generated. Thankfully we get that out of the box with this blog plugin, so I include that setting here.

# Create the articles directory structure

Since I decided to rant on about the post vs article syntax I needed to change config.js to support the obviously better of the two words (😉).

The additional requirement of this change is to ensure we have the right directory structure too. Ergo, we add the /site/_articles directory structure.

# Add README.md files to act as an index page

Since we added a /site directory, we need to ensure there's an index HTML file to be read. This part is a little funky.

The convention is mirroring a Github repo in that VuePress is looking for README.md files to use instead of index.md. I tried using index.md and it worked some of the time but not always. I couldn't figure out when it would work vs not work, so I opted to go back to the convention of using README.md.

So, we add a /site/README.md for the index page.

One thing to note here is that I wanted more options for my layout and knew Markdown wouldn't get me there. A quick peek under the hood of the VueJS and VuePress README.md files and you can quickly see there's some support for regular 'ol HTML intermingled into the Markdown.

I tried it and it worked well. Here's a snippet of my /site/README.md file.

title: Web Chef, CTO...
    Nate Ritter is a CTO and technical entrepreneur...

    Yes, it supports custom CSS too...
    so I put a bunch of that here.
<svg xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 40 40" display="none" width="0"
    It even support SVG files, which I use
    for icons... snipped for brevity

<div style="text-align:center; margin-top: 50px;">
    <img src="/assets/img/nate-ritter-avatar.webp"
        alt="Nate Ritter">

# 👋 Hi there, nice to meet you.

I'm Nate Ritter and I use internet tech to help
people be awesome, turning visions into reality.
Feel free to get in touch at one of these places

One other hiccup I ran into was some routing problems with the blog plugin. Specifically, the configuration is supposed to allow me to create an index/README file for the list of articles. Each convention I tried was either randomly or consistently didn't render. No errors or anything. It just wouldn't render properly. Even stopping yarn dev, running yarn build, and then restarting yarn dev again didn't fix the issue.

I decided instead to overwrite that configuration with a supported convention - page creation.

Adding a directory at /site/articles and then adding a README.md file inside that directory overwrites the blog plugin configuration. I suggest going this direction.

Now we can treat that /site/articles/README.md page as an article index and program it accordingly.

article_index: true
title: Articles


# Articles

Once in a while I write things. I am going
to start writing more things more frequently
and posting them here. I hope they are
interesting, or better yet helpful, to you.

<ArticleIndex />

☝️ Ah! what's that <ArticleIndex /> bit!?

Well, that's a component. We'll talk about that next.

# Write your first component: ArticleIndex

Just to get used to this system a little more, I figured I ought to try out a little modular programming.I'll need some of this knowledge later when I can't depend on a plugin to do all the heavy lifting for me.

First, add a /site/.vuepress/components directory if one doesn't already exist.

Next, add a ArticleIndex.vue file inside that directory.

The contents of my ArticleIndex.vue file look like this:

        <div v-for="article in articles">
                <router-link :to="article.path">
                    {{ article.frontmatter.title }}
            <p>{{ article.frontmatter.description }}</p>

export default {
    computed: {
        articles() {
            return this.$site.pages
                .filter(x => x.path.startsWith('/articles/') && !x.frontmatter.article_index)
                .sort((a, b) => new Date(b.frontmatter.date) - new Date(a.frontmatter.date));

Let's walk through this a little at a time...

One of the nice things about VueJS components is that all the code for the component can be built into one file. There are other ways to do it, but this method is nice when you have small files already and there won't be any overlap in functionality with other components. For instance, I likely won't have a scenario in the future that requires me to use the layout for articles but with different content which uses the exact same structure as the article structure.

So, we put it all together into one file to make this easy to work with.

The <template> tag is required for a VueJS component as well as a single HTML tag inside that. Not two tags. Thou shalt not go to three. Four is right out. Thou shall count only to one. Hence the single <div> inside the <template>.

What we want to end up with, of course, is a list of article titles and descriptions. That's the next section which includes the VueJS syntax using v-for for the loop syntax.

Using the router-link, we bind the path of the article to the <a> tag that will be produced, and the anchor text inside the tag uses template language to render the article's title.

Note here that it's not showing article.title. It's article.frontmatter.title.

frontmatter (opens new window), according to the documentation, is that YAML block at the top of the pages I've been showing you, processed by something called grey-matter (opens new window), which is included in VuePress and used in the build process.

The great thing about frontmatter is you can set variables like titles, descriptions, or whatever you'd like in that YAML block. Those variables then become available to use in the rest of the page and in any components, like the one we're working on now.

Checking out the docs for frontmatter (opens new window) again, we can see a couple predefined options, and even a few more (opens new window) if we stick with the default theme.

Voila, title and description are part of the predefined list. That's why we can use them in the HTML code above.

Finally, the JavaScript portion of the component...

In VueJS we have the ability to create "computed" properties, which are essentially variables which can change at any point in time, and we compute them in real-time. So, articles becomes a useful variable on the fly that we can compute when the component is rendered.

The short version of what's going on here is we're just looking at all the site's pages, only looking for ones with the route starting with /articles/, filtering out those which have the frontmatter variable called article_index where it's set to true, and then sorting what's left by date in reverse chronological order.

Yes, I know I raged against the chronological sorting above. I don't mind it for users who come to the site just browsing around. I do mind it being in the URL because if I'm focused on writing evergreen content, nobody should care when it was written.

That means adding article_index: true to our /site/articles/README.md file will ensure that file is filtered out of the list of articles, even though the route it ends up with would otherwise include it. It will also only grab articles because that's all I should be putting inside the /site/.vuepress/_articles directory.

# Write

Ok, now we're at the good stuff.

Creating a new article is as easy as creating a new file. Of course, there's a convention that still has to be followed to name that file: YYYY-MM-DD-slug.md. For example, the name of this file is 2020-05-18-how-i-built-this-blog-on-vuepress.md.

You'll notice the .md at the end, which is pretty cool because we just simply have to write using the Markdown syntax (opens new window), which is fast and usually does everything you'd want it to do in an article.

If you need more, I actually recommend reading the theme-blog documentation (opens new window) more than the plugin-blog docs for all the options you can use, as they seem a bit more comprehensive. I'll just give a short example of what I'm doing here:

title: How I Built this Blog on VuePress
date: 2020-05-18
    How I built this blog on top of VuePress, step by step,
    including why I made certain adaptations along the way
    and the hiccups I ran into.


# How I Built this Blog on VuePress

I previously wrote about why I started building this site on
VuePress. As mentioned, it will end up being much more than
just a blog.

# Add some images to your articles

This article is getting super long now. To break it up, I'm adding some imagery here and there, like the below screenshot of the yarn build process and results.

To do that, you'll want to add a new directory at /site/.vuepress/_articles/img. Anything you want to include in an article using the typical Markdown syntax should go in there. An example might be something like this ...

![yarn build](./img/yarn-build.webp)

The "yarn build" part of the above becomes the alt attribute text to the image, and the URL specified is a relative one.

But let's say we want to use an asset somewhere else?

For instance, I wanted to put some headers into each page that specifies the URL to the favicons.

I ended up using a lot of trial and error to get favicons and other items added to the <head> tag.

In my config.js I ended up adding the following just below the description property:


head: [
    ['link', {
        rel: "apple-touch-icon",
        sizes: "180x180",
        href: "/assets/img/favicons/apple-touch-icon.png"
    ['link', {
        rel: "icon",
        type: "image/png",
        sizes: "32x32",
        href: "/assets/img/favicons/favicon-32x32.png"
    ['link', {
        rel: "icon",
        type: "image/png",
        sizes: "16x16",
        href: "/assets/img/favicons/favicon-16x16.png"
    ['link', {
        rel: "shortcut icon",
        href: "/assets/img/favicons/favicon.ico"
    ['meta', {
        name: "theme-color",
        content: "#ffffff"


You'll notice it's location includes /assets/img/favicons/. But the VuePress convention is to look for assets like this inside a public folder.

To support this, add the following directory structure: /site/.vuepress/public/assets/img/favicons/.

Then add the favicon images to that new directory. When all is complete, you'll need to stop the yarn dev process and restart it so it picks up the config.js changes.

Finally, confirm it's working by running yarn build and then checking /site/.vuepress/dist/assets/img/favicons/ directory exists and contains the images. More on compiling like this is below.

# Compile static site files and move assets

Even though running yarn dev starts a local webserver with hot reloading so you can see the content you are creating on the fly, that doesn't mean it's production ready.

To get it ready for deployment, we need to run yarn build. Remember, this command works because we put the scripts object inside our package.json file. Without that you'd have to run vuepress build, or vuepress build site if you added the site directory.

yarn build

When this process is done, I now have contents inside /site/.vuepress/dist/. If you look in there, it'll look like a nicely organized set of assets and HTML pages, including a couple directories relating to articles.

# Deploy the site to production

This part will likely be pretty subjective as you may want to use Netlify (opens new window) or Github (opens new window) as your host for cost or speed reasons. For that reason, I'll just give you the overview of my setup.

I have a Digital Ocean (opens new window) server already setup for this domain (among other sites), so I decided to recycle it.

Since I use Laravel for most of my projects, I use Forge (opens new window) for server management and Envoyer (opens new window) for no-downtime deployments.

These systems are already setup, so I just added a project to Envoyer and configured it to deploy the static site to the proper directory on the server whenever I push the repo.

In my case, this meant heading to Forge, navigating to the specific Site, and then Meta.

I edited the Web Directory, changing it to /current/site/.vuepress/dist.

While I waited for the server to update the web directory, I headed over to Envoyer and made sure part of the Deployment Hooks after "Install Composer Dependencies" included commands to run the yarn commands to build the site.

NOTE: In my case here, I needed to update the nodejs version on my server since it was a bit outdated and VuePress requires >= v8.6.

I initiated the deployment via Envoyer, made sure there were no new errors and dropped some whiskey into the glass, getting prepped for a celebration 🎉.

Boom 💥 Enjoy 😎

If you're curious about the things I write about here, you should definitely join my newsletter. It's an occassional email with interesting things I've read or found, plus new articles.

Nate Ritter

Last Updated: 8/3/2020, 10:53:44 PM