Adventures in making this website:
reviewing the basics

Published on 2022-11-20, last updated on 2023-05-19.

Making and managing this personal website has been an adventure. In this series, I go over the technical challenges I’ve encountered and philosophical decisions I’ve made, and I review some of the tools I’ve used along the way. After part 1, this is part 2, followed by part 3 and part 4.

After the previous article about static site generators, let’s take a step back: as you probably know if you’re reading this, a modern websites consists of three components:

  1. HyperText Markup Language (HTML), an Extensible Markup Language (XML) that describes the structure of the page, e.g. top navigation bar, footer, titles, paragraphs, etc. For arguably historic reasons, it does some basic styling too, like making text italic, and it provides basic interactive elements like buttons. In recent years, it’s also been increasingly controlling the browser’s interaction with external resources.
  2. Cascading Style Sheets (CSS) that control the visual design of the page. CSS consists of rules referring to certain HTML elements, and set things like colours and fonts. Due to limitations of HTML, CSS also tells the browser how to handle the page’s HTML structure, by specifying e.g. elements’ widths and positions.
  3. JavaScript (JS), or specifically its ECMAScript (ES) standard, is a programming language that can do many things: it can interact with a “dynamic” copy of the HTML structure to e.g. add interactive animations, it can handle complex server-browser communications, etc…. or it can mine cryptocurrencies behind your back. In principle, JS is used to do anything that HTML and CSS can’t, but in practice some features of HTML/CSS are so obscure that many web developers use JS anyway out of ignorance, or for compatibility.

This website barely uses JS, so the rest of this article is just about HTML and CSS. Let’s start by asking ourselves if those technologies are any good…

The good: HTML

Right-click on this page and select “View Source” (or something similar) to see the HTML file where the magic happens (if you’re reading this on a mobile device, you’ll just have to trust me when I say it’s magic). Looks like a mess, right? Well, in my opinion, HTML (or really any XML) is a good fit for the web, because its hierarchical structure is a natural way to describe UIs.

However, the kind of UI being described by HTML has changed a lot over the decades: once upon a time it was just one big block of text, now we have interactive apps. The web has outgrown a lot of HTML’s original features, but at the same time HTML has kept up with the Internet’s needs just fine, thanks almost entirely to the <div> tag.

Sure, we could have some philosophical debates about HTML’s design decisions. Why is there a distinction between <i> and <em>? What’s the point of some of HTML5’s new semantic tags if I can just replace them with <div>s? Don’t the new media elements (e.g. <picture>) maybe control the browser’s behaviour a bit too explicitly? And whose idea was <marquee> anyway? But overall it holds up pretty well to scrutiny.

I’ve heard people suggest that instead of XML, HTML should’ve used S-expressions. Yes, I wish we lived in a world where HTML and JS were both replaced by a single Lisp! That would be wonderful, but unfortunately this is the world we live in; the sooner we accept it, the better. And if I’m honest, I like how XML shows me the element name in the closing tag)))))).

The bad: CSS

CSS, on the other hand, is a pain. I want the following statement on my gravestone: “At least 33% of the CSS you write is for fixing the unexpected behaviour of the other 67%”. I came up with this rule of thumb a few years ago, and I don’t think it’s failed me yet, although note that I include code churn under “writing CSS”. My site’s main.css is quite simple, but I’ve obsessed over it for many hours, trying to trim all the fat without breaking anything.

Although CSS is well-standardized, browser compatibility appears to be a surprisingly big issue. This why we have projects like normalize.css, destyle.css, and reseter.css, which I learned about later than I should have. See the vertical line under this page’s header? You wouldn’t believe how many times its location surprised me while testing different browsers and devices. Sure, you can argue that using an <hr> isn’t ideal here, but isn’t this its intended purpose?

The fundamental issue is that CSS is trying to handle two things at the same time: formatting and layout. Formatting refers to fonts, colours, text alignment, cursor icon, borders, etc., and is quite simple: it shouldn’t be able to surprise you and barely needs to adapt to its environment, and can therefore easily be handled by CSS.

Layout, however, is a hard problem. Defining the positions and sizes of elements on your device is doable, but what about other devices? Does your text fit in that box? Where did that awkward line break come from? Those elements were side by side, so why are they suddenly under each other, and partially overlapping the rest of the page? Half of writing CSS is getting things to look good on phones; it’s a lot harder than you’d expect! Is this a phone, or an awkwardly-shaped desktop window? Why is my font size not honoured on mobile? Are you sure that isn’t a tablet? Or what if they connect their phone to a monitor? Wait, is that a mouse?

Nowadays, we have technology that makes layout easier, like flex and grid layouts, but these are clearly afterthoughts. The CSS language itself is very basic (but still Turing-complete), resulting in quite repetitive code with a lot of hard-coded values. Some aspects are improved by e.g. Sass, a “syntax wrapper” that helps avoid repetition. But Sass doesn’t address any fundamental issues, and CSS’ newer quality-of-life features don’t work well (or at all) on older browsers.

The ugly: Microsoft

Ah, the subject of backwards compatibility, always a fun one. When Microsoft released Windows 10 in July 2015, they introduced the Edge browser and deprecated Internet Explorer (IE), which in itself is just normal software churn. The catch is that Windows 7 (then in extended support) and Windows 8.1 (then still in mainstream support) weren’t getting Edge, and were thus stuck with IE version 11.0 forever. In effect, they left many users without an up-to-date browser… for 4 years, until they ported Edge to Windows 7 and 8.1 in June 2019.

