Blog Engines and Static Site Generators

· Read in about 8 min · (1685 words) ·

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

Blog Engine vs Static Blog Generator

I have some experience building web apps with Django, RoR, golang (raw, beego). But I was looking for more lightweight solution. I love Golang and first I’ve googled golang blog engine. I found Journey and Ghost.

Screenshot of Ghost
Ghost is simple and lightweight

These solutions are CMS-like (like Wordpress), but more modern and lightweight. They store articles in database, have server, web UI etc.

They looked nice and I was going to use one of them. But then I accidentally stumbled upon an article about static blog generators. They allow to generate Blog HTML pages once and don’t require database or server. It looks better to me because every db or server eats CPU and RAM on my weak AWS EC2 instance.

JAM

And I decided to use static blog generator. After that I found that it’s a trend to precompile website: JAM stack. Main advantages are:

  1. Performance of 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, a lot of 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 line in article it’s changed in a browser automatically. I’ve seen similar things for RoR, but they were very unstable in my development.

Choosing Static Blog Generator

After some research I was comparing next generators:

Generator Language GitHub Stars Advantages Disadvantages
Jekyll Ruby 31k Quick Start. A lot of plugins. Mature and stable solution. High developing speed. Really slow compilation: after 1 year of blogging (50 articles) compilation will be around 1 min. Hard to debug ruby. Ugly documentation website.
Hugo Golang 20k The fastest: compilation will take seconds after years. 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. Looks fresh. Looks too low-level and unstable yet.
Middleman Ruby 6k Similar to Ruby On Rails. Easily extendable to more than blog. High development speed. It’s a solution for corporate website, not for the blog. 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 allows to maximize development speed. And in my practice it leads to two main troubles:

  1. Poor performance of ruby applications. For blog generator it means that compilation will be in minutes after 1-2 years of blogging. It’s really slow and recalls me hard times of Clang (and any another large C/C++ project) source code compilation for 5-120 minutes. I change one css selector or add one paragraph and then wait 1 minute to see it in browser, it’s horrible.
  2. Consequences of #1:
    1. A lot of time to hard-debug it and optimize speed.
    2. Constantly falling development speed because of increasing compilation times.

Project website is it’s face and demonstration of features. When I look to Jekyll website I remember 2000-s and Joomla.

Jekyll website screenshot
Jekyll website is too retro-style

Hugo

Golang stack looks more trending and promising. But I was confused by instability and a small number of plugins. I need 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 easily supports it. Certainly I can setup webpacker to do it, but it’s too low-level, it should work out-of-box.

Project website is it’s face and demonstration of features. Hugo’s website looks modern.

Hugo website screenshot
Hugo website is ok

Nevertheless my choice is Hugo: compilation speed is more important factor for me, everything else can be fixed by writing just more code.

Hugo vs Gatsby

Gatsby looks too low-level comparing to Hugo. Also there is no need for Ajax/React/etc for static website: it’s already fast. If you see re-rendering, you can use Turbolinks. Usage of HTTP2, Turbolinks, proper assets compilation and AMP will solve all performance problems.


Creating Blog with Hugo

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

Deployment

Hugo has binaries for every platform, but I use docker-compose in my server. It allows me to start all my project with one command docker-compose up -d. If I don’t use docker for hugo, I need Chef/Puppet for it’s installation.

Hugo and docker

FROM alpine

ENV HUGO_VERSION 0.29
ENV HUGO_BINARY hugo_${HUGO_VERSION}_Linux-64bit

RUN mkdir /usr/local/hugo
ADD https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${HUGO_BINARY}.tar.gz /usr/local/hugo/
RUN ls -l /usr/local/hugo/
RUN tar xzf /usr/local/hugo/${HUGO_BINARY}.tar.gz -C /usr/local/hugo/ \
	&& ln -s /usr/local/hugo/hugo /usr/local/bin/hugo \
	&& rm /usr/local/hugo/${HUGO_BINARY}.tar.gz

WORKDIR /app/src

docker-compose

And here is docker-compose.yml:

version: '3'

services:
  nginx:
    image: nginx:stable-alpine
    volumes:
      - ./nginx/log:/var/log/nginx
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./blog/public:/app/public:ro
    expose:
      - 80
    networks:
      - reverse_proxy
      - default
    environment:
      - VIRTUAL_HOST=disaev.me
      - LETSENCRYPT_HOST=disaev.me
      - LETSENCRYPT_EMAIL=contact@disaev.me
    restart: always
  hugo:
    build: .
    command: hugo -s /app/src/ -d /app/public -w
    volumes:
      - ./blog/public:/app/public
      - ./blog:/app/src
    restart: always

networks:
  reverse_proxy:
    external: true

It uses nginx-proxy with letsencrypt-nginx-proxy-companion. Hugo here works through HTML files compilation, I don’t run it like a server in production (but do in development). In such mode I can stop hugo service and save RAM: service is needed only for once compilation or fast fixes in production (here is -w watch mode for it).

NGINX config

NGINX config is simple:

server {
  listen 80;
  server_name disaev.me *.disaev.me;

  root /app/public;

  location / {
    try_files $uri $uri/index.html =404;
  }
}

SSL and all complex configuration are handled by upper level container with nginx-proxy.

Development mode

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

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

Theme

I’d like to use Bootstrap. I was looking for Bootstrap 4 theme, but found theme had GPL license. There’s sadly too few bootstrap themes. Also I didn’t found any theme with modern stack (webpack, postcss, react), assets pipelines. I would even pay some money for it. Hugo’s themes are too weak.

Finally I used hugo-bootstrap-premium Bootstrap 3 theme. It’s advantage in Bootswatch themes support. It has MIT license.

Hugo Ugly URLs

It’s the most disappointing part of Hugo: URLs are ugly. Just take a look at the current URL: /p/blog-engines-and-static-site-generators/. Notice a trailing slash. And there are no plans to fix it! There was some close to proper fix: add --uglyURLs to Hugo command or into config and it will remove trailing slash, but will add .html extension.

  1. --uglyURLs clearly shows reluctance of Hugo creators to fix it.
  2. .html in the end is even worse than trailing slash.

I found some solutions with hacking templates code, but it has unpredictable consequences: sitemap broking, SEO, etc. I had to put up with it. If I was at starting point it could change my choice from Hugo to Jekyll.

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 generated image looks like this:

Screenshot of generated figure
Hugo default figure looks ugly: no centering of caption and separation from text

I tried to add class to figure, but it didn’t help. Finally I wrote custom shortcode in path 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 me to shorten image path: it adds /img/ automatically. I store my images into static/img directory.

Table of contents

Default Hugo table of contents looked like this:

Screenshot of generated toc with Hugo
Hugo default ToC looks ugly and it's non-customizable
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 | urlize) }}

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

Resulting table of contents is in the right sidebar of current article.

Result

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