In this tutorial, we’ll build a static site with VuePress and we’ll deploy it to GitHub Pages. The site will be a simple, technical blog which offers the possibility to organize and navigate the content by collections, categories, and tags.
The code for this tutorial can be found on GitHub. You can also view the site running live on GitHub Pages.
VuePress is a simple yet powerful static site generator, powered by Vue.js. It was created by Evan You (the creator of Vue), who made it to facilitate the writing of technical, documentation-heavy sites for the Vue ecosystem. But as we’ll see in this tutorial, with a little tweaking the use cases for VuePress can be broadened.
A site made with VuePress is served as a single page application (SPA), powered by Vue, Vue Router, and webpack as underlying technologies. As an SPA, VuePress uses a mixture of Markdown files and Vue templates/components to generate a static HTML site.
A static site has some significant benefits:
There’s an abundance of static site generators out there. So why might we choose VuePress over the others options?
Well, for me, these are the two strongest selling points of VuePress:
The above benefits are already strong enough, but let’s explore the full feature palette that makes VuePress shine:
In this tutorial, we’ll use the stable VuePress version, which is 0.14.8 at the time of writing. There is also a new version, with great new features, including ones for blogging, but it’s still in alpha stage and it’s not secure to be used yet.
Before we get started, you’ll need a recent version of Node.js installed on your system (version 8 or greater). If you don’t have Node installed, you can download the binaries for from the official website, or use a version manager. This is probably the easiest way, as it allows you to manage multiple versions of Node on the same machine.
Next, create a new directory vuepress-blog
and initialize a new Node project inside:
mkdir vuepress-blog && cd vuepress-blog
npm init -y
Next, install VuePress locally, as a dependency:
npm install -D vuepress
Finally, edit the scripts section of the package.json
file to include the dev
and build
commands:
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
},
Now we can run the dev server with the following command:
npm run docs:dev
If you’ve set up everything correctly, the site will build and be available at http://localhost:8080. Currently there isn’t a whole lot to see apart from a 404 message, but we’ll change that in the next sections.
Right now, our project is empty. But, in this section I want to give you a glimpse of the directory structure of the final project. Our site will be built inside a docs
directory. The docs directory will contain a blog
directory and a .vuepress
directory.
.
├── docs
│ ├── blog
│ ├── README.md
│ └── .vuepress
├── package.json
└── package-lock.json
As you can see, the docs
directory, serves as a root of our VuePress site. A README.md
file is required for the root directory and for all directories containing Markdown files. It will be automatically transformed into an index.html
file, which will serve as a starting point/page for the current directory.
blog
DirectoryThe blog
directory will house all our blog posts and content. This is what it will look like:
.
├── css
│ ├── lists.md
│ └── README.md
├── html
│ ├── lists.md
│ └── README.md
├── http.md
├── javascript
│ ├── functions.md
│ ├── objects.md
│ ├── README.md
│ ├── strings.md
│ └── variables.md
└── README.md
There’s not too much going on here. The posts are represented by Markdown files and the three subfolders (css
, html
and javascript
) represent collections. You’ll hear more about these later.
.vuepress
DirectoryThis will contain all files and folders necessary for the development and building of our site. This is what it will look like:
.
├── components
│ ├── Footer.vue
│ ├── Hero.vue
│ ├── Message.vue
│ └── Navbar.vue
├── config.js
└── theme
├── layouts
│ ├── Blog.vue
│ ├── Home.vue
│ └── Post.vue
├── Layout.vue
└── styles
├── code.styl
└── custom-blocks.styl
As you can see, the .vuepress
directory contains the following:
components
directory for our custom components. All components put in this folder are globally registered, and dynamic and async by default.config.js
file, used for site and theme configuration.theme
directory to hold our custom theme.The theme
directory contains:
layouts
directory, where we put our custom layoutsLayout.vue
file, which is required to create a custom themestyles
directory for the theme’s stylesIf you’d like to follow along with this tutorial, it’d be a good idea to create most of these folders and files now. For those of you on ’nix-based systems, you can do that with the following code.
Project root
mkdir -p docs/{blog,.vuepress}
touch docs/README.md
blog
directory
mkdir docs/blog/{css,html,javascript}
touch docs/blog/{css,html}/{lists.md,README.md}
touch docs/blog/javascript/{functions.md,objects.md,README.md,strings.md,variables.md}
touch docs/blog/{README.md,http.md}
Please note that we won’t fill all of these posts (Markdown files) with content during the tutorial. That will be left to you.
.vuepress
directory
mkdir docs/.vuepress/{components,theme}
touch docs/.vuepress/components/{Footer.vue,Hero.vue,Message.vue,Navbar.vue}
touch docs/.vuepress/config.js
mkdir docs/.vuepress/theme/{layouts,styles}
touch docs/.vuepress/theme/Layout.vue
touch docs/.vuepress/theme/layouts/{Blog.vue,Home.vue,Post.vue}
touch docs/.vuepress/theme/styles/{code.styl,custom-blocks.styl}
Alternatively, you could clone the repo and work with the finished project.
Finally, before we start building the blog, let’s establish some settings. In .vuepress/config.js
:
module.exports = {
title: "Front-end Web School",
description: "Learn HTML, CSS, and JavaScript",
head: [
[
"link",
{
rel: "stylesheet",
href:
"https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css"
}
],
[
"link",
{
rel: "stylesheet",
href: "https://use.fontawesome.com/releases/v5.6.1/css/all.css"
}
]
]
};
Here, we provide a site title and description. And we also add Bulma and Font Awesome to the project. They’ll be injected into the head tag of each compiled HTML page. We’ll use them to develop our custom theme.
Now we’re going to build our blog. In the version we’ll be using, VuePress doesn’t offer blogging features (such as tags and categories), but we’ll try to improvise.
Let’s start by preparing some content for our blog.
In the blog
directory, we have html
, css
, and javascript
subdirectories. Each blog subdirectory will play the role of a collection. So we’ll have the ability to organize the blog’s content by collections and to explore each collection individually.
The blog
directory and all of its subdirectories each have a README.md
file. This file will contain only a Front Matter section with a title
property containing the name of the directory. So for the blog
directory, the README.md
file will contain:
---
title: Blog
---
For the css
directory:
---
title: CSS
---
And so on.
Next, let’s look at our blog posts—the Markdown files. Each file must contain a Front Matter
section, where we put the necessary settings and metadata in YAML format. Here’s an example from the functions.md
file:
---
title: Learn JavaScript Functions
date: 2018-12-12
categories: [Intermediate]
tags: [JavaScript, Function, Callback Function]
---
<!-- Your Markdown content here -->
As mentioned above, we’ll not fill out all of the blog posts here. If you’d like to copy any of the content from the finished site, you can find it here.
Layout.vue
ComponentAfter we get our content done, let’s start creating our custom theme. In .vuepress/theme/Layout.vue
add this:
<template>
<div class="container">
<Navbar/>
<Component :is="currentLayout"/>
<Footer/>
</div>
</template>
<script>
import Home from "./layouts/Home.vue";
import Blog from "./layouts/Blog.vue";
import Post from "./layouts/Post.vue";
export default {
components: {
Home,
Blog,
Post
},
computed: {
currentLayout() {
const { path } = this.$page;
if (path === "/") {
return "Home";
} else if (path.endsWith("/")) {
return "Blog";
} else if (path.endsWith(".html")) {
return "Post";
}
}
}
};
</script>
Layout.vue
will be invoked for each Markdown file. It’s like a root component in a Vue application. The code in this file checks the current page’s path (via the currentLayout
computed property) and according to the result loads the needed layout. For that, we use Vue’s built-in <Component/>
element, which swaps the displayed component via its is
property. In the template, we also put <Navbar/>
and <Footer/>
components, which we’ll be creating in the following section.
VuePress exposes two variables—this.$page
and this.$site
—which give us access to data about the page and the site respectively. In currentLayout
, this.$page
is used to get the path of the current page.
In this section, we’ll look at the partial components, which we’ll import into our layout. These partials are in the .vuepress/components
folder.
In this component, we use Bulma’s Navbar component to create a navbar for our blog:
<template>
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
<div class="navbar-brand is-marginless">
<a class="navbar-item" :href="$withBase('/')">
<span class="icon is-large has-text-warning">
<i class="fas fa-2x fa-laptop-code"></i>
</span>
</a>
<a
role="button"
class="navbar-burger"
:class="{'is-active': isActive}"
@click="isActive = !isActive"
aria-label="menu"
aria-expanded="false"
data-target="navbarMenu"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarMenu" class="navbar-menu" :class="{'is-active': isActive}">
<div class="navbar-end">
<a class="navbar-item" :href="$withBase('/')">HOME</a>
<a class="navbar-item" :href="$withBase('/blog/')">BLOG</a>
<a
class="navbar-item"
:href="$withBase(collection.path)"
v-for="collection in collections"
>{{collection.path | formatNavItems}}</a>
</div>
</div>
</nav>
</template>
<script>
export default {
data: function() {
return {
isActive: false
};
},
computed: {
collections() {
let pages = this.$site.pages.filter(page => {
return page.path.match(/(blog/).+(/$)/);
});
return pages;
}
},
filters: {
formatNavItems(value) {
if (!value) return "";
let pos = value.search(/w+/$/);
let res = value.slice(pos, value.length - 1);
return res.toUpperCase();
}
}
};
</script>
<style>
.navbar {
padding-right: 1em;
}
</style>
In this file, we create a computed collections
property, which filters all pages and matches only the blog directory and subdirectories. We use that property in the template to populate the navbar with a link for each collection. We also use the withBase built-in helper in order to have proper links for our non-root URL when we deploy it. We set the Home
and Blog
links manually.
We also create and use a formatNavItems
filter, which extracts the name of the directory from the path and uppercases it.
The isActive
data property is used to toggle the navbar menu on mobile by binding it with the is-active
Bulma class.
If you run the dev server at this point (using npm run docs:dev
) and navigate to http://localhost:8080, you should be able to see the navbar component rendered to the page.
In this component, we use the Bulma hero component:
<template>
<section class="hero is-success">
<div class="hero-body">
<div class="container">
<h1 class="title">Front-end Web School</h1>
<h2 class="subtitle">Learn to code in HTML, CSS, and JavaScript</h2>
</div>
</div>
</section>
</template>
In this component, we create a message box, which we’ll display on the home later on. We use Bulma’s message component for this:
<template>
<article class="message is-info" :class="{hidden: dismiss}">
<div class="message-header">
<p>Welcome to Our Blog</p>
<button @click="hide" class="delete"></button>
</div>
<div class="message-body">Lorem ipsum dolor sit amet...</div>
</article>
</template>
<script>
export default {
data: function() {
return {
dismiss: false
};
},
methods: {
hide() {
this.dismiss = true;
}
}
};
</script>
<style>
.hidden {
display: none;
}
</style>
This is a simple footer created with Bulma’s footer component:
<template>
<footer class="footer has-background-grey-darker">
<div class="content has-text-centered">
<p>Copyright 2018 All rights reserved.</p>
</div>
</footer>
</template>
<style>
.footer {
margin-top: 2em;
}
</style>
Now it’s time to create the custom layouts for our blog. In this section, we’ll look at the files in the .vuepress/theme/layouts
folder: Home.vue
, Blog.vue
, and Post.vue
.
This will be our home template. It’s super simple:
<template>
<div>
<Hero/>
<Content/>
</div>
</template>
We just add the <Hero/>
partial and the VuePress’ <Content/>
global component.
Take a second to check your progress at this point. You should see the site being rendered with a nice hero element. If you test it out in your browser, you should see that it’s responsive, too.
This is our most complex component, which will render the blog posts for each collection. It will also find and filter the taxonomies (categories and tags) we set in the Front Matter
sections of the Markdown files:
<template>
<div>
<div class="content">
<h1>{{ $page.frontmatter.title }}</h1>
<Content/>
</div>
<div v-if="posts.length">
<div class="columns">
<!-- Filter controls -->
<div class="column is-3">
<nav class="panel">
<p class="panel-heading">Categories</p>
<a
class="panel-block is-capitalized"
:class="{'is-active':i == currentCat}"
v-for="cat, i in findTaxonomy('categories')"
@click.prevent="filterTaxonomies('categories', cat);currentCat = i;currentTag = null"
>
<span class="panel-icon">
<i class="fas fa-tasks"></i>
</span>
{{cat}}
</a>
</nav>
<nav class="panel">
<p class="panel-heading">Tags</p>
<div class="tags panel-block is-capitalized">
<a
v-for="tag, i in findTaxonomy('tags')"
@click.prevent="filterTaxonomies('tags', tag);currentTag = i;currentCat = null"
>
<span class="tag" :class="{'has-text-link':i == currentTag}">{{tag}}</span>
</a>
</div>
</nav>
</div>
<!-- Blog posts -->
<div class="column is-9">
<transition-group tag="ul" name="posts">
<li class="box" v-for="post in posts" :key="post.path">
<router-link :to="post.path">
<div class>
<img v-if="post.frontmatter.image" :src="$withBase(post.frontmatter.image)">
</div>
<p class="title is-3">{{post.frontmatter.title}}</p>
<p class="subtitle is-6">
<span class="icon">
<i class="fas fa-calendar-alt"></i>
</span>
{{post.frontmatter.date | formatDate}}
<span class="icon">
<i class="fas fa-tags"></i>
</span>
<span v-for="tag in post.frontmatter.tags" class="tag">{{tag}}</span>
</p>
</router-link>
</li>
</transition-group>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
posts: [],
currentCat: null,
currentTag: null
};
},
created() {
this.posts = this.allPosts;
},
computed: {
allPosts() {
let currentCollection = this.$page.path;
let posts = this.$site.pages
.filter(page => {
return page.path.match(
new RegExp(`(${currentCollection})(?=.*html$)`)
);
})
.sort((a, b) => {
return new Date(b.frontmatter.date) - new Date(a.frontmatter.date);
});
return posts;
}
},
filters: {
formatDate(value) {
let d = new Date(value);
return d.toDateString();
}
},
methods: {
filterTaxonomies(type, tax) {
let currentCollection = this.$page.path;
let posts = this.$site.pages.filter(post => {
if (post.frontmatter[type]) {
return post.frontmatter[type].includes(tax);
}
});
this.posts = posts;
},
findTaxonomy(type) {
let tax = [];
let posts = this.$site.pages.forEach(function(post) {
if (post.frontmatter[type]) {
tax = tax.concat(post.frontmatter[type]);
}
});
let uniqTax = [...new Set(tax)];
return uniqTax;
}
}
};
</script>
<style>
.tag {
margin: 0 0.3em;
}
.posts-move {
transition: transform 1s;
}
.posts-enter-active,
.posts-leave-active {
transition: all 0.5s;
}
.posts-enter,
.posts-leave-to {
opacity: 0;
transform: translateY(30px);
}
</style>
In this file, we create a posts
data property to hold our blog posts. Then, we create an allPosts
computed property to get all the posts from the current collection and sort them by date. We use the created
hook to populate the sorted posts in the posts
data property.
We then go through the posts with a v-for
directive and populate each one by extracting data from its Front Matter
. We use a formatDate
filter to display the date in human-readable format.
We implement tags and category features by creating two filters:
filterTaxonomies
filters all pages which have a particular taxonomy type and search term.findTaxonomy
goes through each page and collects the desired taxonomy. Then, it extracts and returns only the unique terms from the collected taxonomies.The above two filters are used in the template to create the Categories
and Tags
panels, which display all existing categories and tags.
Finally, we use the <transition-group>
component to add a smooth transition when we filter the blog posts, as well as currentCat
and currentTag
data properties to assign proper active classes to the selected tag or category.
Now, let’s create the layout for the single post:
<template>
<div class="content">
<h1>{{ $page.frontmatter.title }}</h1>
<Content/>
</div>
</template>
<style>
.content {
margin-top: 2em;
padding: 0 2em;
}
</style>
In the single post layout we just display the title and the content.
For the Markdown extensions to be properly displayed, we’ll need some styling. We’ll steal the necessary styles from the default theme and we’ll put them at the end of our Layout.vue
component:
<style src="prismjs/themes/prism-tomorrow.css"></style>
<style lang="stylus">
/* colors */
$accentColor = #3eaf7c;
$textColor = #2c3e50;
$borderColor = #eaecef;
$codeBgColor = #282c34;
$arrowBgColor = #ccc;
/* code */
$lineNumbersWrapperWidth = 3.5rem;
$codeLang = js ts html md vue css sass scss less stylus go java c sh yaml py;
@import './styles/custom-blocks.styl';
@import './styles/code.styl';
</style>
First, we add the necessary CSS file for the syntax highlighting. Then, we add some Stylus variables and import two Stylus files. The Stylus imports are very long, so I don’t want to list their contents here. Rather, you can grab them from our repo: custom-blocks.styl and code.styl.
At this point, you should be able to click around your blog and see all of the posts displayed under the different categories.
As I mentioned at the beginning, we can use templates and components directly in our Markdown. To add the <Message/>
component we created earlier to the home page, we just add it in the home’s README.md
like this (in docs/README.md
):
---
title: Home
---
<Message/>
Now, when you visit http://localhost:8080, you should see a nicely styled Message
component.
VuePress comes with some Markdown extensions optimized for technical documentation. Here are some of them. You can add them to docs/README.md
to see them in action straight away, or view them on the blog’s home page:
Table of contents (you’ll actually need some content to see this in action):
[[toc]]
Code blocks, with line highlighting and optional line numbers:
```js{2}
function add(a, b) {
return a * b; // Line highlighting is cool!
}
```
Custom containers and emoji:
::: warning
Be careful! VuePress is highly addictive! :wink:
:::
To display the line numbers, we have to add the following to the .vuepress/config.js
file:
markdown: {
lineNumbers: true
}
Phew, this was a long ride. So finally our blog is ready to go and we can build it.
Run the following in the terminal:
npm run docs:build
This will generate a dist
folder, inside the .vuepress
directory, with all files compiled and ready to be deployed.
As I mentioned before, we can make our site multilingual. However, I’m not going to explore this feature here. For more information, please consult the documentation.
Once your site is done, you have plenty of options for deploying. You can read about the different choices in the documentation. I deployed my version of the blog to GitHub Pages. Here is how.
vuepress
.base
property in the config.js
to be /vuepress/
.Next, I created a deploy.sh
file with the following code:
# abort on errors
set -e
# build
npm run docs:build
# navigate into the build output directory
cd docs/.vuepress/dist
# if you are deploying to a custom domain
# echo 'www.example.com' > CNAME
git init
git add -A
git commit -m 'deploy'
# if you are deploying to https://<USERNAME>.github.io
# git push -f [email protected]:<USERNAME>/<USERNAME>.github.io.git master
# if you are deploying to https://<USERNAME>.github.io/<REPO>
git push -f [email protected]:codeknack/vuepress.git master:gh-pages
cd -
In this tutorial, we learned what VuePress is, why we should use it, and how to create a simple blog with it. We also deployed the blog to GitHub Pages. All this gave us a solid understanding of the VuePress system and its capabilities. So now we’re ready to apply this knowledge in our future projects, creating more beautiful VuePress-powered sites.
18.224.37.68