Back to Blog
Craft CMS Multi-Site Architecture

Multi-Site Architecture in Craft CMS: Patterns That Actually Work

· 12 min read

Craft's multi-site feature is one of its strongest selling points. Running multiple sites or languages from a single installation, with shared or separate content, and managing it all from one control panel. When done right, it's elegant. When done wrong, it's a mess that's hard to untangle.

I've built multi-site Craft installations for everything from bilingual marketing sites to organizations running 6+ distinct websites from one Craft install. This post covers the patterns I've found that work well and the mistakes I've learned to avoid.

When to Use Multi-Site

Craft's multi-site feature makes sense when:

  • Multi-language. The same content needs to exist in multiple languages. This is the most common use case.
  • Regional variations. Same brand, different content per region (US vs UK vs Australia). Maybe some content is shared, some is localized.
  • Related but distinct sites. A parent company with multiple brands that share some infrastructure (user accounts, asset volumes) but have different designs and content.
  • Microsites. Campaign or product-specific sites that share the same CMS backend for convenience.

Multi-site does not make sense when:

  • The sites are completely unrelated with no shared content or users. Just use separate Craft installations.
  • You're trying to save on hosting costs by cramming unrelated sites into one install. The management complexity isn't worth it.

Pattern 1: Multi-Language (Same Content, Different Languages)

This is the most straightforward multi-site pattern. You have one site structure with content translated into multiple languages. A blog post exists in English, French, and Spanish. The URL structure usually looks like:

  • example.com/ (English, primary)
  • example.com/fr/ (French)
  • example.com/es/ (Spanish)

Or with subdomains: en.example.com, fr.example.com.

Setting It Up

