The Great Site Rewrite

I finally did the thing I kept kicking down the road. The site got a proper rewrite, and I wanted to write down what actually changed so it’s on the record. This is the “what and why” behind the new look.

This replaces the old Minima-based site that was live on antitux.dev. The flat “Recent posts” list with pagination, the old tagline, and the bare Minima shell. Everything you see now is the custom build described below.

The short version

It’s still Jekyll, still plain static HTML, still fast. What changed is the structure, the navigation, the branding, and how I keep the whole thing consistent in one place. The old layout was a single flat list of posts; the new one is a real site with a sidebar, categories, and a home page that you can filter. Plus I added a little photo-gallery tag for the posts that are really about photos.

One source of truth

The biggest change is that all the site-wide stuff: the title, tagline, social links, my bio, the logo, the category list which lives in _config.yml. I didn’t want to remember to update my Twitch link in the header, the footer, and the sidebar every time it changed. Now I update it in one file and it shows up everywhere.

That means the social links in the sidebar, the logo in the hero, and the footer all pull from the same place. If I add a new platform, I add one line to the config and it appears in the one shared site-links include I use everywhere.

The new layout

The site now has a proper shell instead of one long scrolling page:

  • Hero: the site logo and tagline at the top. Clicking anywhere on it takes you home; clicking the logo or an actual link inside it does the normal thing.
  • Sidebar: the main navigation. It’s split into sections: Main (Home, About, plus pinned posts like Twitch Stream, My Homelab, and Extreme Overclocking), Filter (one link per category), and Links (all my socials from the config).
  • Home grid: Posts render as cards with the date, category, title, and a short excerpt, with an optional image at the top of the card.
  • Footer: a single legal line: © year, the Privacy link, and RSS. Kept deliberately boring.

Category filtering

The thing I’m most happy about is the home page filtering. The sidebar’s category links don’t just scroll you to a spot — they filter the grid so you only see posts in that category. Click a category to filter, click it again (or click another) to reset it.

It’s plain JavaScript in assets/main.js, no frameworks. A few details I care about:

  • The active filter is reflected in the URL hash (e.g. #overclocking), so you can share a filtered view and it restores when you load the page.
  • If a category has no posts yet, you get a friendly “no posts in this category yet” note instead of a blank grid.
  • The categories in the sidebar render in a stable case-insensitive order, so adding a new category doesn’t reshuffle the whole list.
  • Filtering is client-side and instant so there’s no page reload.

The site loads one third-party script (Umami) for analytics, but only after you explicitly approve it. A small cookie banner asks up front with Approve / Deny, the choice sticks in localStorage, and the Umami <script> tag never gets injected unless you hit Approve. Denying is the same as not being tracked at all! There’s no fingerprint, no pixel, no fallback. The banner is plain HTML/CSS/JS, no consent-management library.

Hiding the utility pages

Some pages aren’t really “posts” in the way the grid implies — things like the About page, the Twitch Stream page, and the Extreme Overclocking page are more like permanent references. I added hide_from_grid and hide_meta front-matter flags so those pages don’t clutter the home grid but are still reachable from the sidebar.

Custom Liquid filters

Two small Ruby filters live in _plugins/titlecase.rb. Both are the kind of thing where the built-in felt close enough to use, until it wasn’t.

  • titlecase: Jekyll’s capitalize only uppercases the very first letter of the string, so a category like “3D Printing” rendered as “3d printing” in places. This filter uppercases the first letter of each word and leaves the rest alone, which handles digit-leading words correctly.
  • category_names: Jekyll’s site.categories is a hash of pairs, and the built-in sort (and even a for tag over a filtered array) misbehaves on it in this Liquid version. This filter returns a plain, case-insensitive sorted array of category names, which the sidebar then iterates over a variable.

A note for anyone poking at this: these filters are auto-loaded from _plugins/ by Jekyll, so they should not be listed under plugins: in the config, and doing that errors out. That bit me and I’m writing it down so I don’t do it again.

Photo galleries, the lazy way

Some posts are basically just a set of photos with a caption each — the ASUS OC-event writeup is the one I did this for. Instead of hand-stuffing a pile of <img> tags and then a lightbox library, I wrote a single custom tag that does the whole job. One line in the post body:

{% gallery 'assets/img/asus-xoc-event' %}

That’s the whole point of the design. The tag (in _plugins/gallery.rb) scans the folder for images and renders a responsive grid where each item has its thumbnail, a caption, and the full-size URL wired up for the lightbox. The lightbox itself: the overlay, the caption, the prev/next arrows, and the close button, is self-contained HTML/CSS/JS that the tag emits once per page. No jQuery, no lightbox dependency, no separate script to remember to load. Click a thumbnail to zoom, arrow keys or the arrows to step through, Escape or the close button to back out.

The part I didn’t want to fudge was the metadata. A raw camera JPEG carries EXIF and XMP with it, and that’s location data and gear I don’t want sitting in a public image. So at build time, after Jekyll has written the site, the plugin does two ImageMagick passes: it generates the cropped square thumbnails (-auto-orient first, so portrait photos don’t come out sideways) and then it strips every EXIF/XMP/IPTC profile out of the published copies. The originals in assets/ are left untouched, so I keep the full-res files locally and visitors only ever get the clean, metadata-free versions. All of this runs in the Docker build stage — the serving stage is just nginx so the stripping is part of the build, not something I have to remember to do.

The site finally has real branding. Two pieces, both pulled from _config.yml:

  • The logo: the horizontal ANTITUX mark renders at the top of every page in a hero strip with the tagline. Clicking the hero (or the logo) takes you home. Leaving logo blank in the config hides it entirely, so this stays optional.
  • The favicon: I traced the chip from the logo into a crisp SVG favicon (assets/favicon.svg): a white CPU outline on a black square. SVG means it stays sharp at any tab size and it’s a single small file. Wired in with a single <link rel="icon"> in the head include.

Building and shipping

The site builds with a standard Jekyll setup. The important gotchas I hit:

  • Run jekyll clean before jekyll build after touching front-matter or templates. A stale _site/ from a previous build has caused me to “see” bugs that weren’t actually there.

For shipping, I updated the container: an Ubuntu 26.04 build stage installs Ruby and Jekyll and generates the static site, then an nginx stage serves it. So the whole thing is a two-stage build that produces a small, boring, fast static site.

Because the rewrite changed the URL structure, a bunch of the old links (the /xoc/ page, the per-post paths like /My-Homelab/, /Playing-with-Globals/, and so on) would otherwise just 404 once the new site went up.

So I put a small nginx config in nginx/antitux.conf that adds a return 301 for each old path, pointing at its new category-based location. I used relative redirect targets (no hardcoded host) so the same config works whether the site is served as antitux.dev, www.antitux.dev, or localhost, and I wired it into the nginx stage of the Docker build. Real 301s are better for search engines than a meta-refresh bounce, and it means old bookmarks, shared links, and existing search results all still land on the right post instead of a dead-end 404.

What I’m not doing

I deliberately kept it simple. No build tooling, no JavaScript framework, no database, no consent-management SDK, no icon font. It’s a blog. It should stay fast, easy to write in, and easy to host anywhere. If a change needs a whole toolchain to justify it, it’s the wrong change.

That’s the rewrite. If something looks off or a category behaves weird, let me know! The point of all this is that the site should stay out of the way and let the content do the talking.