Intro
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 generators2, 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)3 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 \
src/pandoc-bulma-static-site.md
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="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$>
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
$for(author-meta)$<meta name="author" content="$author-meta$" />
$endfor$
$if(date-meta)$<meta name="dcterms.date" content="$date-meta$" />
$endif$
$if(keywords)$<meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$" />
$endif$<title>Richie's $pagetitle$</title>
$for(css)$<link rel="stylesheet" href="$css$" />
$endfor$<style>
.html()$
$styles</style>
$if(math)$
$math$
$endif$<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
<![endif]-->
$for(header-includes)$
$header-includes$
$endfor$</head>
Pandoc templates have support for various control structures (branches and loops) and inserting variables. For example
$for(css)$<link rel="stylesheet" href="$css$" />
$endfor$
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.
<body>
<section class="hero is-small is-warning is-bold">
$for(include-before)$
$include-before$
$endfor$
$if(title)$<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="/">
/index</a>
<a class="navbar-item"
href="https://gitlab.com/Palethorpe">
<img src="gitlab_logo.svg"></img>
</a>
<a class="navbar-item">
</a>
</div>
</div>
</div>
</nav>
</div>
<div class="hero-body">
<header id="title-block-header">
<h1 class="title">Richie's</h1>
<h2 class="subtitle">$title$</h2>
</header>
</div>
$endif$<div class="hero-foot">
<nav class="tabs is-boxed is-pulled-right">
$if(toc)$<div class="container">
$table-of-contents$</div>
$endif$</nav>
</div>
</section>
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">
$body$</div>
</section>
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>
<p>richiejp@f-m.fm,
<a href="https://twitter.com/jichiep">@jichiep</a>
</p>
</div>
</footer>
</body>
</html>
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,$(inputs:.md=.html))
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/%.md
pandoc -s --css=$(CSS) --template=src/std.tmpl --toc --toc-depth 1 $< > $@
public/css:
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:
- It is available everywhere
- It never changes
- 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
pages:
stage: deploy
script:
- apk add make
- mkdir public
- export CSS=https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css
- make
artifacts:
paths:
- public
only:
- 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…
Fancy links
You know how links to most websites display as a fancy box or picture on Twitter? Well those are “Twitter Cards” and require Twitter Card Tags. Or Open Graph Protocol tags invented by Facebook. I assume the latter works on more websites. I added both.
For this we need to start using YAML metadata in our markdown.
---
title: Creating a static site with Pandoc and Bulma
description: Duck tape edition of static web site generation
---
This goes at the top of each markdown file. It replaces the
%
line(s) which only allow title, date and author meta
data. To use YAML we need to add
--from=markdown+yaml_metadata_block
to the Pandoc
arguments.
The head element of the HTML template has the following added to it.
<head prefix="og: https://ogp.me/ns#">
...<meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="@jichiep" />
<meta property="twitter:creator" content="@jichiep" />
<meta property="og:type" contents="website" />
$if(title)$<meta property="og:title" content="$title$" />
$endif$
$if(description)$<meta property="og:description" content="$description$" />
$endif$ ...
There is more stuff you can add of course. Including an image. Images are always better than just text.
Dev server
Originally I was using python3.9 -m http.server 8000
from the public folder to serve my website locally. That’s not fun
though is it? Instead I have now written a minimal HTTP static file
server.
If you have GCC installed then you can build and run this with
$ mkdir build
$ make build/self-serve
$ build/self-serve public
Then point your browser to localhost:9000
.
TODO
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.
Because I realised I was learning the tools on how to do it anyway↩︎
I have since learned Next.js and SvelteKit. There is a plugin to write Markdown in Svelte files, so it will beat the pants off Pandoc in a straight up fight. However you have to be brave enough to run ‘npm’↩︎
I’m not so popular in some circles for that :-)↩︎