Path prefix¶
The Domain module supports an optional path prefix on domain records, allowing multiple domains to share a single hostname while being distinguished by the first segment of the URL path.
This is useful for sites that cannot add new hostnames (e.g., corporate firewalls, shared hosting, single-origin CDN) but need separate domain contexts for different audiences, regions, or brands.
How it works¶
Each domain record has an optional path_prefix property (empty string by
default). When multiple domains share the same hostname with different
prefixes, the negotiator disambiguates them by matching the request path
against each domain's prefix.
Example¶
Given three domain records:
| Domain name | Hostname | Path prefix | Purpose |
|---|---|---|---|
| Default | example.com |
(empty) | Main site |
| Belgium NL | example.com |
benl |
Belgian Dutch |
| France | example.com |
fr |
French |
A request to example.com/benl/fr/about-us is processed as follows:
- Domain negotiation -- the negotiator loads all domains matching
example.com, finds three candidates, and matches the prefixbenlagainst the request path/benl/fr/about-us. - Inbound path processing -- the
DomainPrefixPathProcessor(priority 350) strips the domain prefix, yielding/fr/about-us. - Language negotiation -- core's language negotiation strips the language
prefix
fr, yielding/about-us. - Path alias resolution -- Drupal resolves
/about-usto the internal path (e.g.,/node/1).
Outbound URL generation reverses the process:
- Path alias --
/node/1becomes/about-us. - Language processor -- adds
fr/prefix. - Domain prefix processor (priority 50) -- prepends
benl/to the URL prefix, producingbenl/fr/about-us.
Prefix matching rules¶
- Longest prefix first -- when prefixes overlap (e.g.,
frandfr-be), the longest matching prefix wins. A request to/fr-be/pagematchesfr-be, notfr. - Empty prefix as fallback -- the domain with no prefix acts as the fallback when no prefix matches the request path.
- Exact segment matching -- the prefix must match a full path segment.
/france/pagedoes not match prefixfr.
Configuration¶
Enabling path prefix support¶
Path prefix support is disabled by default. To enable it, go to the Domain
settings page (/admin/config/domain/settings), expand Experimental
features, and check Enable path prefix support.
When disabled, all path prefix components are removed from the container (zero runtime overhead), the path prefix field is hidden on the domain form, and the prefix column is hidden on the domain list.
Adding a path prefix to a domain¶
Once path prefix support is enabled, the domain add or edit form
(/admin/config/domain/add or /admin/config/domain/edit/{domain})
displays a Path prefix field. The value should be a simple string
without leading or trailing slashes (e.g., fr, benl, asia-pacific).
Uniqueness constraint¶
The combination of hostname and path prefix must be unique. Two domains may share the same hostname only if their path prefixes differ. Attempting to save two domains with the same hostname and the same prefix (including both empty) will trigger a validation error.
Backward compatibility¶
Existing domain records default to an empty path prefix. The feature is
disabled by default and must be enabled in /admin/config/domain/settings.
Interaction with other modules¶
Language negotiation (URL prefixes)¶
The domain prefix is the outermost path segment, placed before any language prefix. The processing order is:
| Direction | Priority | Processor | Action |
|---|---|---|---|
| Inbound | 350 | DomainPrefixPathProcessor |
Strips domain prefix |
| Inbound | 300 | LanguageNegotiationUrl |
Strips language prefix |
| Outbound | 100 | LanguageNegotiationUrl |
Adds language prefix |
| Outbound | 50 | DomainPrefixPathProcessor |
Prepends domain prefix |
A URL like /benl/fr/about-us is decomposed as:
/benl/fr/about-us
^^^^ → domain prefix (stripped first inbound, added last outbound)
^^ → language prefix
^^^^^^^^ → path alias
Domain Access¶
Domain Access assigns content visibility per domain. Path-prefixed domains are full domain entities, so Domain Access field values and node grants work identically -- each prefixed domain can have its own content assignments.
Domain Config / Domain Config UI¶
Domain Config provides per-domain configuration overrides. Each prefixed domain is a distinct config entity, so it receives its own configuration overrides as expected.
Domain Alias¶
Domain Alias provides alternate hostnames for a domain. Aliases match by hostname, not by path prefix.
Important: when multiple domains share the same hostname with different path prefixes, you only need to create aliases on the non-prefixed (default) domain for that hostname. The alias resolves the hostname; the prefix negotiation then selects the correct domain based on the URL path. Creating the same alias pattern on a prefixed domain will fail because alias patterns are globally unique.
For example, if example.com (no prefix) and example.com (prefix fr)
share a hostname, add *.example.com as an alias on the non-prefixed
domain only. Requests to something.example.com/fr/page will resolve the
alias to example.com, then the prefix negotiation picks the fr domain.
Domain Source¶
Domain Source assigns a canonical domain to content for URL generation. When a content entity's source domain has a path prefix, the generated URL automatically includes the prefix.
Domain Path¶
Domain Path operates on internal paths (after inbound prefix stripping), so it works without modification.
Domain Content¶
Domain Content provides per-domain content overview pages. Each prefixed domain appears as a separate entry in the domain filter.
Technical details¶
Programmatic usage¶
// Get the path prefix of a domain entity.
$prefix = $domain->getPathPrefix();
// Set the path prefix.
$domain->setPathPrefix('benl');
$domain->save();
// Load all domains sharing a hostname.
$storage = \Drupal::entityTypeManager()->getStorage('domain');
$domains = $storage->loadMultipleByHostname('example.com');
// getBasePath() returns scheme + hostname + base_path (no prefix).
// Use this when building base URLs for outbound path processors.
$base = $domain->getBasePath();
// e.g. "http://example.com/"
// getPath() returns the full path including the prefix.
// Use this for display and direct linking.
$path = $domain->getPath();
// e.g. "http://example.com/fr/"
Outbound URL generation¶
The DomainPrefixPathProcessor prepends the prefix to the prefix option
used by Drupal's URL generator. For URLs generated with the domain option
(see Cross-domain URL rewriting),
the target domain's prefix is used. For all other URLs, the active domain's
prefix is used.
use Drupal\Core\Url;
// URL targeting a prefixed domain includes the prefix automatically.
$domain = $storage->load('example_com_fr');
$url = Url::fromRoute('entity.node.canonical', ['node' => 1]);
$url->setOption('domain', $domain);
// Generates: http://example.com/fr/node/1
Subdirectory installs¶
Path prefix support works correctly when Drupal runs in a subdirectory
(e.g., example.com/drupal/). The setUrl() and setPath() methods
use Symfony's Request::getPathInfo() and Request::getBasePath() to
separate the subdirectory base path from the route path before prefix
manipulation. The resulting URL preserves the correct order:
scheme + hostname + base_path + prefix + route_path.
For example, with base path /drupal/ and prefix fr, a request to
/drupal/fr/admin/config produces the URL
http://example.com/drupal/fr/admin/config.
Performance¶
The feature has no measurable performance impact:
- When disabled -- the path prefix path processor, language negotiation override, and prefix negotiation logic are completely removed from the container. Zero runtime overhead.
- When enabled but no domain uses a prefix -- all code paths early-return on empty string checks.
- When prefixes are active -- the negotiator sorts a small in-memory array (typically 2-5 entries) and does one string comparison per candidate. No additional storage queries are issued.
- No new cache contexts -- the outbound processor adds the
domaincache context, which is already present on every page of a domain-aware site. No additional cache fragmentation is introduced.
Non-ASCII prefixes¶
The Allow non-ASCII characters setting on the Domain settings page
(/admin/config/domain/settings) also applies to path prefixes. When
enabled, Unicode lowercase letters and numbers are accepted in prefixes
(e.g., belgië, 日本). When disabled (the default), only ASCII
lowercase a-z, digits 0-9, hyphens, and underscores are permitted.
Config schema¶
The path_prefix field is declared in domain.schema.yml as a string
property on domain.record.* with a Regex constraint using Unicode
character classes (\p{L}, \p{N}) as a permissive baseline:
domain.record.*:
type: config_entity
mapping:
# ... existing fields ...
path_prefix:
type: string
label: 'Path prefix'
constraints:
Regex:
pattern: '/^([\p{L}\p{N}][\p{L}\p{N}_\-]*)?$/u'
message: 'The path prefix may only contain ...'
The stricter ASCII-only check is enforced at form validation and entity
preSave() time when the Allow non-ASCII characters setting is
disabled. The schema regex serves as a baseline that catches completely
invalid values during config imports.
This makes the domain.record.* schema fully validatable via
TypedConfigManager::createFromNameAndData()->validate(), catching
invalid values during config imports and form validation without
requiring a save.