I rewrote my website and blog using nuxt.js. The experience did not come without a bit of blood, sweat, and tears, but I'm happy with the end result. This is not so much a tutorial as it is a reflection on the experience and why I did what I did.

What I wanted

Originally, I wrote this blog a couple years ago with jekyll. Jekyll uses something that is or closely resembles liquid templating. I'm not really a fan of liquid or any templating language that obfuscates simple things into some obscure, verbose syntax (such as filters). Want to multiply two numbers together and then divide by 100?

{{ age | times: 2 | divided_by: 100 }}

Imagine doing even more complex math than this. It's absolutely terrible and takes forever to read. This would simply be {{ age * 2 / 100 }} in any other syntax that allows rendering expressions.

Something with a better templating syntax was definitely in order, but I also wanted:

  1. For those who disable JavaScript by default (such as myself) to still be able to view most of the site.
  2. For everything to function as a single page app for those with JavaScript enabled.
  3. To be able to use Vue since it has a pretty good featured templating system.

Thankfully, the good folks from Zeit have my needs covered with their open source project Nuxt. Nuxt is a framework for creating Server Side Rendered (SSR) apps with Vue, but it can also be used to generate a static site.

Nuxt offers pretty much everything I wanted. I could completely pre-render the website to a static site for users with JavaScript disabled while still being able to use Vue for those with it enabled. On top of everything, it also works as a single page app for users with JavaScript enabled. Wowee, sign me up!

I also wanted performance

The few tutorials I looked into on how to create a blog with Nuxt end up loading all posts on every single request, even pages completely unrelated to blogging. If each post is around 15KB of text, highlighted code blocks, etc., then we're talking ~4-6KB gzipped for each post. Just 30 posts is 150K of already gzipped content being loaded on every page. For a small site with only a few posts, this might be ok, but as I write more and more in the future, this is going to add up to be quite a lot.

It didn't seem like there were any modules in the community to turn markdown files into JSON with various metadata. So, being a DIY oriented person, I decided I would just write all of the functionality. I just needed to simply convert the posts to json and load them with axios in the post template.

Converting posts to json

My solution to convert the posts to JSON was to write a webpack plugin. I won't go into detail with code examples of how it works, because there's a lot of code that goes into it. If you want to see the source files, you can find them in here (look in ~/.webpack and nuxt.config.js.

Basically, I use require.context paired with file-loader to load the markdown files. This setup makes the files available to the webpack plugin I wrote and from there allows me to transform each post into a JSON file with all the necessary metadata.

This might not the best way to do it, but was the easiest I could think of with my limited knowledge of writing webpack loaders and plugins. One downside to this approach is the post page doesn't automatically update when making a change to the markdown file. I'm hoping to fix later on, but for now, I'll just have to use good old ctrl+r.

A major Nuxt gotcha

You can not fetch content from the same server when using npm run generate. For example, a request like this: axios.get('/_nuxt/hello-world.json') will fail when running npm run generate and you'll get this lovely little error message.

  nuxt:generate Generate file: /blog/understanding-javascript-prototypes/index.html +0ms
  Error: connect ECONNREFUSED 127.0.0.1:3000

  - util.js:1003 Object._errnoException
    util.js:1003:13

  - util.js:1024 _exceptionWithHostPort
    util.js:1024:20

  - net.js:1181 TCPConnectWrap.afterConnect [as oncomplete]
    net.js:1181:14

This happens because the server doesn't actually run while npm run generate is going. I'm not sure why they decided to do that, but it is what it is.

That's actually all irrelevant because you have to remember that Vue is not running inside a browser when running npm run generate, but inside of a node process. In a browser, axios would assume the root / to be the same domain (which is localhost:3000) but in a node process, the root / defaults to 127.0.0.1:80. So, we end up having another problem in that axios is sending requests to the wrong place.

Solving the gotcha

I solved this in a hacky way with the code below and by keeping the dev server open in another terminal tab. This way, requests are sent to the dev server when running npm run generate.

// ~/core/fetch.js
import axios from 'axios'

const baseURL = process.server
  ? // I set this to 'http://localhost:3000' in the nuxt config
    process.env.app.baseUrl
  : '/'

const request = axios.create({
  baseURL,
})

export default request

This code creates an instance of axios that prefixes requests with http://localhost:3000 so that requests go to the development server when running npm run generate. When running in an actual browser, the requests will be sent to the same domain. Remember that requests starting with a / are relative to the domain the code is running from, so this ends up covering both cases when the app is running in development localhost:3000 and production anthonykoch.com.

With all of this in place, I was able to load a post as normal.

<template>
  <h1>{{ post.title }}</h1>
</template>

<script>
  // ~/pages/blog/_slug.vue
  import axios from '@/core/fetch'

  export default {
    asyncData({ params, error }) {
      return axios
        .get(`posts/${params.slug}.json`)
        .then(({ data: post }) => {
          return {
            post,
          }
        })
        .catch((err) => {
          error({ statusCode: 404, message: 'Post not found' })
        })
    },
  }
</script>

Things I don't like

Besides the gotcha above, there's a few things I found annoying or didn't like about Nuxt.

Not a standard Vue app

The structure of the app strays from a normal Vue app. I would expect as much since we're not just dealing with Vue but with Server Side Rendering (SSR). However, the concept of ~/layouts and automatic router configuration actually end up disallowing the use of some vue-router features, such as named routes and named slots (although it seems this will likely change in the future).

Error page

When there's an error, it will show an overlay that goes away when you fix the error. This is pretty standard for a Vue or React app. But when the app errors and the page is refreshed, it will show the page below. Once the error is fixed, the page doesn't refresh and app doesn't come back. This means you have to manually refresh it. It's kind of annoying, but not the end of the world.

The nuxt error page

nuxt error page

Scroll position

This isn't really a Nuxt thing, but something against vue-router. The scroll position doesn't get remembered on page refresh. They allow customizing this, but it's not done by default. I have to wonder if they don't include this by default because the logic for it is too buggy and/or too difficult to achieve.

The overall experience

Working with Nuxt has been an interesting experience. It took a pretty good amount of time for me to figure out how to get this all to work properly, but I'm pretty happy with the end result and it serves all my needs perfectly.