Repaving the blog: Eleventy to Astro
Why I tore down the Eleventy site and rebuilt this blog on Astro 5 with Tailwind v4 and Cloudflare Pages.
Why bother
The site ran on Eleventy for years and there was nothing really wrong with it. Markdown in, HTML out, deployed somewhere, done. But the longer I sat on it, the more little things piled up: Nunjucks templates I’d forgotten how to read, no typed schema on frontmatter, global JS data that was clever the first time and confusing the second, plugins drifting out of sync.
The real reasons are AI and my skill issue. The kind of optimization and migration work I struggled with a few years ago is now cheap to attempt with an agent in the loop: wiring up a build, picking a framework, untangling someone else’s CSS. Issues I had no idea how to fix before, slow JS bundles, layout shift, animation jank, accessibility regressions, are now solvable in an afternoon. So I finally sat down and fixed them (or Claude did the fix lol).
Picking the stack
I looked at four options.
- Docusaurus. React docs-site with a blog plugin bolted on. Great if the site is mostly docs. For a blog plus a resume page it’s a lot of plumbing.
- Hugo. Fastest builds on the planet, a single Go binary. The resume page I wanted, with print CSS and a custom layout, felt clunky to wrangle in Go templates.
- Stay on Eleventy. The lowest-churn option. I’d still be left with
.njkeverywhere. - Astro. Markdown and MDX through typed content collections, zero JS by default, React islands when I want them, and first-class deploy to Cloudflare Pages.
Astro won. Mostly because of content collections with a zod schema: frontmatter validated at build time, with autocomplete in the editor.
What got ported
The shape stayed roughly the same.
- Posts moved to
src/content/posts/with a normalized frontmatter shape (title,description,date,tags,published). - Projects moved to
src/content/projects/with their own schema. - The resume turned into a typed TypeScript file at
src/data/resume.tsplus a singlesrc/pages/resume.astropage. The old setup rendered the resume out of a Liquid template and a sidecar YAML file, which was fine but harder to extend without templating gymnastics. - About, tags, RSS, sitemap, robots: all rewritten in a few lines each.
I also took the chance to throw out some content I’d been ignoring: a project stub that never got written, an early post that didn’t age well. The blog feels lighter for it.
Animations and React islands
I cribbed the visual idea from the kro site’s IntroFlow component. The pattern is small: an IntersectionObserver fades the card in on scroll, and a button toggles a 3D rotateY(180deg) between a front face and a back face.
In Astro it lives as a React component imported into an .astro or .mdx page with the client:visible directive, so the React runtime only hydrates once the component scrolls into view. Like the one below, rendered live. Click flip on either card.
This may come in handy for future technical blog posts.
What I dropped
- Travis CI config.
- All the Nunjucks:
.njktemplates,_includes/,_data/. - The 11ty plugin bundle: rss, nav, syntaxhighlight, markdown-it and its add-ons.
- A dark-mode default. The new site is light by default.
- The hero flip cards from the landing page (kept the component, removed the import).
- A handful of pages and posts I never wanted to look at again.
What’s next
A few things sitting on the list:
- A real OG image for posts.
- Try MDX on the next post so I can embed live YAML examples without escaping every brace.
- A write-up of a mesh debugging session that’s been pending for too long.