Creating a statically-generated blog with Vuepress

In this post, I am going to illustrate how to use Vuepress to quickly stand up a blog.

I recommend you follow along with the post to set up your own version of a VuePress blog. The codebase is available under the MIT license here https://github.com/jameswpierce/vuepress-blog-demo.

Initialize project

Note: Vuepress recommends Yarn as its package manager.

# create directory
mkdir blog && cd blog

# initialize new git repository
git init

# ignore node dependencies
echo 'node_modules' > .gitignore

# dummy front page
echo '# Hello' > index.md

# install vuepress to project
yarn add -D vuepress

# get this in version control!
git add .
git commit -m 'Initial commit'

# run vuepress dev server
yarn run vuepress dev

Nice work! You now have a hot-reloading development server available at http://localhost:8080. It doesn't do much yet, but we have a nice environment set up to quickly begin iterating on our site.

If you are familiar with Markdown, feel free to add a little spice to your home page. Otherwise, here's a quick cheatsheet to get you started.

Automatic posts

We could create a posts directory with an index.md that we update manually with every new blog post, but it would be a lot nicer if we had a posts index that updated automatically. For that, we are going to use the officially supported blog plugin for VuePress: @vuepress/plugin-blog.

# install vuepress blog plugin
yarn add -D @vuepress/plugin-blog

# create .vuepress directory to override defaults
mkdir .vuepress

# init custom theme config file
touch .vuepress/config.js

Now we need to add the plugin to our site's configuration. Open .vuepress/config.js and add this code:

// .vuepress/config.js

module.exports = {
  plugins: ["@vuepress/blog"]
};
# version controllll
git add .
git commit -m 'install @vuepress/plugin-blog'

Now that the plugin is installed, we need to set up our posts directory and configure the plugin to use it.

# create posts directory
mkdir _posts

# first post!
echo '# Welcome 2 the Future' > _posts/my-first-blog-post.md

and update .vuepress/config.js to look like this:

// .vuepress/config.js
module.exports = {
  plugins: [
    [
      "@vuepress/blog",
      {
        directories: [
          {
            id: "post",
            dirname: "_posts",
            path: "/posts/",
            itemPermalink: "/posts/:slug"
          }
        ]
      }
    ]
  ]
};

You may need to restart your development server to see these changes take effect. After that, you should be able to see a post with the URL: http://localhost:8080/posts/my-first-blog-post/.

Commit your changes to version control, and let's proceed.

Creating the blog index layout

You may have noticed that the index page at http://localhost:8080/posts/ is blank. That is because @vuepress/plugin-blog is looking for a layout component called IndexPost in our project. Because that layout is not there, it falls back to the default Layout, which does not have any idea how to handle any data about the files in the _posts directory, thus rendering a blank page.

# create directory structure for theme and layout
mkdir -p .vuepress/theme/layouts

Next we need to move .vuepress/config.js to the theme folder and change it's name to index.js

mv .vuepress/config.js .vuepress/theme/index.js

Then we need to add a line to let Vuepress know that which theme to inherit defaults from.

// .vuepress/theme/index.js
module.exports = {
  extend: "@vuepress/theme-default", // add this line!
  plugins: [
    [...]
  ]
};

Now lets add a link in the navigation of our site to the /posts page (and throw in one back to the home page as well). Create a new file at .vuepress/config.js and add the code below.

// .vuepress/config.js
module.exports = {
  themeConfig: {
    nav: [{ text: "Home", link: "/" }, { text: "Blog", link: "/posts/" }]
  }
};

Create a new file IndexPost.vue in .vuepress/theme/layouts

# initialize layout
touch .vuepress/theme/layouts/IndexPost.vue

In IndexPost.vue the Vuepress blog plugin exposes a variable $pagination, that gives the layout access to information about the posts contained in the _posts directory. We can use it to generate a simple (and paginated) index like so:

// .vuepress/theme/layouts/IndexPost.vue

<template>
  <div>
    <ul id="posts-list">
      <li v-for="post in $pagination.pages">
        <router-link class="post-link" :to="post.path">
          
        </router-link>
      </li>
    </ul>
    <div id="pagination">
      <router-link v-if="$pagination.hasPrev" :to="$pagination.prevLink"
        >Prev</router-link
      >
      <router-link v-if="$pagination.hasNext" :to="$pagination.nextLink"
        >Next</router-link
      >
    </div>
  </div>
</template>

Inheriting from the parent theme

The posts index is working as advertised, but it isn't rendering inside of the default layout of our theme. Vuepress gives our components access to @parent-theme in our custom layouts, which will allow this page to inherit all the functionality included in the other pages.

In the future this will work...

As of this writing (10/23/2019, version 1.2.0 of vuepress), there is a bug in the default layout that keeps us from using Vue's slots correctly. There is a fix on the master branch, which should be released soon.

// .vuepress/theme/layouts/IndexPost.vue

