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.
- Astro
- Tailwind
- TypeScript
- Accessibility
- GitHub Actions
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.