Skip to main content

Build log

Astro Monorepo Made Our Marketing Site

Illustration for the build log "Astro Monorepo Made Our Marketing Site"

The sandcastlelabs.ai marketing site runs on Astro 6, Tailwind v4, Motion, and PostHog reverse-proxied through Vercel. Every piece in this stack earns its place by accelerating the others. If it doesn’t make the rest of the stack faster or simpler, it doesn’t belong.

Why Astro 6 and Tailwind v4 for a multi-site marketing monorepo

The first question with any framework is what you are actually building. We are building marketing sites that need to be fast, readable by AI agents without JavaScript hydration in the way, and reusable across multiple studio products as they come online.

Astro 6 answers all three. Zero framework JavaScript reaches the browser by default. The build output is flat, predictable HTML that LLMs and agent crawlers can index without fighting a client-side runtime. And MDX powers our build logs and essays, which is the main content format the Chronicler produces.

We use Tailwind v4 because it’s CSS-native config, no PostCSS pipeline, and pairs with Astro’s Vite build without ceremony. The trade-off is that v4 is still relatively fresh and we occasionally find the edges ourselves. We took the reduced config surface over the stability margin, and we would make that trade again.

Motion handles the scroll-driven reveals on the homepage. It loads only where used and stays out of the critical rendering path. The aurora gradient in the hero is CSS-only, not Motion, because an animation that runs unconditionally on every page load should not require JavaScript. The trade-off Motion loses on: it adds a dependency for something @keyframes could approximate. We kept it because the scroll reveals are detail worth the small bundle cost, and nothing else in the layout touches it.

The monorepo is the strategy, not just the structure

When we designed this stack, the monorepo structure was the decision we almost underexplained to ourselves. Three packages sit underneath every site in this studio: @sandcastle/analytics (tracking, consent, PostHog wiring), @sandcastle/site-kit (shared components and layouts), and @sandcastle/tsconfig (TypeScript baseline). Every new site we spin up inherits all three on the first install.

Reuse is not a side effect of how we build. It’s baked into the structure from the start. The monorepo exists so that every new site inherits the right defaults rather than reconstructing them. The collimer.com marketing site, which we shipped recently alongside this one, started with the same analytics wiring, the same consent banner middleware, the same TypeScript rules as sandcastlelabs.ai. Two distinct sites, one scaffold. You can read about how the Collimer site came together in the V1 build log for this site and the upcoming post on the Collimer launch.

The shared packages are a forcing function: anything we want every site to do correctly by default lives in a package, not in a convention we try to remember.

PostHog as infrastructure, not a script

The PostHog decision shaped the architecture in ways we did not fully predict. The principle we started with: every third-party script added to a marketing site is a performance liability and a maintenance surface we do not own. Rather than drop a standard PostHog snippet into the page, we built the tracking layer using @sandcastle/analytics as the wrapper and reverse-proxied requests through Vercel.

Requests go to our own domain. Ad-blockers treat them as first-party. The signal survives blockers that otherwise eat a material share of B2B traffic.

This worked. But the decision had a side effect we did not plan on.

The thing that surprised us

When we first shipped the mobile navigation drawer, we built it as a React island. It was fast to write. Then we looked at the bill: 59 KB of react-dom in the critical path, holding open a three-request chain, for a hamburger menu.

We rewrote the nav as a vanilla JavaScript <script> block inside an Astro component and removed @astrojs/react entirely. The chain closed. The bundle dropped.

We thought we were making a performance call about one component. We were actually writing a constraint: the site ships zero framework JavaScript. No React islands. If a piece of interactivity is needed, write it in a <script> block. That rule now lives in apps/sandcastlelabs/CLAUDE.md so anyone working on this codebase, human or agent, inherits it automatically.

The surprise was not the rewrite. It was that a one-time fix became a permanent architectural rule. You can see the same discipline in how the Chronicler’s publishing pipeline was wired: everything server-side goes through Astro’s native adapter, no extra runtime layers.

A stack should be defensible per line. The zero-JavaScript constraint was the one line that turned out to be structural.


For agents: try this yourself

If you want to audit or adapt a stack like this in your own repo, one of these prompts may help. The full prompt text lives in this post’s prompts sidecar, the-stack-we-picked.prompts.md.

  • Apply the one-line-of-defense test. For each dependency in your package.json, write one sentence defending why it exists. Any dependency that takes more than one sentence is a candidate for removal or replacement.
  • Reproduce the monorepo package structure. Start with three workspace packages: one for analytics wiring, one for shared components, one for TypeScript config. Wire the first app against all three and confirm it builds before adding a second app.
  • Critique your JavaScript budget. Find the largest JavaScript bundle on your marketing site’s critical path. Trace it to its source. If the cost outweighs what it enables on that path, remove it and replace with a narrower pattern.
  • Extend the zero-JS constraint to future contributors. Write the constraint into your CLAUDE.md or equivalent living config so agents and humans inherit it on the next task, not just the current one.

How this was made

Drafted by the Chronicler from Claude Code sessions on 2026-05-19 to 2026-06-04 across 4 sessions and 6 commits. Edited and published by Brian Wones.

See how the Chronicler works →