Please note that this documentation is for the most recent version of this extension. It may not be relevant for older versions. Related documentation can be found in the documentation directory of the extension.
How do I build a glossary with alphabetical grouping?
Do you want to create an A–Z glossary navigation using categories, generating speaking URLs like /letter/term and a correct sitemap.xml, even though your glossary stores categories via N:M (MM table)? Then you've come to the right place!
As a result, we would like to have a glossary like the one on our website:
The URLs in the sitemap.xml should look like this:
- https://www.coding.ms/glossary-entry/a/api
- https://www.coding.ms/glossary-entry/b/bcc
- https://www.coding.ms/glossary-entry/c/csv
- https://www.coding.ms/glossary-entry/f/fal
- …
Prerequisites
- TYPO3 version 11–13
- EXT:seo must be installed for speaking URLs
- EXT:glossaries for the Extbase plugin for list + details
Attention:
If a glossary entry has multiple categories,
/letter/termis ambiguous. For A–Z, you should enforce exactly 1 category per entry (TCA/Editor-UX), otherwise the sitemap/link building will be inconsistent.
Alphabet tabs: Cleanly assigning entries to letters
Procedure
- Create 26 categories
A…Z(title +slug=a…z) - Each glossary entry gets exactly one of these categories (its starting letter)
TCA: Enforce "Exactly one category" (recommended)
In your site package, set in the file Configuration/TCA/Overrides/tx_glossaries_domain_model_glossary.php:
// Reduce assigned categories to exactly one!
$GLOBALS['TCA']['tx_glossaries_domain_model_glossary']['columns']['categories']['config']['maxitems'] = 1;
Attention:
After the adjustment, do not forget to clear the system cache!
URL structure via RouteEnhancer
Goal
The goal is to have a URL like /category_slug/glossary_slug for the detail page.
- List:
.../glossary - Detail:
.../glossary/r/rte
Example: Site-Config RouteEnhancer
config/sites/<siteIdentifier>/config.yaml
routeEnhancers:
GlossariesPlugin:
type: Extbase
limitToPages:
- 123
extension: Glossaries
plugin: Glossary
routes:
-
routePath: '/{category_slug}'
_controller: 'Glossary::list'
_arguments:
category_slug: selectedCategory
-
routePath: '/{category_slug}/{glossary_slug}'
_controller: 'Glossary::show'
_arguments:
category_slug: selectedCategory
glossary_slug: glossary
defaultController: 'Glossary::list'
aspects:
category_slug:
type: PersistedAliasMapper
tableName: tx_glossaries_domain_model_glossarycategory
routeFieldName: slug
routeValuePrefix: /
glossary_slug:
type: PersistedAliasMapper
tableName: tx_glossaries_domain_model_glossary
routeFieldName: slug
routeValuePrefix: /
Note:
This only works stably if
selectedCategoryreally contains the UID of the assigned category (or is cleanly resolvable via Aspect). This is exactly where the standard sitemap often fails with MM fields.
Why does the sitemap.xml always show "a" as the category in the URL?
The standard provider TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider does not reliably fill fieldToParameterMap for N:M fields because the concrete, sorted relation from the MM table is not automatically resolved in the record field (categories).
Effect: In fieldToParameterMap, the number of assigned relations of the N:M relation is used, which in our case is 1 because we only allow exactly one category. In our example, the category with UID 1 happens to be category A – therefore the sitemap always generates URLs with /a/....
Solution: Custom SitemapDataProvider for the "first" category from the MM table
Principle
- Before the URL build:
- Get the first category UID from the MM table (according to
sorting_foreign) - Set
$record['categories'] = <uid>
- Get the first category UID from the MM table (according to
- Then continue using the standard mechanism, so that your
fieldToParameterMap+ RouteEnhancer take effect.
Example Implementation (extended Records Provider)
This XmlSitemap DataProvider is also included in EXT:glossaries in Classes/XmlSitemap/RecordsXmlSitemapDataProvider.php.
<?php
declare(strict_types=1);
namespace CodingMs\Glossaries\XmlSitemap;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider as ParentProvider;
final class RecordsXmlSitemapDataProvider extends ParentProvider
{
/** @var array<int,int> Cache: glossaryUid => firstCategoryUid */
private array $firstCategoryUidCache = [];
protected function defineUrl(array $data): array
{
$record = $data['data'] ?? null;
if (!is_array($record) || empty($record['uid'])) {
return parent::defineUrl($data);
}
$glossaryUid = (int)$record['uid'];
// Only fix if the DB field is not already a usable UID
// (with MM "count" behaviour this is usually 1 for all records, which is wrong)
$firstCategoryUid = $this->resolveFirstCategoryUid($glossaryUid);
if ($firstCategoryUid > 0) {
// Make fieldToParameterMap pick the correct category UID
$record['categories'] = $firstCategoryUid;
$data['data'] = $record;
}
return parent::defineUrl($data);
}
private function resolveFirstCategoryUid(int $glossaryUid): int
{
if (isset($this->firstCategoryUidCache[$glossaryUid])) {
return $this->firstCategoryUidCache[$glossaryUid];
}
$queryBuilder = $this->getQueryBuilder('tx_glossaries_glossary_glossarycategory_mm');
$row = $queryBuilder
->select('uid_foreign')
->from('tx_glossaries_glossary_glossarycategory_mm')
->where(
$queryBuilder->expr()->eq(
'uid_local',
$queryBuilder->createNamedParameter($glossaryUid, ParameterType::INTEGER)
)
)
->orderBy('sorting_foreign', 'ASC')
->addOrderBy('uid_foreign', 'ASC')
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
$firstUid = (int)($row['uid_foreign'] ?? 0);
return $this->firstCategoryUidCache[$glossaryUid] = $firstUid;
}
private function getQueryBuilder(string $table): QueryBuilder
{
return GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
}
}
TypoScript: Switch SEO sitemap to your provider
You only replace the provider with CodingMs\Glossaries\XmlSitemap\RecordsXmlSitemapDataProvider – the rest can remain.
Configuration/TypoScript/setup.typoscript
plugin.tx_seo.config.xmlSitemap.sitemaps.glossary glossaries {
provider = CodingMs\Glossaries\XmlSitemap\RecordsXmlSitemapDataProvider
config {
table = tx_glossaries_domain_model_glossary
sortField = title
lastModifiedField = tstamp
recursive = 1
# Storage location of the records
pid = 1200
url {
# PageID of the detail page
pageId = 2038
fieldToParameterMap {
uid = tx_glossaries_glossary[glossary]
categories = tx_glossaries_glossary[selectedCategory]
}
additionalGetParameters {
tx_glossaries_glossary.controller = Glossary
tx_glossaries_glossary.action = show
}
}
}
}
Note:
The
additionalGetParametersmust match your plugin namespace (tx_<extkey>_<plugin>). If your plugin has a different name, you must adjust this.
