written by Andre Liem
06/09/2017

Build a Static Site with Vue (Part 2)

note: Part 1 & 2 of this series is kept for reference. Please refer to Part 3 as I am
now moving everything to Nuxt for this site and this code will not be used.


This is part 2 of an on going series that explores the process of creating a static file CMS for Vue developers.

Recap

In the first post, I reviewed the idea of managing a website/blog by essentially deploying static files generated by a Vue+Webpack build and using your IDE as the CMS. This site itself (vuejsradar.com) is running off this code so you'll see new features live in action as I release each post. Think of vuejsradar.com as being the guinea pig for this series.

In part 1 I proposed the following feature list for our CMS:

Feature List (* = already supported)

  • CMS for Site Pages *
  • CMS for Blog Posts *
  • Permalinks for each blog post *
  • Blog Pretty URLs (slug) eg- http://vuejsradar.com/post/my-post-name *
  • Site Map
  • SEO Optimized (Pre Rendering?)
  • Attribute Author to Posts *
  • Commenting *
  • Social Sharing
  • Responsive *
  • Ability to easily publish updates to live site *
  • Ability to draft
  • Multiple Layout types per page + blog posts
  • Show Code Prettified in pre tags *
  • In addition to these, I've decided to add a few more:

Markdown Support

  • Ability to utilize an API for content instead of static files
  • Since the first post, I've created a git repo for this project so you can take a look at the real code and understand how it's all structured. Just be warned that this is very early work, and will likely go through several refactoring phases. Only use this code on a new experimental site or project!

Since the first post, I've created a git repo for this project so you can take a look at the real code and understand how it's all structured. Just be warned that this is very early work, and will likely go through several refactoring phases. Only use this code on a new experimental site or project!

The focus for this post will be to examine how the code is structured and the details of the core pieces that are more than just the standard Vue CLI install.

Structure

The best way to understand the project is to look at the folder stucture. The site builds on the standard Vue cli webpack boilerplate project by adding in several conventions on how to structure components to manage key concepts for our site. Currently, we support blog posts and pages with blocks of content.

  • /src
    • /components
      • /renderers
        • Block.vue
        • Post.vue
      • Footer.vue
      • Header.vue
      • Home.vue
      • Post.vue
      • Posts.vue
    • /router
      • index.js
  • /api
    • /authors
    • /blocks
    • /posts
      • 1.html
      • 2.html
      • index.js

Components currently exist as Vue components which handle layouts or specialze in rendering content.

For example, Post.vue is a Vue component which defines the general layout of this blog page you are looking at now. It uses the
route.id to load the appropriate post data, and delegates the responsiblity of actually rendering the post to /renderers/Post.vue.

/components/Post.vue

<template>
  <div>
    <div class="block block-post">
      <div class="container">
        <div class="row">
          <post class="col-sm-9" :post="post" :content="content"></post>
        </div>
      </div>
    </div>
    <div class="block">
      <div class="container">
        <hr>
        <div class="col-sm-8">
          <disqus v-bind:shortname="shortname" :identifier="postId"></disqus>
        </div>
      </div>
    </div>
  </div>
</template>

import PostRenderer from './renderers/Post.vue'
import Disqus from 'vue-disqus/VueDisqus.vue'
import `*` as Posts from '@/api/posts'

export default {
  components: {
    PostRenderer,
    Disqus,
  },
  computed: {
    content() {
      let id = this.$route.params.id.toString()
      let content = require(`../api/posts/${id}.html`)
      return content
    },
    post() {
      let id = this.$route.params.id.toString()
      return Posts.fetch().find((post) => {
        return (post.id == id)
      })
    }
  },
  data() {
    return {
    shortname: "vuejs-radar"
    }
  },
  mounted() {
    window.PR.prettyPrint()
  }
}

The template is pretty straight forward. We have defined a layout which displays two components. One is the post-renderer component which knows how to display content from our CMS. The second component is using the vue-disqus component to include user commenting.

The module code requires a bit more explanation than the template.

Starting from the top, we import the PostRenderer component, Disqus, and then a basic library which pulls in content for us. We are putting this in an api layer in case one day we want to move from storing content as static files and use an actual backend CMS.

import PostRenderer from './renderers/Post.vue'
import Disqus from 'vue-disqus/VueDisqus.vue'
import * as Posts from '@/api/posts'

The majority of the work is handled by 2 computed properties.

