TL:DR
This is part 3 of a 3 part series on MultiLaunch - an Astro Theme built by Bejamas using DatoCMS, Astro, and Vercel.
Part 1 covers the setup of the CMS, the content modeling, and the schema.
Part 2 talks about the overall editorial experience using the project and the use of plugins.
Part 3 covers the monorepo and the overall DX and deployment when working with this theme.
If you're looking for quick links to take it all for a spin, here's what you need 👇
Check out the demo here: https://astro-dato-multilaunch.vercel.app/
Fork the repo here: https://github.com/bejamas/astro-dato-multilaunch
Check out the details on the official Astro theme library here: https://astro.build/themes/details/multilaunch-multi-brand-website-template/
We also had a really nice chat with Mojtaba on his approach to the whole project, so check that out too!
Ok let's get riiiiight into it.
Looking at the repo
MultiLaunch was designed for companies running multiple brand sites. The dev experience needed to match that goal, which is why its built as a monorepo with Astro, DatoCMS, and Vercel. One repo. One CMS. As many brand sites as you need without duplicating code, content models, or deployments.

It had to be DRY. It had to be scalable. It had to let you launch new brands without spinning up new projects or writing new routes.
Which is why, from the start, the answer for Mojtaba was simple: build it as a system, not a one-off template. That meant a monorepo setup, with a shared UI layer, config logic based on slugs, and a CMS that could feed all of it.
At the top level, the repo has:
apps/core
— for the main siteapps/brand
— for individual brand sitespackages/ui
— for shared components and design tokens
Each app is its own Astro instance, but they pull from shared logic and styles in packages/ui
. So if you update a button style or fix a bug in a component, that fix rolls out everywhere.
This also means there's no duplication when building new sites. Everything from layouts to typography to SEO logic is shared, unless you explicitly override it.

On the individual brand side of things, each brand deployment is controlled by a BRAND_SLUG
env variable. That slug maps directly to a brand entry in DatoCMS.
Here’s how it works:
Create a new brand record in DatoCMS
Create a new Vercel project pointing to
apps/brand
Add
BRAND_SLUG=some-brand
in the env varsHit deploy
And just like that, you've got a brand new shiny website up and running for a new brand, complete with consistent branding and structure to match the others.
New site on a custom domain, powered by the same frontend, same schema, same logic. Easy.
You don’t even have to touch your code. You just deploy a new project with a different slug.
When working locally in dev, you do the same thing with .env
files. That means no branching. No boilerplate copying. No guessing where things live.
But why Astro?
Now, we're big fans of Astro here at DatoCMS, so this wasn't just a "We love it let's do it with this" kinda story. There's very well-founded reasons for the Bejamas crew opting for Astro.
First. Performance. Astro ships zero JavaScript by default. For mostly-static brand sites, this is a huge win. Pages load fast, Core Web Vitals are great out of the box, and you don’t need to fine-tune hydration.
Second. Flexibility. Astro works with React, Vue, Svelte, Solid... (I mean islands are just dope). You can bring in what you need, where you need it.
Third. File-based routing and localization are straightforward. With minimal setup, each locale can be its own route. And since everything is based on slugs, the routing logic stays clean.
MultiLaunch also uses the official DatoCMS CDA client (@datocms/cda-client
) to query content. It’s wrapped in a utility function that handles slugs and locales, so components don’t need to deal with query logic directly.
You pass in the brand slug, the locale, and the query, and get back clean, typed data.
This keeps the Astro pages lean and focused on rendering.
Let's talk under the hood
Here's a quick look into the considerations that really contribute to the overall DX when working with this theme.
But first, a quick look into how the repo is structured.
Build Triggers for smoooooth CI/CD
Once a brand site is live, publishing new content is completely decoupled from the dev team. DatoCMS has native build trigger support, which is hooked up to Vercel deployments.
That means any content update can trigger a rebuild and redeploy, no code pushes needed.
You could be running 5, 10, 50, 100 brand sites, and none of them require individual attention after setup.
Bun over NPM
Also, this project now runs with Bun. It’s faster for installs and plays well with Vercel. And they got a hella cute logo.
I switched to Bun after running into an install issue on Vercel. It just worked. It was faster too, so I stuck with it.
That’s one of those small DX quality-of-life things that adds up when you’re managing many deployments.
UI Packaging
All UI components live in packages/ui
. This includes typography, layout sections, buttons, review cards, and form components.

That package is used across both core
and brand
apps. So if the design system evolves, you’re not updating components in multiple places.
And because everything is schema-driven, the components are flexible. They accept props passed from DatoCMS queries like brand colors, section order, content blocks, and render them accordingly.
Built-in SEO
Each page in MultiLaunch pulls its own metadata from DatoCMS. The CDA client queries include fields like metaTitle
, metaDescription
, canonical
, and ogImage
.
_seoMetaTags { tag attributes content }
Those get passed to a layout-level SEO component, which uses the official DatoCMS <StructuredMeta>
component from @datocms/astro
.
It’s easy to implement and ensures that every brand site is search-ready out of the box.
Geo-redirects
As a last flex, The system also supports geo-based redirects.
There’s middleware in Astro that detects a user’s country and sends them to the right localized version of the site. For example, a visitor from Germany gets redirected to /de
.
// Get user's country from Vercel geo data const country = request.headers.get('x-vercel-ip-country')?.toLowerCase() || ''
// Map countries to locales (extend this mapping as needed) let locale = DEFAULT_LOCALE if (['fr', 'be', 'ch'].includes(country)) { locale = 'fr' } else if (['de', 'at', 'ch'].includes(country)) { locale = 'de' } else if (['pl'].includes(country)) { locale = 'pl' } else if (['cn', 'hk', 'tw'].includes(country)) { locale = 'zh' } else if (['sa', 'ae', 'eg', 'iq', 'jo', 'kw', 'lb', 'om', 'qa', 'sy'].includes(country)) { locale = 'ar' }
// Redirect to the appropriate locale return new Response(null, { status: 302, headers: { Location: `/${locale}${pathname === '/' ? '' : pathname}${url.search}` } })}
It’s a small touch, but it makes the whole experience feel polished, especially when you’re managing brands across multiple countries.
To wrap things up, this setup works because it's consistent.
One repo
One CMS
One schema
One query pattern
One UI system
You don’t reinvent anything when launching a new site. You just plug in new data. Everything else flows from that.
It’s fast to work with, easy to extend, and super clean to manage.
And that’s the point. MultiLaunch was built to scale, not just content-wise, but technically. It’s designed so you don’t have to rebuild the same logic every time.
Bonus point? Performance for the end-user. Because no new frontend project is ever really complete without flexing those token Lighthouse scores 💅

So take it for a spin, and let us know what you're building with it!