
Documentation
TYPO3 Extension “Hreflang Multisite”
Version 2.3.1 (2026-09-30)
While the hreflang tags of a page set are being collected, the extension dispatches a PSR-14 event you can use to add, remove, or rewrite tags—for example to inject an externally hosted URL into a set.
What a Listener Cannot Do
The event is dispatched inside the loop that collects the tags, once per translation, and the x-default fallback is appended after the loop from the flagged language’s entry in the finished map.
That order has one consequence worth knowing before you rely on it: an x-default set by a listener is overwritten, because the append runs last and the backend requires every set to have a flagged page. A listener can change where the flagged language points, and the fallback follows—but that moves the alternate tag for that language too. Pointing the fallback at a URL the set does not otherwise contain is not possible today. See x-default.
Records Are Not Handled, and Why
The extension works on pages: things that live in the page tree, have their own URL, and can be edited in the page properties. Page sets are built out of page records, and everything in this documentation follows from that.
Records—news articles, products, events—get no hreflang tags from this extension, and that is a deliberate boundary rather than a missing feature. Three things make records harder than pages, and none of them has a general answer:
- A record does not exist in every language. A news article written for one market often has no counterpart at all, and an incomplete set is worse than no set.
- The counterpart is ambiguous. Two pages at the same position in two trees are recognisably equivalent. Two products in two catalogues may map one to one, one to many, or not at all—and only the project knows which.
- The URL is generated. Record URLs come out of a plugin and a route enhancer, not out of the page tree, so there is nothing to store a relation against.
This event is the extension point for exactly that case: it fires while the tags are being assembled, so a listener can add record URLs to the map using whatever mapping the project actually has. The implementation is specific to each record type, which is why it ships as an event and not as a feature.
ModifyHrefLangMultisiteTagsEvent
B13\HreflangMultisite\Event\ModifyHrefLangMultisiteTagsEvent provides:
| Method | Returns |
|---|---|
getHrefLangs(): array | The tags collected so far, as a map of hreflang code to URL, e.g. ['en-US' => 'https://example.com', 'nl-NL' => 'https://example.com/nl'] |
setHrefLangs(array $hrefLangs): void | Replaces the map |
getSiteLanguage(): SiteLanguage | The site language of the translation currently being processed |
getSite(): Site | The site of the translation currently being processed |
getPage(): array | The page record of the translation currently being processed |
Registering a Listener
use B13\HreflangMultisite\Event\ModifyHrefLangMultisiteTagsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
final class ModifyHrefLangs
{
#[AsEventListener(identifier: 'my-extension/modify-hreflang-multisite')]
public function __invoke(ModifyHrefLangMultisiteTagsEvent $event): void
{
$hrefLangs = $event->getHrefLangs();
// manipulate $hrefLangs based on $event->getSiteLanguage() ...
$event->setHrefLangs($hrefLangs);
}
}
The Event Fires Once per Translation, Not Once per Page
This is the part that decides how you write the listener.
The dispatch sits inside the loop that walks every page of the set and every translation of those pages (HrefLangMultisite::__invoke()). Your listener is called once per translation, and each time:
getHrefLangs()returns an incomplete map—only the entries collected up to that point, ending with the one just addedgetSiteLanguage(),getSite(), andgetPage()describe the translation currently being processed, not the page the visitor requested
So a listener that adds a fixed entry runs that assignment as many times as there are translations. That is harmless for an idempotent assignment like $hrefLangs['fr-CA'] = '…' and wrong for anything that appends, counts, or depends on seeing the finished set.
There is no event that fires once with the complete map. If you need the final state, listen to TYPO3’s own ModifyHrefLangTagsEvent after this extension has run.
What the Extension Does With the Result
After the loop, the collected map replaces all hreflang tags of the page:
// Override all hrefLang tags
if ($hrefLangs !== []) {
$event->setHrefLangs($hrefLangs);
}
Three consequences worth knowing:
- Tags produced by TYPO3’s own generator are discarded for pages that belong to a set, and replaced by the set’s own—including its
x-default. - The
x-defaultis derived after your listener has run. It is copied from the finished map, so rewriting the flagged language’s URL moves the fallback with it, and removing that language removes the fallback too. You do not setx-defaultyourself; you change the entry it points at. - An empty map leaves the core tags in place. If every translation is skipped, the extension writes nothing.
Which Translations Are Skipped
Before an entry is added, the extension skips translations that are hidden, that carry no_index, whose start or end time puts them outside the current date, or whose language is not available in the frontend. Those never reach your listener.
Events