diff --git a/.gitignore b/.gitignore index 0775ac0..bfcdb96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ +*.DS_Store +.idea/ Documentation/* !Documentation/*.rst - -.idea/ diff --git a/Classes/Http/ContentDimensionDetection/BackendUriDimensionPresetDetector.php b/Classes/Http/ContentDimensionDetection/BackendUriDimensionPresetDetector.php old mode 100644 new mode 100755 diff --git a/Classes/Http/ContentDimensionDetection/ContentDimensionPresetDetectorInterface.php b/Classes/Http/ContentDimensionDetection/ContentDimensionPresetDetectorInterface.php old mode 100644 new mode 100755 diff --git a/Classes/Http/ContentDimensionDetection/DimensionPresetDetectorResolver.php b/Classes/Http/ContentDimensionDetection/DimensionPresetDetectorResolver.php old mode 100644 new mode 100755 diff --git a/Classes/Http/ContentDimensionDetection/SubdomainDimensionPresetDetector.php b/Classes/Http/ContentDimensionDetection/SubdomainDimensionPresetDetector.php old mode 100644 new mode 100755 index f6ade7a..eb0fa17 --- a/Classes/Http/ContentDimensionDetection/SubdomainDimensionPresetDetector.php +++ b/Classes/Http/ContentDimensionDetection/SubdomainDimensionPresetDetector.php @@ -33,15 +33,25 @@ final class SubdomainDimensionPresetDetector implements ContentDimensionPresetDe public function detectPreset(string $dimensionName, array $presets, ServerRequestInterface $request, array $overrideOptions = null) { $host = $request->getUri()->getHost(); + foreach ($presets as $availablePreset) { if (empty($availablePreset['resolutionValue'])) { // we leave the decision about how to handle empty values to the detection component continue; } + $valueLength = mb_strlen($availablePreset['resolutionValue']); + $value = mb_substr($host, 0, $valueLength); - if (mb_substr($host, 0, $valueLength) === $availablePreset['resolutionValue']) { - return $availablePreset; + if ($value === $availablePreset['resolutionValue']) { + if (array_key_exists('resolutionHost', $availablePreset)) { + $domain = mb_substr($host, $valueLength+1); + if ($domain === $availablePreset['resolutionHost']) { + return $availablePreset; + } + } else { + return $availablePreset; + } } } diff --git a/Classes/Http/ContentDimensionDetection/TopLevelDomainDimensionPresetDetector.php b/Classes/Http/ContentDimensionDetection/TopLevelDomainDimensionPresetDetector.php old mode 100644 new mode 100755 index 239f833..10eaa78 --- a/Classes/Http/ContentDimensionDetection/TopLevelDomainDimensionPresetDetector.php +++ b/Classes/Http/ContentDimensionDetection/TopLevelDomainDimensionPresetDetector.php @@ -1,4 +1,5 @@ getUri()->getHost(); $hostLength = mb_strlen($host); foreach ($presets as $preset) { - $pivot = $hostLength - mb_strlen($preset['resolutionValue']); + if (array_key_exists('resolutionHost', $preset)) { + if ($host === $preset['resolutionHost']) { + return $preset; + } + } else { + $pivot = $hostLength - mb_strlen($preset['resolutionValue']); - if (mb_substr($host, $pivot) === $preset['resolutionValue']) { - return $preset; + if (mb_substr($host, $pivot) === $preset['resolutionValue']) { + return $preset; + } } } diff --git a/Classes/Http/ContentDimensionDetection/UriPathSegmentDimensionPresetDetector.php b/Classes/Http/ContentDimensionDetection/UriPathSegmentDimensionPresetDetector.php old mode 100644 new mode 100755 diff --git a/Classes/Http/ContentDimensionLinking/ContentDimensionPresetLinkProcessorInterface.php b/Classes/Http/ContentDimensionLinking/ContentDimensionPresetLinkProcessorInterface.php old mode 100644 new mode 100755 diff --git a/Classes/Http/ContentDimensionLinking/DimensionPresetLinkProcessorResolver.php b/Classes/Http/ContentDimensionLinking/DimensionPresetLinkProcessorResolver.php old mode 100644 new mode 100755 diff --git a/Classes/Http/ContentDimensionLinking/NullDimensionPresetLinkProcessor.php b/Classes/Http/ContentDimensionLinking/NullDimensionPresetLinkProcessor.php old mode 100644 new mode 100755 diff --git a/Classes/Http/ContentDimensionLinking/SubdomainDimensionPresetLinkProcessor.php b/Classes/Http/ContentDimensionLinking/SubdomainDimensionPresetLinkProcessor.php old mode 100644 new mode 100755 index 33475fc..abbbaf9 --- a/Classes/Http/ContentDimensionLinking/SubdomainDimensionPresetLinkProcessor.php +++ b/Classes/Http/ContentDimensionLinking/SubdomainDimensionPresetLinkProcessor.php @@ -34,6 +34,10 @@ public function processUriConstraints(Routing\Dto\UriConstraints $uriConstraints $prefixesToBeReplaced[] = $availablePreset['resolutionValue'] . '.'; } } - return $uriConstraints->withHostPrefix($preset['resolutionValue'] ? $preset['resolutionValue'] . '.' : '', $prefixesToBeReplaced); + + if (array_key_exists('resolutionHost', $preset)) + return $uriConstraints->withHost($preset['resolutionValue'].'.'.$preset['resolutionHost']); + else + return $uriConstraints->withHostPrefix($preset['resolutionValue'] ? $preset['resolutionValue'] . '.' : '', $prefixesToBeReplaced); } } diff --git a/Classes/Http/ContentDimensionLinking/TopLevelDomainDimensionPresetLinkProcessor.php b/Classes/Http/ContentDimensionLinking/TopLevelDomainDimensionPresetLinkProcessor.php old mode 100644 new mode 100755 index 115d0d3..562af90 --- a/Classes/Http/ContentDimensionLinking/TopLevelDomainDimensionPresetLinkProcessor.php +++ b/Classes/Http/ContentDimensionLinking/TopLevelDomainDimensionPresetLinkProcessor.php @@ -38,6 +38,9 @@ public function processUriConstraints( $hostSuffixesToBeReplaced[] = '.' . $availablePreset['resolutionValue']; } - return $uriConstraints->withHostSuffix('.' . $preset['resolutionValue'], $hostSuffixesToBeReplaced); + if (array_key_exists('resolutionHost', $preset)) + return $uriConstraints->withHost($preset['resolutionHost']); + else + return $uriConstraints->withHostSuffix('.' . $preset['resolutionValue'], $hostSuffixesToBeReplaced); } } diff --git a/Classes/Http/ContentDimensionLinking/UriPathSegmentDimensionPresetLinkProcessor.php b/Classes/Http/ContentDimensionLinking/UriPathSegmentDimensionPresetLinkProcessor.php old mode 100644 new mode 100755 index 6ca786c..e54d5a4 --- a/Classes/Http/ContentDimensionLinking/UriPathSegmentDimensionPresetLinkProcessor.php +++ b/Classes/Http/ContentDimensionLinking/UriPathSegmentDimensionPresetLinkProcessor.php @@ -45,7 +45,9 @@ public function processUriConstraints( $options = $overrideOptions ? Arrays::arrayMergeRecursiveOverrule($this->defaultOptions, $overrideOptions) : $this->defaultOptions; $pathSegmentPart = $options['offset'] > 0 ? $options['delimiter'] : ''; $pathSegmentPart .= ($preset['resolutionValue'] ?? $preset['uriSegment']); - - return $uriConstraints->withPathPrefix($pathSegmentPart, true); + if (array_key_exists('resolutionHost', $preset)) + return $uriConstraints->withPathPrefix($pathSegmentPart, true)->withHost($preset['resolutionHost']); + else + return $uriConstraints->withPathPrefix($pathSegmentPart, true); } } diff --git a/Classes/Http/ContentDimensionResolutionMode.php b/Classes/Http/ContentDimensionResolutionMode.php old mode 100644 new mode 100755 diff --git a/Classes/Http/ContentSubgraphUriProcessor.php b/Classes/Http/ContentSubgraphUriProcessor.php old mode 100644 new mode 100755 diff --git a/Classes/Http/ContentSubgraphUriProcessorInterface.php b/Classes/Http/ContentSubgraphUriProcessorInterface.php old mode 100644 new mode 100755 diff --git a/Classes/Http/DetectContentSubgraphMiddleware.php b/Classes/Http/DetectContentSubgraphMiddleware.php old mode 100644 new mode 100755 index 7f4e604..2293d4b --- a/Classes/Http/DetectContentSubgraphMiddleware.php +++ b/Classes/Http/DetectContentSubgraphMiddleware.php @@ -72,7 +72,6 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $uriPathSegmentUsed = false; $dimensionValues = $this->detectDimensionSpacePoint($request, $uriPathSegmentUsed); $workspaceName = $this->detectContentStream($request); - $existingParameters = $request->getAttribute(ServerRequestAttributes::ROUTING_PARAMETERS); if ($existingParameters === null) { $existingParameters = RouteParameters::createEmpty(); @@ -81,7 +80,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $parameters = $existingParameters ->withParameter('dimensionValues', json_encode($dimensionValues)) ->withParameter('workspaceName', $workspaceName) - ->withParameter('uriPathSegmentUsed', $uriPathSegmentUsed); + ->withParameter('uriPathSegmentUsed', $uriPathSegmentUsed) + ->withParameter('requestUriHost', $request->getUri()->getHost()); + $request = $request->withAttribute(ServerRequestAttributes::ROUTING_PARAMETERS, $parameters); return $next->handle($request); @@ -127,7 +128,7 @@ protected function detectDimensionSpacePoint(ServerRequestInterface $request, bo $resolutionMode = $presetConfiguration['resolution']['mode'] ?? ContentDimensionResolutionMode::RESOLUTION_MODE_URIPATHSEGMENT; if ($resolutionMode === ContentDimensionResolutionMode::RESOLUTION_MODE_URIPATHSEGMENT) { - $options['delimiter'] = $this->uriPathSegmentDelimiter; + if (!empty($this->uriPathSegmentDelimiter)) $options['delimiter'] = $this->uriPathSegmentDelimiter; } $preset = $detector->detectPreset($dimensionName, $presetConfiguration['presets'], $request, $options); if ($preset && $resolutionMode === ContentDimensionResolutionMode::RESOLUTION_MODE_URIPATHSEGMENT) { diff --git a/Classes/Http/Exception/InvalidDimensionPresetDetectorException.php b/Classes/Http/Exception/InvalidDimensionPresetDetectorException.php old mode 100644 new mode 100755 diff --git a/Classes/Http/Exception/InvalidDimensionPresetLinkProcessorException.php b/Classes/Http/Exception/InvalidDimensionPresetLinkProcessorException.php old mode 100644 new mode 100755 diff --git a/Classes/Routing/FrontendNodeRoutePartHandler.php b/Classes/Routing/FrontendNodeRoutePartHandler.php old mode 100644 new mode 100755 index a055861..8c3c198 --- a/Classes/Routing/FrontendNodeRoutePartHandler.php +++ b/Classes/Routing/FrontendNodeRoutePartHandler.php @@ -151,6 +151,7 @@ protected function matchValue($requestPath) $tagArray = [$contentContext->getWorkspace()->getName(), $node->getIdentifier()]; $parent = $node->getParent(); + while ($parent) { $tagArray[] = $parent->getIdentifier(); $parent = $parent->getParent(); @@ -197,8 +198,7 @@ protected function convertRequestPathToNode($requestPath) if ($requestPathWithoutContext === '') { $node = $siteNode; } else { - $relativeNodePath = $this->getRelativeNodePathByUriPathSegmentProperties($siteNode, $requestPathWithoutContext); - $node = ($relativeNodePath !== false) ? $siteNode->getNode($relativeNodePath) : null; + $node = $this->getNodeFromRequestPath($siteNode, $requestPathWithoutContext) ?? null; } if (!$node instanceof NodeInterface) { @@ -409,8 +409,7 @@ protected function resolveRoutePathForNode(NodeInterface $node) $nodeContextPathSuffix = ($workspaceName !== 'live') ? substr($nodeContextPath, strpos($nodeContextPath, '@')) : ''; $requestPath = $this->getRequestPathByNode($node); - - return trim($requestPath, '/') . $nodeContextPathSuffix; + return $requestPath . $nodeContextPathSuffix; } /** @@ -422,6 +421,7 @@ protected function resolveRoutePathForNode(NodeInterface $node) * * @param NodeInterface $siteNode The site node, used as a starting point while traversing the tree * @param string $relativeRequestPath The request path, relative to the site's root path + * @deprecated Use getNodeFromRequestPath() - return only the node * @return string * @throws \Neos\ContentRepository\Exception\NodeException */ @@ -448,6 +448,37 @@ protected function getRelativeNodePathByUriPathSegmentProperties(NodeInterface $ return implode('/', $relativeNodePathSegments); } + /** + * Return Node from RequestPath + * + * @param NodeInterface $siteNode The site node, used as a starting point while traversing the tree + * @param string $relativeRequestPath The request path, relative to the site's root path + * @return NodeInterface + * @throws \Neos\ContentRepository\Exception\NodeException + */ + protected function getNodeFromRequestPath(NodeInterface $siteNode, $relativeRequestPath) + { + $matchedNode = false; + $node = $siteNode; + + foreach (explode('/', $relativeRequestPath) as $pathSegment) { + $foundNodeInThisSegment = false; + foreach ($node->getChildNodes('Neos.Neos:Document') as $node) { + /** @var NodeInterface $node */ + if ($node->getProperty('uriPathSegment') === $pathSegment) { + $matchedNode = $node; + $foundNodeInThisSegment = true; + break; + } + } + if (!$foundNodeInThisSegment) { + return false; + } + } + + return $matchedNode; + } + /** * Renders a request path based on the "uriPathSegment" properties of the nodes leading to the given node. * @@ -482,6 +513,6 @@ protected function getRequestPathByNode(NodeInterface $node) $currentNode = $currentNode->getParent(); } - return implode('/', array_reverse($requestPathSegments)); + return '/' . implode('/', array_reverse($requestPathSegments)); } } diff --git a/Documentation/index.rst b/Documentation/index.rst deleted file mode 100644 index cfe77e0..0000000 --- a/Documentation/index.rst +++ /dev/null @@ -1,261 +0,0 @@ -======================================== -Flowpack Neos Content Dimension Resolver -======================================== - -Introduction -============ - -For a general overview over content dimension, please refer to the respective sections in the Neos manual. - -This package enhances the default capabilities of detecting and linking to dimension presets by providing both new -features and extension points. - -Dimension Configuration -======================= - -The available dimensions and presets can be configured via settings: - -.. code-block:: yaml - - Neos: - ContentRepository: - contentDimensions: - - # Content dimension "language" serves for translation of content into different languages. Its value specifies - # the language or language variant by means of a locale. - 'language': - # The default dimension that is applied when creating nodes without specifying a dimension - default: 'mul_ZZ' - # The default preset to use if no URI segment was given when resolving languages in the router - defaultPreset: 'all' - label: 'Language' - icon: 'icon-language' - presets: - 'all': - label: 'All languages' - values: ['mul_ZZ'] - resolutionValue: 'all' - # Example for additional languages: - - 'en_GB': - label: 'English (Great Britain)' - values: ['en_GB', 'en_ZZ', 'mul_ZZ'] - resolutionValue: 'gb' - 'de': - label: 'German (Germany)' - values: ['de_DE', 'de_ZZ', 'mul_ZZ'] - resolutionValue: 'de' - -.. note:: - The ``uriSegment`` configuration option provided by default via Neos is still supported but disencouraged. - -Preset resolution -================= - -Using this package, content dimension presets can be resolved in different ways additional to the "classic" way of using an URI path segment. -Thus further configuration and implementation options have been added. - -The dimension resolver comes with three basic `resolution modes` which can be combined arbitrarily and configured individually. - -URI path segment based resolution ---------------------------------- - -The default resolution mode is ``uriPathSegment``. As by default in previous versions, it operates on an additional path segment, -e.g. ``https://domain.tld/{language}_{market}/home.html``. These are the configuration options available: - -.. code-block:: yaml - - Neos: - ContentRepository: - contentDimensions: - 'market': - resolution: - mode: 'uriPathSegment' - options: - # The offset defines the dimension's position in the path segment. Offset 1 means this is the second part. - # This allows for market being the second uriPath part although it's the primary dimension. - offset: 1 - 'language': - resolution: - mode: 'uriPathSegment' - options: - # Offset 0 means this is the first part. - offset: 0 - Flowpack: - Neos: - DimensionResolver: - contentDimensions: - resolution: - # Delimiter to separate values if multiple dimension are present - uriPathSegmentDelimiter: '-' - -With the given configuration, URIs will be resolved like ``domain.tld/{language}-{market}/home.html`` - -.. note:: - An arbitrary number of dimensions can be resolved via uriPathSegment. - The other way around, as long as no content dimensions resolved via uriPathSegment are defined, URIs will not contain any prefix. - -The default preset can have an empty `resolutionValue` value. The following example will lead to URLs that do not contain -`en` if the `en_US` preset is active, but will show the `resolutionValue` for other languages that are defined as well: - -.. code-block:: yaml - - Neos: - ContentRepository: - contentDimensions: - - 'language': - label: 'Language' - icon: 'icon-language' - default: 'en_US' - defaultPreset: 'en_US' - resolution: - mode: 'uriPathSegment' - presets: - 'en_US': - label: 'English (US)' - values: ['en_US'] - resolutionValue: '' - -The only limitation is that all resolution values must be unique across all dimensions that are resolved via uriPathSegment. -If you need non-unique resolution values, you can switch support for non-empty dimensions off: - -.. code-block:: yaml - - Neos: - Neos: - routing: - supportEmptySegmentForDimensions: FALSE - -Subdomain based resolution --------------------------- - -Another resolution mode is ``subdomain``. This mode extracts information from the first part of the host and adds it respectively -when generating URIs. - -.. code-block:: yaml - - Neos: - ContentRepository: - contentDimensions: - 'language': - default: 'en' - defaultPreset: 'en' - resolution: - mode: 'subdomain' - options: - # true means that if no preset can be detected, the default one will be used. - # Also when rendering new links, no subdomain will be added for the default preset - allowEmptyValue: true - presets: - 'en_GB': - label: 'English' - values: ['en'] - resolutionValue: 'en' - 'de': - label: 'German (Germany)' - values: ['de_DE'] - resolutionValue: 'de' - -With the given configuration, URIs will be resolved like ``{language}.domain.tld/home.html`` - -.. note:: - Only one dimension can be resolved via subdomain. - -Top level domain based resolution ---------------------------------- - -The final resolution mode is ``topLevelDomain``. This modes extracts information from the last part of the host and adds it respectively -when generating URIs. - -.. code-block:: yaml - - Neos: - ContentRepository: - contentDimensions: - 'market': - default: 'eu' - defaultPreset: 'eu' - resolution: - mode: 'topLevelDomain' - presets: - 'EU': - label: 'European Union' - values: ['EU'] - resolutionValue: 'eu' - 'GB': - label: 'Great Britain' - values: ['GB'] - resolutionValue: 'co.uk' - 'DE': - label: 'Germany' - values: ['DE', 'EU'] - resolutionValue: 'de' - -With the given configuration, URIs will be resolved like ``domain.{market}/home.html`` - -.. note:: - Only one dimension can be resolved via top level domain. - -Custom resolution ------------------ - -There are planned extension points in place to support custom implementations in case the basic ones do not suffice. - -Defining custom resolution components -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Each resolution mode is defined by two components: An implementation of ``Neos\Neos\Http\ContentDimensionDetection\ContentDimensionPresetDetectorInterface`` -to extract the preset from an HTTP request and an implementation of ``Neos\Neos\Http\ContentDimensionLinking\ContentDimensionPresetLinkProcessorInterface`` -for post processing links matching the given dimension presets. - -These can be implemented and configured individually per dimension: - -.. code-block:: yaml - - Neos: - ContentRepository: - contentDimensions: - weather: - detectionComponent: - implementationClassName: 'My\Package\Http\ContentDimensionDetection\WeatherDimensionPresetDetector' - linkProcessorComponent: - implementationClassName: 'My\Package\Http\ContentDimensionLinking\WeatherDimensionPresetLinkProcessor' - -If your custom preset resolution components do not affect the URI, you can use the ``Flowpack\Neos\DimensionResolver\Http\ContentDimensionLinking\NullDimensionPresetLinkProcessor`` -implementation as the link processor. - -.. note:: - If you want to replace implementations of one of the basic resolution modes, you can do it this way, too. - -Completely replacing resolution behaviour -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The described configuration and extension points assume that all dimension presets can be resolved independently. -There may be more complex situations though, where the resolution of one dimension depends on the result of the resolution of another. -As an example, think of a subdomain (language) and top level domain (market) based scenario where you want to support ``domain.fr``, -``domain.de``, ``de.domain.ch``, ``fr.domain.ch`` and ``it.domain.ch``. Although you can define the subdomain as optional, -the default language depends on the market: ``domain.de`` should be resolved to default language ``de`` and ``domain.fr`` -should be resolved to default language ``fr``. -Those complex scenarios are better served using individual implementations than complex configuration efforts. - -To enable developers to deal with this in a nice way, there are predefined ways to deal with both detection and link processing. - -Detection is done via an HTTP middleware that can be replaced via configuration: - -.. code-block:: yaml - - Neos: - Flow: - http: - chain: - preprocess: - chain: - detectContentSubgraph: - component: Flowpack\Neos\DimensionResolver\Http\DetectContentSubgraphMiddleware - -Link processing is done by the ``Flowpack\Neos\DimensionResolver\Http\ContentSubgraphUriProcessorInterface``. To introduce your custom behaviour, -implement the interface and declare it in ``Objects.yaml`` as usual in Flow. - -.. note:: - Please refer to the default implementations for further hints and ideas on how to implement resolution. diff --git a/composer.json b/composer.json index a21bdab..2332840 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,8 @@ "license": "GPL-3.0+", "description": "A support package for Neos CMS that allows for arbitrary content dimension resolution.", "require": { - "neos/neos": "^7.0 || ^8.0 || dev-master", - "neos/flow": "^7.0 || ^8.0 || dev-master" + "neos/neos": "~4.0||~5.0||~7.0||~8.0 || dev-master", + "neos/flow": "~5.0||~6.0||~7.0||~8.0 || dev-master" }, "autoload": { "psr-4": { diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..bc0ee11 --- /dev/null +++ b/readme.md @@ -0,0 +1,259 @@ +# Flowpack Neos Content Dimension Resolver + +## Introduction + +For a general overview over content dimension, please refer to the respective sections in the Neos manual. + +This package enhances the default capabilities of detecting and linking to dimension presets by providing both new +features and extension points. + +## Dimension Configuration + +The available dimensions and presets can be configured via settings: + +```yaml + Neos: + ContentRepository: + contentDimensions: + + # Content dimension "language" serves for translation of content into different languages. Its value specifies + # the language or language variant by means of a locale. + 'language': + # The default dimension that is applied when creating nodes without specifying a dimension + default: 'mul_ZZ' + # The default preset to use if no URI segment was given when resolving languages in the router + defaultPreset: 'all' + label: 'Language' + icon: 'icon-language' + presets: + 'all': + label: 'All languages' + values: ['mul_ZZ'] + resolutionValue: 'all' + # Example for additional languages: + + 'en_GB': + label: 'English (Great Britain)' + values: ['en_GB', 'en_ZZ', 'mul_ZZ'] + resolutionValue: 'gb' + 'de': + label: 'German (Germany)' + values: ['de_DE', 'de_ZZ', 'mul_ZZ'] + resolutionValue: 'de' +``` + +>The `uriSegment` configuration option provided by default via Neos is still supported but disencouraged. + +## Preset resolution + +Using this package, content dimension presets can be resolved in different ways additional to the "classic" way of using an URI path segment. +Thus further configuration and implementation options have been added. + +The dimension resolver comes with three basic `resolution modes` which can be combined arbitrarily and configured individually. + +### URI path segment based resolution + +The default resolution mode is `uriPathSegment`. As by default in previous versions, it operates on an additional path segment, +e.g. `https://domain.tld/{language}_{market}/home.html`. These are the configuration options available: + +```yaml +Neos: + ContentRepository: + contentDimensions: + 'market': + resolution: + mode: 'uriPathSegment' + options: + # The offset defines the dimension's position in the path segment. Offset 1 means this is the second part. + # This allows for market being the second uriPath part although it's the primary dimension. + offset: 1 + 'language': + resolution: + mode: 'uriPathSegment' + options: + # Offset 0 means this is the first part. + offset: 0 +Flowpack: + Neos: + DimensionResolver: + contentDimensions: + resolution: + # Delimiter to separate values if multiple dimension are present + uriPathSegmentDelimiter: '-' +``` + +With the given configuration, URIs will be resolved like ``domain.tld/{language}-{market}/home.html`` + +>An arbitrary number of dimensions can be resolved via uriPathSegment. +The other way around, as long as no content dimensions resolved via uriPathSegment are defined, URIs will not contain any prefix. + +The default preset can have an empty `resolutionValue` value. The following example will lead to URLs that do not contain +`en` if the `en_US` preset is active, but will show the `resolutionValue` for other languages that are defined as well: + +```yaml +Neos: + ContentRepository: + contentDimensions: + + 'language': + label: 'Language' + icon: 'icon-language' + default: 'en_US' + defaultPreset: 'en_US' + resolution: + mode: 'uriPathSegment' + presets: + 'en_US': + label: 'English (US)' + values: ['en_US'] + resolutionValue: '' +``` + +The only limitation is that all resolution values must be unique across all dimensions that are resolved via uriPathSegment. +If you need non-unique resolution values, you can switch support for non-empty dimensions off: + +```yaml +Neos: + Neos: + routing: + supportEmptySegmentForDimensions: FALSE +``` + +### Subdomain based resolution + +Another resolution mode is `subdomain`. This mode extracts information from the first part of the host and adds it respectively +when generating URIs. + +```yaml +Neos: + ContentRepository: + contentDimensions: + 'language': + default: 'en' + defaultPreset: 'en' + resolution: + mode: 'subdomain' + options: + # true means that if no preset can be detected, the default one will be used. + # Also when rendering new links, no subdomain will be added for the default preset + allowEmptyValue: true + presets: + 'en_GB': + label: 'English' + values: ['en'] + resolutionValue: 'en' + 'de': + label: 'German (Germany)' + values: ['de_DE'] + resolutionValue: 'de' +``` + +With the given configuration, URIs will be resolved like `{language}.domain.tld/home.html` + +>Only one dimension can be resolved via subdomain. + +### Top level domain based resolution + +The final resolution mode is `topLevelDomain`. This modes extracts information from the last part of the host and adds it respectively +when generating URIs. + +```yaml +Neos: + ContentRepository: + contentDimensions: + 'market': + default: 'eu' + defaultPreset: 'eu' + resolution: + mode: 'topLevelDomain' + presets: + 'EU': + label: 'European Union' + values: ['EU'] + resolutionValue: 'eu' + 'GB': + label: 'Great Britain' + values: ['GB'] + resolutionValue: 'co.uk' + 'DE': + label: 'Germany' + values: ['DE', 'EU'] + resolutionValue: 'de' +``` +With the given configuration, URIs will be resolved like `domain.{market}/home.html` + +>Only one dimension can be resolved via top level domain. + +## Additionaly: Hostname based resolution + +You can also change the behaviour based on the hostname. You can use it on every resolution mode additonaly. You just need to add the option `resolutionHost` to your presets. Here a example how it looks like if you use the `topLevelDomain` mode: + +```yaml +presets: + 'EU': + label: 'European Union' + values: ['EU'] + resolutionHost: 'example.eu' + resolutionValue: 'eu' +``` + +With the given configuration, URIs will be resolved like `{resolutionHost}/home.html`. +If you use the option with the `subdomain` mode it will be resolved like `{resolutionValue}.{resolutionHost}/home.html`. + + +## Custom resolution + +There are planned extension points in place to support custom implementations in case the basic ones do not suffice. + +### Defining custom resolution components + +Each resolution mode is defined by two components: An implementation of `Neos\Neos\Http\ContentDimensionDetection\ContentDimensionPresetDetectorInterface` +to extract the preset from an HTTP request and an implementation of `Neos\Neos\Http\ContentDimensionLinking\ContentDimensionPresetLinkProcessorInterface` +for post processing links matching the given dimension presets. + +These can be implemented and configured individually per dimension: + +```yaml +Neos: + ContentRepository: + contentDimensions: + weather: + detectionComponent: + implementationClassName: 'My\Package\Http\ContentDimensionDetection\WeatherDimensionPresetDetector' + linkProcessorComponent: + implementationClassName: 'My\Package\Http\ContentDimensionLinking\WeatherDimensionPresetLinkProcessor' +``` + +If your custom preset resolution components do not affect the URI, you can use the `Flowpack\Neos\DimensionResolver\Http\ContentDimensionLinking\NullDimensionPresetLinkProcessor` +implementation as the link processor. + +>If you want to replace implementations of one of the basic resolution modes, you can do it this way, too. + +### Completely replacing resolution behaviour + +The described configuration and extension points assume that all dimension presets can be resolved independently. +There may be more complex situations though, where the resolution of one dimension depends on the result of the resolution of another. +As an example, think of a subdomain (language) and top level domain (market) based scenario where you want to support ``domain.fr``, +``domain.de``, ``de.domain.ch``, ``fr.domain.ch`` and ``it.domain.ch``. Although you can define the subdomain as optional, +the default language depends on the market: ``domain.de`` should be resolved to default language ``de`` and ``domain.fr`` +should be resolved to default language ``fr``. +Those complex scenarios are better served using individual implementations than complex configuration efforts. + +To enable developers to deal with this in a nice way, there are predefined ways to deal with both detection and link processing. + +Detection is done via an HTTP Middleware that can be replaced via configuration: + +```yaml +Neos: + Flow: + http: + middlewares: + 'detectContentSubgraph': + position: 'before routing' + middleware: 'Flowpack\Neos\DimensionResolver\Http\DetectContentSubgraphComponent' +``` + +Link processing is done by the `Flowpack\Neos\DimensionResolver\Http\ContentSubgraphUriProcessorInterface`. To introduce your custom behaviour, +implement the interface and declare it in ``Objects.yaml`` as usual in Flow. + +>Please refer to the default implementations for further hints and ideas on how to implement resolution.