<template>
  <Layout>
    <template #page-top>
      <div class="theme-default-content content__default">
        <ul id="posts-list">
          <li v-for="post in $pagination.pages">
            <router-link class="post-link" :to="post.path"></router-link>
          </li>
        </ul>
        <div id="pagination">
          <router-link v-if="$pagination.hasPrev" :to="$pagination.prevLink"
            >Prev</router-link
          >
          <router-link v-if="$pagination.hasNext" :to="$pagination.nextLink"
            >Next</router-link
          >
        </div>
      </div>
    </template>
  </Layout>
</template>
<script>
import Layout from "@parent-theme/layouts/Layout.vue";

export default {
  components: {
    Layout
  }
};
</script>

A temporary workaround!

Until then, copy this code into .vuepress/theme/layouts/Layout.vue and use the version of IndexPost.vue below as well. Don't concern yourself too much with this code unless you are feeling particularly adventurous.

// .vuepress/theme/layouts/Layout.vue

<template>
  <div
    class="theme-container"
    :class="pageClasses"
    @touchstart="onTouchStart"
    @touchend="onTouchEnd"
  >
    <Navbar v-if="shouldShowNavbar" @toggle-sidebar="toggleSidebar" />

    <div class="sidebar-mask" @click="toggleSidebar(false)"></div>

    <Sidebar :items="sidebarItems" @toggle-sidebar="toggleSidebar">
      <template #top>
        <slot name="sidebar-top" />
      </template>
      <template #bottom>
        <slot name="sidebar-bottom" />
      </template>
    </Sidebar>

    <Home v-if="$page.frontmatter.home" />

    <Page v-else :sidebar-items="sidebarItems">
      <template #top>
        <slot name="page-top" />
      </template>
      <template #bottom>
        <slot name="page-bottom" />
      </template>
    </Page>
  </div>
</template>

<script>
import Home from "@parent-theme/components/Home.vue";
import Navbar from "@parent-theme/components/Navbar.vue";
import Page from "@parent-theme/components/Page.vue";
import Sidebar from "@parent-theme/components/Sidebar.vue";
import { resolveSidebarItems } from "@parent-theme/util";
export default {
  components: { Home, Page, Sidebar, Navbar },
  data() {
    return {
      isSidebarOpen: false
    };
  },
  computed: {
    shouldShowNavbar() {
      const { themeConfig } = this.$site;
      const { frontmatter } = this.$page;
      if (frontmatter.navbar === false || themeConfig.navbar === false) {
        return false;
      }
      return (
        this.$title ||
        themeConfig.logo ||
        themeConfig.repo ||
        themeConfig.nav ||
        this.$themeLocaleConfig.nav
      );
    },
    shouldShowSidebar() {
      const { frontmatter } = this.$page;
      return (
        !frontmatter.home &&
        frontmatter.sidebar !== false &&
        this.sidebarItems.length
      );
    },
    sidebarItems() {
      return resolveSidebarItems(
        this.$page,
        this.$page.regularPath,
        this.$site,
        this.$localePath
      );
    },
    pageClasses() {
      const userPageClass = this.$page.frontmatter.pageClass;
      return [
        {
          "no-navbar": !this.shouldShowNavbar,
          "sidebar-open": this.isSidebarOpen,
          "no-sidebar": !this.shouldShowSidebar
        },
        userPageClass
      ];
    }
  },
  mounted() {
    this.$router.afterEach(() => {
      this.isSidebarOpen = false;
    });
  },
  methods: {
    toggleSidebar(to) {
      this.isSidebarOpen = typeof to === "boolean" ? to : !this.isSidebarOpen;
      this.$emit("toggle-sidebar", this.isSidebarOpen);
    },
    // side swipe
    onTouchStart(e) {
      this.touchStart = {
        x: e.changedTouches[0].clientX,
        y: e.changedTouches[0].clientY
      };
    },
    onTouchEnd(e) {
      const dx = e.changedTouches[0].clientX - this.touchStart.x;
      const dy = e.changedTouches[0].clientY - this.touchStart.y;
      if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40) {
        if (dx > 0 && this.touchStart.x <= 80) {
          this.toggleSidebar(true);
        } else {
          this.toggleSidebar(false);
        }
      }
    }
  }
};
</script>

Finally, IndexPost.vue!

// .vuepress/theme/layouts/IndexPost.vue

<template>
  <Layout>
    <template #page-top>
      <div class="theme-default-content content__default">
        <ul id="posts-list">
          <li v-for="post in $pagination.pages">
            <router-link class="post-link" :to="post.path"></router-link>
          </li>
        </ul>
        <div id="pagination">
          <router-link v-if="$pagination.hasPrev" :to="$pagination.prevLink"
            >Prev</router-link
          >
          <router-link v-if="$pagination.hasNext" :to="$pagination.nextLink"
            >Next</router-link
          >
        </div>
      </div>
    </template>
  </Layout>
</template>
<script>
import Layout from "@theme/layouts/Layout.vue";

export default {
  components: {
    Layout
  }
};
</script>

Annnnnnd it works! Commit this sucker to version control. You've got yourself a basic blog that builds to an ultra-fast static website, that you can easily deploy for free.

I will tackle deployment in an upcoming post. Until then:

Dig Deeper

READ: VuePress

READ: @vuepress/plugin-blog

BONUS POINTS: ZEIT

READ: Markdown

CHEAT: Markdown Cheatsheet

Thanks for reading!