blog tech stack, 8 years later

programming meta

Recently, I made a thing that updates my friends in Discord whenever someone in our webring releases a new blog post, by scraping our RSS/Atom feeds. Because I’m expecting an actual audience now—and these people know me—I’ve been working on some miscellaneous updates to my blog, and I’d like to give an update on the current state of my blog’s tech stack after 8 years.

Switching to Jekyll

In my initial post about the blog, I described how I used a Node.js build script to update my blog. However, three years after, in 2021, I decided to switch my blog to Jekyll. The reasons for this switch have been lost to time, but my best guess is:

  • I got too lazy to have to run a build script every time I made a change.
  • The repo’s file tree was getting increasingly long because every post’s generated HTML was in its own folder at the top level of the repo.1
  • Having any number of npm dependencies was annoying because I kept getting Dependabot notifications—I hadn’t yet realized I could turn those off. Plus, every Markdown feature I wanted had to be a separate npm dependency.
  • Having a build script offered flexibility, but in general it’s a pain to maintain scripts over many years because I end up having to re-figure out how everything works, my coding styles change, and dependencies become abandoned.

Some of these issues can nowadays be mitigated by using a GitHub workflow, but that wasn’t yet an option back when I made the switch to Jekyll. Back then, Jekyll was the only static site generator natively supported by GitHub Pages.

Switching to Jekyll turned out to be fairly straightforward because my build script had ultimately been reimplementing Jekyll’s features from scratch, like Markdown rendering, code highlighting, and templating, so not too many changes were needed to my posts and templates.

Jekyll features

Despite the lack of arbitrary scripting, Jekyll and its plugins supported by GitHub Pages offer far more than just basic Markdown rendering and HTML templating.

Jekyll, by default, uses Kramdown’s flavor of Markdown. Recently, I discovered that Kramdown introduces several Markdown features not seen in GitHub-flavored Markdown, including “inline attribute lists,” which allows you to syntax highlight inline code:

Minifiers often convert `undefined`{:.language-js} to `void 0`{:.language-js}, even though they could just declare a valueless variable once (e.g. with `var u`{:.language-js}) and reuse it.

Minifiers often convert undefined to void 0, even though they could just declare a valueless variable once (e.g. with var u) and reuse it.

I’ve retroactively applied this to all previous posts, and my CTF writeups now have new splashes of color (example).

Jekyll also has a plugin, Jekyll Feed, that can generate an Atom feed2 for the posts in my blog. I’ve since dropped the plugin and copied the template they use so I can have full customizability over it without needing to read their docs.

Commenting

I was inspired by Matt’s Minesweeper in his GitHub profile readme, which uses a prefilled GitHub issue link and a GitHub Actions workflow to let anyone persist their Minesweeper changes to the repo.

The comments system on my website uses a similar setup:

  1. At the bottom of each post, “Write a comment” links to a pre-filled GitHub issue that stores the post ID in the issue title and body. It also selects an issue template that adds a comment label to the issue.

  2. A GitHub workflow runs when an issue is created, and it checks for the comment label.

  3. If the issue is a comment, it runs a Node script that,
    1. Extracts the post ID from the issue title or body.

    2. Renders the issue body to Markdown.

      This essentially reintroduces a Markdown build script back into my blog, but because I’m injecting untrusted user input into my blog3, I felt that it was better to store the generated HTML in my repo rather than letting Jekyll render it.

    3. Adds the comment to a YAML file.

  4. The workflow pushes the changes, which in turn closes the original issue and triggers a GitHub Pages deployment.

The entire comments YAML file is available in Jekyll under site.data.comments, so my post page template uses it to render the comments under the corresponding post.

While the comments feature hasn’t seen much usage, I ended up pulling the implementation out to a separate project, my guestbook, which did receive more usage.

SEO

Similar to what I did for the Atom feed, I copied the Jekyll SEO Tag plugin’s template and customized it for my blog. The template is allegedly battle-tested for SEO4, but this change is too recent for me to confirm.

