Skip to main content
← back to /dev/timeline

Same site, three years later: a full modernization (with an AI pair programmer)

Svelte 5, Tailwind 4, markdown macros, tests, Storybook, CI — and how an AI agent helped rebuild it all in one session.

source

Why touch a working website?

The original version of this site served me well since early 2023. But three years is a geological era in frontend land: Svelte gained runes, Tailwind moved its config into CSS, daisyUI grew a proper timeline component, and my dependency tree had started to fossilize.

So I did what any reasonable engineer does in 2026: I described what I wanted to an AI coding agent, reviewed everything it produced, and shipped the whole modernization in a day. This article is both the changelog and a live demo of the new toys.

Drag the slider — same homepage, three years apart:

2026
2023
2023 2026
2023 (flowbite + daisyUI 2) vs 2026 (daisyUI 5, custom espresso theme)

What changed under the hood

Before (2023)After (2026)
Svelte 3 + SvelteKit 1Svelte 5 (runes) + SvelteKit 2
Tailwind 3 + daisyUI 2Tailwind 4 (CSS-first) + daisyUI 5
flowbite-svelte componentsa handful of small custom components
Font Awesome CDN kitinline SVG icons, zero requests
Google-hosted fontsself-hosted variable fonts
manual gh-pages deploysGitHub Actions build + deploy
no tests 😬65 unit tests + Playwright e2e + Storybook

The markdown-first philosophy stayed. Articles are still just .md files with a bit of frontmatter — the timeline, article pages and the new RSS feed are all derived from them at build time.

Dark mode that actually sticks

The old theme toggle was flaky: the choice lived only in a checkbox and the page could flash the wrong theme on load. The new setup resolves the theme before first paint (stored preference, falling back to prefers-color-scheme), persists it in localStorage, and broadcasts changes so embedded widgets can follow along.

To resolve the theme before the initial Svelte paint and prevent visual flashing, we inject a blocker script directly in src/app.html:

// Prevent Flash of Unstyled Content (FOUC)
const theme =
	localStorage.getItem('theme') ||
	(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'espresso' : 'latte');
document.documentElement.setAttribute('data-theme', theme);

There is a Playwright test that toggles, reloads and checks it stuck — flakiness is now a regression, not a mood.

Markdown macros

The best part of the rebuild: articles now have superpowers without any imports. mdsvex injects a set of components into every markdown file, so embedding things is a one-liner.

A YouTube embed that only loads the player (cookie-free) when you click it:

An in-browser 3D viewer for STL files — three.js, lazy-loaded on the client, running fine within GitHub Pages’ static confines. No server, no WebAssembly needed, just WebGL:

A 3D render of a mushroom, demonstrating lazy-loaded WebGL STL parsing on static hosting

Plus <Figure> for captioned images and the <Compare> slider you already used above.

Tests, stories, robots

  • Unit tests (Vitest + Testing Library) cover the article parsing, theme logic, timeline merging and every component — including one that would have caught the classic “icon name doesn’t exist” bug I hit while building it.
  • Playwright smoke-tests the built site: navigation, both themes, RSS.

Here is the Playwright E2E test verifying that the theme toggles and survives page reloads:

test('theme toggle switches theme and survives reload', async ({ page }) => {
	await page.goto('/');
	const toggle = page.getByRole('button', { name: 'Toggle theme' });
	const initialTheme = await page.evaluate(() =>
		document.documentElement.getAttribute('data-theme')
	);

	await toggle.click();
	const newTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
	expect(newTheme).not.toBe(initialTheme);

	await page.reload();
	const reloadedTheme = await page.evaluate(() =>
		document.documentElement.getAttribute('data-theme')
	);
	expect(reloadedTheme).toBe(newTheme);
});
  • Storybook hosts every component with a theme switcher in the toolbar, so design work doesn’t require clicking through the whole site.
  • GitHub Actions runs all of it on every PR and deploys main straight to Pages.

And because the site is now maintained with AI agents rather than just by me: the repo carries an AGENTS.md (tool-agnostic instructions), a CLAUDE.md pointing at it, and .claude/ skills that teach an agent how to write articles like this one — down to the commit style (semantic, lowercase, imperative) and a strict “comment only what the code can’t say” policy.

Comments (soon)

Article pages are wired for giscus — comments backed by GitHub Discussions, sign-in with GitHub, no tracking, free. It lights up the moment Discussions are enabled on the repo. Until then: the contact page works, and so does the shiny new RSS feed.

Happy coding, see you in another three years 😀