CMS Architecture
Multi-Site & Multi-Locale CMS Strategy: The Practical Approach
How to model multi-brand and multi-locale content without over-engineering the schema — one CMS vs. many, where locale actually belongs in the data model, and the query pattern that keeps it manageable at scale.

Multi-site and multi-locale requirements tend to arrive late and vague — "we might expand to a European market next year" during a discovery call that's otherwise about a single-market marketing site. The instinct is to either ignore it (and re-architect painfully when "might" becomes "did") or over-build for it (adding locale and site dimensions to every content type from day one, on a project that may never need them). Neither is right. The practical approach models the dimension explicitly where you already know you'll need it, and keeps the door open — but not the full weight — everywhere else.
One CMS instance, or separate instances per site
This is the first real decision, and it's more consequential than the schema details that follow it.
One CMS instance with a site (or "channel") dimension is the right default for most multi-brand or multi-market projects. Every piece of content carries a field or relation indicating which site(s) it belongs to; the frontend filters by the current site at query time. This gives you one admin, one deployment, one place to manage editorial permissions — and, critically, the ability to share content across sites deliberately (a shared legal notice, a shared component library, a shared author bio) instead of duplicating it.
Separate CMS instances per site make sense in narrower cases: fully independent brands with no content overlap, different teams who shouldn't see each other's drafts, or genuinely different content schemas per property (one site is a blog, another is a product catalog — different enough that forcing a shared schema creates more awkwardness than it saves). This is less common than teams initially assume; most "we need separate instances" requirements turn out to be a permissions question, not a data-model question, and permissions are solvable within one instance.
Default to one instance with a site dimension unless you have a specific, named reason not to. Splitting later, if you're genuinely wrong, is more work than merging would have been — but it's still less work than running two fully separate CMS deployments for years when one would have served the actual requirement.
Modeling site and locale: where they actually belong
Site is usually a required field or relation on every content type that varies by property — a site reference on Post, Page, and Product, resolved against a small Site collection that stores per-site config (domain, name, theme tokens if applicable). Single types like global settings need their own pattern: either one record per site, or a single record structured as a keyed object per site — the right choice depends on how differently each site's settings actually vary.
Locale is a genuinely separate dimension from site, even though they're often deployed together, and conflating them is the most common modeling mistake we see. A locale field (or your CMS's native i18n support, if it has one — Strapi, Contentful, and Sanity all offer some version) should exist independently, because the real-world combinations aren't always "one site, one locale": some projects need one site with multiple locales (/en/, /es/), others need multiple sites each with their own single locale, and some need the full cross product.
Decide the URL pattern early, because it drives both the content model and the routing layer downstream:
- Path-based locales (
/en/blog,/es/blog) — simplest to implement, one deployment, locale resolved from the URL segment. - Separate domains per locale (
example.com,example.es) — better for strong regional SEO signals and brand separation, more infrastructure (DNS, SSL, deployment config per domain). - Subdomain per locale (
en.example.com) — a middle ground, less common but occasionally the right fit for specific hosting or brand constraints.
Whichever pattern you pick, the content query pattern is the same underneath: every request needs to resolve site and locale from the URL, then pass both explicitly to every content fetch.
Querying and routing: one thin layer, always used
The pattern that keeps this manageable at any scale: build a single API layer that always injects the current site and locale into every content request, rather than trusting every individual page component to remember to filter correctly. This is the difference between "forgot to scope one query" being a rare, caught-in-review mistake versus a recurring bug that ships to production because it's easy to forget when it's manual every time.
getContent(contentType, { site, locale, ...filters })
Every page-level data fetch goes through this layer, not a direct CMS query. For the homepage or any single-type content, this resolves to "give me the Homepage record filtered by the current site and locale" — the same pattern as any collection query, just applied to a singleton. Index or cache by the site+locale combination so repeated requests for the same combination stay fast, especially important if your CMS's native filtering is slower than a cached lookup for high-traffic combinations.
Shared vs. localized content: model the exception explicitly
Most content is per-site and per-locale by default — that's the common case and doesn't need special handling. The content that's genuinely shared (a global legal disclaimer, a company-wide announcement, shared navigation structure) is the exception, and it's worth modeling that exception explicitly rather than letting it default silently:
- A
globalflag or a dedicated "shared" site/locale value that the query layer recognizes and handles differently (fetch once, don't filter by the current site). - For media specifically, decide deliberately whether images are per-locale (a hero image with localized text baked in, or cultural context that differs by region) or shared (a product photo that's identical everywhere) — this affects both your schema and how you resolve asset URLs, and it's a much more annoying retrofit than an upfront decision.
Without an explicit shared/localized distinction, the failure mode is predictable: someone builds a page assuming content is per-locale, a genuinely shared piece of content gets duplicated across five locales "to be safe," and now a legal team's single approved disclaimer text has five copies that will inevitably drift out of sync the first time it needs to change.
When to build this, and when not to
Build the site/locale dimension into the model now if you already know you're launching in multiple markets or maintaining multiple brands within the next planning horizon — retrofitting it onto content that was never scoped that way means touching every content type, every query, and often every component that assumed a single implicit context.
Don't build it speculatively for a "might expand someday" requirement with no committed timeline. The cost of adding a site/locale dimension to a well-modeled CMS later is real but bounded — it's a schema migration and a query-layer change, not a rebuild, provided the content model was reasonably clean to begin with. The cost of carrying unused complexity through every content type and every query for a market that never materializes is a permanent tax on every future change, for a requirement that may never justify it.
We scope this decision explicitly during discovery on every headless CMS implementation with any multi-market ambiguity — it's one of the first questions that changes the shape of the content model, and it's much cheaper to get right in week one than to retrofit in year two. If you're planning a multi-brand or multi-market build and aren't sure whether to model it now or defer it, get in touch — this is usually a short conversation that saves a much longer migration later.