Contents

Blog Engines and Static Site Generators

October 07, 2017 | 8 min read

I’ve just decided to start a blog. Here’s some history behind it.

Blog Engine vs Static Blog Generator

I have some experience building web apps with Django, RoR and Go. But I was looking for a more lightweight solution to reduce development and operations cost.

First, I’ve found a Ghost CMS.

Ghost is simple and lightweight
Ghost is simple and lightweight

It’s the CMS-like (like Wordpress) solution, but more modern and lightweight. It stores articles in a database, has an admin panel, etc.

It looked nice and I was going to use it. But then I accidentally stumbled upon an article about static blog generators. They allow generating HTML pages once and don’t require a database or a complex runtime: just serve static HTML files by NGINX. That’s it! It can save money on server instances and reduce operational costs (disk / CPU / memory consumption, security updates, etc).

JAM

And I’ve decided to use a static site generator. After that, I’ve found the term JAM stack. Its main advantages are the following:

  1. Performance of a website: it’s just serving of HTML files.
  2. Security: no database, no executable code (only NGINX or another serving proxy).
  3. Simplicity: static site generators are simple enough.
  4. Easy to deploy: upload to GitHub Pages, AWS S3, CDN, or to your server. No need to configure database, server, containers.
  5. Easy to develop

    1. Markdown-based: writing of articles in Markdown is a pleasure comparing to HTML/ERB/HAML/etc
    2. Live reloading: when you change a line in an article it’s changed in a browser automatically.
  6. Pricing: there are services like Netlify where one can host a static site for free.

Choosing Static Blog Generator

I was comparing the following site generators:

Generator Language GitHub Stars Advantages Disadvantages
Jekyll Ruby 31k Quick Start. A lot of plugins. Mature and stable solution. High developing speed. Slow compilation. Hard to debug Ruby. Poor documentation on the website.
Hugo Go 20k The fastest: compilation will take seconds. Pretty documentation website. Young and unstable. Weak plugins system, few plugins. Development speed is lower than for Ruby-based.
Gatsby JS+React 13k Usage of React. It looks fresh and very promising. Good website performance. It looks unstable at the moment of writing.
Middleman Ruby 6k Similar to Ruby On Rails. Easily extendable to more than ф blog. High development speed. It’s a solution for a corporate website, not for the blog: it’s too heavyweight.

I used Static Site Generators TOP to find these generators. I’ve excluded a lot of them from comparison for reasons of outdated technologies or non-popularity.

Jekyll vs Middleman

I like RoR, but it’s too heavyweight for the blog. Jekyll is much easier. I didn’t see any advantages of Middleman for the blog. So my choice is Jekyll.

Jekyll vs Hugo

Jekyll

I like Ruby and RoR stack especially. It maximizes development speed. And in my practice it leads to the following troubles:

  1. The poor performance of Ruby applications. For a blog generator, it means that compilation will be in minutes after 1-2 years of blogging. It’s slow and reminds hard times of Clang (or any other large C/C++ project) source code compilation for 5-120 minutes.
  2. Too much magic in Ruby: it’s harder to debug.

A project website is its face and demonstration of features. When I look at the Jekyll website I remember 2000-s and Joomla.

Jekyll website is too retro-style
Jekyll website is too retro-style

Hugo

Go (Golang) stack looks more trending and promising. But I was confused by instability and a small number of plugins. I needed at least these two plugins:

  1. AMP (accelerated mobile pages). There is a plugin for Jekyll, but no for Hugo. Yes, I’ve seen some hacks and one theme to support it. But I shouldn’t limit myself with one theme for AMP support.
  2. Assets pipeline like in RoR: compressing, transforming and minification of assets. Jekyll supports that. I can set up webpack to do it, but I would like it to work out-of-box.

A project website is its face and demonstration of features. Hugo’s website looks modern.

Hugo website is fine
Hugo website is fine

Nevertheless, my choice is Hugo: compilation speed and debugging complexity are more important factors to me.

I’m going to try Jekyll for one of my websites: migrate it from RoR to Jekyll. But it makes sense to use Jekyll there to reuse the existing Ruby code.

Hugo vs Gatsby

On the one hand, there is no need for Ajax/React/etc for a static website: it’s already fast. Also, we can use Turbolinks, HTTP/2, proper assets compilation, and AMP.

One the other hand, JS and React have a large ecosystem for web development: e.g. images, JS, CSS processing.

I would have chosen Gatsby if it was more mature. But now let’s choose Hugo.


Creating Blog with Hugo

I won’t repeat a lot of tutorials and official documentation. Here are only some interesting moments.

Development mode

In development I use live-reloading: it automatically reloads webpage in the browser when something changes in source code. It’s amazing. I have the following target for it:

Makefile
run_dev:
	sudo hugo server -w -v -p 80 --baseURL "http://dev.disaev.me"

