A Tailwind v4 migration that also required patching a shared package
This site had been pinned to Tailwind v3 since a dependabot grouped update tried to bump it to v4 and broke the build outright. The real migration sat in the backlog for a while. When I finally picked it up, the actual work wasn’t the config swap - it was finding a bug that a clean build would never have shown me.
The mechanical part of a v3-to-v4 Tailwind migration on Astro is well documented: swap the @astrojs/tailwind integration for the native @tailwindcss/vite plugin, and replace @tailwind base/components/utilities with @import "tailwindcss"; in your CSS. The part I had to actually verify was whether I could keep my existing tailwind.config.mjs instead of rewriting everything into v4’s CSS-first @theme format. Turns out yes, via a @config directive - but I didn’t want to just trust the docs on that. This config file does two load-bearing things: it points at content living inside node_modules/@vdaluz/* packages, and it defines colors using the rgb(var(--x) / <alpha-value>) placeholder syntax. Either one silently breaking would be invisible in a build log.
I’d been burned by exactly this kind of silent breakage before (a Tailwind content glob that missed a package and just never generated the classes it needed, no error anywhere). So instead of building and calling it done, I re-ran that same regression check: load the privacy page, grab the shared PrivacyExplainer links, and check their computed text-decoration-line. Still underline. The node_modules content glob survived the migration. Same story for the alpha-value colors - checked a real computed box-shadow/color value in the browser instead of assuming the placeholder syntax still worked in v4.
What I didn’t expect was finding a real bug in a completely different repo. Tailwind v4 renamed or removed a handful of “bare” utility names - plain shadow and flex-grow among them. I grepped my own markup for those names before starting (found one: a leftover bare rounded class on the 404 page, easy fix). But I also grepped the vendored @vdaluz/astro-blog package sitting in node_modules, since its components render on every blog post here. Its RelatedPosts.astro component used both shadow and flex-grow. Since that’s a pinned tarball dependency, not code I can edit inside this repo, that would have shipped with silently missing shadows and a broken flex layout on every related-posts card. No build error, no warning. Just a slightly worse-looking card until someone happened to notice.
Fixing it wasn’t as simple as a find-and-replace rename, though. That same package also serves vdaluz.com, which is still on Tailwind v3. And v4 didn’t just remove shadow - it renamed shadow-sm to mean what v3’s bare shadow used to mean. So swapping shadow for shadow-sm in the shared package would have changed vdaluz.com’s visual weight too, as a side effect of a migration that has nothing to do with vdaluz.com. I ended up writing the shadow as an explicit arbitrary value - the literal CSS Tailwind v3’s default shadow produces - so it renders byte-identical under both versions. flex-grow was simpler: grow has been a safe alias since Tailwind v3.3, no visual difference on either version. Released as astro-blog v0.6.2, tested via the package’s own release process (pack, install into a real consumer, check and build) before pointing this repo’s dependency at the new tag.
The lesson that’ll stick with me: a dependency bump’s blast radius isn’t bounded by your own src/ folder. If you’re consuming shared packages, the markup that matters is whatever’s actually installed in node_modules, and it’s worth grepping that too before you trust a clean build.
Related reading
A bug that fixed itself, and a button that finally matched
Picking two small UI/UX review findings off the backlog: one had already been fixed by an unrelated change, the other was a real one-line button fix.
Adopting a shared component surfaced a bug the component didn't have
A privacy-policy page from a shared library rendered perfectly by every check I ran, except the one that actually mattered.
The filter pills existed, just not where anyone could find them
Per-project filtered views with a nice pill nav - reachable only from homepage cards. The main /blog index, where everyone actually lands, had no way in at all.