I want to say something that took me an embarrassingly long time to accept: copying and pasting a component across seventeen files is not a workflow. It is a debt payment plan with compounding interest. I did not understand this until I had already shipped somewhere around two dozen Astro themes — each one slightly wrong in a slightly different way — and spent weeks hunting down the same footer bug across a codebase that had no business existing.
This is not a tutorial on how to build components. Plenty of those exist. This is an honest account of what happens when you skip that step, why it feels rational at the time, and what the cost actually looks like once you add it up.
How it starts (and why it feels fine)
The first theme is fast. You build it from scratch, you are learning Astro, everything is new and interesting. You ship it. Then someone asks for a variation — same layout, different niche. You duplicate the project folder, do a find-and-replace on the business name, swap the color palette, and you are done in a day. That feels like productivity. It is not. It is the moment the clock starts.
By theme three or four, you have three or four slightly different versions of the same navigation component. They all work. The hero sections have minor variations — one has a subtitle, one does not, one has a slightly different CTA button. None of this feels wrong yet because each individual file is fine. The problem is invisible from inside any one file. It only becomes visible when you need to change something everywhere.
The moment you feel it
For me it was a mobile menu bug. A client previewing a theme on their phone noticed the hamburger icon was not closing the menu after navigation. Simple fix — one line of Alpine.js. I opened the relevant file, patched it, tested it, done.
Then I realized that same bug existed in every theme I had built. Every single one. Because they all shared the same copied navigation HTML. Some had already diverged — one theme had added a dropdown, another had restructured the markup — so the fix was not identical across all of them. I spent the better part of two days finding every instance, understanding how each one had drifted, and applying the right variant of the fix.
Two days for one line of logic. That is when I did the math, and the math was bad.
What reusable components actually cost to skip
When you have no shared components, every improvement you make is a one-off. The time cost is not just "fix it in N files instead of one." The real cost is:
- Audit time. You have to find every instance of the thing you are changing. In a monorepo with no consistent structure, this means reading files, not just searching them.
- Divergence tax. Files that started identical eventually drift. A fix that is trivial in the original is a careful surgery in a modified copy.
- Fear of touching things. After a few of these experiences, you start leaving known bugs in older themes because the risk of breaking something outweighs the cost of the bug. This is a terrible position to be in.
- Design inconsistency. Small visual differences accumulate across themes until your portfolio looks like it was built by several different people with different standards.
None of this shows up on any individual task. Each task looks manageable. The cost only appears when you zoom out and look at the year.
What I should have built instead
An Astro component-first architecture is not complicated. The core idea is that anything that appears in more than one place lives in src/components/ and is imported, not duplicated. Astro makes this almost frictionless once you commit to it.
A navigation component that varies by theme does not need to be duplicated. It needs props:
---
// src/components/SiteNav.astro
interface Props {
links: { label: string; href: string }[];
ctaLabel?: string;
ctaHref?: string;
}
const { links, ctaLabel = "Get a quote", ctaHref = "/contact" } = Astro.props;
---
<nav x-data="{ open: false }">
<button @click="open = !open" aria-label="Toggle menu">
<span x-show="!open">Menu</span>
<span x-show="open" x-cloak>Close</span>
</button>
<ul :class="open ? 'flex' : 'hidden'" class="flex-col xl:flex xl:flex-row">
{links.map(link => (
<li><a href={link.href} @click="open = false">{link.label}</a></li>
))}
</ul>
<a href={ctaHref}>{ctaLabel}</a>
</nav>
That @click="open = false" on the nav links is the fix for the bug I spent two days deploying. With a shared component, it is one line changed once, and every theme using the component gets the fix on the next build. No audit, no divergence risk, no fear.
The theme variation problem
The objection I made to myself for too long was: "But my themes are all different. I cannot share components across different designs." This is partially true and mostly wrong.
The structure of a local service business website is remarkably consistent. There is a hero. There is a services section. There is a trust section with some social proof. There is a contact form or CTA. There is a footer. The content varies. The color palette varies. The layout details vary. But the underlying logic — navigation state, form handling, accordion behavior, lazy-loading images — is identical.
The right split is: share logic and structure in components, allow design variation through props, slots, and CSS custom properties. In Tailwind v4, this becomes even cleaner because you can define your design tokens in a CSS-first @theme block and override them per theme without changing any component markup:
/* src/styles/theme-hvac.css */
@import "tailwindcss";
@theme {
--color-brand: oklch(55% 0.2 240);
--color-brand-dark: oklch(40% 0.2 240);
--font-heading: "Inter", sans-serif;
}
/* src/styles/theme-roofing.css */
@import "tailwindcss";
@theme {
--color-brand: oklch(50% 0.18 30);
--color-brand-dark: oklch(35% 0.18 30);
--font-heading: "Outfit", sans-serif;
}
Same components. Different CSS file imported in the layout. The visual difference is real; the maintenance surface is shared.
What a base theme actually is
The clearest mental model I have now: a base theme is not a template you copy. It is a library you import from. Your theme-specific code lives on top of it. If you find yourself editing the base theme files inside a theme project, you have done something wrong — you should be extending, not modifying.
In practice this means your monorepo has something like:
packages/base-components/— the shared Astro components, utilities, and base stylesthemes/aircrest-hvac/— imports from base, adds niche-specific pages, overrides tokensthemes/copperline-plumbing/— same
Adding a new theme means writing only the things that are genuinely new: the business content, the niche-specific copy, the color palette, any truly unique page layouts. Everything else — the nav, the footer, the contact form, the testimonial carousel, the service card grid — comes from the base package at zero marginal cost.
This is the only way the economics of building many themes make sense. If theme twenty costs you as much as theme one, you are not building a product. You are doing client work at a discount.
The year I got back
I am not going to claim the refactor was fast. Moving from a file-per-theme architecture to a shared component system took meaningful time up front. There were a few weeks where I was working on infrastructure instead of shipping. That felt uncomfortable.
What I got on the other side was a codebase where fixing a bug once fixes it everywhere, where a design improvement propagates automatically, and where I can start a new theme by writing a data file and an asset folder rather than duplicating fifteen hundred lines of HTML. The maintenance load dropped noticeably. The quality consistency across themes went up. The time from "I want to add this feature" to "it is live across all themes" went from days to minutes.
That is the year I got back — not all at once, but as a steady compounding return on a decision I should have made at theme two instead of theme twenty-four.
The one thing worth internalizing
If you are building multiple themes, or multiple anything, the question to ask at the start of every file is: will this ever exist in more than one place? If the answer is yes — even maybe — make it a component now. The cost of componentizing something early is thirty minutes. The cost of componentizing it after it has drifted across twenty copies is a bad week.
Build the component. Import it everywhere. Thank yourself later.