Loading...
Skip navigation
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:

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/term is 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 AZ (title + slug = az)
  • 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>
  • 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 additionalGetParameters must match your plugin namespace (tx_<extkey>_<plugin>). If your plugin has a different name, you must adjust this.

Documentation
TYPO3 Glossaries Extension

TYPO3 Glossaries

The glossaries extension for TYPO3 enables you to create and manage your website glossar.

Menu

Contact request

You can contact us at any time

Stop! Playing in the meantime?
Stop! Playing in the meantime?
Stop! Playing in the meantime?

Stop! Playing in the meantime?

Break the highscore

Press Start
Contact request
Screenreader label
Security question
BTC_________I99______
B_1____8____S_____1WU
M_G___3EX___Q8C______
H_4____2____G_D___NT4
FWH_________X3O______