Pagenodes with categories/tags and a menu

I want to add a selection field for user defined categories to a page (subpage) node.
These categories should be defined by the user as well.
The goal is to create a “categories” menu with page links to the subpages on the frontpage or in the menu.

For example: the customer has a few different events like hiking, bungee jumping and wellness weekend. Now there should be a menu with the categories: “Indoor”, “Outdoor” and “One-Person-Event”.

Each category should contain several data fields (like a title and a short description).

How do I start to create those categories. What kind of nodes could these categories be?
At which point in the backend could these categories be edited?

Hi Marvin,
You’d ideally model these categories as document nodes. They could be placed under one common “Categories” parent page that is a child of the site root. So your document node tree could look like this:

yoursite (Your.Site:RootPage)
  - page 1 (Neos.NodeTypes:Page)
  - page 2 (Neos.NodeTypes:Page)
  - page n (Neos.NodeTypes:Page)
  - categories (Your.Site:Categories)
    - category 1 (Your.Site:Category)
    - category 2 (Your.Site:Category)
    - category n (Your.Site:Category)

The node type constraints for Your.Site:Categories and Your.Site:Category should be set accordingly. Your.Site:Categories could be an auto-created child node of your site root.

A link from a page to a category can be modelled with a property of type reference or references, like this:

categories:
  type: references
  ui:
    label: 'Categories'
    inspector:
      group: 'document'
      editorOptions:
        nodeTypes: ['Your.Site:Category']

Listing all linked pages for a category is a bit more tricky. I created my own FlowQuery operation that filters a collection of nodes and checks if they link to a certain other node (which would be your category node). This is probably not an ideal solution performance-wise, doing it with ElasticSearch might be faster. Nevertheless, here goes:

<?php
namespace Your\Site\Fusion\Eel\FlowQueryOperations;

use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Eel\FlowQuery\FlowQueryException;
use Neos\Eel\FlowQuery\Operations\AbstractOperation;
use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Domain\Model\NodeInterface;

/**
 * FlowQuery operation to filter by properties of type reference or references
 */
class FilterByReferenceOperation extends AbstractOperation
{
    /**
     * {@inheritdoc}
     */
    protected static $shortName = 'filterByReference';
    /**
     * {@inheritdoc}
     */
    protected static $priority = 100;

    /**
     * {@inheritdoc}
     *
     * We can only handle CR Nodes.
     */
    public function canEvaluate($context)
    {
        return (!isset($context[0]) || ($context[0] instanceof NodeInterface));
    }

    /**
     * {@inheritdoc}
     *
     * @param array $arguments The arguments for this operation.
     *                         First argument is property to filter by, must be of reference of references type.
     *                         Second is object to filter by, must be Node.
     *
     * @return void
     * @throws FlowQueryException
     */
    public function evaluate(FlowQuery $flowQuery, array $arguments)
    {
        if (empty($arguments[0])) {
            throw new FlowQueryException('filterByReference() needs reference property name by which nodes should be filtered', 1332492263);
        }
        if (empty($arguments[1])) {
            throw new FlowQueryException('filterByReference() needs node reference by which nodes should be filtered', 1332493263);
        }
        /** @var NodeInterface $nodeReference */
        list($filterByPropertyPath, $nodeReference) = $arguments;
        $filteredNodes = [];
        foreach ($flowQuery->getContext() as $node) {
            /** @var NodeInterface $node */
            $propertyValue = $node->getProperty($filterByPropertyPath);
            if ($nodeReference === $propertyValue || (is_array($propertyValue) && in_array($nodeReference, $propertyValue, true))) {
                $filteredNodes[] = $node;
            }
        }
        $flowQuery->setContext($filteredNodes);
    }
}

This operation can be used like this in Fusion to find all nodes linking to a category, assuming you put this in the Fusion prototype of the Your.Site:Category document nodetype:

pagesLinkingToCurrentCategory = ${q(site).find('[instanceof Neos.Neos:Document]').filterByReference('categories', node)}

Hint: q(site).find() searches your entire CR, so this is really not optimal performance-wise. Also, it’s probably better to put this in its own content nodetype (e.g. a Your.Site:ListOfPagesInCategory) in order to configure the cache properly.

A category menu can be created by using the default Neos.Fusion:Menu prototype and restricting it to your category nodes only:

categoryMenu = Neos.Fusion:Menu {
	filter = 'Your.Site:Categories'
}

Hope that helps.
Regards,
Bastian

1 Like

Hi Bastian,
That worked great. Thank you very much!

I have two more questions:

  1. What is the best way to hide the “Your.Site:Categories” or “Your.Site:Category” pages from the frontend or the sitemap or the standard menu? Could that be done programmatically?

  2. What is the best way to exclude nodes that should not be visible in frontend (by these properties: “hidden”, “hiddenbeforedatetime”, “hiddenafterdatetime”, “hiddeninindex”) from the query:
    ${q(site).find('[instanceof Your.Site:Categories]').filterByReference('categories', node)}