How I built this website

averne

May 2025

Introduction

I’ve been wanting to set up a blog for a long time, but was deterred by the initial setup time and my total inexperience with web technologies. Applying to Google Summer of Code finally forced my hand, as I have to produce easily accessible reports on my progress.

One thing I wanted for it was to use a theme reminiscent of Tufte’s work.Tufte’s work distinguishes itself from its recurrent use of sidenotes (such as this one), as a way to provide context, examples, figures, etc. that are not strictly necessary to convey the main idea.
This theme was used in several textbooks (such as the Feynman lectures books), and I liked it enough to want it for my blog.

The first thing I tried was the Hugo static site generator. While the initial setup was easy, and it allowed me to write content in Markdown, it broke down when I tried to apply the Tufte themes that were developed (but largely unmaintained) by the community, probably because of some deprecated features. I also saw a similar theme for Jekyll, also mostly inactive, and after being burnt out from wrangling with Hugo I didn’t bother giving it a try.

Instead, I decided to take the high road and use Pandoc with the regular tufte-css stylesheet, along with a custom Lua filter to make use of certain features of the CSS, particularly sidenotes.

Pandoc setup

Pandoc allows converting Markdown to HTML, and features a huge array of language variants and extensions. In addition, it supports Lua filters which operate on its internal AST, making it very flexible.

CSS sheets

Pandoc allows passing stylesheets with the --css/-c command switch. When paired with the --standalone/-s, it will insert a linkA subtlety is that since it is simply an unprocessed URL, one can pass web URLs as well, which is what I do for the math font (Libertinus). to the provided URL in the HTML header.
The Tufte CSS sheet, as well as another sheet for an icon font, are included this way.

Markdown variant and extensions

The language variant I use is gfm (“GitHub flavored Markdown”), since it supports double-space linebreaks, LaTeX maths, footnotes,Though gfm unfortunately doesn’t support inline footnotes, at the time of writing. etc. In addition, I enable the extensions yaml_metadata_block (which allows specifying a title, author and date for the document in yaml format), bracketed_spans (which allows convenience syntax for things like small caps), and smart (which detects quotes, em- and en-dashes, ellipses, and replaces them with the correct typographic output).

Lua filter

The Tufte CSS stylesheets needs some HTML tags to function correctly, namely <article> which denotes the main body, as well as <section> for individual text blocks. These are injected with a Lua filter which processes the whole document AST, looking for headings. Top-level headings are enclosed in the <article> tags, while others are surrounded with <section>.

In addition, the script gives each heading its own local link, and the CSS will inject an icon which changes opacity when hovered.
Cross-file links are also fixed-up to point to the correct HTML file.

Sidenotes

The Lua filter script converts footnotes into sidenotes that fit the Tufte theme. This is done by injecting the correct HTML tags to let the CSS style them correctly.
In addition, figures are given captions through Markdown’s alternative text. The captions are turned into margin notes by the Lua filter.

Code & Math support

Pandoc will perform syntax highlighting on code blocks, and the CSS sheets encloses them in scrollable boxes.

Math is written in LaTeX syntax. Pandoc supports several options for turning this into web-ingestible content. I chose MathML (--mathml), because it is a web-native feature, and does not rely on pre-rendering or an external service.
However, the rendering isn’t perfect. In particular, Pandoc sets the stretchy property to true on every bracket, leading to Chrome adding a lot of extraneous whitespace.See this issue.
Firefox is also better at this, for some reason.

Header/footer

Custom code is injected in the HTML header, as well as before and after the main body, respectively using the --include-in-header/-H, --include-before-body/-B, --include-after-body/-A options. The code adds a link to the homepage, and some decorations.

Build & Deployment

The build process is managed by a Makefile, which performs HTML compilation, and copies resources to the web folder. The Make script supports multi-threaded and incremental builds, though the whole process is so fastAround 0.25s for a clean build. that it barely gives a noticeable speedup.

I wrote the homepage HTML by hand, and for convenience I added a target to the Makefile that autogenerates a list of blog posts by grepping the title blocks in the Markdown. At the moment, this list must be updated by hand, though in the future I might automate it.

In addition, the Makefile can launch a simple HTTP serverI use python -m http.server. to test things locally before pushing to the web.

Finally, deployment is done on repository push from a GitHub action.

Conclusion

Though setting up this website took me a couple days, I’m very satisfied with how it looks, and overall happy about the time I put into it. I also finally have a modicum of web experience.

This page weighs around 400 kB across 11 requests, requires no JavaScript, and loads in 50 ms according to Chrome Devtools. It could probably be optimised somewhat but at the moment I’m comfortable with it.
Building is also extremely fast, and requires no dependencies outside of Pandoc and common shell utilities.