Skip to content

Commit

Permalink
Merge pull request #173 from neos/feature/escr-compatible-xml-sitemap
Browse files Browse the repository at this point in the history
FEATURE: Refactor XmlSitemap and robots.txt to work with the new ESCR
  • Loading branch information
ahaeslich authored May 2, 2023
2 parents f7a5fec + 4928e87 commit 9990416
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 158 deletions.
156 changes: 88 additions & 68 deletions Classes/Fusion/XmlSitemapUrlsImplementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, array<int, string>>
*/
protected $assetPropertiesByNodeType = null;
protected $assetPropertiesByNodeType = [];

/**
* @var bool
Expand Down Expand Up @@ -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<Neos\Media\Domain\Model\Asset>' => true,
Expand All @@ -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) {
Expand All @@ -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
);
}
}
3 changes: 3 additions & 0 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Neos:
'Neos.Seo.Image': 'Neos\Seo\Fusion\Helper\ImageHelper'

Seo:
# hreflang settings
alternateLanguageLinks:
excludedDimensionsPresets: []
# robots.txt settings
robotsTxt:
dimensionsPresets: null
Expand Down
58 changes: 9 additions & 49 deletions Documentation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

::

<link rel="alternate" hreflang="en_US" href="http://neos.dev/"/>
<link rel="alternate" hreflang="en_UK" href="http://neos.dev/uk"/>
<link rel="alternate" hreflang="de" href="http://neos.dev/de/"/>

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
------------------
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 9990416

Please sign in to comment.