{"id":138,"date":"2026-11-04T16:00:00","date_gmt":"2026-11-04T15:00:00","guid":{"rendered":"https:\/\/hoja.in\/blog\/?p=138"},"modified":"2026-10-16T14:00:13","modified_gmt":"2026-10-16T12:00:13","slug":"changing-hreflang-and-canonical-tags-with-psr-14-events","status":"publish","type":"post","link":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/","title":{"rendered":"Changing hreflang and canonical tags with PSR-14 events"},"content":{"rendered":"<p>When EXT:seo is installed, TYPO3 renders hreflang and canonical tags for you. Most of the time the default output is exactly right. But there is always one project where it is not: a language which is online but not ready for search engines, a partner site on another system that should appear in the hreflang list, or a filter parameter that should never end up in the canonical URL.<\/p>\n<p>In older TYPO3 versions people solved such things with TypoScript tricks, XCLASSes or by switching off the core tags and printing their own. The clean way is two PSR-14 events (both exist since TYPO3 v10), and since v13 you register the listeners with the <code>#[AsEventListener]<\/code> attribute. No hook arrays, no Services.yaml tags needed.<\/p>\n<h2>The two events<\/h2>\n<ul>\n<li><code>TYPO3CMSFrontendEventModifyHrefLangTagsEvent<\/code> is dispatched by the frontend <code>RequestHandler<\/code>. The core listener <code>HrefLangGenerator<\/code> from EXT:seo fills it, using the identifier <code>typo3-seo\/hreflangGenerator<\/code>. Your listener gets <code>getHrefLangs()<\/code>, <code>setHrefLangs()<\/code>, <code>addHrefLang()<\/code> and the request.<\/li>\n<li><code>TYPO3CMSSeoEventModifyUrlForCanonicalTagEvent<\/code> is dispatched by <code>CanonicalGenerator<\/code> just before the canonical tag is built. It has <code>getUrl()<\/code>, <code>setUrl()<\/code>, <code>getRequest()<\/code>, <code>getPage()<\/code> and <code>getCanonicalGenerationDisabledException()<\/code>.<\/li>\n<\/ul>\n<p>One detail is easy to miss. Since v13.0 (<a href=\"https:\/\/docs.typo3.org\/c\/typo3\/cms-core\/main\/en-us\/Changelog\/13.0\/Breaking-99807-RelocatedModifyUrlForCanonicalTagEvent.html\">Breaking-99807<\/a>) the canonical event is dispatched after the core has done its work, and it is also dispatched when canonical generation is disabled, for example because the page has <code>no_index<\/code> set. So <code>getUrl()<\/code> can already contain a URL, or it can be empty for a good reason. Your listener must check this, otherwise you add a canonical to a page that should not have one.<\/p>\n<p>I checked the class signatures in TYPO3 14.3. The canonical event got its current shape in v13.0 (<a href=\"https:\/\/docs.typo3.org\/c\/typo3\/cms-core\/main\/en-us\/Changelog\/13.0\/Feature-99807-ImproveModifyUrlForCanonicalTagEvent.html\">Feature-99807<\/a>), so the code below is written for 13.4 and 14.3.<\/p>\n<h2>Step 1: adjust the hreflang list<\/h2>\n<p>Example requirement: the site has an English language which editors are still filling. It is reachable, but hreflang should not advertise it yet. At the same time the organisation has a French site on a different system, and that one should be listed for the start page.<\/p>\n<pre><code class=\"language-php\">&lt;?php\n\ndeclare(strict_types=1);\n\nnamespace VendorSitepackageEventListener;\n\nuse TYPO3CMSCoreAttributeAsEventListener;\nuse TYPO3CMSFrontendEventModifyHrefLangTagsEvent;\n\n#[AsEventListener(\n    identifier: 'sitepackage\/hreflang',\n    after: 'typo3-seo\/hreflangGenerator',\n)]\nfinal readonly class AdjustHrefLangTags\n{\n    private const HIDDEN_LANGUAGES = ['en-GB'];\n\n    public function __invoke(ModifyHrefLangTagsEvent $event): void\n    {\n        $hrefLangs = $event-&gt;getHrefLangs();\n        foreach (self::HIDDEN_LANGUAGES as $code) {\n            unset($hrefLangs[$code]);\n        }\n\n        $pageId = $event-&gt;getRequest()-&gt;getAttribute('frontend.page.information')?-&gt;getId();\n        if ($pageId === 1) {\n            $hrefLangs['fr-FR'] = 'https:\/\/fr.example.org\/';\n        }\n\n        \/\/ x-default only makes sense when there is still more than one language\n        if (count($hrefLangs) &lt; 3) {\n            unset($hrefLangs['x-default']);\n        }\n        $event-&gt;setHrefLangs($hrefLangs);\n    }\n}<\/code><\/pre>\n<p>The <code>after<\/code> argument is the important part. Without it, your listener may run before the core generator, and then there is nothing to remove yet. The core only prints the tags when the final array has more than one entry, so if you remove too much, the tags disappear completely. That is correct behaviour, not a bug.<\/p>\n<p>A small warning: hardcoding page ids like I did above is fine for a blog example, but in a real project read them from site settings. Uids change when a tree is copied or content is imported again, and such constants are the first thing that breaks.<\/p>\n<h2>Step 2: clean the canonical URL<\/h2>\n<p>Second example: a list page has filter parameters like <code>?tx_events_list[category]=3<\/code>. The core fallback builds the canonical from the current page link including the cHash-relevant parameters, so every filtered view gets its own canonical. Say you want all filtered views to point to the plain list page instead.<\/p>\n<pre><code class=\"language-php\">&lt;?php\n\ndeclare(strict_types=1);\n\nnamespace VendorSitepackageEventListener;\n\nuse TYPO3CMSCoreAttributeAsEventListener;\nuse TYPO3CMSCoreHttpUri;\nuse TYPO3CMSSeoEventModifyUrlForCanonicalTagEvent;\n\n#[AsEventListener(identifier: 'sitepackage\/canonical')]\nfinal readonly class StripFilterFromCanonical\n{\n    public function __invoke(ModifyUrlForCanonicalTagEvent $event): void\n    {\n        \/\/ Disabled by TypoScript (disableCanonical) or by no_index: respect that\n        if ($event-&gt;getCanonicalGenerationDisabledException() !== null) {\n            return;\n        }\n        $url = $event-&gt;getUrl();\n        if ($url === '' || !str_contains(urldecode($url), 'tx_events_list[')) {\n            return;\n        }\n        $event-&gt;setUrl((string)(new Uri($url))-&gt;withQuery(''));\n    }\n}<\/code><\/pre>\n<p>An explicit canonical which an editor sets in the page properties wins in the core before the event, and it will not contain the filter parameter, so this listener leaves it alone. If you need more page data, <code>getPage()<\/code> gives you a <code>TYPO3CMSCoreDomainPage<\/code> record object. Use <code>has()<\/code> before <code>get()<\/code> on it, because <code>get()<\/code> throws an exception for a field that is not there.<\/p>\n<h2>Step 3: registration<\/h2>\n<p>Nothing more. With <code>autoconfigure: true<\/code> in your <code>Configuration\/Services.yaml<\/code> (which every modern sitepackage has), the attribute registers the listener. You can check the result in the backend module System \u2192 Configuration \u2192 Event Listeners (PSR-14). After adding a new listener, flush the caches once: <code>ddev typo3 cache:flush<\/code>.<\/p>\n<h2>Step 4: a functional test that reads the rendered tags<\/h2>\n<p>Event listeners are small, so people often skip tests. But SEO tags break silently: nobody sees them in the browser. A functional test with <a href=\"https:\/\/github.com\/TYPO3\/testing-framework\">typo3\/testing-framework<\/a> (version 9 supports both v13 and v14) renders a real page and checks the head. Here for the hreflang listener on the start page:<\/p>\n<pre><code class=\"language-php\">&lt;?php\n\ndeclare(strict_types=1);\n\nnamespace VendorSitepackageTestsFunctional;\n\nuse PHPUnitFrameworkAttributesTest;\nuse TYPO3CMSCoreConfigurationSiteWriter;\nuse TYPO3TestingFrameworkCoreFunctionalFrameworkFrontendInternalRequest;\nuse TYPO3TestingFrameworkCoreFunctionalFunctionalTestCase;\n\nfinal class HrefLangTest extends FunctionalTestCase\n{\n    protected array $coreExtensionsToLoad = ['seo'];\n    protected array $testExtensionsToLoad = ['vendor\/sitepackage'];\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        \/\/ root page uid 1 and its English translation\n        $this-&gt;importCSVDataSet(__DIR__ . '\/Fixtures\/pages.csv');\n        $this-&gt;setUpFrontendRootPage(1, ['EXT:sitepackage\/Tests\/Functional\/Fixtures\/page.typoscript']);\n        $this-&gt;get(SiteWriter::class)-&gt;write('test', [\n            'rootPageId' =&gt; 1,\n            'base' =&gt; 'https:\/\/www.example.org\/',\n            'languages' =&gt; [\n                ['languageId' =&gt; 0, 'title' =&gt; 'Deutsch', 'locale' =&gt; 'de_DE.UTF-8', 'base' =&gt; '\/', 'hreflang' =&gt; 'de-DE'],\n                ['languageId' =&gt; 1, 'title' =&gt; 'English', 'locale' =&gt; 'en_GB.UTF-8', 'base' =&gt; '\/en\/', 'hreflang' =&gt; 'en-GB'],\n            ],\n        ]);\n    }\n\n    #[Test]\n    public function startPageListsPartnerSiteButNotHiddenLanguage(): void\n    {\n        $response = $this-&gt;executeFrontendSubRequest(new InternalRequest('https:\/\/www.example.org\/'));\n        $html = (string)$response-&gt;getBody();\n\n        self::assertStringContainsString('hreflang=\"fr-FR\" href=\"https:\/\/fr.example.org\/\"', $html);\n        self::assertStringContainsString('hreflang=\"de-DE\"', $html);\n        self::assertStringNotContainsString('hreflang=\"en-GB\"', $html);\n    }\n}<\/code><\/pre>\n<p>The TypoScript fixture only needs a minimal <code>page = PAGE<\/code> with some output. Run it in DDEV, for example with <code>ddev exec typo3DatabaseDriver=pdo_sqlite vendor\/bin\/phpunit -c Build\/phpunit\/FunctionalTests.xml<\/code>. For the canonical listener a request with filter parameters needs a valid cHash, so I find a plain unit test easier there: create the event with a URL, call the listener, compare <code>getUrl()<\/code>.<\/p>\n<h2>What I learned<\/h2>\n<ul>\n<li>Always set <code>after<\/code> for the hreflang listener, the core generator must run first.<\/li>\n<li>Respect <code>getCanonicalGenerationDisabledException()<\/code>; <code>no_index<\/code> pages should stay without canonical.<\/li>\n<li>Do not fight an explicit canonical from the editor. The core respects it, and it also leaves a translation out of the hreflang list when that translation has its own canonical.<\/li>\n<li>Test the rendered HTML, not only the listener class.<\/li>\n<\/ul>\n<p>If this post helped you, I will be really happy. Feedbacks are the champion&#8217;s breakfast, so drop a comment if your project needed a different hreflang trick.<\/p>\n<h2>Further reading<\/h2>\n<ul>\n<li><a href=\"https:\/\/docs.typo3.org\/m\/typo3\/reference-coreapi\/main\/en-us\/ApiOverview\/Events\/Events\/Frontend\/ModifyHrefLangTagsEvent.html\">TYPO3 Explained: ModifyHrefLangTagsEvent<\/a><\/li>\n<li><a href=\"https:\/\/docs.typo3.org\/m\/typo3\/reference-coreapi\/main\/en-us\/ApiOverview\/Events\/Events\/Seo\/ModifyUrlForCanonicalTagEvent.html\">TYPO3 Explained: ModifyUrlForCanonicalTagEvent<\/a><\/li>\n<li><a href=\"https:\/\/docs.typo3.org\/c\/typo3\/cms-core\/main\/en-us\/Changelog\/13.0\/Feature-101544-IntroducePHPAttributeToAutoconfigureEventListeners.html\">Feature-101544: PHP attribute to autoconfigure event listeners<\/a><\/li>\n<li><a href=\"https:\/\/docs.typo3.org\/m\/typo3\/reference-coreapi\/main\/en-us\/Testing\/FunctionalTesting\/Index.html\">TYPO3 Explained: Functional testing<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Two small event listeners for EXT:seo: one adjusts the hreflang list, one fixes the canonical URL. Registered with attributes only, and covered by a functional test.<\/p>\n","protected":false},"author":1,"featured_media":139,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[17],"tags":[48,41,31,5,33,24],"class_list":["post-138","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-typo3","tag-psr-14-events","tag-seo","tag-testing","tag-typo3","tag-typo3-v13","tag-typo3-v14"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>TYPO3 hreflang and canonical tags with PSR-14 events<\/title>\n<meta name=\"description\" content=\"Adjust hreflang and canonical tags in TYPO3 v13 and v14 with ModifyHrefLangTagsEvent and ModifyUrlForCanonicalTagEvent, #[AsEventListener] and a test.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"TYPO3 hreflang and canonical tags with PSR-14 events\" \/>\n<meta property=\"og:description\" content=\"Adjust hreflang and canonical tags in TYPO3 v13 and v14 with ModifyHrefLangTagsEvent and ModifyUrlForCanonicalTagEvent, #[AsEventListener] and a test.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/\" \/>\n<meta property=\"og:site_name\" content=\"Hoja Mustaffa Abdul Latheef \u2013 Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-11-04T15:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/changing-hreflang-and-canonical-tags-with-psr-14-events.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Hoja Mustaffa Abdul Latheef\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@hojaonline\" \/>\n<meta name=\"twitter:site\" content=\"@hojaonline\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Hoja Mustaffa Abdul Latheef\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/\"},\"author\":{\"name\":\"Hoja Mustaffa Abdul Latheef\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\"},\"headline\":\"Changing hreflang and canonical tags with PSR-14 events\",\"datePublished\":\"2026-11-04T15:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/\"},\"wordCount\":784,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\"},\"image\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/changing-hreflang-and-canonical-tags-with-psr-14-events.jpg\",\"keywords\":[\"PSR-14 events\",\"SEO\",\"Testing\",\"TYPO3\",\"TYPO3 v13\",\"TYPO3 v14\"],\"articleSection\":[\"TYPO3\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/\",\"url\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/\",\"name\":\"TYPO3 hreflang and canonical tags with PSR-14 events\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/changing-hreflang-and-canonical-tags-with-psr-14-events.jpg\",\"datePublished\":\"2026-11-04T15:00:00+00:00\",\"description\":\"Adjust hreflang and canonical tags in TYPO3 v13 and v14 with ModifyHrefLangTagsEvent and ModifyUrlForCanonicalTagEvent, #[AsEventListener] and a test.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/#primaryimage\",\"url\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/changing-hreflang-and-canonical-tags-with-psr-14-events.jpg\",\"contentUrl\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/changing-hreflang-and-canonical-tags-with-psr-14-events.jpg\",\"width\":1200,\"height\":630,\"caption\":\"hreflang and canonical via events\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/changing-hreflang-and-canonical-tags-with-psr-14-events\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/hoja.in\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Changing hreflang and canonical tags with PSR-14 events\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/hoja.in\\\/blog\\\/\",\"name\":\"Hoja Mustaffa Abdul Latheef \u2013 Blog\",\"description\":\"Notes from daily work with TYPO3, PHP and AI-assisted engineering\",\"publisher\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/hoja.in\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\",\"name\":\"Hoja Mustaffa Abdul Latheef\",\"logo\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/image\\\/\"},\"description\":\"Senior TYPO3 Developer at jweiland.net, TYPO3 CMS Certified Developer. I write about TYPO3 upgrades, extension development, PHP and AI-assisted engineering with Claude Code.\",\"sameAs\":[\"https:\\\/\\\/hoja.in\\\/\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"TYPO3 hreflang and canonical tags with PSR-14 events","description":"Adjust hreflang and canonical tags in TYPO3 v13 and v14 with ModifyHrefLangTagsEvent and ModifyUrlForCanonicalTagEvent, #[AsEventListener] and a test.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/","og_locale":"en_US","og_type":"article","og_title":"TYPO3 hreflang and canonical tags with PSR-14 events","og_description":"Adjust hreflang and canonical tags in TYPO3 v13 and v14 with ModifyHrefLangTagsEvent and ModifyUrlForCanonicalTagEvent, #[AsEventListener] and a test.","og_url":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/","og_site_name":"Hoja Mustaffa Abdul Latheef \u2013 Blog","article_published_time":"2026-11-04T15:00:00+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/changing-hreflang-and-canonical-tags-with-psr-14-events.jpg","type":"image\/jpeg"}],"author":"Hoja Mustaffa Abdul Latheef","twitter_card":"summary_large_image","twitter_creator":"@hojaonline","twitter_site":"@hojaonline","twitter_misc":{"Written by":"Hoja Mustaffa Abdul Latheef","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/#article","isPartOf":{"@id":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/"},"author":{"name":"Hoja Mustaffa Abdul Latheef","@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b"},"headline":"Changing hreflang and canonical tags with PSR-14 events","datePublished":"2026-11-04T15:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/"},"wordCount":784,"commentCount":0,"publisher":{"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b"},"image":{"@id":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/#primaryimage"},"thumbnailUrl":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/changing-hreflang-and-canonical-tags-with-psr-14-events.jpg","keywords":["PSR-14 events","SEO","Testing","TYPO3","TYPO3 v13","TYPO3 v14"],"articleSection":["TYPO3"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/","url":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/","name":"TYPO3 hreflang and canonical tags with PSR-14 events","isPartOf":{"@id":"https:\/\/hoja.in\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/#primaryimage"},"image":{"@id":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/#primaryimage"},"thumbnailUrl":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/changing-hreflang-and-canonical-tags-with-psr-14-events.jpg","datePublished":"2026-11-04T15:00:00+00:00","description":"Adjust hreflang and canonical tags in TYPO3 v13 and v14 with ModifyHrefLangTagsEvent and ModifyUrlForCanonicalTagEvent, #[AsEventListener] and a test.","breadcrumb":{"@id":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/#primaryimage","url":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/changing-hreflang-and-canonical-tags-with-psr-14-events.jpg","contentUrl":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/changing-hreflang-and-canonical-tags-with-psr-14-events.jpg","width":1200,"height":630,"caption":"hreflang and canonical via events"},{"@type":"BreadcrumbList","@id":"https:\/\/hoja.in\/blog\/changing-hreflang-and-canonical-tags-with-psr-14-events\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/hoja.in\/blog\/"},{"@type":"ListItem","position":2,"name":"Changing hreflang and canonical tags with PSR-14 events"}]},{"@type":"WebSite","@id":"https:\/\/hoja.in\/blog\/#website","url":"https:\/\/hoja.in\/blog\/","name":"Hoja Mustaffa Abdul Latheef \u2013 Blog","description":"Notes from daily work with TYPO3, PHP and AI-assisted engineering","publisher":{"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/hoja.in\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b","name":"Hoja Mustaffa Abdul Latheef","logo":{"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/image\/"},"description":"Senior TYPO3 Developer at jweiland.net, TYPO3 CMS Certified Developer. I write about TYPO3 upgrades, extension development, PHP and AI-assisted engineering with Claude Code.","sameAs":["https:\/\/hoja.in\/"]}]}},"_links":{"self":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts\/138","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/comments?post=138"}],"version-history":[{"count":1,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts\/138\/revisions"}],"predecessor-version":[{"id":147,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts\/138\/revisions\/147"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/media\/139"}],"wp:attachment":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/media?parent=138"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/categories?post=138"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/tags?post=138"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}