I also wanted to add support for Open Graph preview images to my blog posts, but most of my blog posts don’t need or have any images. I decided to generate a default preview image using Typst CLI, which is what Cargo uses for its preview images. I think it looks pretty nice.

"Longer Tweets" and the blog post title on a blurred photo of leaves in rain

Unfortunately, this requires me to run a build command to generate the preview image, so in a way, I’ve circled back where I started.

Writing posts

I author these posts in Markdown in VS Code, but I personally find monospace prose difficult to read. Since VS Code supports proportional fonts, I’ve set the following settings for this workspace:

{
  "[markdown]": {
    "editor.wordWrapColumn": 80,
    "editor.wordWrap": "bounded",
    // This font reminds me of early 2010s websites, like Scratch forums, so I
    // think it's the most legible sans serif font
    "editor.fontFamily": "Verdana"
  },
  "workbench.colorTheme": "Dark 2026"
}

Screenshot of my VS Code setup for this repo

I hope it doesn’t look too cursed.

  • I set the text to wrap at 80 characters5 because long lines of text, especially for a more dense font like Verdana, are hard to read.

    This also reduces the need for me to manually wrap text in the Markdown source. In the past, I’ve tried wrapping text with Rewrap, but the diffs become messy, so then I tried writing one phrase or sentence per line, but I found it affected my writing style.

    VS Code’s zen mode could work for this, but I don’t want to have to explicitly switch in and out of it, and apparently its width is not configurable.

  • As alluded to in the comment, I chose Verdana because it’s a bland, inoffensive font that I’m used to reading on older-designed forums, like Old Reddit and Hacker News.6

    One downside of proportional fonts is that spaces are very narrow, so nested lists are even more difficult to follow.

  • Normally, I use the Tomorrow Night theme for code, which is also the color scheme used on this blog, because I grew up with it as the default scheme on Codecademy, where I first learned to code.

    However, VS Code’s built-in Tomorrow Night theme doesn’t seem to be as featureful for Markdown—it doesn’t highlight bold, italic, or inline code text—and it was apparently archived in 2024. At least for this repo, where I expect to be writing more Markdown than actual code, I’ve switched to VS Code’s default theme, which is probably better supported.

    Dark 2026 Tomorrow Night
    Screenshot of bold, italic, and code text in Dark 2026 color theme Screenshot of bold, italic, and code text in Tomorrow Night color theme

Other recent design changes

I’ve changed the default background image to make the brightness more consistent throughout. It is a blurred photo of a flowering plant in the rain, and the white flower created a bright spot under the text that made me feel like there was something in my eye. I’ve regenerated the blurred background and adjusted the curves in Photopea to dim the bright spot.

I’ve also increased the text contrast by dimming the background and making the text brighter.

I think in the future, I’ll explore how to make the “Enable accessible theme” button more discoverable. I think the accessible theme is tolerable, but the default browser dark theme makes the text harder to read with my mild astigmatism.

In iOS 26, the new Safari design doesn’t allow websites to put anything under the browser UI, even though it shows the rest of the page content under it as you scroll, resulting in an ugly black background at the top and bottom edges of the website. I don’t think I’ll bother addressing this for now until iOS Safari fixes itself, e.g. by accepting viewport-fit=cover, which ironically Apple themselves invented for the iPhone X.

I’ve also added semantic HTML to the blog pages, which allows browsers like Firefox and Safari to detect and suggest reader mode as an alternative to the accessible theme.

  1. For whatever reason, I wasn’t able to get gh-pages working with my build script back then. 

  2. Atom is like a newer version of RSS. 

  3. Rendering untrusted input as Markdown is generally unsafe for two reasons:

    Both unsafe features are enabled when rendering my blog because, well, I trust my blog posts, but they should not be enabled for the untrusted comments people can post under my blog. 

  4. SEO = search engine optimization 

  5. The width of a “character” for a proportional font is probably based on the glyph for zero (0) because that’s what CSS uses for 1ch, and I think that’s the standard for typography in general. 

  6. Apparently the Scratch forums use Arial, not Verdana. 

See source and revision history on GitHub.

Write a comment