Problem/Motivation
Domain allows content to be assigned to different target audiences. The target audience may present itself on a different domain:
However, the target group could also be presented as a subdirectory if a single hostname is important:
- https://apple.com/
- https://www.apple.com/fr/
- https://www.apple.com/benl/
- https://www.apple.com/befr/
Currently this can be implemented with https://www.drupal.org/project/country_path. However, given how tightly coupled the domain path prefix is with the Domain logic, maybe this use case can be supported by Domain itself?
Proposed resolution
Support a path prefix for domains to allow a single hostname to host multiple domains
Remaining tasks
- Write a merge request
- Review
- Commit
User interface changes
To be determined
API changes
To be determined
Data model changes
To be determined
Issue fork domain-3575947
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
mably commentedI do not see how this relates to the Domain module.
Comment #3
mably commentedClosing as out-of-scope based on the architecture analysis below:
Architecture assessment
The Domain module is hostname-only by design. The negotiation flow is:
DomainSubscriber(priority 256) triggers negotiation onKernelEvents::REQUESTDomainNegotiator::negotiateActiveHostname()extractsHTTP_HOSTDomainStorage::loadByHostname()does exact config entity lookuphook_domain_request_alter()allows override (used bydomain_alias)Path prefix routing is a fundamentally different concern — closer to language negotiation or the country_path module already mentioned in the issue.
Possible approaches
1. Close as out-of-scope — Path prefix routing doesn't relate to the Domain module's concept of "domain". Document how
hook_domain_request_alter()can be used for custom matching instead.2. Sub-module
domain_prefix— Implement viahook_domain_request_alter():path_prefixproperty to the Domain entitydomain_pathin complex ways3. Core integration in
DomainNegotiator— Extend the negotiator to check both hostname + path prefix:path_prefixto Domain entity schemaloadByHostname()to also filter by prefixDomainSubscriberto strip prefix from requestRecommendation
Given that:
country_pathalready solves this use casedomain_access,domain_path,domain_alias) all assume hostname-based matchingClosing as out-of-scope seems the most appropriate. For custom needs,
hook_domain_request_alter()provides a clean extension point — it has access to the current request and can match domains based on any criteria, including path prefixes.Comment #5
mably commentedWill investigate whether approach 2 can be implemented as a submodule of the domain_extras project.
Comment #7
mably commentedComment #9
mably commentedWhile working on the domain_prefix submodule for domain_extras, I eventually realized that the feature is not very complicated to add.
@idebr could you give a try to this issue's MR and tell me what you think?
Comment #11
mably commentedSummary
Adds optional
path_prefixproperty to Domain entities, allowing multiple domains to share a single hostname distinguished by URL path prefixes (e.g.example.com/myprefix/...vsexample.com/...). This is useful for sites that cannot add new hostnames (corporate firewalls, shared hosting, single-origin CDN) but need separate domain contexts.Domain entity changes
path_prefix(string, empty by default) onDomainentity withgetPathPrefix()/setPathPrefix()methods.path_prefixtodomain.record.*mapping.DomainUniqueHostnameConstraintValidatornow validates the (hostname + path_prefix) pair — two domains may share a hostname if their prefixes differ.path_prefix: ''on all existing domain records.Domain negotiation
DomainNegotiatorInterface::negotiateByPathPrefix()— given multiple domains sharing a hostname, matches the request path against each domain's prefix (longest prefix first, empty prefix as fallback).DomainStorageInterface::loadMultipleByHostname()— loads all domains for a given hostname.setRequestDomain()now callsnegotiateByPathPrefix()when multiple candidates share a hostname.Path processing
DomainPrefixPathProcessor— handles both inbound and outbound path processing.$options['domain']if set, otherwise the active domain./myprefix/fr/about-us= domain prefix + language prefix + path alias.Domain Alias integration
DomainAliasHooks::domainRequestAlter()now callsnegotiateByPathPrefix()after alias resolution, so that alias + prefix combinations work correctly (e.g.alias.example.com/myprefix/pageresolves the alias then selects the prefixed domain).Admin UI
example.com.myprefix→example_com_myprefix).Drush
domain:add: new--path-prefixoption.domain:list:path_prefixcolumn in default output fields.Documentation
docs/domain/path_prefix.md— comprehensive documentation covering negotiation flow, matching rules, configuration, interaction with all submodules, technical details, and performance.Tests (28 new test methods)
Files changed
28 files, 2105 insertions, 41 deletions.
Comment #12
mably commentedPerformance impact analysis
This feature introduces four new code paths that run per request. Here is a detailed breakdown of the cost in each scenario.
1. Domain negotiation (
setRequestDomain)Before:
loadByHostname()— callsloadByProperties(['hostname' => $value]), returns the first match.After:
loadMultipleByHostname()— sameloadByProperties()call, but returns all matches instead of just the first.loadByProperties()loadByProperties()+count()check → early returncount <= 1). Negligible.loadByProperties()+usort()on 2–5 entries + onestr_starts_with()per candidateloadByProperties()operates on config entity storage which is cached in memory after the first read per request. No additional database queries are introduced.2. Path prefix disambiguation (
negotiateByPathPrefix)Called once during negotiation and potentially once more from Domain Alias (if an alias matches).
count <= 1. Cost: zero.usort()on 2–5 entries by string length (integer comparison), then onestr_starts_with()per candidate until a match is found. All in-memory, no I/O.3. Inbound path processor (
DomainPrefixPathProcessor::processInbound)Runs once per request during routing (priority 350, before language at 300).
getDomain()returns null →instanceofcheck → return. One comparison.getDomain()+getPathPrefix()returns''→ string comparison → return. Two property reads + one comparison.str_starts_with()+ onesubstr(). Three string operations.4. Outbound path processor (
DomainPrefixPathProcessor::processOutbound)Runs once per generated URL (priority 50, after language at 100).
!empty($options['external'])→ return. One check.$options['domain'], no active domain)instanceofcheck × 2 → return. Two comparisons.getPathPrefix()returns''→ return. Three checks total.addCacheContexts(). Trivial.For a typical page rendering ~100 URLs on a site that does not use prefixes, this adds ~300 cheap boolean/instanceof checks total — well under a microsecond.
5. Uniqueness constraint (
DomainUniqueHostnameConstraintValidator)Runs only on domain save (admin form or Drush), not on every request.
Before: loaded all domains with matching hostname, failed if any existed (besides self).
After: same
loadByProperties()call, but iterates matches comparingpath_prefix— only fails if both hostname and prefix match.Cost difference: one additional
getPathPrefix()string comparison per existing domain with the same hostname. Only runs during admin operations. Zero runtime impact.6. Domain Alias integration
Before:
domainRequestAlter()loaded a single domain by alias target ID.After: loads the target domain, then calls
loadMultipleByHostname()to get all domains sharing that hostname, then callsnegotiateByPathPrefix().DOMAIN_MATCHED_EXACTearly return — unchanged, zero cost.loadByProperties()(in-memory, config cache) +count <= 1early return.usort()+str_starts_with()per candidate (2–5 entries).The additional
loadMultipleByHostname()uses the config entity static cache, so if the target domain was already loaded (which it was, via$domain_storage->load()), the underlyingloadByProperties()filters in memory with no additional storage reads.7. New cache context
The outbound processor adds
url.pathcache context only when a prefix is actually prepended to a URL. Sites that do not use path prefixes will never see this context added. Sites that do use prefixes will have render cache entries varied by path — this is necessary for correctness and has no impact beyond the expected cache granularity.Summary
count()checkConclusion: for sites that do not use path prefixes (the vast majority), the overhead is unmeasurable — a handful of boolean checks and empty-string comparisons per request. For sites that use prefixes, the cost is a small in-memory sort and string comparison on 2–5 domain entities, with no additional database or storage queries.
Comment #13
mably commentedArchitecture comparison: domain path_prefix vs country_path module
domain_path)path_prefix) in schemahook_domain_request_alter()— runs after negotiation, overrides the resultsetRequestDomain()vianegotiateByPathPrefix()— part of core negotiationexample.com/usaparsed withexplode('/')path_prefixfield on the formhook_entity_type_build()preSave()on custom entity classpreg_match+preg_replacestr_starts_with()+substr()$options['prefix'] = "$suffix/" . $options['prefix']LanguageNegotiationCountryPathUrlplugin that parses 2 path segments/country_path, custom validator allowing/in patternsnegotiateByPathPrefix()after alias resolution — aliases stay hostname-onlyDomainAliasValidatorto allow/in patternsurl.country— always addedurl.path— only added when prefix is prepended$options['active_domain'])$options['domain']with fallback to active domainKey architectural differences
1. Integration depth: country_path is a bolt-on that replaces 4 classes (entity, form, list builder, alias validator) via hooks. The path_prefix feature is integrated directly into the domain entity, negotiator, and storage — no class replacements.
2. Negotiation flow: country_path runs its detection in
hook_domain_request_alter(), meaning the negotiator first picks a domain (potentially wrong), then country_path overrides it. The path_prefix approach loads all hostname candidates upfront and picks the right one during initial negotiation — one pass, no override.3. Language handling: country_path needs a custom language negotiation plugin that manually parses two path segments (country + language). The path_prefix approach relies on processing order — the domain prefix is stripped at priority 350, then core's
LanguageNegotiationUrlat priority 300 sees a clean path. No custom language plugin needed.4. Alias handling: country_path embeds the prefix into alias patterns (
cdn.example.com/usa), requiring a modified validator that allows/in patterns. The path_prefix approach keeps aliases hostname-only and runs prefix disambiguation after alias resolution — aliases don't need to know about prefixes.5. Performance: country_path uses regex (
preg_match+preg_replace) for path stripping. The path_prefix approach usesstr_starts_with()+substr()— simpler and faster. country_path always adds a cache context; path_prefix only adds one when a prefix is actually in use.Comment #14
mably commentedShould we merge this? Waiting for your RTBC love.
Comment #15
mably commentedUpdates since #13
Domain negotiation
negotiateByPathPrefix()now returnsNULLwhen no prefix matches the request path, instead of returning an arbitrary fallback domain. The caller insetRequestDomain()handles this by falling through tohook_domain_request_alter()/ default domain logic.=== 0/=== 1) instead of the less readablereset($candidates) ?: NULL.URL generation (setPath / setUrl)
setPath()now includes the domain's path prefix in the base URL. Cost: onegetPathPrefix()property read + one string concatenation per call. Negligible.setUrl()now usesDomainPrefixPathProcessor::processInbound()to strip the active domain's prefix from the request URI, then prepends the target domain's prefix. Cost: one service container lookup (singleton, cached after first call), twoparse_url()calls (native C function), and onestr_starts_with()check. Runs once per domain entity per request (lazy initialization). Negligible.Domain Alias integration
domainLoad(), domains sharing the same canonical hostname as the active domain (i.e. differing only by path prefix) are now rewritten directly — no per-domain alias lookup needed. This is both simpler and faster than the previous domain ID check + separate elseif.negotiateByPathPrefix()returnsNULLafter alias resolution (matching the existing pattern for failed domain target loads).prefixMessageproperty on the constraint).Tugboat
defaulttolocalenvironment — fixes hostname rewriting for both the default and prefixed domains in the preview.Documentation
--environment=localis needed.Tests (31 test methods, up from 28)
DomainPrefixTest: 13 tests (was 10) — addedtestSetPathIncludesPrefix,testSetUrlSwapsPrefix,testSetUrlStripsActivePrefix. UpdatedtestSameHostnameSamePrefixRejectedandtestValidationConstraintwith message assertions.DomainAliasPrefixTest: 4 tests (was 3) — addedtestEnvironmentRewritePrefixedDomains.DomainPrefixLanguageTest: 5 tests (unchanged).Updated performance summary
For sites without path prefixes: the only new cost compared to #13 is one extra empty-string comparison in
setPath()and one service lookup + twoparse_url()calls insetUrl()— both lazy and per-entity. Unmeasurable.For sites with path prefixes:
setUrl()reuses the existingDomainPrefixPathProcessorto swap prefixes (no logic duplication). IndomainLoad(), same-hostname domains skip alias lookup entirely. Total overhead remains a handful of in-memory string operations per loaded domain entity.Files changed
31 files, ~2400 insertions, ~50 deletions.
Comment #16
mably commentedIt's ultimately much more complicated than I initially thought. 😉
Comment #17
mably commentedGate path prefix feature behind a config toggle
Added a
domain.settings:path_prefixboolean (default:FALSE) under Experimental features on the Domain settings page. When disabled, all path prefix components are removed from the container — zero runtime overhead. Follows the same pattern as the existingwww_prefixsetting.What the toggle guards
DomainServiceProvider::alter()domain.prefix_path_processorservice + FQCN alias from containerDomainNegotiator::setRequestDomain()loadByHostname()instead of multi-candidate prefix negotiationDomainHooks::languageNegotiationInfoAlter()LanguageNegotiationUrlwithLanguageNegotiationDomainUrlDomainAliasHooks::domainRequestAlter()Domain::setUrl()DomainForm::form()DomainListBuilderInfrastructure
DomainServiceProvider::register()setsdomain.path_prefixcontainer parameter from bootstrap configConfigSubscriber::onConfigSave()invalidates the container whenpath_prefixchanges#[Autowire(param:)], constructor injection viacreate()/createInstance()) — no\Drupal::static calls except in theDomainentity class where DI is unavailableLanguageNegotiationDomainUrlnow implementsContainerFactoryPluginInterfacefor proper DIFiles changed
domain module:
config/install/domain.settings.yml— addedpath_prefix: falseconfig/schema/domain.schema.yml— added schema entrysrc/Form/DomainSettingsForm.php— checkbox in experimental featuressrc/DomainServiceProvider.php— sets parameter + removes service when disabledsrc/DomainNegotiator.php— guards prefix negotiationsrc/Hook/DomainHooks.php— guards language negotiation swapsrc/Entity/Domain.php— guards setUrl() prefix logicsrc/Plugin/LanguageNegotiation/LanguageNegotiationDomainUrl.php— DI via ContainerFactoryPluginInterfacesrc/EventSubscriber/ConfigSubscriber.php— invalidates container on path_prefix changesrc/Form/DomainForm.php— hides prefix fieldsrc/DomainListBuilder.php— hides prefix columntests/src/Kernel/DomainPrefixTest.php— enables setting in setUptests/src/Kernel/DomainPrefixLanguageTest.php— enables setting in setUpdomain_alias module:
src/Hook/DomainAliasHooks.php— guards prefix disambiguation in alias resolutiontests/src/Kernel/DomainAliasPrefixTest.php— enables setting in setUpOther:
.tugboat/config.yml— enables path_prefix setting for preview environmentsdocs/domain/path_prefix.md— documents the toggle and updated performance sectiondocs/domain/index.md— moved path prefix under experimental featuresComment #18
mably commentedBackward Compatibility Analysis
Interface additions (additive, non-breaking for consumers)
These add new methods to interfaces — not BC-breaking for consumers, but BC-breaking for implementations (any class implementing the interface directly must add the new methods):
DomainInterface— newgetPathPrefix(): stringandsetPathPrefix(string): static. Any custom class implementingDomainInterfacedirectly (not extendingDomain) will break.DomainNegotiatorInterface— newnegotiateByPathPrefix(array): ?DomainInterface. Any custom negotiator implementing the interface directly will break.DomainStorageInterface— newloadMultipleByHostname(string): array. Any custom storage implementing the interface directly will break.Constructor changes (minor risk)
The following constructors gain a new
$pathPrefixEnabledparameter with defaultFALSE, so existing code using the container is unaffected. Custom service decorators or subclasses overridingcreate()may need adjustment:DomainNegotiator::__construct()DomainHooks::__construct()DomainAliasHooks::__construct()DomainForm::__construct()Behavioral changes (non-breaking)
Domain::validateHostnameUniqueness()— relaxed from rejecting duplicate hostnames to allowing shared hostnames with different path prefixes. Existing domains have empty prefixes, so the check is equivalent for current data.DomainUniqueHostnameConstraintValidator::validate()— loops over all hostname matches instead of checking just the first. Adds the prefix dimension but does not change behavior for existing single-hostname domains.DomainTestTrait::domainCreateTestDomains()— new$prefixesparameter with defaultFALSE. Non-breaking for existing callers.Summary
The commit is mostly backward-compatible when the feature is disabled (default
path_prefix: false). The main concern is the 3 interface additions — any third-party code that directly implementsDomainInterface,DomainNegotiatorInterface, orDomainStorageInterface(rather than extending the concrete classes) will need to add the new methods. This is standard for a minor version feature addition in Drupal contrib, but it should be documented in the release notes.Comment #19
mably commentedPerformance impact summary
What's new at runtime
DomainPrefixPathProcessorLanguageNegotiationDomainUrlLanguageNegotiationUrlusedDomainNegotiator::setRequestDomain()loadByHostname()(single match)loadMultipleByHostname()+ prefix matchingDomainCacheContext(required)domainreplacesurl.sitein required render cache contexts (unconditional)Per-request cost breakdown
1. Domain negotiation (runs once, early in kernel boot)
loadByHostname()→ callsloadByProperties(['hostname' => $host])— in-memory filter on config entities. Same as before.loadMultipleByHostname()→ sameloadByProperties()call (returns all matches instead of first), thennegotiateByPathPrefix()— sorts candidates by prefix length (usort, typically 2–5 items) + onestr_starts_withper candidate. Negligible overhead — a handful of string comparisons on short strings, cached domain entities already in memory.2. Inbound path processing (runs once per request)
str_starts_with), strips prefix viasubstr. ~3 method calls, all string ops.3. Outbound path processing (runs per generated URL)
$options['prefix'], addsdomaincache context to bubbleable metadata. ~5 method calls per URL. For a page generating 50 URLs, that's ~250 trivial method calls — microsecond territory.4. Language negotiation override (runs once per request, only when language module is active)
LanguageNegotiationUrl::getLangcode()readsgetPathInfo()directly.LanguageNegotiationDomainUrl::getLangcode()strips domain prefix from path before language prefix lookup. One extrastr_starts_with+substr. No additional config reads — samelanguage.negotiationconfig as core.5. Cache context:
domainreplacesurl.siteurl.sitecalled$request->getSchemeAndHttpHost()— one string read.domaincalls$negotiationContext->getDomainId()— one property read on an already-resolved object. Identical cost.Cache storage impact
No increase in cache entries for subdomain setups. Path prefix setups get correct per-domain entries where they previously shared a single (incorrect) entry.
The
domain.path_prefixguardThe config toggle
domain.settings.path_prefixis read once at container build time (BootstrapConfigStorageFactory), set as a container parameter, and used to:DomainPrefixPathProcessorfrom the container when disabledloadMultipleByHostname()+ prefix matching in the negotiatorhook_language_negotiation_info_alteris a no-op)When the feature is off, the only runtime cost compared to the previous code is:
domaincache context instead ofurl.site(identical cost)boolparameter injected into the negotiator constructor (negligible)Bottom line: zero measurable performance impact when prefix is disabled, and microsecond-level overhead when enabled.
Comment #20
idebr commentedWow, nice to see the path prefix integrated in Domain. Fine work 🙏
A key insight in the country path module was to update the output for Domain::getPath to include the path prefix. This allows other Domain code to work with path prefixes without any moderations, such as DomainNavBlock:
See https://git.drupalcode.org/project/country_path/-/merge_requests/19/diffs
The path prefix could probably use some property validation, such as 'no slashes': https://git.drupalcode.org/project/country_path/-/blob/8.x-1.x/src/Count...
Comment #21
mably commented@idebr I tried to implement your suggestions. Let me know what you think.
Add getBasePath(), fix setUrl() for subdirectory installs
Problem
setUrl()usedparse_url($request->getRequestUri(), PHP_URL_PATH)to get the current path, then passed it toDomainPrefixPathProcessor::processInbound()for prefix stripping. In subdirectory installs (e.g., Drupal running at/drupal/),getRequestUri()includes the base path, so the raw path is/drupal/fr/admin/config.processInbound()expects the prefix immediately after/, so it fails to find and strip/fr. The prefix then gets prepended at the wrong position:/fr/drupal/admin/configinstead of/drupal/fr/admin/config.Additionally,
getPath()returned the raw base URL (scheme + hostname + base_path) without the path prefix, which forced callers likegetLink()andDomainNavBlockto manually append the prefix — duplicating logic and risking inconsistency.setPath()also relied onglobal $base_pathinstead of using Symfony's request API.Solution
New
getBasePath()method — returns the raw base URL (scheme + hostname + base_path) without the path prefix. This is whatDomainPathProcessor,DomainValidator,DomainForm, andDomainAccessFieldneed, since the prefix is handled separately byDomainPrefixPathProcessor.getPath()now includes the prefix — appends the path prefix to the base path when set. This eliminates manual prefix additions ingetLink()andDomainNavBlock::build().setUrl()rewritten — uses$request->getPathInfo()(which excludes the base path) for prefix manipulation, then rebuilds the URL usinggetBasePath(). Without the path prefix feature enabled, preserves the original simple behavior:scheme + hostname + requestUri.setPath()uses$request->getBasePath()instead ofglobal $base_path.Changes
getBasePath()method declarationgetBasePath();getPath()now includes prefix;setPath()uses request API;setUrl()usesgetPathInfo()/getBasePath();getLink()simplified (no more manual prefix)getPath())getBasePath()forbase_urloptiongetBasePath()for server response check URLgetBasePath()for error message URLgetBasePath()forbase_urloptionsetUrl()with subdirectory base_path + prefix; updatedtestBasePathExcludesPrefixto verify both methodstestRewriteWithSubdirectoryInstallto use request server vars instead ofglobal $base_pathComment #22
mably commentedNon-ASCII path prefix support
This patch extends the existing
allow_non_asciisetting (used for hostnames and aliases) to also apply to path prefixes. This allows sites using internationalized domain names to also use non-ASCII prefixes likebelgiëor日本.Validation (three-tier)
path_prefixRegex constraint now uses Unicode character classes (\p{L}\p{N}) as a permissive baseline. This catches completely invalid values (slashes, spaces, leading hyphens) during config imports without needing a save.allow_non_asciiis disabled (default), a stricter ASCII-only pattern ([a-z0-9][a-z0-9_\-]*) is enforced. The HTML5#patternattribute is also only added in ASCII mode since it doesn't support\p{}classes.Percent-encoding fix
Browsers send non-ASCII path segments as percent-encoded UTF-8 (e.g.,
belgië→belgi%C3%AB). Symfony'sRequest::getPathInfo()returns the encoded form, but the stored prefix is raw UTF-8. Without decoding, prefix matching fails — the negotiator can't find the domain, the inbound processor can't strip the prefix, andsetUrl()adds a second copy of the prefix on top of the encoded one.Fix:
rawurldecode()is applied togetPathInfo()in three places before prefix comparison:DomainNegotiator::negotiateByPathPrefix()— decodes path before matching candidatesDomainPrefixPathProcessor::processInbound()— decodes path before stripping prefixDomain::setUrl()— decodes path before prefix swapPerformance implication
rawurldecode()is a trivial string operation (single pass, no memory allocation for ASCII-only input since there's nothing to decode). For non-ASCII input, it performs one UTF-8 decode pass on a short string (the path info). This has zero measurable performance impact — it's a few microseconds per request at most. The three call sites only execute when path prefix support is enabled, so sites not using this feature pay nothing.Note: hostnames don't need this treatment because IDN domains use punycode in DNS (pure ASCII), and
Request::getHttpHost()returns the raw ASCII hostname.UI update
The setting label on the Domain settings page is updated from "Allow non-ASCII characters in domains and aliases" to "Allow non-ASCII characters in domains, aliases, and prefixes".
Test coverage (8 new test methods)
testNonAsciiPrefixRejectedByDefault— preSave() throws ConfigValueException whenallow_non_asciiis FALSEtestNonAsciiPrefixAcceptedWhenEnabled— Unicode prefix saves and persists when enabledtestSchemaValidationAcceptsUnicodePrefix— schema Regex accepts CJK characterstestSchemaValidationRejectsInvalidPrefix— schema Regex rejects slashes, spaces, leading hyphenstestAsciiPrefixStillWorksWhenNonAsciiEnabled— ASCII prefixes unaffectedtestNonAsciiPrefixNegotiation— negotiator selects correct domain from percent-encoded request pathtestNonAsciiPrefixInboundProcessing— inbound processor strips both encoded and raw prefixestestNonAsciiPrefixSetUrl— setUrl() produces a single prefix, not a doubled oneComment #23
kergand commentedPath prefix is very good feature. This is the feature what I would use.
Try to test it out.
Nice hob 🙌
Comment #24
mably commentedRisks of merging the path prefix feature
1. Experimental flag — The feature is behind a settings checkbox and a DomainServiceProvider that removes all prefix services when disabled. Zero runtime overhead when off. But once sites start using it, removing it becomes a breaking change.
2. Non-ASCII encoding boundary — Non-ASCII prefix matching is handled in a single method (Domain::matchPathPrefix()) which decodes only the first path segment when needed. The fallback is guarded by three checks (container parameter, prefix byte check, percent-encoding presence) and only runs for non-ASCII prefixes on encoded paths. The risk of missed decode sites is low because all prefix comparison goes through this one method.
3. Language negotiation interaction — LanguageNegotiationDomainUrl must strip the domain prefix before the language prefix. The ordering is tested but multilingual + multi-domain + prefix is a complex matrix that may surface edge cases.
4. Contrib module compatibility — Modules that build URLs using $domain->getPath() get the prefix for free. Modules that build URLs manually from getHostname() won't include the prefix. This is a documentation/adoption concern, not a bug.
@idebr, waiting for your suggestions or your RTBC if you think we are ready 😉
Comment #25
idebr commentedThanks for your continued work on this issue! I think this is now ready:
Applied the patch to a project running Domain + Country path and performed the following changes:
url.countrywithdomaininparameters.renderer.config.required_cache_contextsI opened a follow-up issue in the country_path module to implement these steps as an upgrade path and mark itself obsolete: #3578287: Provide upgrade path to Domain path prefix feature
Comment #26
mably commentedThanks @idebr for your review and valuable feedback.
Point 1 is normally automatically handled by the
DomainServiceProviderclass.Let's get this merged.
Comment #28
mably commented