computed: {
  content() {
    let id = this.$route.params.id.toString()
    let content = require(`../api/posts/${id}.html`)
    return content
  },
  post() {
    let id = this.$route.params.id.toString()
    return Posts.fetch().find((post) => {
        return (post.id == id)
    })
  }
},

content() uses the route Id to fetch a html file that is currently stored in our api folder as a plain html file.

post() uses the route Id as well to identify which post to return from the "API". I put API in quotes because it's not really the best name for this right now but it will do ;).

We are using computed properties for these because the content will not re-render when the URL changes without this. Perhaps if we moved to a vuex architecture we would not have to do this.

Post.fetch() simply returns an array of post data like you would retrieve from a JSON REST API.

The last part of this component which may need some explanation is in mounted()

mounted() {
  window.PR.prettyPrint()
}

Right now we are relying on the google prettyprint script to do code syntax highlighting. Because it's not supported as a Vuejs directive, I'm hacking it for now by including it as a basic script in index.html
and calling it from the global window object. We'll work on improving this someday, perhaps creating a reusable directive, but for now this works well enough. We have to call it in this component specifically so that HMR changes have syntax highlighting as we write posts. The actual prettyprinting used for end users is the one we'll see later which is included in post renderer.

The Post Renderer

Next, lets look at what the Post Renderer component does. Here is a snippet of the file.

<template>
  <div>
    <h3 class="title">{{ post.title }}</h3>

    <h6 class="mb-2">published by <a v-bind:href="post.author.url" target="_blank">{{post.author.name}}</a> on {{ post.date }}
        <div class="float-right">
            <span v-html="keywords"></span>
        </div>
    </h6>

    <div class="content" v-html="contentParsed"></div>
  </div>
</template>

  
<script type="text/babel">
  const util = {
    escapeHtml(element) {
        return element.text(element.html()).html()
    }
  }

  export default {
    props: ['content','post'],
    computed: {
        keywords() { ... },
        contentParsed() {
            let $content = $(this.content)
            let preContent = $content.find('pre.lang-html')

            preContent.each((index, element) => {
              ...escape pre content
            })
            return $content.html()
        }
    },
    watch: {
        'content' (curr, old) {
            setTimeout(() => {
                window.PR.prettyPrint()
            }, 500)

        }
    }
  }
</script>

The template is responsible for the layout of the post content.
Notice how the v-html tag loads a computed property called contentParsed

Looking at the computed property contentParsed(), far from being
straightforward, what we are doing here is taking the content prop passed in,
finding any <pre> tags used for HTML and escaping the content.

If we don't escape the content, all the content within the pre tags will be rendered since
we use v-html to display our post. This enables us to include HTML snippets in our posts. It relies on jQuery and is far from being elegant, but for now it works and is something we can look at improving in the future.

Lastly, we are watching for changes on the prop content to apply the prettyPrint highlighter when content changes due to a change in the URL. We delay the application of this half of a second so that there's content to apply it to. I know there must be a better way of doing this, so if you have some feedback here please do send them my way. This one is needed for the actual functionality to work for users, whereas the prettyprint used in the parent Post component is only needed for development purposes. In the future, I will look at relying on the Vue lifecycle instead of a hack setTimeout to have the highlighting applied in a Vue way.

Wrapping Up

With these two conventions, the Post component which handles the general layout, and the renderer, we have a really basic structure for writing posts. The workflow can be summarized as:

  1. Add a Post record to /src/api/posts/index.js (or to blocks if you are composing several pieces of content)
  2. Create a corresponding HTML file in /src/api/posts/ID.html (where ID matches the ID defined in index.js)
  3. Write/Edit the post with IDE
  4. Preview the post as you edit with the browser relying on HMR
  5. Done? Publish it. I use a hook to deploy updates whenever origin master is updated.

I personally find the experience of blogging as you code a much more enjoyable experience than writing through an online WSYWYG editor. It's hard for any WSYWYG editor to beat the productivity of your own trusted IDE or code editor.

Next

In the next post, we're going to look at the following topics:

  • SEO optimization - add pretty urls
  • SEO search - pre rendering
  • Reviewing how block content can be used to compose custom pages

If you want to know when the next post comes out, please subscribe to the newsletter! I hope you enjoyed this post, please feel free to share with fellow Vue developers. I'm looking to get as much feedback as possible in the early days before this CMS starts to take shape.