Header logo.
Tonghe's Notes

Creating My Own Static Site Generator

Since I take a lot of notes, rcently I thought I could edit some of my notes and turn them into a blog. I wasn't satisfied with WordPress, because it's bloated. I didn't feel like to get familiar with the settings and configurations of other static site generators either. So I decided to write my own. I named it “Lysekil”, where I visited in late March and loved it there. I published it on GitHub and provided a brief documentation.

The color scheme used in the template was borrowed from the default “Red Graphite” theme of Bear, my favorite note-taking app. (It also happens to be Andy Matuschak's favorite note-taking app. I used to dither over a few other options, including Obsidian, RemNote and RoamResearch. In the end I would always come back to Bear.)

How I made it

I used third-party Python packages to process markdown, generate the Atom feed and highlight code syntax.


Notes in Bear are in a plaintext format very similar to Markdown. Writing blogs in Markdown feels natural.

There are two Python packages available to convert Markdown files into HTML: python-markdown and python-markdown2.

I chose the latter because it allowed for eaiser integration with an extra that enables syntactic highlighting for code. It also seems easier to enable extras (or extension) in general with python-markdown2 than python-markdown.

Syntax highlighting

Both packages allow syntax highlighting using Pygments. It takes a lot more effort to enable code highlighting in python-markdown, according to their documentation.

With markdown2, I could enable “footnotes”1 and “fenced-code-blocks” extras with much ease. To my delight, the “fenched-code-blocks” work exactly the way highlighted code blocks work in Bear.


There are people on the web who call their personal websites blogs without providing an Atom or RSS feed. (I'm opinionated on this. It's not a blog if there's not a feed)

The package I used to create the Atom feed is python-feedgen. It does support both Atom and RSS. But there are tiny differences between the two. I chose Atom over RSS for no particular reason.

One hitch at this step was that Feedgen required timezone info to indicate the time of creation of entries. To make it work, we needed to pass this information using a timezone object.

This timezone object is defined in datetime package. One way you could specify a timezone prior to Python 3.9 was adding an offset to UTC using a timedelta.

Lucky for me, I've upgraded to Python 3.9.5, which provides a zoneinfo package that allows you to specify timezone info using a string, like this: ZoneInfo('Europe/Stockholm'). (The datetime package in Python 3.9.x also makes it easier to manipulate date and time in strings of ISO format.)

Blogs and pages

Blogs (or “notes”) are presented in chronological order and are organized with tags. They are grouped according to quarters of the year. Pages, on the other hand, are not presented in any timeline.

If you look at the source code, you'd see the implementation of this part is not particularly “DRY”.


I used SCSS to write the CSS sheetsheet. SCSS allowed nesting CSS selectors and that makes it a lot easier to organize your CSS file.

I tried adding animation effects for the header using JavaScript. I relied on window.onscroll event and it was a bad idea.

It did work on my MacBook Pro, iPhone and iPad. But because of the offset of scrolling on the y-axis and the difference between widths of shown elements, this doesn't work when the page is not long enough for certain screens.

When the page is not long enough, as the user scrolls up, there isn't an unambiguously large offset that triggers the change of width that can persist. Not to mention the (at least theoretical) performance issue caused by too many function calls as the page goes up. (This in itself can be talked about in more detail.)

I then tried using { position: sticky } to place the navigation bar on top of the article page. It did look nice. But since it's rather easy to navigate around here, letting the navigation bar take up too much space at the expense of reading experience didn't feel right. This was especially true on a smartphone.

What I did wrong

I began working on this SSG on a whim, without clearly planning out what functions I wanted and how I'd like to achieve them.

At the beginning I thought a single script that converted a bunch of .md files to HTML would do. I ended up with a template that consists of HTML code snippets sprinkled across six helper functions. (If anyone wants to create a new template for this SSG, I would recommend not to. LOL.)

To organize timelines, tags and archive groupings, I ended up creating two classes for content (notes and pages) and three classes for listings (tags, archive groupings and the home pagination). Had I planned it beforehand, with more proper inheritance, the classes would have looked DRY-er.

  1. Titta! Detta är en fotnot. (Look! This is a footnote.