Theme

I’d like to use Bootstrap. There are sadly too few bootstrap themes for Hugo.

Finally I’ve used hugo-bootstrap-premium Bootstrap 3 theme. Its advantage is in the Bootswatch themes support.

Hugo Ugly URLs

It’s the most disappointing part of Hugo: URLs end with a slash. Just take a look at the current URL: /p/blog-engines-and-static-site-generators/. We can add --uglyURLs to Hugo command or into config and it will remove trailing slash but will add .html extension.

I’ve found some solutions with hacking templates code, but it can have the following consequences: sitemap broking, SEO, etc. I had to put up with trailing slashes.

Hugo and Bootstrap Images

Hugo’s way to insert images is shortcodes.

figure src="/img/gohugo_screenshot.png" alt="Hugo website screenshot" caption="Hugo website is ok"

But the generated image looks like this:

Hugo default figure looks bad: no centering of caption and separation from a text
Hugo default figure looks bad: no centering of caption and separation from a text

I tried to add class to figure, but it didn’t help. Finally, I wrote the custom shortcode:

layouts/shortcodes/img.html
<div class="row">
  {{ if .Get "w"}}
    {{ $w := len (seq (.Get "w")) }}
  <div class="col-sm-{{ $w }} col-sm-offset-{{ div (sub 12 $w) 2 }}">
  {{else}}
  <div class="col-sm-12">
  {{end}}
    <div class="thumbnail">
      <img alt="{{ .Get "alt" }}" src="/img/{{ .Get "src" }}">
      <div class="caption text-center">
        {{ .Get "caption" }}
      </div>
    </div>
  </div>
</div>

Also, it allows shortening an image path: it adds /img/ automatically. I store my images in the static/img directory.

Table of contents

Default Hugo table of contents looked like this:

Hugo default ToC looks bad
Hugo default ToC looks bad

I found this hack and this library. I combined them and got this ToC partial:

<!-- ignore empty links with + -->
{{ $headers := findRE "<h[1-3].*?>(.|\n])+?</h[1-3]>" .Content }}
<!-- at least one header to link to -->
{{ $has_headers := ge (len $headers) 1 }}
<!-- a post can explicitly disable Table of Contents with toc: false -->
{{ $show_toc := (eq $.Params.toc true) }}
{{ if and $has_headers $show_toc }}
<nav id="toc" data-toggle="toc">
  <!-- TOC header -->
  <h4 class="text-muted">Table of Contents</h4>
  <ul class="nav">
    {{ range $i, $header := $headers }}
      {{ $headerLevel := index (findRE "[1-3]" . 1) 0 }}
      {{ $headerLevel := len (seq $headerLevel) }}

      {{ $anchorID := ($header | plainify | htmlEscape | anchorize) }}

      {{ if ne $i 0 }}
        {{ $prevHeaderLevel := index (findRE "[1-3]" (index $headers (sub $i 1)) 1) 0 }}
        {{ $prevHeaderLevel := len (seq $prevHeaderLevel) }}

          {{ if gt $headerLevel $prevHeaderLevel }}
            {{ range seq (sub $headerLevel $prevHeaderLevel) }}
              <ul class="nav">
            {{end}}
          {{end}}

          {{ if lt $headerLevel $prevHeaderLevel }}
            {{ range seq (sub $prevHeaderLevel $headerLevel) }}
              </li></ul></li>
            {{end}}
          {{end}}

          {{ if eq $headerLevel $prevHeaderLevel }}
            </li>
          {{end}}

          <li>
            <a href="#{{ $anchorID }}">{{ $header | plainify | htmlEscape }}</a>

          {{ if eq $i (sub (len $headers) 1) }}
            {{ range seq (sub $prevHeaderLevel $headerLevel) }}
              </li></ul></li>
            {{end}}
          {{end}}
      {{else}}
      <li>
        <a href="#{{ $anchorID }}">{{ $header | plainify | htmlEscape }}</a>
      {{end}}
    {{end}}

    {{ $firstHeaderLevel := len (seq (index (findRE "[1-3]" (index $headers 0) 1) 0)) }}
    {{ $lastHeaderLevel := len (seq (index (findRE "[1-3]" (index $headers (sub (len $headers) 1)) 1) 0)) }}
    {{ range seq (sub $lastHeaderLevel $firstHeaderLevel) }}
      </li></ul></li>
    {{end}}

  </ul>
</nav>
{{end}}

The resulting table of contents is in the right sidebar of the current article.

Result

Finally, I’m fine with Hugo and the current blog is the result.

Update from 2020: Gatsby is mature now and I’ve rewritten the blog using Gatsby. The blog became much faster.


© 2020

Hi, my name is Denis Isaev and I'm a senior engineering manager at Yandex.

Contents