Okay, some people are using old Windows versions, but who uses IE anyway, am I right? Isn’t its only function to download other browsers’ installers? Well, as of writing, IE makes up about 0.8% of the global desktop market (source). In fact, for my job I need to use a virtual environment with only IE (although it fortunately isn’t exposed to the Internet). When Microsoft decided to ship a browser with their OS, it became their responsibility to maintain it for the fast-evolving Internet. Microsoft, if you’re reading this, think of all the revenue you missed out on by not tracking many of your users’ browsing for 4 years! Don’t you want to be Google?!

The reason I bring it up is that IE still casts a long shadow over web development, due to its lack of support for many recent features in HTML, CSS and JS. If you try to do something fancy, or just use modern best practices, you need to ask yourself if you want to support IE. If you choose yes, your implementation will probably look quite different. Take a look at the “Can I use?” website to check which browsers have which features; often IE lags behind.

Some tips and tricks

As this website gradually became more advanced, I found some interesting tricks to optimize things or enable new kinds of content. In this series’ future posts, I will dive into some topics in great detail, but here I’m putting anything that won’t fit elsewhere.

Automatic light/dark theme

Depending on how you’ve configured your browser or operating system (pretty much the same thing nowadays), this website should have either a light or dark background. I implemented the default light colour scheme as follows in CSS:

/* Light theme (default) */
:root {
	--b: #ededed; /* background color */
	--f: #121212; /* foreground color */
	--a: #0000ff; /* link color */
}

The prefix -- is required for so-called custom properties, which will act as variables containing the colours of the theme. The dark theme automatically overwrites these variables:

/* Dark theme */
@media only screen and (prefers-color-scheme: dark) {
:root {
	--b: #121212;
	--f: #ededed;
	--a: #ffff00;
}
}

Here @media is a media query, which tells CSS how to behave depending on the environment in which it’s being interpreted. The magic words here are prefers-color-scheme: dark, such that, if your browser knows you like dark themes, it will apply the corresponding CSS rules.

If you were wondering: only screen means that the rule only applies when the website is shown on a screen, instead of being printed. But actually, that isn’t the reason it’s there (I mean, who prints webpages?). The real reason is backwards compatibility with older browsers (i.e. IE) that don’t understand prefers-color-scheme (but do know screen), to prevent them from applying the rules underneath anyway. Basically, an old browser will see only while it’s looking for a media type like screen; since only isn’t a valid type, it decides “this media query doesn’t apply to me”, and moves on, ignoring the rules in this block.

So, now that the variables --b, --f, and --a are set correctly, we apply them using the function var() as follows… assuming the browser even supports CSS variables like this:

body {
	/* Light theme fallback (for IE, duh) */
	background: #ededed;
	color: #121212;
	/* Apply selected theme */
	background: var(--b);
	color: var(--f);
}

In modern browsers that do support var(), the latter lines simply overwrite the earlier ones.

But what if there’s some content on the website with hard-coded colours, like a logo? Well, then things get a lot more complicated. Either you rethink your design to allow the same image to be used in multiple themes, or you serve different images. The latter approach is becoming easier thanks to the brand-new Sec-CH-Prefers-Color-Scheme HTTP Header, which asks the server to send over dark-themed resources. If you’re interested, read this article.

For me that’s all too much work, and this is supposed to be a static website after all, meaning there shouldn’t be any fancy client-server negotiations. Instead, I chose my colours such that the dark theme is simply the inverse of the light one: if an image is suitable to invert in this way too, I simply give it the .darkinv class and apply the following CSS:

.darkinv {}
@media only screen and (prefers-color-scheme: dark) {
	.darkinv {filter: invert(100%);}
}

This is what I’ve done on the front page. Obviously, IE doesn’t support the filter property.

Collapsible content

A common thing you might want to create is collapsible content: a box that shrinks or expands when the user clicks a toggle. Fortunately, HTML can do this out-of-the-box using the <details> and <summary> elements. Great, no JS required! These tags were only recently added to HTML, so (can you see where this is going?) they’re unsupported in IE. A demonstration:

Click me, I dare you Here we can bully IE users, because they can't close this `<details>` box: IE is old and slow, and its users are even older and slower, and they're also smelly! Take that!

So… time for JS? Well, fortunately, there’s another way, using just old-school HTML and CSS:


This is a safe space where users of all browsers can rejoice together in peace.

Pretty cool eh? This box is structured as follows, using only <div>, <input> and <label>:

<!-- Wrapper for applying CSS to the entire collapsible object -->
<div class="collapse">
<!-- (Invisible) checkbox that controls the content's visibility -->
<input type="checkbox" class="collapse-toggle" id="my-example"/>
<!-- (Visible) text label standing in for the checkbox -->
<label for="my-example">Click to show</label>
<!-- Wrapper for what should be hidden when the checkbox isn't checked -->
<div class="collapse-hidden">
<!-- Replacement text label for when the content is being shown -->
<label for="my-example">Click to hide</label>
<!-- The actual content that we want to show/hide -->
...
</div>
</div>

And then the magic happens in CSS, where x + y means “if <x> is immediately followed by <y> in the HTML document, then apply this rule to the <y> element”:

/* Hide the content by default */
.collapse-hidden {display: none;}
/* Make the checkbox invisible */
.collapse-toggle {display: none;}
/* If it's checked, then show the content... */
.collapse-toggle:checked + label + .collapse-hidden {display: block;}
/* ... and hide the original text label */
.collapse-toggle:checked + label {display: none;}

Compared to <details>, this even gives some extra flexibility! For an example of a page using this technique, click here and scroll down. If you look at the source, you’ll see that my HTML and CSS are a bit more complicated than described above, but it’s still the same trick.