Amazing website improvements

2025-10-19

I made some amazing improvements to my website! I completely revamped the backend and vibe coded it in Rust using Codex CLI.

dllup-rs

As you may know, I have my own markup language called dllup so the dllup-rs project contains a full AST and parser of this markup language.

Previously I wrote the website stuff 10 years ago. It was a jumbled mess of about 600 lines of unmaintanable Python. Now, it’s a vastly more jumbled mess of almost 5000 lines of unmaintainable Rust!

But honestly though, the vibe coding experience was fine. It basically just did what I told it to. If I noticed any inefficiencies when running the code, I could just tell it to fix it, and it would do so. What a time to be alive!

1 Structural changes

1.1 Everything is a blog post

Previously, I had sections like “programming”, “engineering”, and “design”. It made sense at the time when I arrogantly considered myself a “renaissance man” during college that could design stuff, program stuff, and engineer stuff.

But I haven’t updated those sections in a long time while I’ve been putting both engineering and programming projects as blog posts so I might as well just migrate all the old stuff as well.

Don’t worry, old links still work as they redirect to the new links with HTTP 301.

1.2 Search engine optimized URLs

Previously, blog posts just had the date as the URL, like y2018m06d18. This was very SEO-unfriendly and also it was impossible to remember which blog post URL corresponds to what content.

Now, y2018m06d18 is 2018-06-18-chicken-rice-in-san-francisco which is a lot more semantic and nice.

2 Styling changes

2.1 Dark mode

The website now supports dark mode using your default browser preference.

2.2 Other tweaks

The rotating DLLU logo is now always big and centered and I removed some nav stuff after simplifying the website structure.

The table of contents always appears under the DLLU logo.

I also slightly adjusted the font (from Source Sans Pro to Inter, just because I had been using the former for a decade) and spacing.

3 Image handling

One of my greatest passions in life is photography and my philosophy to photography is: more pixels = more better. As such, I’ve invested a huge amount into getting the maximum image quality.

However, it is quite difficult to share my 100 megapixel files, so I had to engineer a bunch of stuff for my blog.

3.1 Multiple image sizes

Previously, I had either:

And then I used retina.js to support high DPI displays by having another copy of an image twice the resolution.

But that only worked for local iamges and not remote ones, and sometimes I just forgot to resize the image manually ahead of time, so this was quite annoying. Now, with HTML supporting the srcset attribute in the img tag for years, it is easy to add multiple sizes.

<img
  src="image-600.jpg"
  srcset="
    image-600.jpg   600w,
    image-1200.jpg 1200w,
    image-2000.jpg 2000w"
  sizes="600px"
  width="600" height="450"
  alt="">

Then I added fetching all remote images, automatic resizing of all images and uploading to Backblaze B2 which makes generating all the sizes easy.

Another thing is that previously clicking on the image loaded the full size. This was annoying since you cannot share a 100 megapixel image easily — for example, on Discord, it wouldn’t unfurl the preview, and it may even crash Safari on iPhone.

Now, I have a list of download links for various sizes, so you can download lower resolutions to share.

3.2 Wide images

Previously, all images were the same width. But I often take wide images, like trains.

FIGURE 1 Shinkansen N700 Series, Himeji, Japan, 2017

Now, these are shown wider (mostly on desktop).

3.3 Hosting on Backblaze B2

I store all the images in a directory and upload them to Backblaze B2 with rclone. Then I serve the bucket from behind cloudflare at i.dllu.net. So far, this has worked pretty well to avoid the Hacker News hug of death.

3.4 EXIF data

For my photos, many people are curious about the EXIF data. I implemented automatic EXIF data embedding in photos which makes it nice.

FIGURE 2 Florence cathedral. The f-number on this one is NaN since it’s a manual lens (the Leica Apo Telyt-R 180mm f/3.4) that doesn’t transmit EXIF data.

Image metadata
  • Camera: FUJIFILM GFX100S
  • Lens: 180
  • Aperture: f/NaN
  • Shutter speed: 18.0 s
  • ISO: ISO 100
  • Software: darktable 4.6.1
  • Original date: 2023:02:17 18:09:40

3.5 Page no longer jumps around

While computing the sizes, I also store the image aspect ratio and set the width and height attributes of the image so that the page doesn’t jump around.

Also, I set the decoding="async" loading="lazy" attributes so that they don’t all start loading at once, preventing people’s browers from choking when loading my long blog posts with hundreds of photos.

4 Math

I now use KaTeX to render math on the serverside instead of using mathjax-node-cli to generate SVG equations that are embedded as <img> elements.

The SVG equations worked really nicely actually. I’m quite proud of being able to use the offsets and sizing in em units from the SVG directly to ensure that they flow beautifully in text.

But it was difficult to achieve dark mode toggle, and the file sizes were slightly bigger, so I ended up switching to KaTeX. KaTeX also supports some accessibility stuff nicer and also can wrap around in text.

The drawback is that now it looks uglier in Lynx.

lynx math

FIGURE 3 Screenshot in Lynx with ugly KaTeX equations.

With SVG equations in <img> tags, I set the alt text to be the LaTeX math, so it looked nice in Lynx, and you could simply select it and copy it to copy the LaTeX code for it. Oh well.

5 Tree-sitter code snippets

Now I’m using tree-sitter to syntax highlight code snippets using the inkjet crate. Previously, syntax highlighting was handled by Pygments.

fn highlight_with_inkjet(language: Option<&str>, code: &str) -> Option<String> {
    let mut highlighter = Highlighter::new();
    let theme = Theme::from_helix(ONEDARKER).ok()?;
    let formatter = ThemedHtml::new(theme);
    let lang = language.and_then(Language::from_token).unwrap_or_else(|| {
        Language::from_token("plaintext").unwrap_or(Language::from_token("none").unwrap())
    });
    highlighter.highlight_to_string(lang, &formatter, code).ok()
}

6 RSS feed and sitemap.xml

I also added an RSS feed and sitemap.xml.

RSS feed

7 To-dos

Currently, some things don’t work.

I’ll be fixing all the issues in due time. Please let me know if you find any other bugs.