In the Craft control panel, go to Settings > Sites and create a site for each language. Each site gets:

  • A name (e.g., "English", "French")
  • A language (e.g., en, fr, es)
  • A base URL (e.g., https://example.com/, https://example.com/fr/)
  • Optionally, its own set of templates

Then for each section, you enable propagation. This means when an editor creates an entry on the English site, Craft automatically creates a linked entry on the French and Spanish sites. The editor then translates the content for each language.

Template Considerations

Most multi-language sites share the same templates. You use Craft's translation features for static strings:

Twig
{# Static strings use the |t filter #}
<h2>{{ "Read More"|t }}</h2>
<a href="{{ entry.url }}">{{ "View Article"|t }}</a>

{# Dynamic content comes from the entry (already translated per site) #}
<h1>{{ entry.title }}</h1>
<div>{{ entry.body }}</div>

Static translations live in PHP files at translations/fr/site.php:

translations/fr/site.php
<?php

return [
    'Read More' => 'Lire la suite',
    'View Article' => 'Voir l\'article',
    'Search' => 'Rechercher',
    'No results found.' => 'Aucun résultat trouvé.',
];

Language Switcher

Every multi-language site needs a language switcher. Here's the pattern I use:

Twig language switcher
<nav aria-label="Language">
  {% for site in craft.app.sites.allSites %}
    {% if site.id != currentSite.id %}
      {# Find this entry on the other site #}
      {% set localizedEntry = entry is defined
        ? craft.entries.id(entry.id).siteId(site.id).one()
        : null %}

      <a href="{{ localizedEntry ? localizedEntry.url : site.baseUrl }}"
         lang="{{ site.language }}"
         hreflang="{{ site.language }}">
        {{ site.name }}
      </a>
    {% endif %}
  {% endfor %}
</nav>

This checks if the current entry has a translated version on the other site. If it does, it links to that specific page. If not, it falls back to the other site's homepage. This prevents broken links when not all content is translated.

Don't forget the hreflang tags in your <head> for SEO. SEOmatic handles this automatically if you configure your sites correctly. If you're doing it manually, add a <link rel="alternate"> tag for each language version of the page.

Pattern 2: Regional Sites (Shared Structure, Different Content)

This pattern is for organizations that have the same type of content in different regions but the actual content is different. A real estate company with a US site and a UK site, for example. Both have "Listings" and "Blog Posts," but the listings are completely different per region.

The key decision here is propagation. For multi-language, you usually want entries to propagate (create linked copies). For regional sites, you often want sections to be enabled per site but not propagated. Each region creates its own entries independently.

Configuration

When setting up a section for regional sites, you have control over propagation behavior:

  • Propagate entries across all sites: Every entry exists on every site (good for multi-language)
  • Only save entries to the site they were created in: Each site manages its own entries independently (good for regional)
  • Let each entry choose: Editors decide per entry whether it should appear on other sites

For regional sites, I usually go with "only save entries to the site they were created in" for content like listings and local news, but "propagate across all sites" for things like company info pages that should be consistent globally.

Pattern 3: Multi-Brand (Different Sites, Shared Backend)

This is the most complex pattern. A parent company runs multiple brands, each with its own domain, design, and content. But they share a single Craft installation for efficiency.

Example: A hospitality group with three restaurant brands. Each restaurant has its own website at its own domain, but the marketing team manages all three from one Craft control panel.

Template Organization

Each site gets its own template directory:

templates/
├── _shared/
│   ├── _layouts/
│   │   └── base.twig
│   └── _partials/
│       ├── footer.twig
│       └── nav.twig
├── restaurant-a/
│   ├── _layouts/
│   │   └── base.twig ← extends _shared/_layouts/base
│   ├── index.twig
│   └── menu/_entry.twig
├── restaurant-b/
│   ├── _layouts/
│   │   └── base.twig
│   ├── index.twig
│   └── menu/_entry.twig
└── restaurant-c/
    └── ...

Shared components (header structure, footer, utility partials) live in _shared/. Each brand overrides the parts that are unique to them (colors, logos, layouts, page templates).

In your site settings, point each site's template path to its own directory. You can set this in config/general.php:

config/general.php
return [
    '*' => [
        // Global settings
    ],
    'restaurant-a' => [
        'aliases' => [
            '@siteTemplates' => '@templates/restaurant-a',
        ],
    ],
    'restaurant-b' => [
        'aliases' => [
            '@siteTemplates' => '@templates/restaurant-b',
        ],
    ],
];

Common Mistakes

Not Planning Propagation Upfront

Changing propagation settings after content has been created can cause data loss or duplicated entries. Decide on your propagation strategy before building anything and document it. Changing it later is possible but painful.

Overusing Multi-Site

I've seen people create separate sites for things that should just be different sections or categories within one site. A blog and a knowledge base on the same domain don't need to be separate sites. They're just different sections. Multi-site adds complexity. Don't add it where it's not needed.

Ignoring Asset Volumes

Decide early whether your sites share asset volumes or have separate ones. For multi-language, shared volumes usually make sense (the same image is used in every language). For multi-brand, separate volumes keep each brand's assets organized and prevent editors from accidentally using another brand's images.

Forgetting About Search

Craft's built-in search is per-site by default. If you have a global search that should span all sites, you need to explicitly query across sites:

Cross-site search
{# Search across all sites #}
{% set results = craft.entries
  .search(query)
  .site('*')
  .unique()
  .limit(20)
  .all() %}

The .site('*') tells Craft to search all sites, and .unique() prevents the same entry from appearing multiple times (once per site).

Environment Configuration

Multi-site installations need site-specific environment variables. I use a naming convention in the .env file:

.env
# Site URLs
SITE_URL_EN=https://example.com
SITE_URL_FR=https://example.com/fr
SITE_URL_ES=https://example.com/es

# Or for multi-domain
SITE_URL_RESTAURANT_A=https://restaurant-a.com
SITE_URL_RESTAURANT_B=https://restaurant-b.com

Then reference these in your site settings through the control panel or Project Config, using the $VARIABLE_NAME syntax that Craft supports for environment variables.


Multi-site in Craft is genuinely well-built. It handles the complex cases (shared content with per-site overrides, mixed propagation strategies, different domains from one install) in ways that other CMSs struggle with. The key is planning your architecture before you start building, because restructuring a multi-site installation after content exists is significantly harder than getting it right the first time.

If you're planning a multi-site Craft project and want to make sure the architecture is solid before you start building, let's talk through it.