dan dragon

dandragon.com

This site — a personal site rebuilt from Jekyll to Astro 7 and Tailwind 4, deployed to GitHub Pages. The low-poly beard logo was re-derived as SVG, and the whole design targets WCAG 2.2 AA with AAA contrast wherever it was free.

Source for dandragon.com on GitHub

The goal

My old site was a Jekyll blog with a theme I had stopped liking years earlier. I rebuilt it on Astro 7 and Tailwind 4, but the point was never the framework — it was to have somewhere I control end to end, where the accessibility work is done properly rather than retrofitted. The target is WCAG 2.2 AA, with AAA contrast anywhere it was free to take.

Constraints

It’s a static site on GitHub Pages, so there is no server to lean on: no runtime theming, no personalization, nothing that can’t be decided at build time or in a few lines of inline script. The old Jekyll URLs had to keep working, because bookmarks and feed readers don’t care that I changed generators. And it’s maintained by exactly one person, so content had to stay Markdown with a schema strict enough to fail the build when I get it wrong.

Decisions worth writing down

The palette is measured, not picked

The brand teal sampled from my logo is a nice color and a bad text color — it doesn’t clear the contrast bar at body size. So it’s demoted to decoration only: the > chevron before headings, list markers, the selection highlight. Two other steps carry actual text — a dark teal at 7.2:1 on the light background, a light teal at 10:1 on the dark one — which is how the site gets AAA link contrast without giving up the brand. Every measured ratio is written as a comment next to the token that owns it in global.css, so a future me who nudges a hex has to walk past the number they’re about to break. Components never reference a hex directly; they reference semantic aliases (--bg, --fg, --accent, --line) that flip on [data-theme='dark'].

The .prose override has to be unlayered

Article bodies use the Tailwind typography plugin, remapped onto those semantic tokens so prose follows the theme. My first version of that override lost silently, and an axe scan in dark mode is what caught it. The plugin emits .prose into the utilities cascade layer, and layered rules lose to unlayered ones no matter how specific you get. The fix was to pull the block out of @layer entirely and leave a comment saying why, because it looks like a mistake until you know.

The theme can’t flash

Theme choice lives in localStorage, which means it isn’t known until JavaScript runs, which normally means a white flash before the dark theme lands. So a small inline script in <head> runs before first paint: read the stored preference inside a try/catch, because blocked storage throws, then fall back to prefers-color-scheme and set data-theme on the root element. Tailwind’s dark variant is wired to that same attribute, and the CSS color-scheme property flips with it so scrollbars and form controls follow along. Astro ships no framework runtime, so apart from that script and the toggle that drives it, the pages are plain HTML and CSS.

The logo is a clip path

The mark started as a raster low-poly beard I’d been using for personal branding. Rebuilding it as SVG meant tracing the silhouette into a clipPath, then generating a regular diamond grid across it — a few hundred facets, each filled with a color sampled from the original — so the faceting is real geometry rather than an embedded bitmap. It scales, it has a light and a dark variant, and it’s decorative, so it carries an empty alt.

Outcome

Keyboard and motion behavior is handled once, globally: a skip link hidden with clip-path rather than display: none so it stays focusable, a single :focus-visible outline in the accent color that nothing is allowed to suppress, and a prefers-reduced-motion block that clamps animation and transition durations site-wide. Layout uses logical properties, so it doesn’t assume left-to-right.

Content is three typed collections — posts, classes, projects — validated by Zod at build time. A push to main triggers a GitHub Actions workflow that builds and deploys to GitHub Pages. Legacy Jekyll paths are handled by redirects in astro.config.mjs, and /feed.xml survives as a real endpoint rather than a redirect, re-exporting the same RSS handler as /rss.xml, so old subscribers never had to notice any of this.

Back to all projects