Giggable

Tech blog

How we split marketing from the console

September 4, 2026 · Giggable

The public Giggable site is now a static Astro app. The logged-in console stays a React SPA. Same hostname, same look, two very different jobs.

I had been heads-down on the product for months. The home page, the pricing section, the “what even is this?” copy — all of that lived inside the same Vite React app as setlists, gigs, and band chat. Fine for shipping a v1. Terrible for writing a blog post, terrible for search engines, and a little embarrassing every time I wanted to publish something without wrapping it in a client-side router.

So I split it.

Why bother

A marketing site and a band console want opposite things from a bundler.

The console is a logged-in app. It can hydrate a pile of JavaScript. It already has Apollo, session cookies, band switching, the whole deal. Nobody lands there from Google looking for “how do I keep my band in sync.”

The public site wants HTML. Real files. A product blog and a tech blog that exist as pages, not routes that appear after a spinner. I also wanted to write posts in Markdown and ship them with the rest of the site, not invent a CMS for two collections.

Astro was the obvious fit: static output, Markdown content collections, and React islands for the bits that actually need a browser.

Same host, two builds

I did not want blog.giggable.app. I wanted / to be the marketing home, /blog and /tech to be static, and every existing console URL — /login, /onboarding, /<band>/… — to keep working exactly as it does today.

Netlify still publishes one directory: the Vite SPA’s dist. After both packages build, a small merge script overlays the Astro output on top of it.

The awkward part is index.html. The SPA needs to keep occupying that file so client-side routing still has a shell. Marketing home gets copied to marketing/index.html, and Netlify rewrites / to that file with a forced 200. Blog and tech directories are copied straight in, so those URLs win as static files. Everything else falls through to the SPA.

/          → Astro marketing home
/blog/*    → static Markdown pages
/tech/*    → static Markdown pages
/*         → existing React console

If you are already logged in and hit giggable.app, you still get the same top nav you know from the console. You can switch bands and jump straight into the app. You just do not download the console bundle to read a blog post.

Shared UI, props only

The nav and pricing section have to look identical on both sites. Duplicating ShadCN buttons, the band switcher, and the user menu would have gone stale in a week.

So those presentational pieces moved into a new shared-web-ui package. Buttons, cards, dialogs, the wordmark, the band switcher, the user menu, later the pricing cards and the theme toggle. The console and marketing both import them.

The rule that made this work: no routing, no Apollo, no “go get the current band” inside the shared components. They take props and call callbacks. Marketing wires those up with window.location. The console wires them up with React Router. Same pixels, different hosts for the data.

On the marketing home, only two things hydrate as React islands: the top nav and the pricing section. Everything else is just HTML.

It was not a clean cut

The first merge compiled. Then the next few days were a greatest hits of “why is this package suddenly someone else’s problem.”

  • The shared-frontend barrel is a trap. Importing @giggable/shared-frontend from the nav island pulled in the entire package — including rrule — during Astro SSR. The nav needed a session query, a sort helper, and some enums. It did not need recurrence math. Granular exports (./navSession, ./hooks/useRecentBands) keep the island small and keep Astro from loading the kitchen sink.
  • CSS tokens vanished. Marketing imports the shared design tokens before Tailwind. Without postcss-import inlining that file first, the custom properties inside @layer did not survive the build. The page rendered. It just looked wrong.
  • localhost:4321 is not localhost:5173. The nav island talks to the same GraphQL API the Vite app does. CORS had to learn about the Astro dev origin or a logged-in session on the marketing site was a brick.
  • TypeScript had opinions. Content collection types, CSS module declarations, GraphQL error-extension augmentations that used to arrive “for free” through the barrel — all of that showed up the moment marketing typecheck became a real CI job.

None of this was conceptually hard. All of it was the tax of taking a package that had only ever been consumed by one Vite app and asking a second bundler to be polite about it.

What I would do again

Extracting presentational UI before adding the second app was the right call. If I had copied the nav into Astro “just to ship,” I would be maintaining two user menus right now.

Keeping one hostname was also right. Bands already have bookmarks. Login, invites, and onboarding URLs do not get to move because I wanted a nicer home page.

The merge script is a little weird. I still like it more than running two Netlify sites and praying the cookies agree.

What’s next

This post is itself the test. If you are reading it on giggable.app/tech, the split worked.

I want more of the public site to stay static — legal pages, the Android testers flow, anything that does not need a session to render. Islands only where a session or a checkout actually matters.

And I want to keep writing here. The console can stay a console. The front door should be a website.