Chapter 3: Build Your First Static Site with VuePress

by Ivaylo Gerchev

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.

Code and Live Site

The code for this tutorial can be found on GitHub. You can also view the site running live on GitHub Pages.

What Is VuePress?

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.

The Benefits of a Static Site.

A static site has some significant benefits:

  • Simplicity. A static site is written in plain, simple HTML files. As all files are physically present on the server—in contrast to a database-driven site—you can move them, deploy them, and back them up quickly and easily.
  • Speed. Because the files are already compiled and ready to be served, the page load time of a static site is pretty fast.
  • SEO friendly. Metadata-rich pages plus human-readable URLs and plain text content make a static site extremely accessible to search engines.
  • Security. A static site is far more secure than a dynamic one. According to a WP WhiteSecurity report, about 70% of WordPress sites are at risk of getting hacked. Because a static site doesn’t rely on a database, CMS and/or plugins, the chances of getting hacked is minimal.
  • Salability. A static site can be easily scaled up by just increasing the bandwidth.
  • Version Control Support. Adding version control to a static site is super easy.

Why Use VuePress?

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:

  1. VuePress is built with Vue. This means that we can use all the superpowers of Vue while we build our site.
  2. We can add a VuePress site to any existing project, at any time.

The above benefits are already strong enough, but let’s explore the full feature palette that makes VuePress shine:

  • There’s close integration between Vue templates/components and Markdown files. We can use Vue templates/components directly in Markdown, which give us great flexibility.
  • VuePress uses markdown-it, one of the best Markdown parsers out there, which has a rich plugin ecosystem. Some of its plugins, optimized for technical documentation, are already bundled with VuePress. And we can add more if we want.
  • VuePress has a Vue-powered custom theme system, with which we can create a full-featured theme by using Vue SFC (single file components). The sky’s the limit.
  • VuePress offers multi-language support. So we can turn our site multilingual fairly easily.
  • VuePress has Google Analytics integration.
  • VuePress has a responsive default theme, specifically tailored for technical documentation, with a great set of features out of the box. These include:
    • Optional home page
    • Header-based search functionality (optional Algolia search)
    • Customizable navbar and searchbar
    • Auto-generated GitHub link and page edit links.

Getting Started

VuePress Versions

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.

Exploring the Directory Structure of the Final Project

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.

The blog Directory

The 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.

The .vuepress Directory

This 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:

  • A components directory for our custom components. All components put in this folder are globally registered, and dynamic and async by default.
  • A config.js file, used for site and theme configuration.
  • A theme directory to hold our custom theme.

The theme directory contains:

  • a layouts directory, where we put our custom layouts
  • a Layout.vue file, which is required to create a custom theme
  • a styles directory for the theme’s styles

Following Along at Home

If 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.

Configuring Your Site/Theme

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.

Building Your Site

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.

Preparing the Blog Content

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 -->

Grabbing the Content

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.

Creating the Layout.vue Component

After 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.

Creating the Partial Components

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.

Hero.vue

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>

Message.vue

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>

Creating the Layout Components

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.

Home.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.

Blog.vue

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.

Post.vue

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.

Adding Some Styles

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.

Using Vue Components in Markdown

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.

Using Markdown Extensions

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
    }
    

Build the Site

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.

Internationalize Your Site

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.

Deploy Your Site to GitHub Pages

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.

  1. I created a repo vuepress.
  2. Then, I set the base property in the config.js to be /vuepress/.
  3. 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 -
    
  4. Finally, I ran it. And that’s all.

Conclusion

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.37.68