Creating a static site with Pandoc and Bulma


The other day I got this incredible urge to resurrect my website and create a portfolio1. It is something I have been meaning to do for a long time, but have been putting off due to the pain of dealing with web technology.

The web is awash with unnecessary complexity and bloat. I don’t even have the patience for learning static site generators, I don’t need anything dynamic and I don’t want to maintain it. I’m almost happy to write raw HTML, but despite claims to the contrary the structure of HTML is tightly coupled with the rendering of it (unless you reprocess it). Plus, even as an intermediate language without all the extra elements needed for styling, it is still ugly and verbose. It is nicer to use Markdown or similar.

To be clear, I don’t want to spend ages learning about a new tool when I can increase my knowledge of ones I already use that have more utility in other domains. Don’t get me wrong, sometimes (OK, historically, most of the time)2 I’m happy to rewrite everything from scratch in a language no one has heard of using technologies that are barely invented, but this is not one of those occasions.

It so happens I have already been using Pandoc, which can generate HTML amongst other formats. Using Pandoc I can convert from a single source format to HTML and Latex/PDF. I don’t like the default HTML/CSS which Pandoc produces by default, but I also have some basic familiarity with a CSS framework called Bulma which I can use without any CSS knowledge.

GitLab CI makes it easy to host a static site. So I decided to duct tape together Pandoc and Bulma with GNU Make to create a static site generator. I’m not saying anything about how good or bad these are in relation to similar stuff, but my patience was low with this one, so I went with what worked quickly in the past.

Pandoc & Bulma

For now I am writing the page source in Markdown, Pandoc can turn this into fairly generic HTML. Either as a standalone page or a fragment. I decided to the use the standalone variant, which uses a template to generate the document header and footer.

So when executing pandoc it looks something like this:

pandoc --standalone --template=src/std.tmpl --css=bulma.css \

By default the HTML document doesn’t have much style to speak of except for the code highlights. So I can freely add Bulma which doesn’t conflict much with the code highlighting. Bulma does require particular classes on some of the HTML, so this must be added in the template.

Luckily Bulma has a class simply called content which nicely handles the HTML Pandoc produces (that is not part of the template). Note that Pandoc allows one to customize the formatter to change all output using Lua script, but so far I haven’t needed it (thankfully).

Let’s go through the template (at the time of writing).

<!DOCTYPE html>
<html xmlns="" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$>
  <meta charset="utf-8" />
  <meta name="generator" content="pandoc" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
  <meta name="author" content="$author-meta$" />
  <meta name="" content="$date-meta$" />
  <meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$" />
  <title>Richie's $pagetitle$</title>
  <link rel="stylesheet" href="$css$" />
  <!--[if lt IE 9]>
    <script src="//"></script>

Pandoc templates have support for various control structures (branches and loops) and inserting variables. For example

  <link rel="stylesheet" href="$css$" />

Which is where the Bulma style sheet will be linked to. I have removed some bits from the default template, but otherwise this is just what Pandoc uses by default.

  <section class="hero is-small is-warning is-bold">
    <div class="hero-head">
      <nav class="navbar is-pulled-right">
    <div class="container">
          <div id="navbarMenuHeroA" class="navbar-menu">
            <div class="navbar-end">
              <a class="navbar-item" href="/">
          <a class="navbar-item"
                  <img src="gitlab_logo.svg"></img>
          <a class="navbar-item">
    <div class="hero-body">
      <header id="title-block-header">
    <h1 class="title">Richie's</h1>
    <h2 class="subtitle">$title$</h2>
    <div class="hero-foot">
      <nav class="tabs is-boxed is-pulled-right">
    <div class="container">

Next up is the ‘hero’ banner, this was more or less copied from Bulma’s documentation, I just added some modifiers (e.g. is-warning, is-pulled-right) and removed a few bits to customise it. I think it looks great!

Pandoc can generate a table of contents (--toc), which I have hacked into the hero-foot. Luckily the HTML is rendered OK when --toc-depth 1.

  <section class="section">
    <div class="content">

This is the important part; the $body is wedged into a content div. Pandoc mostly outputs HTML content like the following.

<h1 id="intro">Intro</h1>
<p>The other day I got this <em>incredible</em> urge to resurrect my website and create a portfolio. It is something I have been meaning to do for a <em>long time</em>, but have been putting off due to the pain of dealing with web technology.</p>
<p><strong>The web is awash with unnecessary complexity and bloat</strong>. I don’t even have the patience for learning static site generators, I don’t need anything dynamic and I don’t want to maintain it.

I guess these are HTML ‘content’ elements which Bulma styles sensibly when they are in an HTML element with the content class.

  <footer class="footer">
    <div class="content">
      <p><strong>Richard Palethorpe</strong></p>
    <a href="">@jichiep</a>

And that is the footer…

Making the Pages

OK, brace yourself, I use GNU make to build the site and it is not pretty.

CSS ?= css

inputs = $(wildcard src/*.md)
pages = $(subst src,public,$(
svgs = $(subst src,public,$(wildcard src/*.svg))
pngs = $(subst src,public,$(wildcard src/*.png))
imgs = $(svgs) $(pngs)

all: $(pages) $(imgs)

$(svgs): public/%.svg: src/%.svg
    cp $< $@
$(pngs): public/%.png: src/%.png
    cp $< $@
$(pages): src/std.tmpl
$(pages): public/%.html: src/
    pandoc -s --css=$(CSS) --template=src/std.tmpl --toc --toc-depth 1 $< > $@

    cp res/bulma-0.9.0/css/bulma.css public/css

clean: $(pages)
    rm $(pages)

This will only rebuild files when they change, everything will be rebuilt if the template changes. If you drop an .md file in src/ it will be automatically built. Make has the advantages:

  1. It is available everywhere
  2. It never changes
  3. I have a vague understanding of how it works

I won’t pretend I’m sure this how the Makefile should be written, in fact I’m not sure anyone really knows, but it works. Finally we want to run this on Gitlab CI.

image: pandoc/core:latest

  stage: deploy
    - apk add make
    - mkdir public
    - export CSS=
    - make
      - public
    - master

This is the .gitlab-ci.yml file which more-or-less just calls Make after installing it. The Pandoc image is specified so we don’t need to install that, I could create my own Docker image based on Pandoc’s image, but with Make too. I could do that…


  • Pandoc is fast (enough), so I could write an inotify script to monitor the directory for changes and rebuild/redisplay automatically on save.

  • It might be best to generate parts of the Index. This could be done with the Pandoc JSON filter, that is, outputting the AST, injecting some elements into it (most likely with shell and jq) and passing it back to Pandoc.

  • I’m not entirely sure the header is fully correct on mobile. It seems like the wrong parts disappear. The TOC is a hack and it shows.

  1. Because I realised I was learning the tools on how to do it anyway↩︎

  2. I’m not so popular in some circles for that :-)↩︎