Skip to main navigation Skip to main content Skip to page footer

Wie baue ich ein Glossar mit alphabetischer Gruppierung?

Du möchtest eine A–Z-Glossar-Navigation über Kategorien erstellen, dabei sprechende URLs nach /buchstabe/begriff und eine korrekte sitemap.xml erzeugen, obwohl Dein Glossar die Kategorie per N:M (MM-Tabelle) speichert? Dann bist Du hier genau richtig!

Als Ergebnis möchten wir ein Glossar wie auf unserer Website erhalten:

Die URLs in der Sitemap.xml sollen wie folgt aussehen:

Voraussetzungen

  • TYPO3 in Version 11–13
  • EXT:seo muss für die sprechenden URLs installiert sein
  • EXT:glossaries für Extbase-Plugin der Liste + Details

Achtung:

Wenn ein Glossar-Eintrag mehrere Kategorien hat, ist /buchstabe/begriff mehrdeutig. Für A–Z solltest du genau 1 Kategorie je Eintrag erzwingen (TCA/Editor-UX), sonst wird Sitemap/Linkbuilding inkonsistent.

Alphabet-Reiter: Einträge sauber Buchstaben zuordnen

Vorgehen

  • Lege 26 Kategorien AZ an (Titel + slug = az)
  • Jeder Glossar-Eintrag bekommt genau eine dieser Kategorien (sein Anfangsbuchstabe)

TCA: „Genau eine Kategorie“ erzwingen (empfohlen)

Setze in deinem Site-Package in der Datei 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;

Achtung:

Nach der Anpassung nicht vergessen den System-Cache zu leeren!

URL-Aufbau via RouteEnhancer

Ziel

Ziel ist es eine URL wie /category_slug/glossary_slug für die Detailseite zu haben.

  • Liste: .../glossary
  • Detail: .../glossary/r/rte

Beispiel: 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: /

Hinweis:

Das funktioniert nur stabil, wenn selectedCategory wirklich die UID der zugeordneten Kategorie enthält (oder per Aspect sauber auflösbar ist). Genau da hakt die Standard-Sitemap bei MM-Feldern oft.

Warum zeigt die sitemap.xml immer a als Kategorie in der URL?

Der Standard-Provider TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider befüllt fieldToParameterMap bei N:M-Feldern nicht zuverlässig, weil im Datensatzfeld (categories) nicht automatisch die konkrete, sortierte Relation aus der MM-Tabelle aufgelöst wird.

Effekt: In fieldToParameterMap wird die Anzahl der zugewiesenen Relationen der N:M-Relation verwendet, welcher in unserem Fall 1 ist, weil wir ja nur genau eine Kategorie zulassen. Die Kategorie mit der UID 1 ist in unserem Beispiel zufällig Kategorie A – daher generiert die Sitemap immer URLs mit /a/....

Lösung: Custom SitemapDataProvider für die „erste“ Kategorie aus der MM-Tabelle

Prinzip

  • Vor dem URL-Build:

    • erste Kategorie-UID aus der MM-Tabelle holen (nach sorting_foreign)
    • $record['categories'] = <uid> setzen
  • Dann den Standard-Mechanismus weiter nutzen, damit dein fieldToParameterMap + RouteEnhancer greifen

Beispiel-Implementierung (erweiterter Records-Provider)

Dieser XmlSitemap-DataProvider wird auch in der EXT:glossaries in Classes/XmlSitemap/RecordsXmlSitemapDataProvider.php mitgeliefert.

<?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: SEO-Sitemap auf deinen Provider umstellen

Du ersetzt nur den Provider auf CodingMs\Glossaries\XmlSitemap\RecordsXmlSitemapDataProvider – der Rest kann bleiben.

Configuration/TypoScript/setup.typoscript

plugin.tx_seo.config.xmlSitemap.sitemaps.glossaries {
    provider = CodingMs\Glossaries\XmlSitemap\RecordsXmlSitemapDataProvider
    config {
        table = tx_glossaries_domain_model_glossary
        sortField = title
        lastModifiedField = tstamp
        recursive = 1
        # Speicherort der Records
        pid = 1200
        url {
            # PageID der Detailseite
            pageId = 2038
            fieldToParameterMap {
                uid = tx_glossaries_glossary[glossary]
                categories = tx_glossaries_glossary[selectedCategory]
            }
            additionalGetParameters {
                tx_glossaries_glossary.controller = Glossary
                tx_glossaries_glossary.action = show
            }
        }
    }
}

Hinweis:

Die additionalGetParameters müssen zu deinem Plugin-Namespace passen (tx_<extkey>_<plugin>). Wenn dein Plugin anders heißt, musst du das anpassen.