How do you structure Craft CMS for multi-site/multilingual?
Craft CMS Developer
answer
Use Craft CMS multi-site to separate locales/brands via Site Groups, with fields marked localizable only when needed. Model content with Sections per content type and Entry Types for variants; use Matrix/Neo as a “content builder.” Keep templates DRY: base layout + partials/macros, and site-aware includes. Centralize config in project.yaml; use environment-specific aliases and volumes. Pick translation methods per field (None/Each Site/Copy). Add governance: naming, propagation rules, and editorial permissions.
Long Answer
A maintainable Craft CMS multi-site or multilingual setup starts with clean content modeling, a DRY template system, and explicit rules for what varies per site. The aim is to let editors manage content once where possible, localize only where valuable, and keep developers shipping quickly without duplication.
1) Sites, groups, and propagation
Create a Site Group per brand or channel (e.g., “Marketing”), and add Sites for each locale or region (en-US, de-DE, fr-FR) or country variant (EU, UK). Set base URLs via environment variables so each site deploys correctly per environment. For Sections, choose Propagation deliberately: “Save entries to all sites” when structure is shared, “Only save to the site they were saved in” when content differs, and use Propagation Method: All Sites with Unique URLs per site when slugs/paths must diverge. This avoids accidental content loss or duplication.
2) Content modeling and translation strategy
Keep the model stable across sites: Sections map to content types (Blog, Pages, Products), while Entry Types capture meaningful variants (Landing vs. Article). Mark only truly variable fields as Translatable (Translate for each site)—titles, slugs, body text—while keeping IDs, references, and switchable flags as Not translatable. For content reuse, leverage “Copy from primary” on first creation, then localize selectively. Categories and Globals can also be localizable; use them for site-wide navigation, SEO, and settings.
3) “Content builder” without chaos
Matrix/Neo blocks let editors assemble rich pages. Keep block types few and purposeful (Hero, Media, Quote, FeatureGrid, CTABanner). Within blocks, favor small, composable fields (heading, intro, image, link) over giant WYSIWYG. For multilingual sites, make text fields localizable; keep design toggles non-localizable. Provide sensible defaults in the primary site, so localized sites start from a usable baseline.
4) DRY templates and site-aware rendering
Organize templates as:
- layouts/_base.twig with global structure and UI tokens.
- partials/ for components (_nav.twig, _footer.twig, _card.twig).
- macros/ for repeated markup logic.
- pages/ or per-section templates (blog/_entry.twig, pages/_entry.twig).
Use {% extends %} and {% block %} for layouts; include partials with a site context (e.g., pass currentSite or resolve craft.app.sites.currentSite). Where copy or menus differ, read from localizable Globals or Categories scoped by siteId. For cross-site assets, prefer site-specific Volumes or per-site subfolders to keep media tidy.
5) Queries, performance, and consistency
Always scope Element Queries by site: craft.entries().section('blog').site(currentSite). Add eager loading for relations to prevent N+1 queries when rendering lists. Normalize time zones and formatting per site (intl filters) and ensure slugs/URIs are unique per site language. Precompute route patterns that include locale segments when required (e.g., /de/blog/{slug}).
6) SEO, hreflang, and governance
Expose hreflang tags that map each entry across sites. Keep canonical URLs consistent and language-aware. Centralize SEO fields (title, description, noindex) in a localizable Global or via an SEO plugin; default values come from the entry but can be overridden per site. Document editorial policy: which fields are localizable, when to fork content vs. reuse, and how redirects are handled across sites. Provide editor-visible labels and instructions on every field.
7) Configuration and environments
Commit project.yaml to version control for deterministic schema. Store base URLs, Volume roots, and API keys as environment variables. Use path aliases for per-site assets, and separate transform caches per site if imagery differs. Backups and content migrations (content seeding scripts, element export/import) must be repeatable between environments.
8) When to split projects
If brands diverge heavily in design and editorial rules, consider separate Craft installs (or a monorepo of multiple apps) with shared design tokens via a front-end library. For moderate divergence, keep a single project: DRY blocks and templates, plus per-site theme switches (design tokens in Globals) are usually enough.
The net effect: a scalable Craft CMS project structure where multi-site and multilingual variants are first-class but don’t explode template or content complexity. Editors get predictable workflows; developers get a clean, testable codebase; and the templates and content stay manageable as the software talent market—and your product catalog—evolves.
Table
Common Mistakes
- Making every field translatable, creating needless work and drift.
- Duplicating Sections per language instead of using Sites and propagation.
- Forking templates per locale rather than DRY partials with site context.
- Using one global Volume for all locales—media becomes untraceable.
- Ignoring site() in Element Queries and rendering wrong-locale entries.
- Letting Matrix grow into 30+ block types with overlapping purpose.
- Missing hreflang/canonical maps; search engines index the wrong pages.
- Storing base URLs in templates instead of env variables and aliases.
- No editorial guidelines—teams guess what to localize and break consistency.
Sample Answers
Junior:
“I’d enable Craft CMS multi-site, add Sites for locales, and mark only text fields localizable. Sections stay shared; Entry Types cover variants. Templates extend a base layout with partials, and queries use .site(currentSite).”
Mid:
“I set Propagation to ‘all sites’ for shared structures and ‘only saved site’ where content differs. Matrix has a small set of reusable blocks. Assets live in per-site Volumes. SEO uses localizable Globals with hreflang and canonical links.”
Senior:
“I design a stable content model with Entry Types, strict i18n rules per field, and DRY templates via macros. We centralize config in project.yaml, use env-based base URLs, eager-load relations, and govern via docs/permissions. Localization is selective, with ‘Copy from primary’ seeding and per-site overrides.”
Evaluation Criteria
- Architecture: Clear use of Sites/Site Groups and correct Propagation.
- Content model: Sections vs. Entry Types used well; fields localizable only when valuable.
- Templates: DRY hierarchy (layouts/partials/macros) with site-aware rendering.
- Queries: Proper .site() scoping, eager loading, and unique URIs per site.
- Assets: Sensible per-site Volumes/aliases; env-driven base URLs.
- SEO: hreflang/canonical strategy; localized SEO fields.
- Governance: Naming conventions, editor guidance, permissions.
Red flags: per-locale template forks; everything translatable; missing hreflang; unscoped queries; bloated Matrix; base URLs hard-coded; no project.yaml.
Preparation Tips
- Spin up a demo with three Sites (en, de, fr) under one Site Group.
- Create two Sections (Pages, Blog) and two Entry Types (Landing, Article).
- Decide translation per field; test “Copy from primary.”
- Build a base layout and 3–4 partials; pass currentSite to includes.
- Add Matrix with 5 focused blocks; localize only text fields.
- Configure per-site Volumes and env-based base URLs.
- Implement hreflang and canonical, and verify per entry.
- Write queries with .site() and eager load relations.
- Document an editorial checklist: what to translate, how to preview, who approves.
Real-world Context
- SaaS marketing: One brand, five locales. Shared Sections with “save to all sites,” localized text/SEO only. Result: ~60% less editor time vs. fully translatable fields.
- Retail EU/UK: Same templates, different pricing/legal blocks via non-localizable toggles + localizable text. Per-site Volumes route to region CDNs. Result: faster regional launches and clean media.
- Publisher: 3 languages, 2 layouts. Matrix limited to 8 block types; Storybook for partials. hreflang map generated from related entries. Result: stable indexing and fewer template forks.
Key Takeaways
- Use Sites + deliberate Propagation; don’t duplicate Sections per locale.
- Localize only text that must change; keep structure/design global.
- Keep templates DRY with layouts/partials/macros and site context.
- Scope queries by site, and organize assets per site with env aliases.
- Ship SEO right: hreflang + canonical + localizable SEO fields.
Practice Exercise
Scenario:
You’re launching a Craft project for one brand in three locales (en, de, fr) plus a UK regional variant. Marketing needs shared structures, localized copy, and region-specific legal/promo blocks. Editors complain about duplication and messy templates.
Tasks:
- Create a Site Group and four Sites (en, de, fr, en-GB) with env-based base URLs.
- Build Sections: Pages (channel) and Blog (channel). Add Entry Types: Landing, Article.
- Define fields: text fields (localizable), design toggles (not translatable), legal notice (localizable), promo flag (not).
- Configure Propagation so entries save to all sites but allow per-site URI/slug overrides.
- Implement a Matrix builder with five blocks (Hero, RichText, Media, FeatureGrid, CTA). Localize only human text.
- Templates: layouts/_base.twig, partials for nav/footer/card; pass currentSite and read navigation from localizable Globals.
- Add hreflang/canonical and verify for an Article.
- Seed one Landing and one Article; localize to de/fr and tweak en-GB legal. Prove queries use .site() and eager loading.
Deliverable:
A repo with project.yaml, env config, and two localized entries rendering correctly across all Sites, plus a short README explaining localization rules and editorial workflow.

