Building this site without a framework
This site is a handful of HTML files, one stylesheet, and a pixel font. No framework, no bundler, no node_modules folder in what gets deployed. This page, with the stylesheet and font it loads, is about 46 KB, a fraction of what most sites send in JavaScript alone.
In a previous role I worked on a dedicated UI team at a large tech company, and I remember the spaghetti CSS. It could take hours to dig through layers of conflicting styles to make one corrective change, and we once spent months on a project just to reskin existing components in a monolithic codebase. Frameworks and component libraries were how teams kept that under control, and reaching for one became a habit, even for small sites.
With a good coding agent, that trade-off has flipped. For something small, plain HTML and CSS are now the fastest way to build, and the result loads instantly and will still work in ten years. Here's how this one is put together.
The whole toolchain
There's no framework in the browser. To preview the site, I first ran Python's built-in web server from the project folder:
$ python -m http.server 8000 --directory site
Serving HTTP on :: port 8000 (http://[::]:8000/) ...
Paths are root-relative (/style.css), so opening the files straight from disk doesn't work. A local server was the only tooling the site needed. Weighing everything that gets deployed (the _site folder) takes one line of PowerShell:
PS C:\speclabs> Get-ChildItem _site -Recurse -File |
>> Measure-Object Length -Sum
Count : 18
Average :
Sum : 113109
Maximum :
Minimum :
Property : Length
One grid for the whole page
The layout is a three-column CSS grid on <body>: two flexible gutters and an 85-character column in the middle. The header uses subgrid, so the wordmark lines up with the main column without any extra math.
body {
display: grid;
grid-template-columns: 1fr minmax(0, 85ch) 1fr;
gap: 2rem 1.5rem;
}
/* The header shares body's columns, so its contents align with <main> */
body > header {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
}
85 characters is a comfortable line length, and it leaves just enough room for a 736-pixel image, exactly twice the width of a standard post preview.
Borrowing from text mode
The look comes from the PC's text mode: a grid of 80 by 25 character cells, each holding a character and a color. DOS programs often skipped the operating system and wrote straight into video memory at segment B800. In Turbo C, that took a few lines:
#include <dos.h>
/* Text mode: 80x25 cells, two bytes each (character, color) */
unsigned char far *screen = MK_FP(0xB800, 0);
void put_str(int x, int y, const char *s, unsigned char color)
{
unsigned int offset = (y * 80 + x) * 2;
while (*s) {
screen[offset++] = *s++; /* the character */
screen[offset++] = color; /* 0x1F: white on blue */
}
}
int main(void)
{
put_str(36, 12, "SpecLabs", 0x1F);
return 0;
}
The wordmark uses the 8×16 font the VGA card had in ROM, the same character cells the HackerZ Hideout's ANSI art was drawn in, rebuilt as a web font by VileR. It's shown at exactly 32 pixels, twice its native size, so every pixel of a glyph lands on whole pixels of the screen and stays sharp.
The JavaScript I deleted
The first version had one script. The menu needed to know which page you were on, and while the pages were still placeholders, a few lines of JavaScript read the URL and marked the right link:
// Mark the menu link for the current page. A stopgap while
// every "page" was a #hash on one placeholder file.
const here = location.hash || "#home";
for (const link of document.querySelectorAll(".menu a")) {
if (link.getAttribute("href") === here) {
link.setAttribute("aria-current", "page");
}
}
Once each page was a real file, the script had nothing left to do. Every page now marks its own link with aria-current="page" in the HTML. That's the pattern throughout: the menu is a native <details> element, post previews are cut off by line-clamp, and the hover effects and the walking sprite on the home page are plain CSS.
There's one script I may still add. Copying a snippet out of one of these code windows means dragging across it by hand, and a few lines could give each window a [copy] button instead:
// Add a [copy] button to every code window except terminals
for (const figure of document.querySelectorAll(".code:not(.terminal)")) {
const button = document.createElement("button");
button.className = "more";
button.textContent = "copy";
button.addEventListener("click", async () => {
const code = figure.querySelector("code").textContent;
await navigator.clipboard.writeText(code);
button.textContent = "copied";
});
figure.append(button);
}
The cost: copied markup
Going without a framework has one real cost. The header, menu and footer were pasted into every page, and every new post added another copy. The smallest fix is a script that stitches the shared pieces around each page. Since .NET 10, a single C# file can run as a script, with no project file:
// Stitch the shared header and footer around every page.
// Run it with: dotnet run build.cs
var header = File.ReadAllText("partials/header.html");
var footer = File.ReadAllText("partials/footer.html");
var pages = Directory.EnumerateFiles(
"pages", "*.html", SearchOption.AllDirectories);
foreach (var page in pages)
{
var body = File.ReadAllText(page);
var output = Path.Combine("site", Path.GetRelativePath("pages", page));
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
File.WriteAllText(output, header + body + footer);
Console.WriteLine($"built {output}");
}
In the end I went with Eleventy, which does the same stitching at build time and colors code snippets like the ones on this page. Visitors still get nothing but plain HTML and CSS.