How this site works
A static site built with Bun and React, JavaScript only where needed, and size budgets as tests.
When I started this site, I had three goals. It should be small and fast, even on a slow phone. It should work without JavaScript. And it should not load anything from other servers. This note shows how the code keeps them, and how I check that it still does.
Plain HTML
Every page is plain HTML, rendered once at build time. I still write the pages in React, but React only runs during the build. This loop turns each route into HTML and writes the file to dist/.
for (const r of routes) await write(r.outFile, await renderPage(r.render(), r.meta));
The dev server calls the same renderPage on every request, so what I see locally is what ships. The notes are Markdown files with a bit of YAML at the top, and Bun parses both on its own.
const parsed = Bun.YAML.parse(m[1] ?? "");
return Bun.markdown.html(body, { headings: { ids: true } });
So the project doesn't need a Markdown library.
Islands
Most pages only show text, and text doesn't need JavaScript. That's why this note, the notes list and the legal pages load none at all. Even the filter on the notes page works without it. It is a group of three radio buttons with labels that look like buttons, and every note row carries its type in data-type. When one button is checked, a CSS rule with :has() hides the rows of the other types.
.notes-browser:has(#notes-filter-til:checked) .note-row:not([data-type="til"]) {
display: none;
}
So the filter needs no script at all.
Only three parts of the site need JavaScript: the terminal on the home page, the ASCII grid behind it and the contact form. I built them as islands, small interactive parts of a static page. The server renders an island to HTML like the rest of the page and wraps it in a div with data-island (the island's name) and data-props (its props as JSON). Then renderPage searches the HTML for data-island and adds one script per island it finds. Each script is a small browser entry like this one for the terminal.
import { TerminalHero } from "../components/TerminalHero.tsx";
import { hydrateIslands } from "../lib/island-client.tsx";
hydrateIslands("terminal", TerminalHero);
hydrateIslands finds every element with data-island="terminal", reads its props and hydrates the component there. Hydration means React attaches its event handlers to the HTML that is already on the page instead of drawing it again. Try the terminal with ls, cat now.md or grep -i bun.
Budgets as tests
A fast site gets slower one small change at a time, so I wrote the limits down as a test. It builds the site, measures every page and fails as soon as one goes over budget.
test("every page is within the budgets", () => {
expect(checkBudgets(report)).toEqual([]);
});
report holds the measured sizes, and checkBudgets returns one entry for each limit a page breaks. These are the limits.
| Budget | Limit |
|---|---|
| HTML per page (gzip) | 15 KB |
| CSS for the whole site (gzip) | 15 KB |
| JavaScript on notes, legal, 404 | 0 KB |
| Preloaded fonts | 2, latin only |
With these limits, a note page is about 74 KB on the first visit, fonts included. The home page is heavier because of the terminal. I measure it with a script that loads the page in headless Chromium as a phone with a four times slower CPU and a slow 4G network. There, the largest contentful paint (when the biggest element appears) is 1.6 seconds. It once went up to 6.2 seconds, because the terminal's intro animation created its rows again after the web fonts loaded, and the browser counted them as a new, later paint. Now the rows are on the page from the start and stay hidden until the animation reaches them.
The ASCII grid also starts late on purpose. It waits until the browser is idle.
const id = win.requestIdleCallback(run, { timeout });
requestIdleCallback asks the browser to call run when it has nothing else to do, and the timeout of two seconds makes it run by then at the latest.
Accessibility checks
A second script checks accessibility. It runs axe, a tool that tests pages against WCAG 2.2 AA, on every page in light and dark mode, at desktop and phone width, and without JavaScript. Then it tabs through each page and checks that focus is always visible and never gets stuck in the terminal.
Fonts and analytics
The third goal was nothing from other servers. The fonts come from npm packages, and the build copies them into dist/, so they load from this domain. There are no embeds. The analytics script also comes from this domain, and it only loads after you say yes in the banner. A browser that sends Global Privacy Control counts as a no.