Header logo.
small hallucinations
homeyearstagsaboutrss

Creating My Own Static Site Generator

Since I take a lot of notes, I recently thought that I could edit some of them and turn them into a blog. I wasn't satisfied with WordPress because it felt bloated. I didn't feel like familiarizing myself with the settings and configuration of another static site generator, either. So I decided to write my own. I named it “Lysekil” after a place I visited and loved in late March. I published it on GitHub and provided brief documentation.

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

How I made it

I used third-party Python packages to process Markdown*, generate the Atom feed, and apply syntax highlighting to code.

Markdown

Notes in Bear use a plaintext format very similar to Markdown. Writing blog posts 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 integrated more easily with an extra that enables syntax highlighting. It also seemed easier to enable extras (or extensions) in general with python-markdown2 than with python-markdown.

Syntax highlighting

Both packages support syntax highlighting with Pygments. According to the documentation, enabling code highlighting in python-markdown takes a lot more effort.

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

Feed

Some people on the web call their personal websites blogs without providing an Atom or RSS feed. (I'm opinionated about this: it's not a blog if it doesn't have a feed.)

The package I used to create the Atom feed was python-feedgen. It supports both Atom and RSS, but there are small differences between the two. I chose Atom over RSS for no particular reason.

One hitch at this step was that Feedgen required timezone information to record when entries were created. To make it work, I needed to pass this information in a timezone object.

This timezone object is defined in the datetime package. Before Python 3.9, one way to specify a timezone was to add an offset to UTC using a timedelta.

Luckily for me, I had upgraded to Python 3.9.5, which provides the zoneinfo package and allows you to specify timezone information with a string, like this: ZoneInfo('Europe/Stockholm'). (The datetime package in Python 3.9.x also makes it easier to manipulate dates and times in ISO-formatted strings.)

Blogs and pages

Blog posts (or “notes”) are presented in chronological order, organized by tags, and grouped by quarter. Pages, on the other hand, are not presented on a timeline.

If you looked at the source code, you would see that this part's implementation is not particularly “DRY.”

Styling

I used SCSS to write the CSS stylesheet. SCSS allows you to nest CSS selectors, making it much easier to organize a CSS file.

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

It worked on my MacBook Pro, iPhone, and iPad. But because of differences in vertical scroll offsets and the widths of visible elements, it didn't work when the page was not long enough on some screens.

When the page isn't long enough, scrolling up doesn't create a sufficiently large offset to trigger and sustain the width change. There is also the possible performance issue created by too many function calls while the page scrolls. (This topic deserves a more detailed discussion of its own.)

I then tried using { position: sticky } to place the navigation bar at the top of the article page. It looked nice. But since it's already easy to navigate the site, letting the navigation bar take up space at the expense of the 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 which features I wanted or how I would implement them.

At first, I thought a single script that converted a bunch of .md files to HTML would suffice. I ended up with a template consisting of HTML snippets sprinkled across six helper functions. (If anyone wants to create a new template for this SSG, I would recommend against it. LOL.)

To organize timelines, tags, and archive groupings, I ended up creating two content classes (notes and pages) and three listing classes (tags, archive groupings, and home-page pagination). Had I planned it in advance and used a better inheritance structure, the classes would have been DRY-er.


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