diff --git a/Classes/Fusion/XmlSitemapUrlsImplementation.php b/Classes/Fusion/XmlSitemapUrlsImplementation.php index a499f77..57170cf 100644 --- a/Classes/Fusion/XmlSitemapUrlsImplementation.php +++ b/Classes/Fusion/XmlSitemapUrlsImplementation.php @@ -12,34 +12,35 @@ * source code. */ -use Neos\ContentRepository\NodeAccess\NodeAccessorManager; -use Neos\ContentRepository\Projection\ContentGraph\Node; -use Neos\ContentRepository\SharedModel\NodeType\NodeType; -use Neos\ContentRepository\SharedModel\NodeType\NodeTypeConstraintParser; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\NodeType\NodeTypeNames; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTypeConstraints; +use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Doctrine\PersistenceManager; use Neos\Fusion\FusionObjects\AbstractFusionObject; use Neos\Media\Domain\Model\ImageInterface; +use Neos\Utility\Exception\PropertyNotAccessibleException; class XmlSitemapUrlsImplementation extends AbstractFusionObject { - /** - * @Flow\Inject - * @var ContentRepositoryRegistry - */ - protected $contentRepositoryRegistry; + #[Flow\Inject(lazy: false)] + protected ContentRepositoryRegistry $contentRepositoryRegistry; /** - * @Flow\Inject * @var PersistenceManager */ + #[Flow\Inject(lazy: true)] protected $persistenceManager; /** - * @var array + * @var array> */ - protected $assetPropertiesByNodeType = null; + protected $assetPropertiesByNodeType = []; /** * @var bool @@ -97,10 +98,41 @@ public function getStartingPoint(): Node return $this->startingPoint; } + /** + * Evaluate this Fusion object and return the result + * + * @return array + */ + public function evaluate(): array + { + if ($this->items === null) { + $items = []; + + $startingPoint = $this->getStartingPoint(); + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($startingPoint); + + $nodeTypeManager = $this->contentRepositoryRegistry->get($startingPoint->subgraphIdentity->contentRepositoryId)->getNodeTypeManager(); + $nodeTypeNames = NodeTypeNames::fromArray(array_map( + fn(NodeType $nodeType): NodeTypeName => $nodeType->name, + $nodeTypeManager->getSubNodeTypes('Neos.Neos:Document', false) + )); + + $subtree = $subgraph->findSubtree( + $startingPoint->nodeAggregateId, + FindSubtreeFilter::create(NodeTypeConstraints::create($nodeTypeNames, NodeTypeNames::createEmpty())) + ); + + $this->collectItems($items, $subtree); + $this->items = $items; + } + + return $this->items; + } + private function getAssetPropertiesForNodeType(NodeType $nodeType): array { - if ($this->assetPropertiesByNodeType[$nodeType->getName()] === null) { - $this->assetPropertiesByNodeType[$nodeType->getName()] = []; + if (!array_key_exists($nodeType->name->value, $this->assetPropertiesByNodeType)) { + $this->assetPropertiesByNodeType[$nodeType->name->value] = []; if ($this->getIncludeImageUrls()) { $relevantPropertyTypes = [ 'array' => true, @@ -110,51 +142,69 @@ private function getAssetPropertiesForNodeType(NodeType $nodeType): array foreach ($nodeType->getProperties() as $propertyName => $propertyConfiguration) { if (isset($relevantPropertyTypes[$nodeType->getPropertyType($propertyName)])) { - $this->assetPropertiesByNodeType[$nodeType->getName()][] = $propertyName; + $this->assetPropertiesByNodeType[$nodeType->name->value][] = $propertyName; } } } } - return $this->assetPropertiesByNodeType[$nodeType->getName()]; + return $this->assetPropertiesByNodeType[$nodeType->name->value]; } - /** - * @param array & $items - * @param Node $node - * @return void - * @throws NodeException - */ - protected function appendItems(array &$items, Node $node) + protected function collectItems(array &$items, Subtree $subtree): void { + $node = $subtree->node; + if ($this->isDocumentNodeToBeIndexed($node)) { $item = [ 'node' => $node, - //'lastModificationDateTime' => $node->getNodeData()->getLastModificationDateTime(), + 'lastModificationDateTime' => $node->timestamps->lastModified ?: $node->timestamps->created, 'priority' => $node->getProperty('xmlSitemapPriority') ?: '', 'images' => [], ]; if ($node->getProperty('xmlSitemapChangeFrequency')) { $item['changeFrequency'] = $node->getProperty('xmlSitemapChangeFrequency'); } + if ($this->getIncludeImageUrls()) { - $this->resolveImages($node, $item); + $nodeTypeManager = $this->contentRepositoryRegistry->get($node->subgraphIdentity->contentRepositoryId)->getNodeTypeManager(); + $collectionNodeTypeNames = array_map( + fn(NodeType $nodeType): NodeTypeName => $nodeType->name, + $nodeTypeManager->getSubNodeTypes('Neos.Neos:ContentCollection', false) + ); + $collectionNodeTypeNames['Neos.Neos:ContentCollection'] = NodeTypeName::fromString('Neos.Neos:ContentCollection'); + $contentNodeTypeNames = array_map( + fn(NodeType $nodeType): NodeTypeName => $nodeType->name, + $nodeTypeManager->getSubNodeTypes('Neos.Neos:Content', false) + ); + $nodeTypeNames = NodeTypeNames::fromArray(array_merge($collectionNodeTypeNames, $contentNodeTypeNames)); + + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); + $contentSubtree = $subgraph->findSubtree( + $node->nodeAggregateId, + FindSubtreeFilter::create(NodeTypeConstraints::create($nodeTypeNames, NodeTypeNames::createEmpty())) + ); + + $this->resolveImages($contentSubtree, $item); } + $items[] = $item; } - foreach ($node->getChildNodes('Neos.Neos:Document') as $childDocumentNode) { - $this->appendItems($items, $childDocumentNode); + + foreach ($subtree->children as $childSubtree) { + $this->collectItems($items, $childSubtree); } } /** - * @param Node $node + * @param Subtree $subtree * @param array & $item * @return void - * @throws NodeException + * @throws PropertyNotAccessibleException */ - protected function resolveImages(Node $node, array &$item) + protected function resolveImages(Subtree $subtree, array &$item): void { + $node = $subtree->node; $assetPropertiesForNodeType = $this->getAssetPropertiesForNodeType($node->nodeType); foreach ($assetPropertiesForNodeType as $propertyName) { @@ -169,53 +219,23 @@ protected function resolveImages(Node $node, array &$item) } } - $contentRepository = $this->contentRepositoryRegistry->get($node->subgraphIdentity->contentRepositoryIdentifier); - $nodeTypeConstraintParser = NodeTypeConstraintParser::create($contentRepository->getNodeTypeManager()); - - $childNodes = $this->contentRepositoryRegistry->subgraphForNode($node) - ->findChildNodes( - $node->nodeAggregateIdentifier, - $nodeTypeConstraintParser->parseFilterString('Neos.Neos:ContentCollection,Neos.Neos:Content') - ); - - foreach ($childNodes as $childNode) { - $this->resolveImages($childNode, $item); + foreach ($subtree->children as $childSubtree) { + $this->resolveImages($childSubtree, $item); } } /** * Return TRUE/FALSE if the node is currently hidden; taking the "renderHiddenInIndex" configuration * of the Menu Fusion object into account. - * - * @param Node $node - * @return bool - * @throws NodeException */ protected function isDocumentNodeToBeIndexed(Node $node): bool { - return !$node->nodeType->isOfType('Neos.Seo:NoindexMixin')// TODO?? && $node->isVisible() - && ($this->getRenderHiddenInIndex())// TODO?? || !$node->isHiddenInIndex()) && $node->isAccessible() + return !$node->nodeType->isOfType('Neos.Seo:NoindexMixin') + && ($this->getRenderHiddenInIndex() || $node->getProperty('hiddenInIndex') !== true) && $node->getProperty('metaRobotsNoindex') !== true - && ((string)$node->getProperty('canonicalLink') === '' || substr($node->getProperty('canonicalLink'), 7) === $node->getNodeAggregateIdentifier()->getValue()); - } - - /** - * Evaluate this Fusion object and return the result - * - * @return array - */ - public function evaluate(): array - { - if ($this->items === null) { - $items = []; - - try { - $this->appendItems($items, $this->getStartingPoint()); - } catch (NodeException $e) { - } - $this->items = $items; - } - - return $this->items; + && ( + (string)$node->getProperty('canonicalLink') === '' + || substr($node->getProperty('canonicalLink'), 7) === $node->nodeAggregateId->value + ); } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 4612ad9..eeb3054 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -22,6 +22,9 @@ Neos: 'Neos.Seo.Image': 'Neos\Seo\Fusion\Helper\ImageHelper' Seo: + # hreflang settings + alternateLanguageLinks: + excludedDimensionsPresets: [] # robots.txt settings robotsTxt: dimensionsPresets: null diff --git a/Documentation/index.rst b/Documentation/index.rst index ed66592..783d978 100755 --- a/Documentation/index.rst +++ b/Documentation/index.rst @@ -252,64 +252,24 @@ Alternatively you can change the caching behavior and have a cron job that recre Alternate Language Tag ------------------------ -The `Alternate Language Tag` provides information that the site is also available in other languages. By default the tags -are rendered with the `Neos.Neos:DimensionMenu` and the `language` dimension. Given the Neos Demo Site Package as an +The `Alternate Language Tag` provides information that the site is also available in other languages. Given the Neos Demo Site Package as an example the rendered tags for the homepage would be. :: + -According to the following dimension settings, there would be a lot more tags expected. However only two variants of the -homepage exists, thus only `en_US` and its fallback `en_UK` are rendered. - -In case the dimension that contains the language is not named `language` you have to set the alternative name with the -property `ContentRepository.dimensionTypes.language`. +If you only want to render a subset of the available language dimensions (e.g., if the content is not yet ready) +you can configure this in the `Settings.yaml`:: -:: + Neos: + Seo: + alternateLanguageLinks: + # Show all but exclude German + excludedDimensionsPresets: ['de'] - ContentRepository: - contentDimensions: - 'language': - label: 'Language' - icon: 'icon-language' - default: 'en_US' - defaultPreset: 'en_US' - presets: - 'all': ~ - 'en_US': - label: 'English (US)' - values: ['en_US'] - uriSegment: 'en' - 'en_UK': - label: 'English (UK)' - values: ['en_UK', 'en_US'] - uriSegment: 'uk' - 'de': - label: 'German' - values: ['de'] - uriSegment: 'de' - 'fr': - label: 'French' - values: ['fr'] - uriSegment: 'fr' - 'nl': - label: 'Dutch' - values: ['nl', 'de'] - uriSegment: 'nl' - 'dk': - label: 'Danish' - values: ['dk'] - uriSegment: 'dk' - 'lv': - label: 'Latvian' - values: ['lv'] - uriSegment: 'lv' - dimensionTypes: - language: 'language' - -You can exclude presets by overriding `Neos.Seo:AlternateLanguageLinks`. Dynamic robots.txt ------------------ diff --git a/README.md b/README.md index 2b0abc7..ca73cec 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Check the [documentation](https://neos-seo.readthedocs.io/en/stable/) for all fe 1. Run the following command f.e. in your site package: ```bash - composer require --no-update "neos/seo:^3.3" + composer require --no-update "neos/seo:^4.1" ``` 2. Update your dependencies by running the following command in your project root folder: diff --git a/Resources/Private/Fusion/Metadata/AlternateLanguageLinks.fusion b/Resources/Private/Fusion/Metadata/AlternateLanguageLinks.fusion index e6c7459..30a18c6 100644 --- a/Resources/Private/Fusion/Metadata/AlternateLanguageLinks.fusion +++ b/Resources/Private/Fusion/Metadata/AlternateLanguageLinks.fusion @@ -5,7 +5,7 @@ prototype(Neos.Seo:AlternateLanguageLinks) < prototype(Neos.Fusion:Component) { node = ${documentNode} dimension = 'language' - excludedPresets = ${[]} + excludedPresets = ${Configuration.setting('Neos.Seo.alternateLanguageLinks.excludedDimensionsPresets')} # The hreflang value needs to have a format like 'en-US', therefore internally used values # like 'en_US' will be modified to match. diff --git a/Resources/Private/Fusion/RobotsTxt/RobotsTxt.fusion b/Resources/Private/Fusion/RobotsTxt/RobotsTxt.fusion index dcc5e76..a767391 100644 --- a/Resources/Private/Fusion/RobotsTxt/RobotsTxt.fusion +++ b/Resources/Private/Fusion/RobotsTxt/RobotsTxt.fusion @@ -12,50 +12,44 @@ prototype(Neos.Seo:RobotsTxt) < prototype(Neos.Fusion:Component) { excludedDimensionPresets = ${Configuration.setting('Neos.Seo.robotsTxt.excludedDimensionsPresets')} linebreak = ${String.chr(10)} - renderer = Neos.Fusion:Component { - sitemaps = Neos.Fusion:Case { - @if.shouldRender = ${props.renderXMLSitemapLinks} - hasLanguage { - condition = ${Configuration.setting('Neos.ContentRepository.contentDimensions.' + props.languageDimension) != null} - renderer = Neos.Fusion:Loop { - items = Neos.Neos:DimensionsMenuItems { - dimension = ${props.languageDimension} - presets = ${Type.isArray(props.dimensionsPresets) ? props.dimensionsPresets : null} - includeAllPresets = true - } - itemRenderer = Neos.Neos:NodeUri { - absolute = true - format = 'xml.sitemap' - node = ${item.node} - @process.prefix = ${'Sitemap: ' + value + props.linebreak} - @if.notExcluded = ${!props.excludedDimensionPresets || Array.indexOf(props.excludedDimensionPresets, item.dimensions[props.languageDimension][0]) == -1} - } - } - } - noLanguage { - condition = true - renderer = Neos.Neos:NodeUri { + @private.sitemaps = Neos.Fusion:Case { + @if.shouldRender = ${props.renderXMLSitemapLinks} + hasLanguage { + condition = ${props.languageDimension && Neos.Dimension.currentValue(site, props.languageDimension) != null} + + renderer = Neos.Fusion:Loop { + items = ${Neos.Dimension.allDimensionValues(site, props.languageDimension)} + itemName = 'dimensionValue' + itemRenderer = Neos.Neos:NodeUri { absolute = true format = 'xml.sitemap' - node = ${site} - @process.prefix = ${'Sitemap: ' + value} + node = ${Neos.Dimension.findVariantInDimension(site, props.languageDimension, dimensionValue)} + @process.prefix = ${'Sitemap: ' + value + props.linebreak} + @if.isIncluded = ${!props.dimensionsPresets || Array.indexOf(props.dimensionsPresets, dimensionValue.value) != -1} + @if.notExcluded = ${!props.excludedDimensionPresets || Array.indexOf(props.excludedDimensionPresets, dimensionValue.value) == -1} } } - @process.prefix = ${props.linebreak + value} } - - data = ${props.data} - linebreak = ${props.linebreak} - - renderer = Neos.Fusion:Http.Message { - httpResponseHead.headers.Content-Type = 'text/plain;' - body = afx` - - {item}{props.linebreak} - - {props.sitemaps} - ` + noLanguage { + condition = true + renderer = Neos.Neos:NodeUri { + absolute = true + format = 'xml.sitemap' + node = ${site} + @process.prefix = ${'Sitemap: ' + value} + } } + @process.prefix = ${props.linebreak + value} + } + + renderer = Neos.Fusion:Http.Message { + httpResponseHead.headers.Content-Type = 'text/plain;' + body = afx` + + {item}{props.linebreak} + + {private.sitemaps} + ` } @cache { diff --git a/Resources/Private/Fusion/XmlSitemap/XmlSitemap.fusion b/Resources/Private/Fusion/XmlSitemap/XmlSitemap.fusion index 7600154..5055536 100644 --- a/Resources/Private/Fusion/XmlSitemap/XmlSitemap.fusion +++ b/Resources/Private/Fusion/XmlSitemap/XmlSitemap.fusion @@ -24,7 +24,7 @@ prototype(Neos.Seo:XmlSitemap) < prototype(Neos.Fusion:Http.Message) { @cache { mode = 'cached' entryIdentifier { - startingPoint = ${startingPoint} + startingPoint = ${Neos.Caching.entryIdentifierForNode(startingPoint)} } entryTags { 1 = ${Neos.Caching.nodeTag(startingPoint)}