
Custom slug handling in the TYPO3 backend
Starting point
The TYPO3 standard functionality usually sets sensible defaults that work well for most use cases.
However, there are projects where the standard functionality does not fit perfectly and therefore needs to be modified.
In this case, the requirement was that the exact position of an individual page in the page tree should be reflected in the URL, although the editor should be able to freely customise the last segment (‘slug’) of the URL.
There are two standard options in TYPO3:
- The URL is generated automatically and the editor is not allowed to change it.
- The URL is generated automatically and the editor may change it completely. This includes parts of the URL that are below the current page in the page tree.
We have decided to allow the editor to change the URL, but to check the corresponding entry when saving and adjust it if necessary to ensure that sub-parts of the URL cannot be manipulated and that the URL structure consistently reflects the page tree.
Register Hook
We use a hook for this, which we will introduce below.
Firstly, the hook must be registered in ext_localconf.php. This can be achieved using the following code:
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['sitepackage’] = \MyVendor\Sitepackage\Hooks\SlugCorrectBeforeSaveHook::class;
This code tells TYPO3 that we want to pass the data to a new class ‘SlugCorrectBeforeSaveHook’ when saving records, which may (or may not) change this data.
Implement Hook
The logic is implemented in the class itself. The corresponding class could therefore look like this:
<?php
namespace MyVendor\Sitepackage\Hooks;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* SlugCorrectOnSaveHook
*/
class SlugCorrectBeforeSaveHook
{
/**
* @var mixed|\TYPO3\CMS\Core\Domain\Repository\PageRepository
*/
public $pageRepository;
public LocalizationUtility $languageUtility;
/**
* Correct slug url if it does not match parent node
*
* @array $params
*/
public function processDatamap_preProcessFieldArray(array &$fieldArray, $table, $id, DataHandler &$pObj): void
{
$initialPID = $GLOBALS['_REQUEST']['popViewId'] ?? null;
$this->pageRepository = GeneralUtility::makeInstance(PageRepository::class);
$this->languageUtility = GeneralUtility::makeInstance(LocalizationUtility::class);
$page = $this->pageRepository->getPage($id, false);
// Check if table pages is edited, if it is the first call of the request and do a undefinedArrayKey prevention
// Check if slug is edited
if ($table == 'pages' && $id == $initialPID && isset($fieldArray['slug']) && isset($page['slug']) && $fieldArray['slug'] != $page['slug']) {
$parentPage = $this->pageRepository->getPage($page['pid'], false);
// Check if parent slug changed in new slug segment and parent page is not root
if (strncmp(
$fieldArray['slug'],
$parentPage['slug'] . '/',
strlen($parentPage['slug'] . '/'),
) != 0 && $parentPage['uid'] != 1) {
$slug = trim(substr($fieldArray['slug'], strrpos($fieldArray['slug'], '/') + 1));
// Adjust slug to ensure consistency
$fieldArray['slug'] = $parentPage['slug'] . '/' . $slug;
// Notify about the change
// FlashMessage($message, $title, $severity = self::OK, $storeInSession)
$message = GeneralUtility::makeInstance(
FlashMessage::class,
$this->languageUtility->translate('LLL:EXT:sitepackage/Resources/Private/Language/locallang_be.xlf:slugChangeDescription', 'sitepackage', [$fieldArray['slug']]),
$this->languageUtility->translate('LLL:EXT: sitepackage/Resources/Private/Language/locallang_be.xlf:slugChangeHeadline', 'sitepackage'),
FlashMessage::WARNING,
true
);
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$messageQueue = $flashMessageService->getMessageQueueByIdentifier();
$messageQueue->addMessage($message);
}
}
}
}
The hook method processDatamap_preProcessFieldArray() is called to check and change the fields to be saved and their values. The first part of the code ensures that the changes are only made to page records that are currently being processed. It also checks whether the ‘slug’ field has been updated and whether the new value differs from the original value. If these conditions are met, the slug is automatically updated to match the parent node.
First, the parent node of the current page is determined. It then checks whether the slug of the parent node is contained in the new URL of the updated slug segment. If this is not the case and the parent node is not the root page, the slug is automatically updated to match the parent node. This is achieved by adding the parent node slug to the new slug segment.
Finally, the user is informed of the change to the slug by generating a Flash message. This is done using TYPO3's Flash Message class, which creates a message with a specific severity and an optional title. The message is then stored in the message queue so that it can be displayed to the user.
Overall, this simple hook example also shows how powerful and flexible TYPO3 hooks are and how they can be used to extend or change the functionality of the system. It is also a good example of how to tackle a problem that may not have been considered in the standard TYPO3 implementation.