├── Configuration ├── Sets │ └── Sitemap │ │ ├── config.yaml │ │ ├── setup.typoscript │ │ ├── route-enhancers.yaml │ │ ├── settings.definitions.yaml │ │ └── labels.xlf ├── Backend │ └── DashboardWidgetGroups.php ├── TCA │ └── Overrides │ │ ├── sys_template.php │ │ └── pages.php ├── Services.yaml ├── page.tsconfig ├── TypoScript │ └── XmlSitemap │ │ ├── constants.typoscript │ │ └── setup.typoscript └── Services.php ├── Documentation ├── Includes.rst.txt ├── Images │ ├── site.png │ ├── SiteSet.png │ ├── tab-seo.png │ ├── tab-general.png │ ├── tab-meta-data.png │ └── InstallActivate.png ├── Configuration │ ├── _site_config.diff │ ├── _site_package_set.diff │ ├── SettingsEditor.rst │ ├── Settings.rst │ └── Index.rst ├── Sitemap.rst ├── Features │ ├── _xmlSitemap │ │ ├── _recordUnsorted.typoscript │ │ ├── _config.yaml │ │ ├── _multiple.typoscript │ │ └── _record.typoscript │ ├── Index.rst │ └── XmlSitemap.rst ├── Developer │ └── Index.rst ├── guides.xml ├── Introduction │ └── Index.rst ├── Index.rst └── Installation │ └── Index.rst ├── ext_tables.sql ├── Resources ├── Public │ ├── Icons │ │ └── Extension.svg │ └── CSS │ │ └── Sitemap.xsl └── Private │ ├── Language │ ├── locallang_webinfo.xlf │ ├── locallang_dashboard.xlf │ └── db.xlf │ └── Templates │ ├── XmlSitemap │ ├── Index.fluid.xml │ └── Sitemap.fluid.xml │ └── Widget │ └── PagesWithoutDescription.fluid.html ├── README.rst ├── ext_emconf.php ├── Classes ├── XmlSitemap │ ├── Exception │ │ ├── InvalidConfigurationException.php │ │ └── MissingConfigurationException.php │ ├── XmlSitemapDataProviderInterface.php │ ├── AbstractXmlSitemapDataProvider.php │ ├── PagesXmlSitemapDataProvider.php │ ├── RecordsXmlSitemapDataProvider.php │ └── XmlSitemapRenderer.php ├── Exception │ └── CanonicalGenerationDisabledException.php ├── PageTitle │ └── SeoTitlePageTitleProvider.php ├── Event │ └── ModifyUrlForCanonicalTagEvent.php ├── MetaTag │ ├── OpenGraphMetaTagManager.php │ ├── TwitterCardMetaTagManager.php │ └── MetaTagGenerator.php ├── Widgets │ ├── PagesWithoutDescriptionWidget.php │ └── Provider │ │ └── PagesWithoutDescriptionDataProvider.php ├── HrefLang │ └── HrefLangGenerator.php └── Canonical │ └── CanonicalGenerator.php ├── ext_localconf.php ├── composer.json └── LICENSE.txt /Configuration/Sets/Sitemap/config.yaml: -------------------------------------------------------------------------------- 1 | name: typo3/seo-sitemap 2 | -------------------------------------------------------------------------------- /Documentation/Includes.rst.txt: -------------------------------------------------------------------------------- 1 | .. You can put central messages to display on all pages here 2 | -------------------------------------------------------------------------------- /Documentation/Images/site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TYPO3-CMS/seo/HEAD/Documentation/Images/site.png -------------------------------------------------------------------------------- /Documentation/Images/SiteSet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TYPO3-CMS/seo/HEAD/Documentation/Images/SiteSet.png -------------------------------------------------------------------------------- /Documentation/Images/tab-seo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TYPO3-CMS/seo/HEAD/Documentation/Images/tab-seo.png -------------------------------------------------------------------------------- /Configuration/Sets/Sitemap/setup.typoscript: -------------------------------------------------------------------------------- 1 | @import 'EXT:seo/Configuration/TypoScript/XmlSitemap/setup.typoscript' 2 | -------------------------------------------------------------------------------- /Documentation/Images/tab-general.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TYPO3-CMS/seo/HEAD/Documentation/Images/tab-general.png -------------------------------------------------------------------------------- /Documentation/Images/tab-meta-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TYPO3-CMS/seo/HEAD/Documentation/Images/tab-meta-data.png -------------------------------------------------------------------------------- /Documentation/Images/InstallActivate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TYPO3-CMS/seo/HEAD/Documentation/Images/InstallActivate.png -------------------------------------------------------------------------------- /Documentation/Configuration/_site_config.diff: -------------------------------------------------------------------------------- 1 | base: 'https://example.com/' 2 | rootPageId: 1 3 | dependencies: 4 | - typo3/fluid-styled-content-css 5 | + - typo3/seo-sitemap 6 | -------------------------------------------------------------------------------- /ext_tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE pages ( 2 | # @todo: db analyzer makes this varchar which would be ok, but the default is lost. needs review 3 | sitemap_priority decimal(2,1) DEFAULT '0.5' NOT NULL, 4 | ); 5 | -------------------------------------------------------------------------------- /Documentation/Sitemap.rst: -------------------------------------------------------------------------------- 1 | :template: sitemap.html 2 | 3 | .. include:: /Includes.rst.txt 4 | 5 | ======= 6 | Sitemap 7 | ======= 8 | 9 | .. The sitemap.html template will insert here the page tree automatically. 10 | -------------------------------------------------------------------------------- /Configuration/Backend/DashboardWidgetGroups.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'title' => 'LLL:EXT:seo/Resources/Private/Language/locallang_dashboard.xlf:widget.group.seo', 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /Configuration/TCA/Overrides/sys_template.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Configuration/Sets/Sitemap/route-enhancers.yaml: -------------------------------------------------------------------------------- 1 | routeEnhancers: 2 | PageTypeSuffix: 3 | type: PageType 4 | map: 5 | sitemap.xml: 1533906435 6 | Sitemap: 7 | type: Simple 8 | routePath: 'sitemap-type/{sitemap}' 9 | aspects: 10 | sitemap: 11 | type: StaticValueMapper 12 | map: 13 | pages: pages 14 | _arguments: 15 | sitemap: 'tx_seo/sitemap' 16 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | TYPO3 extension ``seo`` 3 | ======================= 4 | 5 | This extension offers special fields for SEO purposes, HTML meta tags display 6 | and sitemaps. 7 | 8 | :Repository: https://github.com/typo3/typo3 9 | :Issues: https://forge.typo3.org/ 10 | :Read online: https://docs.typo3.org/c/typo3/cms-seo/main/en-us/ 11 | :TER: https://extensions.typo3.org/extension/seo/ 12 | -------------------------------------------------------------------------------- /Configuration/page.tsconfig: -------------------------------------------------------------------------------- 1 | mod.web_info.fieldDefinitions { 2 | seo { 3 | label = LLL:EXT:seo/Resources/Private/Language/locallang_webinfo.xlf:seo 4 | fields = title,uid,slug,seo_title,description,no_index,no_follow,canonical_link,sitemap_changefreq,sitemap_priority 5 | } 6 | social_media { 7 | label = LLL:EXT:seo/Resources/Private/Language/locallang_webinfo.xlf:social_media 8 | fields = title,uid,og_title,og_description,twitter_title,twitter_description 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Documentation/Features/_xmlSitemap/_config.yaml: -------------------------------------------------------------------------------- 1 | routeEnhancers: 2 | PageTypeSuffix: 3 | type: PageType 4 | map: 5 | /: 0 6 | sitemap.xml: 1533906435 7 | Sitemap: 8 | type: Simple 9 | routePath: 'sitemap-type/{sitemap}' 10 | aspects: 11 | sitemap: 12 | type: StaticValueMapper 13 | map: 14 | pages: pages 15 | tx_news: tx_news 16 | my_other_sitemap: my_other_sitemap 17 | _arguments: 18 | sitemap: 'tx_seo/sitemap' 19 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_webinfo.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | SEO 8 | 9 | 10 | Social Media 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'TYPO3 CMS SEO', 5 | 'description' => 'SEO features including specific fields for SEO purposes, rendering of HTML meta tags and sitemaps.', 6 | 'category' => 'fe', 7 | 'author' => 'TYPO3 Core Team', 8 | 'author_email' => 'typo3cms@typo3.org', 9 | 'author_company' => '', 10 | 'state' => 'stable', 11 | 'version' => '14.1.0', 12 | 'constraints' => [ 13 | 'depends' => [ 14 | 'typo3' => '14.1.0', 15 | ], 16 | 'conflicts' => [], 17 | 'suggests' => [ 18 | 'dashboard' => '', 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /Documentation/Developer/Index.rst: -------------------------------------------------------------------------------- 1 | .. include:: /Includes.rst.txt 2 | 3 | 4 | .. _developer: 5 | 6 | ================ 7 | Developer Corner 8 | ================ 9 | 10 | When you work with a pages-only approach you most likely don't need to use the meta tag APIs. If you have a special 11 | edge-case or have a detail view to show a record, you need the APIs to render the corresponding meta information for your 12 | page. You can refer to the documentation of the following APIs: 13 | 14 | - The MetaTagApi (see :ref:`t3coreapi:metatagapi`) to define what metatags should be rendered on your page 15 | - The PageTitleAPI (see :ref:`t3coreapi:pagetitle`) will give you the possibility to set the title of the page 16 | -------------------------------------------------------------------------------- /Classes/XmlSitemap/Exception/InvalidConfigurationException.php: -------------------------------------------------------------------------------- 1 | ` is included, 10 | the settings for sitemaps become available in the editor: 11 | 12 | You can find the available site settings in module 13 | :guilabel:`Sites > Setup > Settings` 14 | 15 | You can change individual settings here. If the site settings are writable 16 | you can hit the :guilabel:`Save` button and the settings will be written 17 | directly to the site settings. 18 | 19 | If the settings are not writable you can click the :guilabel:`YAML export` 20 | button to export the settings. These can then be added by a developer with 21 | sufficient rights. 22 | 23 | The available settings are also described in detail in :ref:`site-sets`. 24 | -------------------------------------------------------------------------------- /Resources/Private/Templates/XmlSitemap/Index.fluid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {sitemap.lastMod -> f:format.date(format: 'c')} 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Configuration/Sets/Sitemap/settings.definitions.yaml: -------------------------------------------------------------------------------- 1 | categories: 2 | seo: ~ 3 | seo.templates: 4 | parent: seo 5 | 6 | settings: 7 | seo.sitemap.view.templateRootPath: 8 | default: 'EXT:seo/Resources/Private/Templates/' 9 | type: string 10 | category: seo.templates 11 | seo.sitemap.view.partialRootPath: 12 | default: 'EXT:seo/Resources/Private/Partials/' 13 | type: string 14 | category: seo.templates 15 | seo.sitemap.view.layoutRootPath: 16 | default: 'EXT:seo/Resources/Private/Layouts/' 17 | type: string 18 | category: seo.templates 19 | seo.sitemap.pages.excludedDoktypes: 20 | default: '3, 4, 6, 7, 199, 254' 21 | type: string 22 | category: seo 23 | seo.sitemap.pages.excludePagesRecursive: 24 | default: '' 25 | type: string 26 | category: seo 27 | seo.sitemap.pages.additionalWhere: 28 | default: "{#no_index} = 0 AND {#canonical_link} = ''" 29 | type: string 30 | category: seo 31 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_dashboard.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | SEO 8 | 9 | 10 | Pages missing meta description 11 | 12 | 13 | Find and display pages with missing meta description 14 | 15 | 16 | All pages have a proper meta description — everything looks good. 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Documentation/Features/_xmlSitemap/_record.typoscript: -------------------------------------------------------------------------------- 1 | plugin.tx_seo { 2 | config { 3 | xmlSitemap { 4 | sitemaps { 5 | myNewsSitemap { 6 | provider = TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider 7 | config { 8 | table = news_table 9 | sortField = sorting 10 | lastModifiedField = tstamp 11 | changeFreqField = news_changefreq 12 | priorityField = news_priority 13 | additionalWhere = AND ({#no_index} = 0 OR {#no_follow} = 0) 14 | pid = 15 | recursive = 16 | url { 17 | pageId = 18 | fieldToParameterMap { 19 | uid = tx_extension_pi1[news] 20 | } 21 | additionalGetParameters { 22 | tx_extension_pi1.controller = News 23 | tx_extension_pi1.action = detail 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Classes/PageTitle/SeoTitlePageTitleProvider.php: -------------------------------------------------------------------------------- 1 | request->getAttribute('frontend.page.information'); 31 | return (string)($pageInformation->getPageRecord()['seo_title'] ?? ''); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Configuration/TypoScript/XmlSitemap/constants.typoscript: -------------------------------------------------------------------------------- 1 | # customsubcategory=sitemap=XML Sitemap 2 | 3 | plugin.tx_seo { 4 | view { 5 | # cat=plugin.tx_seo/file; type=string; label=Path to template root (FE) 6 | templateRootPath = EXT:seo/Resources/Private/Templates/ 7 | # cat=plugin.tx_seo/file; type=string; label=Path to template partials (FE) 8 | partialRootPath = EXT:seo/Resources/Private/Partials/ 9 | # cat=plugin.tx_seo/file; type=string; label=Path to template layouts (FE) 10 | layoutRootPath = EXT:seo/Resources/Private/Layouts/ 11 | } 12 | 13 | settings { 14 | xmlSitemap { 15 | sitemaps { 16 | pages { 17 | # cat=plugin.tx_seo/sitemap; type=string; label=Doktypes to exclude 18 | excludedDoktypes = 3, 4, 6, 7, 199, 254 19 | # cat=plugin.tx_seo/sitemap; type=string; label=List of page uids which should be excluded recursive 20 | excludePagesRecursive = 21 | # cat=plugin.tx_seo/sitemap; type=string; label=Additional where clause 22 | additionalWhere = {#no_index} = 0 AND {#canonical_link} = '' 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Resources/Private/Templates/XmlSitemap/Sitemap.fluid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {item.loc} 8 | {item.lastMod -> f:format.date(format: 'c')} 9 | 10 | {item.changefreq} 11 | 12 | 13 | {item.priority} 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Documentation/Configuration/Settings.rst: -------------------------------------------------------------------------------- 1 | :navigation-title: Settings 2 | 3 | .. include:: /Includes.rst.txt 4 | 5 | .. _configuration-site-set-settings: 6 | 7 | ============================= 8 | Site sets settings of EXT:seo 9 | ============================= 10 | 11 | The following settings are available via the 12 | :ref:`site set settings ` and can be adjusted in 13 | the :ref:`settings-editor`. 14 | 15 | .. versionchanged:: 14.0 16 | The names of the GET parameters used in the sitemap generated by EXT:seo have 17 | been changed from `page` and `sitemap` to `tx_seo[page]` and `tx_seo[sitemap]` 18 | respectively. If you are overriding the templates for the sitemap, provide 19 | separate sets for TYPO3 13.4 and 14.x support. 20 | 21 | .. versionchanged:: 14.1 22 | The site set `typo3/seo-sitemap` now ships a sitemap route enhancer. See 23 | also `Automatic routing for the XML sitemap `_. 24 | 25 | .. typo3:site-set-settings:: PROJECT:/Configuration/Sets/Sitemap/settings.definitions.yaml 26 | :name: seo-settings 27 | :type: 28 | :Label: Settings of the site set of EXT:seo 29 | -------------------------------------------------------------------------------- /Documentation/guides.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /Classes/XmlSitemap/XmlSitemapDataProviderInterface.php: -------------------------------------------------------------------------------- 1 | `_. 30 | -------------------------------------------------------------------------------- /ext_localconf.php: -------------------------------------------------------------------------------- 1 | generate'; 17 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags']['canonical'] = 18 | CanonicalGenerator::class . '->generate'; 19 | 20 | $metaTagManagerRegistry = GeneralUtility::makeInstance(MetaTagManagerRegistry::class); 21 | $metaTagManagerRegistry->registerManager( 22 | 'opengraph', 23 | OpenGraphMetaTagManager::class 24 | ); 25 | $metaTagManagerRegistry->registerManager( 26 | 'twitter', 27 | TwitterCardMetaTagManager::class 28 | ); 29 | unset($metaTagManagerRegistry); 30 | 31 | // Add module configuration 32 | ExtensionManagementUtility::addTypoScriptSetup(trim(' 33 | config.pageTitleProviders { 34 | seo { 35 | provider = TYPO3\CMS\Seo\PageTitle\SeoTitlePageTitleProvider 36 | before = record 37 | } 38 | } 39 | ')); 40 | -------------------------------------------------------------------------------- /Documentation/Index.rst: -------------------------------------------------------------------------------- 1 | .. include:: /Includes.rst.txt 2 | 3 | .. _start: 4 | 5 | ================================ 6 | TYPO3 Search Engine Optimization 7 | ================================ 8 | 9 | :Extension key: 10 | seo 11 | 12 | :Package name: 13 | typo3/cms-seo 14 | 15 | :Version: 16 | |release| 17 | 18 | :Language: 19 | en 20 | 21 | :Author: 22 | TYPO3 contributors 23 | 24 | :License: 25 | This document is published under the 26 | `Open Content License `__. 27 | 28 | :Rendered: 29 | |today| 30 | 31 | ---- 32 | 33 | This extension offers special fields for search engine optimization (SEO) 34 | purposes, HTML meta tags display and sitemaps. 35 | 36 | General information on topics about SEO can be found in 37 | :ref:`Search engine optimization (SEO) `. 38 | 39 | Information for editors, including how to use the 40 | `dashboard widgets `_ 41 | can be found in 42 | `Tutorial for Editors, Search engine optimization (SEO) for TYPO3 editors `_. 43 | 44 | ---- 45 | 46 | **Table of Contents:** 47 | 48 | .. toctree:: 49 | :maxdepth: 2 50 | :titlesonly: 51 | 52 | Introduction/Index 53 | Installation/Index 54 | Features/Index 55 | Configuration/Index 56 | Developer/Index 57 | 58 | .. Meta Menu 59 | 60 | .. toctree:: 61 | :hidden: 62 | 63 | Sitemap 64 | -------------------------------------------------------------------------------- /Documentation/Installation/Index.rst: -------------------------------------------------------------------------------- 1 | .. include:: /Includes.rst.txt 2 | 3 | .. _installation: 4 | 5 | ============ 6 | Installation 7 | ============ 8 | 9 | Target group: **Administrators** 10 | 11 | This extension is part of the TYPO3 Core. 12 | 13 | .. contents:: Table of contents 14 | :local: 15 | 16 | Installation with Composer 17 | ========================== 18 | 19 | Check whether you are already using the extension with: 20 | 21 | .. code-block:: bash 22 | 23 | composer show | grep seo 24 | 25 | This should either give you no result or something similar to: 26 | 27 | .. code-block:: none 28 | 29 | typo3/cms-seo v12.4.11 30 | 31 | If it is not installed yet, use the ``composer require`` command to install 32 | the extension: 33 | 34 | .. code-block:: bash 35 | 36 | composer require typo3/cms-seo 37 | 38 | The given version depends on the version of the TYPO3 Core you are using. 39 | 40 | Installation without Composer 41 | ============================= 42 | 43 | In an installation without Composer, the extension is already shipped but might 44 | not be activated yet. Activate it as follows: 45 | 46 | #. In the backend, navigate to the :guilabel:`System > Extensions` 47 | module. 48 | #. Click the :guilabel:`Activate` icon for the SEO extension. 49 | 50 | .. figure:: /Images/InstallActivate.png 51 | :class: with-border 52 | :alt: Extension manager showing SEO extension 53 | 54 | Extension manager showing SEO extension 55 | -------------------------------------------------------------------------------- /Configuration/Sets/Sitemap/labels.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | SEO Sitemap 8 | 9 | 10 | SEO Sitemap 11 | 12 | 13 | Template Paths 14 | 15 | 16 | Path to template root (FE) 17 | 18 | 19 | Path to template partials (FE) 20 | 21 | 22 | Path to template layouts (FE) 23 | 24 | 25 | Doktypes to exclude 26 | 27 | 28 | List of page uids which should be excluded recursive 29 | 30 | 31 | Additional where clause 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Documentation/Configuration/Index.rst: -------------------------------------------------------------------------------- 1 | .. include:: /Includes.rst.txt 2 | 3 | 4 | .. _configuration: 5 | 6 | ============= 7 | Configuration 8 | ============= 9 | 10 | Target group: **Developers, Integrators** 11 | 12 | .. seealso:: 13 | General SEO recommendations for TypoScript and Site Configuration can be 14 | found in `TYPO3 explained, Suggested configuration options for improved 15 | SEO in TYPO3 `_. 16 | 17 | .. toctree:: 18 | :caption: Subpages 19 | :glob: 20 | 21 | * 22 | 23 | .. _configuration-site-sets: 24 | 25 | Site sets 26 | ========= 27 | 28 | .. versionadded::13.3 29 | EXT:seo now offers a site set "SEO Sitemap" to include the TypoScript to 30 | output the XML sitemap. 31 | 32 | Include the site set "SEO Sitemap", `typo3/seo-sitemap` via the :ref:`site set in the site 33 | configuration ` or the custom 34 | :ref:`site package's site set `. 35 | 36 | Settings for the included set can be adjusted in the :ref:`settings-editor`. 37 | 38 | .. figure:: /Images/SiteSet.png 39 | 40 | Add the site set "SEO Sitemap" 41 | 42 | This will change your site configuration file as follows: 43 | 44 | .. literalinclude:: _site_config.diff 45 | :caption: config/sites/my-site/config.yaml (diff) 46 | 47 | If your site has a custom :ref:`site package `, you 48 | can also add the "SEO Sitemap" set as dependency in your site's configuration: 49 | 50 | .. literalinclude:: _site_package_set.diff 51 | :caption: EXT:my_site_package/Configuration/Sets/MySite/config.yaml (diff) 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typo3/cms-seo", 3 | "type": "typo3-cms-framework", 4 | "description": "TYPO3 CMS SEO - SEO features including specific fields for SEO purposes, rendering of HTML meta tags and sitemaps.", 5 | "homepage": "https://typo3.community/", 6 | "funding": [ 7 | { 8 | "type": "membership", 9 | "url": "https://typo3.org/membership" 10 | } 11 | ], 12 | "license": [ 13 | "GPL-2.0-or-later" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "TYPO3 Core Team", 18 | "email": "typo3cms@typo3.org", 19 | "role": "Developer" 20 | } 21 | ], 22 | "support": { 23 | "issues": "https://forge.typo3.org/issues/", 24 | "forum": "https://talk.typo3.org/", 25 | "source": "https://github.com/TYPO3/typo3/", 26 | "docs": "https://docs.typo3.org/c/typo3/cms-seo/main/en-us/", 27 | "rss": "https://news.typo3.com/rss/", 28 | "chat": "https://typo3.community/meet/slack/", 29 | "security": "https://typo3.org/security/" 30 | }, 31 | "config": { 32 | "sort-packages": true 33 | }, 34 | "require": { 35 | "typo3/cms-core": "14.1.*@dev", 36 | "typo3/cms-frontend": "14.1.*@dev", 37 | "typo3/cms-extbase": "14.1.*@dev" 38 | }, 39 | "conflict": { 40 | "typo3/cms": "*" 41 | }, 42 | "suggest": { 43 | "typo3/cms-dashboard": "TYPO3 users can add widgets that can help to optimise their website for search engines" 44 | }, 45 | "extra": { 46 | "branch-alias": { 47 | "dev-main": "14.1.x-dev" 48 | }, 49 | "typo3/cms": { 50 | "extension-key": "seo", 51 | "Package": { 52 | "partOfFactoryDefault": true 53 | } 54 | } 55 | }, 56 | "autoload": { 57 | "psr-4": { 58 | "TYPO3\\CMS\\Seo\\": "Classes/" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Classes/Event/ModifyUrlForCanonicalTagEvent.php: -------------------------------------------------------------------------------- 1 | url; 39 | } 40 | 41 | public function setUrl(string $url): void 42 | { 43 | $this->url = $url; 44 | } 45 | 46 | public function getRequest(): ServerRequestInterface 47 | { 48 | return $this->request; 49 | } 50 | 51 | public function getPage(): Page 52 | { 53 | return $this->page; 54 | } 55 | 56 | public function getCanonicalGenerationDisabledException(): ?CanonicalGenerationDisabledException 57 | { 58 | return $this->canonicalGenerationDisabledException; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Configuration/Services.php: -------------------------------------------------------------------------------- 1 | services(); 17 | 18 | /** 19 | * Check if WidgetRegistry is defined, which means that EXT:dashboard is available. 20 | * Registration directly in Services.yaml will break without EXT:dashboard installed! 21 | */ 22 | if ($containerBuilder->hasDefinition(WidgetRegistry::class)) { 23 | $services->set('dashboard.widget.pagesWithoutMetaDescription') 24 | ->class(PagesWithoutDescriptionWidget::class) 25 | ->arg('$dataProvider', new Reference(PagesWithoutDescriptionDataProvider::class)) 26 | ->arg('$backendViewFactory', new Reference(BackendViewFactory::class)) 27 | ->arg('$options', ['refreshAvailable' => true]) 28 | ->tag('dashboard.widget', [ 29 | 'identifier' => 'seo-pagesWithoutMetaDescription', 30 | 'groupNames' => 'seo', 31 | 'title' => 'LLL:EXT:seo/Resources/Private/Language/locallang_dashboard.xlf:widget.pagesWithoutMetaDescription.title', 32 | 'description' => 'LLL:EXT:seo/Resources/Private/Language/locallang_dashboard.xlf:widget.pagesWithoutMetaDescription.description', 33 | 'iconIdentifier' => 'content-widget-list', 34 | 'height' => 'large', 35 | 'width' => 'medium', 36 | ]); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /Classes/MetaTag/OpenGraphMetaTagManager.php: -------------------------------------------------------------------------------- 1 | by default 31 | * 32 | * @var string 33 | */ 34 | protected $defaultNameAttribute = 'property'; 35 | 36 | /** 37 | * Array of properties that can be handled by this manager 38 | * 39 | * @var array 40 | */ 41 | protected $handledProperties = [ 42 | 'og:type' => [], 43 | 'og:title' => [], 44 | 'og:description' => [], 45 | 'og:site_name' => [], 46 | 'og:url' => [], 47 | 'og:audio' => [], 48 | 'og:video' => [], 49 | 'og:determiner' => [], 50 | 'og:locale' => [ 51 | 'allowedSubProperties' => [ 52 | 'alternate' => [ 53 | 'allowMultipleOccurrences' => true, 54 | ], 55 | ], 56 | ], 57 | 'og:image' => [ 58 | 'allowMultipleOccurrences' => true, 59 | 'allowedSubProperties' => [ 60 | 'url' => [], 61 | 'secure_url' => [], 62 | 'type' => [], 63 | 'width' => [], 64 | 'height' => [], 65 | 'alt' => [], 66 | ], 67 | ], 68 | ]; 69 | } 70 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Widget/PagesWithoutDescription.fluid.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 25 | 35 | 36 | 37 |
21 | {page.title} 22 |
23 | {page.frontendUrl} 24 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
38 |
39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /Classes/Widgets/PagesWithoutDescriptionWidget.php: -------------------------------------------------------------------------------- 1 | request = $request; 44 | } 45 | 46 | public function renderWidgetContent(): string 47 | { 48 | $view = $this->backendViewFactory->create($this->request, ['typo3/cms-dashboard', 'typo3/cms-seo']); 49 | $view->assignMultiple([ 50 | 'pages' => $this->dataProvider->getPages(), 51 | 'options' => $this->getOptions(), 52 | 'configuration' => $this->configuration, 53 | ]); 54 | return $view->render('Widget/PagesWithoutDescription'); 55 | } 56 | 57 | public function getOptions(): array 58 | { 59 | return $this->options; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Classes/MetaTag/TwitterCardMetaTagManager.php: -------------------------------------------------------------------------------- 1 | [], 34 | 'twitter:site' => [ 35 | 'allowedSubProperties' => [ 36 | 'id' => [], 37 | ], 38 | ], 39 | 'twitter:creator' => [ 40 | 'allowedSubProperties' => [ 41 | 'id' => [], 42 | ], 43 | ], 44 | 'twitter:description' => [], 45 | 'twitter:title' => [], 46 | 'twitter:image' => [ 47 | 'allowedSubProperties' => [ 48 | 'alt' => [], 49 | ], 50 | ], 51 | 'twitter:player' => [ 52 | 'allowedSubProperties' => [ 53 | 'width' => [], 54 | 'height' => [], 55 | 'stream' => [], 56 | ], 57 | ], 58 | 'twitter:app' => [ 59 | 'allowedSubProperties' => [ 60 | 'name:iphone' => [], 61 | 'id:iphone' => [], 62 | 'url:iphone' => [], 63 | 'name:ipad' => [], 64 | 'id:ipad' => [], 65 | 'url:ipad' => [], 66 | 'name:googleplay' => [], 67 | 'id:googleplay' => [], 68 | 'url:googleplay' => [], 69 | ], 70 | ], 71 | ]; 72 | } 73 | -------------------------------------------------------------------------------- /Configuration/TypoScript/XmlSitemap/setup.typoscript: -------------------------------------------------------------------------------- 1 | seo_sitemap = PAGE 2 | seo_sitemap { 3 | typeNum = 1533906435 4 | 5 | config { 6 | cache_period = 900 7 | disableAllHeaderCode = 1 8 | admPanel = 0 9 | removeDefaultJS = 1 10 | removeDefaultCss = 1 11 | additionalHeaders { 12 | 10.header = Content-Type:application/xml;charset=utf-8 13 | 20.header = X-Robots-Tag:noindex 14 | } 15 | # Prevent sitemap.xml from appearing in the search results of EXT:indexed_search 16 | index_enable = 0 17 | } 18 | 19 | 10 = USER 20 | 10.userFunc = TYPO3\CMS\Seo\XmlSitemap\XmlSitemapRenderer->render 21 | } 22 | 23 | plugin.tx_seo { 24 | view { 25 | templateRootPaths { 26 | 0 = EXT:seo/Resources/Private/Templates/XmlSitemap 27 | 10 = {$plugin.tx_seo.view.templateRootPath ?? $seo.sitemap.view.templateRootPath} 28 | } 29 | partialRootPaths { 30 | 0 = EXT:seo/Resources/Private/Partials/XmlSitemap 31 | 10 = {$plugin.tx_seo.view.partialRootPath ?? $seo.sitemap.view.partialRootPath} 32 | } 33 | layoutRootPaths { 34 | 0 = EXT:seo/Resources/Private/Layouts/XmlSitemap 35 | 10 = {$plugin.tx_seo.view.layoutRootPath ?? $seo.sitemap.view.layoutRootPath} 36 | } 37 | } 38 | 39 | config { 40 | # Here you can override the xslFile for all sitemaps 41 | # xslFile = EXT:seo/Resources/Public/CSS/Sitemap.xsl 42 | xmlSitemap { 43 | sitemaps { 44 | # Here you can override the xslFile for all sitemaps of a certain sitemapType 45 | # xslFile = EXT:seo/Resources/Public/CSS/Sitemap.xsl 46 | pages { 47 | provider = TYPO3\CMS\Seo\XmlSitemap\PagesXmlSitemapDataProvider 48 | config { 49 | # Here you can override the xslFile for a single sitemap 50 | # xslFile = EXT:seo/Resources/Public/CSS/Sitemap.xsl 51 | excludedDoktypes = {$plugin.tx_seo.settings.xmlSitemap.sitemaps.pages.excludedDoktypes ?? $seo.sitemap.pages.excludedDoktypes} 52 | # comma-separated list of page uids which should be excluded recursive 53 | excludePagesRecursive = {$plugin.tx_seo.settings.xmlSitemap.sitemaps.pages.excludePagesRecursive ?? $seo.sitemap.pages.excludePagesRecursive} 54 | additionalWhere = {$plugin.tx_seo.settings.xmlSitemap.sitemaps.pages.additionalWhere ?? $seo.sitemap.pages.additionalWhere} 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Classes/XmlSitemap/AbstractXmlSitemapDataProvider.php: -------------------------------------------------------------------------------- 1 | key = $key; 39 | $this->config = $config; 40 | $this->request = $request; 41 | $this->cObj = $cObj ?? GeneralUtility::makeInstance(ContentObjectRenderer::class); 42 | } 43 | 44 | public function getKey(): string 45 | { 46 | return $this->key; 47 | } 48 | 49 | public function getNumberOfPages(): int 50 | { 51 | return (int)ceil(count($this->items) / $this->numberOfItemsPerPage); 52 | } 53 | 54 | public function getLastModified(): int 55 | { 56 | $lastMod = 0; 57 | foreach ($this->items as $item) { 58 | if ((int)($item['lastMod'] ?? 0) > $lastMod) { 59 | $lastMod = (int)$item['lastMod']; 60 | } 61 | } 62 | 63 | return $lastMod; 64 | } 65 | 66 | protected function defineUrl(array $data): array 67 | { 68 | return $data; 69 | } 70 | 71 | public function getItems(): array 72 | { 73 | $pageNumber = (int)($this->request->getQueryParams()['tx_seo']['page'] ?? 0); 74 | $page = $pageNumber > 0 ? $pageNumber : 0; 75 | $items = array_slice( 76 | $this->items, 77 | $page * $this->numberOfItemsPerPage, 78 | $this->numberOfItemsPerPage 79 | ); 80 | 81 | return array_map([$this, 'defineUrl'], $items); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Classes/Widgets/Provider/PagesWithoutDescriptionDataProvider.php: -------------------------------------------------------------------------------- 1 | getBackendUser(); 44 | $items = []; 45 | if (!$backendUser->check('tables_modify', 'pages')) { 46 | // Early return in case user is not allowed to modify pages at all 47 | return $items; 48 | } 49 | $rowCount = 0; 50 | $pagesResult = $this->getPotentialPages(); 51 | while ($row = $pagesResult->fetchAssociative()) { 52 | if (!$backendUser->doesUserHaveAccess($row, Permission::PAGE_EDIT)) { 53 | continue; 54 | } 55 | BackendUtility::workspaceOL('pages', $row, $backendUser->workspace); 56 | $pageId = $row['l10n_parent'] ?: $row['uid']; 57 | try { 58 | $site = $this->siteFinder->getSiteByPageId($pageId); 59 | // make sure the language of the row actually exists in the site 60 | $site->getLanguageById($row['sys_language_uid']); 61 | } catch (SiteNotFoundException | \InvalidArgumentException) { 62 | continue; 63 | } 64 | $router = $site->getRouter(); 65 | $row['frontendUrl'] = (string)$router->generateUri($pageId, ['_language' => $row['sys_language_uid']]); 66 | $items[] = $row; 67 | $rowCount++; 68 | if ($rowCount >= $this->limit) { 69 | return $items; 70 | } 71 | } 72 | return $items; 73 | } 74 | 75 | /** 76 | * Fetches potential candidates for the list from the database. 77 | * Doktypes that do not require a meta description (such as directories and links) are ignored. 78 | * Pages with noindex or a canonical are also ignored for this reason. 79 | * Language versions are considered individually. 80 | * Workspace versions are considered for the workspace the user is in. 81 | */ 82 | private function getPotentialPages(): Result 83 | { 84 | $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); 85 | $queryBuilder->getRestrictions()->add(new WorkspaceRestriction($this->getBackendUser()->workspace)); 86 | return $queryBuilder 87 | ->select('uid', 'pid', 'title', 'slug', 'sys_language_uid', 'l10n_parent', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody') 88 | ->from('pages') 89 | ->where( 90 | $queryBuilder->expr()->notIn('doktype', $this->excludedDoktypes), 91 | $queryBuilder->expr()->and( 92 | $queryBuilder->expr()->eq('no_index', $queryBuilder->createNamedParameter(0)), 93 | $queryBuilder->expr()->eq('canonical_link', $queryBuilder->createNamedParameter('')), 94 | ), 95 | $queryBuilder->expr()->or( 96 | $queryBuilder->expr()->eq('description', $queryBuilder->createNamedParameter('')), 97 | $queryBuilder->expr()->isNull('description') 98 | ), 99 | ) 100 | ->orderBy('tstamp', 'DESC') 101 | ->executeQuery(); 102 | } 103 | 104 | private function getBackendUser(): BackendUserAuthentication 105 | { 106 | return $GLOBALS['BE_USER']; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Classes/XmlSitemap/PagesXmlSitemapDataProvider.php: -------------------------------------------------------------------------------- 1 | generateItems(); 41 | } 42 | 43 | protected function generateItems(): void 44 | { 45 | $pageRepository = GeneralUtility::makeInstance(PageRepository::class); 46 | $pages = $pageRepository->getPagesOverlay($this->getPages()); 47 | $languageAspect = $this->getCurrentLanguageAspect(); 48 | foreach ($pages as $page) { 49 | if (!$pageRepository->isPageSuitableForLanguage($page, $languageAspect)) { 50 | continue; 51 | } 52 | 53 | $this->items[] = $page + [ 54 | 'lastMod' => (int)($page['SYS_LASTCHANGED'] ?: $page['tstamp']), 55 | 'changefreq' => $page['sitemap_changefreq'], 56 | 'priority' => (float)$page['sitemap_priority'], 57 | ]; 58 | } 59 | } 60 | 61 | protected function getPages(): array 62 | { 63 | if (!empty($this->config['rootPage'])) { 64 | $rootPageId = (int)$this->config['rootPage']; 65 | } else { 66 | $site = $this->request->getAttribute('site'); 67 | $rootPageId = $site->getRootPageId(); 68 | } 69 | 70 | $excludePagesRecursive = GeneralUtility::intExplode(',', (string)($this->config['excludePagesRecursive'] ?? ''), true); 71 | 72 | $pageRepository = GeneralUtility::makeInstance(PageRepository::class); 73 | $pageIds = $pageRepository->getDescendantPageIdsRecursive($rootPageId, 99, 0, $excludePagesRecursive); 74 | $pageIds = array_merge([$rootPageId], $pageIds); 75 | 76 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 77 | ->getQueryBuilderForTable('pages'); 78 | 79 | $constraints = [ 80 | $queryBuilder->expr()->in('uid', $pageIds), 81 | ]; 82 | 83 | if (!empty($this->config['additionalWhere'])) { 84 | $constraints[] = QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), QueryHelper::stripLogicalOperatorPrefix($this->config['additionalWhere'])); 85 | } 86 | 87 | if (!empty($this->config['excludedDoktypes'])) { 88 | $excludedDoktypes = GeneralUtility::intExplode(',', (string)$this->config['excludedDoktypes']); 89 | if (!empty($excludedDoktypes)) { 90 | $constraints[] = $queryBuilder->expr()->notIn('doktype', implode(',', $excludedDoktypes)); 91 | } 92 | } 93 | $pages = $queryBuilder->select('*') 94 | ->from('pages') 95 | ->where(...$constraints) 96 | ->orderBy('uid', 'ASC') 97 | ->executeQuery() 98 | ->fetchAllAssociative(); 99 | 100 | return $pages; 101 | } 102 | 103 | protected function getCurrentLanguageAspect(): LanguageAspect 104 | { 105 | return GeneralUtility::makeInstance(Context::class)->getAspect('language'); 106 | } 107 | 108 | protected function defineUrl(array $data): array 109 | { 110 | $typoLinkConfig = [ 111 | 'page' => new Page($data), 112 | 'parameter' => $data['uid'], 113 | 'forceAbsoluteUrl' => 1, 114 | ]; 115 | 116 | $data['loc'] = $this->cObj->createUrl($typoLinkConfig); 117 | return $data; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Resources/Private/Language/db.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | SEO 8 | 9 | 10 | General SEO settings 11 | 12 | 13 | Title for search engines 14 | 15 | 16 | Robot instructions 17 | 18 | 19 | No index 20 | 21 | 22 | Index this page 23 | 24 | 25 | No follow 26 | 27 | 28 | Follow this page 29 | 30 | 31 | Open Graph (Facebook) 32 | 33 | 34 | Open Graph Title 35 | 36 | 37 | Open Graph Description 38 | 39 | 40 | Open Graph Image 41 | 42 | 43 | X / Twitter Cards 44 | 45 | 46 | X / Twitter Title 47 | 48 | 49 | X / Twitter Description 50 | 51 | 52 | X / Twitter Image 53 | 54 | 55 | Type of card to show 56 | 57 | 58 | Summary Card 59 | 60 | 61 | Summary Card with a large image 62 | 63 | 64 | Canonical 65 | 66 | 67 | Canonical link 68 | 69 | 70 | A canonical URL is the URL of a page that search engines are advised to choose as the most representative from a set of duplicate pages. Often called deduplication, this process helps search engines show only one version of the otherwise duplicate content in its search results. TYPO3 automatically sets this to the most likely URL of the page if not set explicitly - which is most likely the URL of the page itself. If set here, the page will be removed from the XML sitemap. 71 | 72 | 73 | Social media 74 | 75 | 76 | Sitemap 77 | 78 | 79 | Change frequency 80 | 81 | 82 | None 83 | 84 | 85 | Always 86 | 87 | 88 | Hourly 89 | 90 | 91 | Daily 92 | 93 | 94 | Weekly 95 | 96 | 97 | Monthly 98 | 99 | 100 | Yearly 101 | 102 | 103 | Never 104 | 105 | 106 | Priority 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /Classes/HrefLang/HrefLangGenerator.php: -------------------------------------------------------------------------------- 1 | getRequest(); 48 | $pageInformation = $request->getAttribute('frontend.page.information'); 49 | $pageRecord = $pageInformation->getPageRecord(); 50 | if ((int)$pageRecord['no_index'] === 1) { 51 | return; 52 | } 53 | $this->cObj->setRequest($event->getRequest()); 54 | $languages = $this->languageMenuProcessor->process($this->cObj, [], [], []); 55 | $site = $request->getAttribute('site'); 56 | $siteLanguage = $request->getAttribute('language', $site->getDefaultLanguage()); 57 | $pageId = $pageInformation->getId(); 58 | 59 | $hrefLangs = $event->getHrefLangs(); 60 | foreach ($languages['languagemenu'] as $language) { 61 | if (!empty($language['link']) && $language['hreflang']) { 62 | if ($language['languageId'] === 0) { 63 | // No need to fetch default language 64 | $page = ($pageRecord['_TRANSLATION_SOURCE'] ?? null)?->toArray(true) ?? $pageRecord; 65 | } elseif ($language['languageId'] === ($pageRecord['_REQUESTED_OVERLAY_LANGUAGE'] ?? false)) { 66 | // No need to fetch current language 67 | $page = $pageRecord; 68 | } else { 69 | $page = $this->getTranslatedPageRecord($pageId, $language['languageId'], $site); 70 | } 71 | // do not set hreflang if a page is not translated explicitly 72 | if (empty($page)) { 73 | continue; 74 | } 75 | // do not set hreflang when canonical is set explicitly 76 | if (!empty($page['canonical_link'])) { 77 | continue; 78 | } 79 | 80 | $href = $this->getAbsoluteUrl($language['link'], $siteLanguage); 81 | $hrefLangs[$language['hreflang']] = $href; 82 | } 83 | } 84 | if (count($hrefLangs) > 1) { 85 | if (array_key_exists($languages['languagemenu'][0]['hreflang'], $hrefLangs)) { 86 | $hrefLangs['x-default'] = $hrefLangs[$languages['languagemenu'][0]['hreflang']]; 87 | } 88 | } 89 | $event->setHrefLangs($hrefLangs); 90 | } 91 | 92 | protected function getAbsoluteUrl(string $url, SiteLanguage $siteLanguage): string 93 | { 94 | $uri = new Uri($url); 95 | if (empty($uri->getHost())) { 96 | $url = $siteLanguage->getBase()->withPath($uri->getPath()); 97 | 98 | if ($uri->getQuery()) { 99 | $url = $url->withQuery($uri->getQuery()); 100 | } 101 | } 102 | return (string)$url; 103 | } 104 | 105 | protected function getTranslatedPageRecord(int $pageId, int $languageId, Site $site): array 106 | { 107 | $targetSiteLanguage = $site->getLanguageById($languageId); 108 | $languageAspect = LanguageAspectFactory::createFromSiteLanguage($targetSiteLanguage); 109 | 110 | $context = clone GeneralUtility::makeInstance(Context::class); 111 | $context->setAspect('language', $languageAspect); 112 | 113 | $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context); 114 | $pageRecord = $pageRepository->getPage($pageId); 115 | // Overlay was requested but did not apply 116 | if ($languageId > 0 && !isset($pageRecord['_LOCALIZED_UID'])) { 117 | return []; 118 | } 119 | return $pageRecord; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Documentation/Features/Index.rst: -------------------------------------------------------------------------------- 1 | :navigation-title: Features 2 | 3 | .. include:: /Includes.rst.txt 4 | .. _features: 5 | 6 | ============================================ 7 | Features of the TYPO3 system extension "seo" 8 | ============================================ 9 | 10 | The TYPO3 system extension :composer:`typo3/cms-seo` offers multiple tools and 11 | fields that can be used to improve visibility of a TYPO3 site in search engines: 12 | 13 | .. contents:: 14 | 15 | .. toctree:: 16 | :glob: 17 | :hidden: 18 | 19 | * 20 | 21 | .. _seo-page-properties: 22 | 23 | Additional tabs "SEO" and "Social media" in the page properties 24 | =============================================================== 25 | 26 | After `Installation `_ 27 | two additional tabs are available in the page properties. 28 | 29 | SEO 30 | This tab contains additional fields for the 31 | `title tag `_ 32 | in the HTML header, for the description meta tag, for robots instruction, the 33 | `Canonical URL `_ 34 | and for priorities used in the 35 | `XML Sitemap `_. 36 | Social media 37 | This tab contains additional fields to manage data for the 38 | Open Graph (Facebook) meta tags and the X / Twitter Cards. 39 | 40 | Usage of these additional fields is described in the Editors Tutorial, 41 | `Search engine optimization (SEO) for TYPO3 editors `_. 42 | 43 | .. _seo-page-dashboard: 44 | 45 | A Dashboard Widget for SEO 46 | ========================== 47 | 48 | The extension also offers an additional Dashboard widget. 49 | :composer:`typo3/cms-dashboard` needs to be installed. Usage is described in 50 | the Editors Tutorial, chapter 51 | `Dashboard widgets for Search engine optimization (SEO) in TYPO3 `_. 52 | 53 | If your editors have one of the standard user groups "Editor" or "Advanced Editor", 54 | created by the command `typo3 setup:begroups:default` they have permissions to 55 | use the widget. 56 | 57 | If you created the use groups manually you users need to have "Dashboard" in 58 | their allowed modules and "Pages missing Meta Description" in the 59 | allowed dashboard widgets list: 60 | `Dashboard manual, permissions of widgets `_ 61 | 62 | .. _xml-sitemap: 63 | 64 | XML Sitemap 65 | =========== 66 | 67 | The extension :composer:`typo3/cms-seo` comes with the site set 68 | `typo3/seo-sitemap `_, 69 | which you can use to provide an XML sitemap like `https://example.org/sitemap.xml`. 70 | 71 | See chapter `XML sitemap `_ 72 | for details. 73 | 74 | .. _canonical-url: 75 | 76 | Canonical URL 77 | ============= 78 | 79 | When :composer:`typo3/cms-seo` is installed, pages automatically contain a 80 | canonical link tag in their HTML head, unless disabled via TypoScript. 81 | 82 | .. code-block:: html 83 | :caption: example output of a canonical link in the head of a TYPO3 page 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | You can use the event `ModifyUrlForCanonicalTagEvent `_ 93 | to provide an alternative canonical URL if needed. 94 | 95 | The API of the canonical link is described in 96 | `Canonical API, TYPO3 Explained `_. 97 | 98 | .. warning:: 99 | If you have other SEO extensions installed that generate canonical links, 100 | you have to make sure only one is responsible to embed into your frontend 101 | output. 102 | 103 | If both the Core and another extension are generating a canonical link, 104 | it will result in 2 canonical links which might cause confusion for search 105 | engines. 106 | 107 | .. _seo-page-title-provider: 108 | 109 | SEO page title provider 110 | ======================= 111 | 112 | While the `Page title API `_, 113 | providing a `` tag in the HTML head is part of a minimal TYPO3 installation, 114 | :composer:`typo3/cms-seo` provides an additional field, `seo_title` in the page 115 | properties. The :php:`\TYPO3\CMS\Seo\PageTitle\SeoTitlePageTitleProvider` 116 | provides this title as an alternative title for the `<title>` tag. 117 | 118 | The following default TypoScript setup is provided for the page title provider: 119 | 120 | .. code-block:: typoscript 121 | 122 | config.pageTitleProviders { 123 | seo { 124 | provider = TYPO3\CMS\Seo\PageTitle\SeoTitlePageTitleProvider 125 | before = record 126 | } 127 | } 128 | 129 | .. _seo-meta-tag-provider: 130 | 131 | Additional meta tag handling 132 | ============================ 133 | 134 | While the `MetaTag API <https://docs.typo3.org/permalink/t3coreapi:metatagapi>`_ 135 | is part of the minimal TYPO3 Core, the meta tag providers for the description 136 | meta tag commonly used for search engine optimazation, and the social preview 137 | meta tags of Open Graph and Twitter / X are part of :composer:`typo3/cms-seo`. 138 | 139 | .. _seo-hreflang: 140 | 141 | Hreflang tags 142 | ============= 143 | 144 | :html:`hreflang` link-tags are added automatically for multi-language websites 145 | based on the one-tree principle. 146 | 147 | The links are based on the site configuration and depend on translations of a page. 148 | 149 | :html:`hreflang="x-default"` indicates the link of the current page in the default language. 150 | 151 | The value of :html:`hreflang` is set for each language in 152 | :guilabel:`Sites > Setup` (see :ref:`t3coreapi:sitehandling-addingLanguages`) 153 | -------------------------------------------------------------------------------- /Resources/Public/CSS/Sitemap.xsl: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <xsl:stylesheet version="2.0" 3 | xmlns:html="http://www.w3.org/TR/REC-html40" 4 | xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" 5 | xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" 6 | xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 7 | <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/> 8 | <xsl:template match="/"> 9 | <html xmlns="http://www.w3.org/1999/xhtml"> 10 | <head> 11 | <title>TYPO3 XML Sitemap 12 | 13 | 62 | 63 | 64 |
65 |

TYPO3 XML Sitemap

66 | 67 |

68 | This XML Sitemap Index file contains sitemaps. 69 |

70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 89 | 90 | 91 | 92 |
SitemapLast modified
84 | 85 | 87 | 88 |
93 |
94 | 95 |

96 | This XML Sitemap contains URLs. 97 |

98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 120 | 123 | 126 | 129 | 130 | 131 | 132 |
URLLast Mod.Change freq.Priority
113 | 114 | 115 | 116 | 117 | 118 | 119 | 121 | 122 | 124 | 125 | 127 | 128 |
133 |
134 |
135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /Classes/XmlSitemap/RecordsXmlSitemapDataProvider.php: -------------------------------------------------------------------------------- 1 | tcaSchemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class); 46 | $this->generateItems(); 47 | } 48 | 49 | /** 50 | * @throws MissingConfigurationException 51 | */ 52 | public function generateItems(): void 53 | { 54 | $table = $this->config['table']; 55 | if (!$this->tcaSchemaFactory->has($table)) { 56 | throw new MissingConfigurationException( 57 | 'No configuration found for sitemap ' . $this->getKey(), 58 | 1535576053 59 | ); 60 | } 61 | $schema = $this->tcaSchemaFactory->get($table); 62 | 63 | $pids = !empty($this->config['pid']) ? GeneralUtility::intExplode(',', (string)$this->config['pid']) : []; 64 | $lastModifiedField = $this->config['lastModifiedField'] ?? 'tstamp'; 65 | $sortField = $this->config['sortField'] ?? 'sorting'; 66 | 67 | $changeFreqField = $schema->hasField($this->config['changeFreqField'] ?? '') ? $this->config['changeFreqField'] : ''; 68 | $priorityField = $schema->hasField($this->config['priorityField'] ?? '') ? $this->config['priorityField'] : ''; 69 | 70 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 71 | ->getQueryBuilderForTable($table); 72 | 73 | $constraints = []; 74 | 75 | if ($schema->isLanguageAware()) { 76 | /** @var LanguageAwareSchemaCapability $languageCapability */ 77 | $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); 78 | $constraints[] = $queryBuilder->expr()->in( 79 | $languageCapability->getLanguageField()->getName(), 80 | [ 81 | -1, // All languages 82 | $this->getLanguageId(), // Current language 83 | ] 84 | ); 85 | } 86 | 87 | if (!empty($pids)) { 88 | $recursiveLevel = isset($this->config['recursive']) ? (int)$this->config['recursive'] : 0; 89 | $pids = GeneralUtility::makeInstance(PageRepository::class)->getPageIdsRecursive($pids, $recursiveLevel); 90 | $constraints[] = $queryBuilder->expr()->in('pid', $pids); 91 | } 92 | 93 | if (!empty($this->config['additionalWhere'])) { 94 | $constraints[] = QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), QueryHelper::stripLogicalOperatorPrefix($this->config['additionalWhere'])); 95 | } 96 | 97 | $queryBuilder->getRestrictions()->add( 98 | GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getCurrentWorkspaceAspect()->getId()) 99 | ); 100 | 101 | $queryBuilder->select('*') 102 | ->from($table); 103 | 104 | if (!empty($constraints)) { 105 | $queryBuilder->where( 106 | ...$constraints 107 | ); 108 | } 109 | 110 | $rows = $queryBuilder->orderBy($sortField) 111 | ->executeQuery() 112 | ->fetchAllAssociative(); 113 | 114 | foreach ($rows as $row) { 115 | $item = [ 116 | 'data' => $row, 117 | 'lastMod' => (int)$row[$lastModifiedField], 118 | ]; 119 | if (!empty($changeFreqField)) { 120 | $item['changefreq'] = $row[$changeFreqField]; 121 | } 122 | $item['priority'] = !empty($priorityField) ? $row[$priorityField] : 0.5; 123 | $this->items[] = $item; 124 | } 125 | } 126 | 127 | protected function defineUrl(array $data): array 128 | { 129 | $pageId = $this->request->getAttribute('frontend.page.information')->getId(); 130 | $pageId = $this->config['url']['pageId'] ?? $pageId; 131 | $additionalParams = []; 132 | 133 | $additionalParams = $this->getUrlFieldParameterMap($additionalParams, $data['data']); 134 | $additionalParams = $this->getUrlAdditionalParams($additionalParams); 135 | 136 | $additionalParamsString = http_build_query( 137 | $additionalParams, 138 | '', 139 | '&', 140 | PHP_QUERY_RFC3986 141 | ); 142 | 143 | $typoLinkConfig = [ 144 | 'parameter' => $pageId, 145 | 'additionalParams' => $additionalParamsString ? '&' . $additionalParamsString : '', 146 | 'forceAbsoluteUrl' => 1, 147 | ]; 148 | 149 | $data['loc'] = $this->cObj->createUrl($typoLinkConfig); 150 | 151 | return $data; 152 | } 153 | 154 | protected function getUrlFieldParameterMap(array $additionalParams, array $data): array 155 | { 156 | if (!empty($this->config['url']['fieldToParameterMap']) && 157 | \is_array($this->config['url']['fieldToParameterMap'])) { 158 | foreach ($this->config['url']['fieldToParameterMap'] as $field => $urlPart) { 159 | $additionalParams[$urlPart] = $data[$field]; 160 | } 161 | } 162 | 163 | return $additionalParams; 164 | } 165 | 166 | protected function getUrlAdditionalParams(array $additionalParams): array 167 | { 168 | if (!empty($this->config['url']['additionalGetParameters']) && 169 | is_array($this->config['url']['additionalGetParameters'])) { 170 | foreach ($this->config['url']['additionalGetParameters'] as $extension => $extensionConfig) { 171 | foreach ($extensionConfig as $key => $value) { 172 | $additionalParams[$extension . '[' . $key . ']'] = $value; 173 | } 174 | } 175 | } 176 | 177 | return $additionalParams; 178 | } 179 | 180 | protected function getLanguageId(): int 181 | { 182 | $context = GeneralUtility::makeInstance(Context::class); 183 | return (int)$context->getPropertyFromAspect('language', 'id'); 184 | } 185 | 186 | protected function getCurrentWorkspaceAspect(): WorkspaceAspect 187 | { 188 | return GeneralUtility::makeInstance(Context::class)->getAspect('workspace'); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Classes/Canonical/CanonicalGenerator.php: -------------------------------------------------------------------------------- 1 | getAttribute('frontend.page.information')->getPageRecord(); 51 | $canonicalGenerationDisabledException = null; 52 | 53 | $href = ''; 54 | try { 55 | $typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray(); 56 | if ($typoScriptConfigArray['disableCanonical'] ?? false) { 57 | throw new CanonicalGenerationDisabledException('Generation of the canonical tag is disabled via TypoScript "disableCanonical"', 1706104146); 58 | } 59 | if ((int)$pageRecord['no_index'] === 1) { 60 | throw new CanonicalGenerationDisabledException('Generation of the canonical is disabled due to "no_index" being set active in the page properties', 1706104147); 61 | } 62 | 63 | // 1) Check if page has canonical URL set 64 | $href = $this->checkForCanonicalLink($request); 65 | if ($href === '') { 66 | // 2) Check if page show content from other page 67 | $href = $this->checkContentFromPid($request); 68 | } 69 | if ($href === '') { 70 | // 3) Fallback, create canonical URL 71 | $href = $this->checkDefaultCanonical($request); 72 | } 73 | } catch (CanonicalGenerationDisabledException $canonicalGenerationDisabledException) { 74 | } finally { 75 | $event = $this->eventDispatcher->dispatch( 76 | new ModifyUrlForCanonicalTagEvent($request, new Page($pageRecord), $href, $canonicalGenerationDisabledException) 77 | ); 78 | $href = $event->getUrl(); 79 | } 80 | 81 | if ($href !== '') { 82 | $canonical = ' 'canonical', 84 | 'href' => $href, 85 | ], true) . ($this->pageRenderer->getDocType()->isXmlCompliant() ? '/' : '') . '>' . LF; 86 | $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); 87 | $pageRenderer->addHeaderData($canonical); 88 | return $canonical; 89 | } 90 | return ''; 91 | } 92 | 93 | protected function checkForCanonicalLink(ServerRequestInterface $request): string 94 | { 95 | $pageRecord = $request->getAttribute('frontend.page.information')->getPageRecord(); 96 | $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); 97 | $cObj->setRequest($request); 98 | $cObj->start($pageRecord, 'pages'); 99 | if (!empty($pageRecord['canonical_link'])) { 100 | return $cObj->createUrl([ 101 | 'parameter' => $pageRecord['canonical_link'], 102 | 'forceAbsoluteUrl' => true, 103 | ]); 104 | } 105 | return ''; 106 | } 107 | 108 | protected function checkContentFromPid(ServerRequestInterface $request): string 109 | { 110 | $pageInformation = $request->getAttribute('frontend.page.information'); 111 | $id = $pageInformation->getId(); 112 | $contentPid = $pageInformation->getContentFromPid(); 113 | if ($id !== $contentPid) { 114 | $targetPid = $contentPid; 115 | if ($targetPid > 0) { 116 | $pageRepository = GeneralUtility::makeInstance(PageRepository::class); 117 | $targetPageRecord = $pageRepository->getPage($contentPid, true); 118 | if (!empty($targetPageRecord['canonical_link'])) { 119 | $targetPid = $targetPageRecord['canonical_link']; 120 | } 121 | $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); 122 | $cObj->setRequest($request); 123 | $cObj->start($request->getAttribute('frontend.page.information')->getPageRecord(), 'pages'); 124 | return $cObj->createUrl([ 125 | 'parameter' => $targetPid, 126 | 'forceAbsoluteUrl' => true, 127 | ]); 128 | } 129 | } 130 | return ''; 131 | } 132 | 133 | protected function checkDefaultCanonical(ServerRequestInterface $request): string 134 | { 135 | $pageInformation = $request->getAttribute('frontend.page.information'); 136 | $id = $pageInformation->getId(); 137 | // We should only create a canonical link to the target, if the target is within a valid site root 138 | $inSiteRoot = $this->isPageWithinSiteRoot($id); 139 | if (!$inSiteRoot) { 140 | return ''; 141 | } 142 | 143 | // Temporarily remove current mount point information as we want to have the 144 | // URL of the target page and not of the page within the mount point if the 145 | // current page is a mount point. 146 | $pageInformation = clone $pageInformation; 147 | $pageInformation->setMountPoint(''); 148 | $request = $request->withAttribute('frontend.page.information', $pageInformation); 149 | $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); 150 | $cObj->setRequest($request); 151 | $cObj->start($pageInformation->getPageRecord(), 'pages'); 152 | return $cObj->createUrl([ 153 | 'parameter' => $id . ',' . $request->getAttribute('routing')->getPageType(), 154 | 'forceAbsoluteUrl' => true, 155 | 'addQueryString' => true, 156 | 'addQueryString.' => [ 157 | 'exclude' => implode( 158 | ',', 159 | CanonicalizationUtility::getParamsToExcludeForCanonicalizedUrl( 160 | $id, 161 | (array)$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters'] 162 | ) 163 | ), 164 | ], 165 | ]); 166 | } 167 | 168 | protected function isPageWithinSiteRoot(int $id): bool 169 | { 170 | $rootline = GeneralUtility::makeInstance(RootlineUtility::class, $id)->get(); 171 | foreach ($rootline as $page) { 172 | if ($page['is_siteroot']) { 173 | return true; 174 | } 175 | } 176 | return false; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Classes/MetaTag/MetaTagGenerator.php: -------------------------------------------------------------------------------- 1 | getAttribute('frontend.page.information')->getPageRecord(); 54 | if (!empty($pageRecord['description'])) { 55 | $manager = $this->metaTagManagerRegistry->getManagerForProperty('description'); 56 | $manager->addProperty('description', $pageRecord['description']); 57 | } 58 | 59 | if (!empty($pageRecord['og_title'])) { 60 | $twitterCardTagRequired = true; 61 | $manager = $this->metaTagManagerRegistry->getManagerForProperty('og:title'); 62 | $manager->addProperty('og:title', $pageRecord['og_title']); 63 | } 64 | 65 | if (!empty($pageRecord['og_description'])) { 66 | $twitterCardTagRequired = true; 67 | $manager = $this->metaTagManagerRegistry->getManagerForProperty('og:description'); 68 | $manager->addProperty('og:description', $pageRecord['og_description']); 69 | } 70 | 71 | if (!empty($pageRecord['og_image'])) { 72 | $fileCollector = GeneralUtility::makeInstance(FileCollector::class); 73 | $fileCollector->addFilesFromRelation('pages', 'og_image', $pageRecord); 74 | $manager = $this->metaTagManagerRegistry->getManagerForProperty('og:image'); 75 | 76 | $ogImages = $this->generateSocialImages($fileCollector->getFiles()); 77 | foreach ($ogImages as $ogImage) { 78 | $twitterCardTagRequired = true; 79 | $subProperties = []; 80 | $subProperties['url'] = $ogImage['url']; 81 | $subProperties['width'] = $ogImage['width']; 82 | $subProperties['height'] = $ogImage['height']; 83 | 84 | if (!empty($ogImage['alternative'])) { 85 | $subProperties['alt'] = $ogImage['alternative']; 86 | } 87 | 88 | $manager->addProperty( 89 | 'og:image', 90 | $ogImage['url'], 91 | $subProperties 92 | ); 93 | } 94 | } 95 | 96 | if (!empty($pageRecord['twitter_title'])) { 97 | $twitterCardTagRequired = true; 98 | $manager = $this->metaTagManagerRegistry->getManagerForProperty('twitter:title'); 99 | $manager->addProperty('twitter:title', $pageRecord['twitter_title']); 100 | } 101 | 102 | if (!empty($pageRecord['twitter_description'])) { 103 | $twitterCardTagRequired = true; 104 | $manager = $this->metaTagManagerRegistry->getManagerForProperty('twitter:description'); 105 | $manager->addProperty('twitter:description', $pageRecord['twitter_description']); 106 | } 107 | 108 | if (!empty($pageRecord['twitter_image'])) { 109 | $fileCollector = GeneralUtility::makeInstance(FileCollector::class); 110 | $fileCollector->addFilesFromRelation('pages', 'twitter_image', $pageRecord); 111 | $manager = $this->metaTagManagerRegistry->getManagerForProperty('twitter:image'); 112 | 113 | $twitterImages = $this->generateSocialImages($fileCollector->getFiles()); 114 | foreach ($twitterImages as $twitterImage) { 115 | $twitterCardTagRequired = true; 116 | $subProperties = []; 117 | 118 | if (!empty($twitterImage['alternative'])) { 119 | $subProperties['alt'] = $twitterImage['alternative']; 120 | } 121 | 122 | $manager->addProperty( 123 | 'twitter:image', 124 | $twitterImage['url'], 125 | $subProperties 126 | ); 127 | } 128 | } 129 | 130 | $twitterCard = $pageRecord['twitter_card'] ?: ($twitterCardTagRequired ? 'summary' : ''); 131 | if (!empty($twitterCard)) { 132 | $manager = $this->metaTagManagerRegistry->getManagerForProperty('twitter:card'); 133 | $manager->addProperty('twitter:card', $twitterCard); 134 | } 135 | 136 | $noIndex = ($pageRecord['no_index']) ? 'noindex' : 'index'; 137 | $noFollow = ($pageRecord['no_follow']) ? 'nofollow' : 'follow'; 138 | 139 | if ($noIndex === 'noindex' || $noFollow === 'nofollow') { 140 | $manager = $this->metaTagManagerRegistry->getManagerForProperty('robots'); 141 | $manager->addProperty('robots', implode(',', [$noIndex, $noFollow])); 142 | } 143 | } 144 | 145 | /** 146 | * @param list $fileReferences 147 | */ 148 | protected function generateSocialImages(array $fileReferences): array 149 | { 150 | $socialImages = []; 151 | 152 | foreach ($fileReferences as $fileReference) { 153 | $arguments = $fileReference->getProperties(); 154 | $image = $this->processSocialImage($fileReference); 155 | $socialImages[] = [ 156 | 'url' => $this->imageService->getImageUri($image, true), 157 | 'width' => floor((float)$image->getProperty('width')), 158 | 'height' => floor((float)$image->getProperty('height')), 159 | 'alternative' => $arguments['alternative'], 160 | ]; 161 | } 162 | 163 | return $socialImages; 164 | } 165 | 166 | protected function processSocialImage(FileReference $fileReference): FileInterface 167 | { 168 | $arguments = $fileReference->getProperties(); 169 | $cropVariantCollection = CropVariantCollection::create((string)($arguments['crop'] ?? '')); 170 | $cropVariantName = ($arguments['cropVariant'] ?? false) ?: 'social'; 171 | $cropArea = $cropVariantCollection->getCropArea($cropVariantName); 172 | $crop = $cropArea->makeAbsoluteBasedOnFile($fileReference); 173 | 174 | $processingConfiguration = [ 175 | 'crop' => $crop, 176 | 'maxWidth' => 2000, 177 | ]; 178 | 179 | // The image needs to be processed if: 180 | // - the image width is greater than the defined maximum width, or 181 | // - there is a cropping other than the full image (starts at 0,0 and has a width and height of 100%) defined 182 | $needsProcessing = $fileReference->getProperty('width') > $processingConfiguration['maxWidth'] 183 | || !$cropArea->isEmpty(); 184 | if (!$needsProcessing) { 185 | return $fileReference->getOriginalFile(); 186 | } 187 | 188 | return $fileReference->getOriginalFile()->process( 189 | ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, 190 | $processingConfiguration 191 | ); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /Documentation/Features/XmlSitemap.rst: -------------------------------------------------------------------------------- 1 | .. include:: /Includes.rst.txt 2 | .. index:: XML sitemap 3 | .. _xmlsitemap: 4 | 5 | =========== 6 | XML sitemap 7 | =========== 8 | 9 | :composer:`typo3/cms-seo` provides a ready to use XML sitemap that can be 10 | included via `Site sets `_ 11 | (recommended) or include in your TypoScript record. 12 | 13 | .. versionchanged:: 14.0 14 | The names of the GET parameters used in the sitemap generated by EXT:seo have 15 | been changed from `page` and `sitemap` to `tx_seo[page]` and `tx_seo[sitemap]` 16 | respectively. 17 | 18 | If you are overriding the :ref:`automatic routing configuration `, 19 | the customized routing needs to be slightly adopted. 20 | 21 | If the templates of `EXT:seo/Resources/Private/Templates/XmlSitemap/Index.xml` 22 | have been modified, adopt the generated links to fit the original ones. 23 | 24 | In case the URL to a single sitemap has been provided to a third party tool 25 | like a crawler, search engine, ... it must be added again with the new URL. 26 | 27 | .. contents:: Table of Contents 28 | :depth: 1 29 | :local: 30 | 31 | .. _xmlsitemap-url: 32 | 33 | How to access your XML sitemap 34 | ============================== 35 | 36 | You can access the sitemaps by visiting `https://example.org/sitemap.xml`. 37 | You will first see the sitemap index. By default, there is one sitemap in the 38 | index. This is the sitemap for pages. 39 | 40 | .. note:: 41 | Each site root and language configured in the 42 | `Site handling `_ 43 | has its own XML sitemap depending on the entry point. 44 | 45 | **Example:** 46 | 47 | - Entry point `/` - :samp:`https://example.org/sitemap.xml`: for default language 48 | - Entry point `/fr/` - :samp:`https://example.org/fr/sitemap.xml`: for French 49 | - Entry point `/it/` - :samp:`https://example.org/it/sitemap.xml`: for Italian 50 | 51 | .. _xmlsitemap-routing: 52 | 53 | Automatic routing for the XML sitemap 54 | ===================================== 55 | 56 | .. versionchanged:: 14.1 57 | The SEO extension now ships its sitemap route enhancers as part of 58 | the `typo3/seo-sitemap` site set. 59 | 60 | Previously, these route enhancers had to be manually configured in each 61 | site's `config.yaml`. 62 | 63 | The SEO extension now ships its sitemap route enhancers as part of 64 | the `typo3/seo-sitemap` site set. When this set is used as a dependency, 65 | the route enhancers for XML sitemaps are automatically configured. 66 | 67 | This enables clean URLs for sitemaps out of the box: 68 | 69 | * `/sitemap.xml` - Main sitemap index 70 | * `/sitemap-type/pages/sitemap.xml` - Pages sitemap 71 | 72 | The routing can be overridden in the projects :file:`config/sites/my-site/config.yaml` 73 | and in site sets extending the `typo3/seo-sitemap` site set. 74 | 75 | .. index:: XmlSitemapDataProviders 76 | 77 | 78 | .. _xmlsitemap-data-providers: 79 | 80 | Data providers for XML sitemaps 81 | =============================== 82 | 83 | The rendering of sitemaps is based on data providers implementing 84 | :php:`\TYPO3\CMS\Seo\XmlSitemap\XmlSitemapDataProviderInterface`. 85 | 86 | :composer:`typo3/cms-seo` ships with the following data providers for XML 87 | sitemaps: 88 | 89 | .. _xmlsitemap-data-providers-pages: 90 | 91 | For pages: PagesXmlSitemapDataProvider 92 | -------------------------------------- 93 | 94 | The :php:`\TYPO3\CMS\Seo\XmlSitemap\PagesXmlSitemapDataProvider` will generate a 95 | sitemap of pages based on the detected site root. You can configure whether you 96 | have additional conditions for selecting the pages. 97 | 98 | Via setting :ref:`seo.sitemap.pages.excludedDoktypes ` 99 | it is possible to exclude certain `Types of pages `_. 100 | 101 | Additionally, you may exclude page subtrees from the sitemap 102 | (for example internal pages). This can be 103 | configured using setting 104 | :ref:`seo.sitemap.pages.excludePagesRecursive `. 105 | 106 | If your site still depend on TypoScript records instead of site sets, you can 107 | make these settings via TypoScript constants. 108 | 109 | For special use cases you might want to override the default TypoScript provided 110 | by the set. 111 | 112 | .. _xmlsitemap-data-providers-records: 113 | 114 | For database records: RecordsXmlSitemapDataProvider 115 | --------------------------------------------------- 116 | 117 | If you have an extension installed and want a sitemap of those records, the 118 | :php:`\TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider` can be used. The 119 | following example shows how to add a sitemap for news records: 120 | 121 | .. literalinclude:: _xmlSitemap/_record.typoscript 122 | :caption: EXT:my_extension/Configuration/Sets/XmlSitemapNews/setup.typoscript 123 | 124 | You can add multiple sitemaps and they will be added to the sitemap index 125 | automatically. Use different types to have multiple, independent sitemaps: 126 | 127 | .. literalinclude:: _xmlSitemap/_multiple.typoscript 128 | :caption: EXT:my_extension/Configuration/Sets/XmlSitemapMultiple/setup.typoscript 129 | 130 | .. _xmlsitemap-changefreq-priority: 131 | 132 | Change frequency and priority 133 | ============================= 134 | 135 | Change frequencies define how often each page is approximately updated and hence 136 | how often it should be revisited (for example: News in an archive are "never" 137 | updated, while your home page might get "weekly" updates). 138 | 139 | Priority allows you to define how important the page is compared to other pages 140 | on your site. The priority is stated in a value from 0 to 1. Your most important 141 | pages can get an higher priority as other pages. This value does not affect how 142 | important your pages are compared to pages of other websites. All pages and 143 | records get a priority of 0.5 by default. 144 | 145 | The settings can be defined in the TypoScript configuration of an XML sitemap by 146 | mapping the properties to fields of the record by using the options 147 | :typoscript:`changeFreqField` and :typoscript:`priorityField`. 148 | :typoscript:`changeFreqField` needs to point to a field containing string values 149 | (see :typoscript:`pages` TCA definition of field 150 | :typoscript:`sitemap_changefreq`), :typoscript:`priorityField` needs to point to 151 | a field with a decimal value between 0 and 1. 152 | 153 | .. note:: 154 | Both the priority and the change frequency have no impact on your rankings. 155 | These options only give hints to search engines in which order and how often 156 | you would like a crawler to visit your pages. 157 | 158 | .. _xmlsitemap-without-sorting: 159 | 160 | Sitemap of records without sorting field 161 | ======================================== 162 | 163 | Sitemaps are paginated by default. To ensure that as few pages of the sitemap 164 | as possible are changed after the number of records is changed, the items in the 165 | sitemaps are ordered. By default, this is done using a sorting field. If you do 166 | not have such a field, make sure to configure this in your sitemap configuration 167 | and use a different field. An example you can use for sorting based on the uid 168 | field: 169 | 170 | .. literalinclude:: _xmlSitemap/_recordUnsorted.typoscript 171 | :caption: EXT:my_extension/Configuration/Sets/XmlSitemapTableWithoutSorting/setup.typoscript 172 | 173 | .. _xmlsitemap-custom-provider: 174 | 175 | Create a custom XML sitemap provider 176 | ==================================== 177 | 178 | If you need more logic in your sitemap, you can also write your own 179 | sitemap provider. You can do this by extending the 180 | :php:`\TYPO3\CMS\Seo\XmlSitemap\AbstractXmlSitemapDataProvider` class or 181 | implementing :php:`\TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider`. 182 | 183 | The main methods of interest are :php:`getLastModified()` and :php:`getItems()`. 184 | 185 | The :php:`getLastModified()` method is used in the sitemap index and has to 186 | return the date of the last modified item in the sitemap. 187 | 188 | The :php:`getItems()` method has to return an array with the items for the 189 | sitemap: 190 | 191 | .. code-block:: php 192 | :caption: EXT:my_extension/Classes/XmlSitemap/MyXmlSitemapProvider.php 193 | 194 | $this->items[] = [ 195 | 'loc' => 'https://example.org/page1.html', 196 | 'lastMod' => '1536003609' 197 | ]; 198 | 199 | The :php:`loc` element is the URL of the page to be crawled by a search engine. 200 | The :php:`lastMod` element contains the date of the last update of the 201 | specific item. This value is a UNIX timestamp. In addition, you can include 202 | :php:`changefreq` and :php:`priority` as keys in the array to give 203 | :ref:`search engines a hint `. 204 | 205 | .. _sitemap-xslFile: 206 | 207 | Use a customized sitemap XSL file 208 | ================================= 209 | 210 | The XSL file used to create a layout for an XML sitemap can be configured at 211 | three levels: 212 | 213 | #. For all sitemaps: 214 | 215 | .. code-block:: typoscript 216 | :caption: EXT:my_extension/Configuration/TypoScript/setup.typoscript 217 | 218 | plugin.tx_seo.config { 219 | xslFile = EXT:my_extension/Resources/Public/CSS/mySite.xsl 220 | } 221 | 222 | #. For all sitemaps of a certain sitemapType: 223 | 224 | .. code-block:: typoscript 225 | :caption: EXT:my_extension/Configuration/TypoScript/setup.typoscript 226 | 227 | plugin.tx_seo.config.mySitemapType.sitemaps { 228 | xslFile = EXT:my_extension/Resources/Public/CSS/mySite.xsl 229 | } 230 | 231 | #. For a specific sitemap: 232 | 233 | .. code-block:: typoscript 234 | :caption: EXT:my_extension/Configuration/TypoScript/setup.typoscript 235 | 236 | plugin.tx_seo.config.xmlSitemap.sitemaps.myNewsSitemap.config { 237 | xslFile = EXT:my_extension/Resources/Public/CSS/mySite.xsl 238 | } 239 | 240 | The value is inherited until it is overwritten. 241 | 242 | If no value is specified at all, :file:`EXT:seo/Resources/Public/CSS/Sitemap.xsl` 243 | is used as default. 244 | -------------------------------------------------------------------------------- /Classes/XmlSitemap/XmlSitemapRenderer.php: -------------------------------------------------------------------------------- 1 | getAttribute('frontend.typoscript')->getSetupTree()->getChildByName('plugin')->getChildByName('tx_seo'); 72 | $configurationArrayWithoutDots = $this->typoScriptService->convertTypoScriptArrayToPlainArray($settingsTree->toArray()); 73 | $viewConfiguration = $configurationArrayWithoutDots['view'] ?? []; 74 | $viewFactoryData = new ViewFactoryData( 75 | templateRootPaths: $viewConfiguration['templateRootPaths'] ?? [], 76 | partialRootPaths: $viewConfiguration['partialRootPaths'] ?? [], 77 | layoutRootPaths: $viewConfiguration['layoutRootPaths'] ?? [], 78 | request: $request, 79 | format: 'xml', 80 | ); 81 | $view = $this->viewFactory->create($viewFactoryData); 82 | $sitemapType = $typoScriptConfiguration['sitemapType'] ?? 'xmlSitemap'; 83 | $view->assign('type', $request->getAttribute('routing')->getPageType()); 84 | $view->assign('sitemapType', $sitemapType); 85 | $configConfiguration = $configurationArrayWithoutDots['config'] ?? []; 86 | if (!empty($sitemapName = ($request->getQueryParams()['tx_seo']['sitemap'] ?? null))) { 87 | $xslResource = $this->getXslResource($configConfiguration, $sitemapType, $sitemapName); 88 | $this->applyDynamicContentSecurityPolicy($xslResource); 89 | $view->assign('xslFile', (string)$this->resourcePublisher->generateUri($xslResource, $request)); 90 | return $this->renderSitemap($request, $view, $configConfiguration, $sitemapType, $sitemapName); 91 | } 92 | $xslResource = $this->getXslResource($configConfiguration, $sitemapType); 93 | $this->applyDynamicContentSecurityPolicy($xslResource); 94 | $view->assign('xslFile', (string)$this->resourcePublisher->generateUri($xslResource, $request)); 95 | return $this->renderIndex($request, $view, $configConfiguration, $sitemapType); 96 | } 97 | 98 | private function renderIndex(ServerRequestInterface $request, ViewInterface $view, array $configConfiguration, string $sitemapType): string 99 | { 100 | $sitemaps = []; 101 | foreach ($configConfiguration[$sitemapType]['sitemaps'] as $sitemapName => $sitemapConfig) { 102 | $sitemapProvider = $sitemapConfig['provider'] ?? null; 103 | if (is_string($sitemapName) 104 | && is_string($sitemapProvider) 105 | && class_exists($sitemapProvider) 106 | && is_subclass_of($sitemapProvider, XmlSitemapDataProviderInterface::class) 107 | ) { 108 | /** @var XmlSitemapDataProviderInterface $provider */ 109 | $provider = GeneralUtility::makeInstance($sitemapProvider, $request, $sitemapName, $sitemapConfig['config'] ?? []); 110 | $pages = $provider->getNumberOfPages(); 111 | for ($page = 0; $page < $pages; $page++) { 112 | $sitemaps[] = [ 113 | 'key' => $sitemapName, 114 | 'page' => $page, 115 | 'lastMod' => $provider->getLastModified(), 116 | ]; 117 | } 118 | } 119 | } 120 | $view->assign('sitemaps', $sitemaps); 121 | return $view->render('Index'); 122 | } 123 | 124 | private function renderSitemap(ServerRequestInterface $request, ViewInterface $view, array $configConfiguration, string $sitemapType, string $sitemapName): string 125 | { 126 | $sitemapConfig = $configConfiguration[$sitemapType]['sitemaps'][$sitemapName] ?? null; 127 | if ($sitemapConfig) { 128 | $sitemapProvider = $sitemapConfig['provider'] ?? null; 129 | if (is_string($sitemapProvider) 130 | && class_exists($sitemapProvider) 131 | && is_subclass_of($sitemapProvider, XmlSitemapDataProviderInterface::class) 132 | ) { 133 | /** @var XmlSitemapDataProviderInterface $provider */ 134 | $provider = GeneralUtility::makeInstance($sitemapProvider, $request, $sitemapName, $sitemapConfig['config'] ?? []); 135 | $items = $provider->getItems(); 136 | $view->assign('items', $items); 137 | $template = $sitemapConfig['config']['template'] ?? $sitemapConfig['template'] ?? 'Sitemap'; 138 | return $view->render($template); 139 | } 140 | throw new InvalidConfigurationException('No valid provider set for ' . $sitemapName, 1535578522); 141 | } 142 | throw new PropagateResponseException( 143 | $this->errorController->pageNotFoundAction( 144 | $request, 145 | 'No valid configuration found for sitemap ' . $sitemapName 146 | ), 147 | 1535578569 148 | ); 149 | } 150 | 151 | /** 152 | * @throws CanNotResolvePublicResourceException 153 | * @throws CanNotResolveSystemResourceException 154 | */ 155 | private function getXslResource(array $configConfiguration, string $sitemapType, ?string $sitemapName = null): PublicResourceInterface & SystemResourceInterface 156 | { 157 | $resourceIdentifier = $configConfiguration[$sitemapType]['sitemaps'][$sitemapName ?? '']['config']['xslFile'] 158 | ?? $configConfiguration[$sitemapType]['sitemaps']['xslFile'] 159 | ?? $configConfiguration['xslFile'] 160 | ?? 'EXT:seo/Resources/Public/CSS/Sitemap.xsl'; 161 | $xslResource = $this->resourceFactory->createPublicResource($resourceIdentifier); 162 | if (!$xslResource instanceof SystemResourceInterface) { 163 | throw new \InvalidArgumentException('Can not resolve xslFile "%s" to a system resource', 1761032332); 164 | } 165 | return $xslResource; 166 | } 167 | 168 | /** 169 | * Applies `Content-Security-Policy` mutations for `unsafe-hashes` for XSLT styles. 170 | * This is done dynamically, since XSLT styles might change some day... 171 | * 172 | * The expected hash for the default XSLT styles is `sha256-d0ax6zoVJBeBpy4l3O2FJ6Y1L4SalCWw2x62uoJH15k=`. 173 | */ 174 | private function applyDynamicContentSecurityPolicy(SystemResourceInterface $xslResource): void 175 | { 176 | try { 177 | $dom = new \DOMDocument(); 178 | $dom->loadXML($xslResource->getContents()); 179 | } catch (SystemResourceDoesNotExistException) { 180 | return; 181 | } 182 | if (!$dom instanceof \DOMDocument) { 183 | return; 184 | } 185 | $hashes = []; 186 | foreach ($dom->getElementsByTagName('style') as $node) { 187 | if (!$node instanceof \DOMElement || $node->getAttribute('type') !== 'text/css') { 188 | continue; 189 | } 190 | $hashes[] = HashValue::hash($node->textContent); 191 | } 192 | if ($hashes === []) { 193 | return; 194 | } 195 | $this->policyRegistry->appendMutationCollection( 196 | new MutationCollection( 197 | new Mutation( 198 | MutationMode::Extend, 199 | Directive::StyleSrcElem, 200 | SourceKeyword::unsafeHashes, 201 | ...$hashes 202 | ) 203 | ) 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Configuration/TCA/Overrides/pages.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'cropVariants' => [ 8 | 'default' => [ 9 | 'disabled' => true, 10 | ], 11 | 'social' => [ 12 | 'title' => 'core.wizards:imwizard.crop_variant.social', 13 | 'coverAreas' => [], 14 | 'cropArea' => [ 15 | 'x' => '0.0', 16 | 'y' => '0.0', 17 | 'width' => '1.0', 18 | 'height' => '1.0', 19 | ], 20 | 'allowedAspectRatios' => [ 21 | '1.91:1' => [ 22 | 'title' => 'core.wizards:imwizard.ratio.191_1', 23 | 'value' => 1200 / 630, 24 | ], 25 | 'NaN' => [ 26 | 'title' => 'core.wizards:imwizard.ratio.free', 27 | 'value' => 0.0, 28 | ], 29 | ], 30 | 'selectedRatio' => '1.91:1', 31 | ], 32 | ], 33 | ], 34 | ]; 35 | 36 | $tca = [ 37 | 'palettes' => [ 38 | 'seo' => [ 39 | 'label' => 'core.form.palettes:seo', 40 | 'showitem' => 'seo_title', 41 | ], 42 | 'robots' => [ 43 | 'label' => 'core.form.palettes:robots', 44 | 'showitem' => 'no_index, no_follow', 45 | ], 46 | 'canonical' => [ 47 | 'label' => 'core.form.palettes:canonical', 48 | 'showitem' => 'canonical_link', 49 | ], 50 | 'sitemap' => [ 51 | 'label' => 'core.form.palettes:sitemap', 52 | 'showitem' => 'sitemap_changefreq, sitemap_priority', 53 | ], 54 | 'opengraph' => [ 55 | 'label' => 'core.form.palettes:opengraph', 56 | 'showitem' => 'og_title, --linebreak--, og_description, --linebreak--, og_image', 57 | ], 58 | 'twittercards' => [ 59 | 'label' => 'core.form.palettes:twittercards', 60 | 'showitem' => 'twitter_title, --linebreak--, twitter_description, --linebreak--, twitter_image, --linebreak--, twitter_card', 61 | ], 62 | ], 63 | 'columns' => [ 64 | 'seo_title' => [ 65 | 'exclude' => true, 66 | 'l10n_mode' => 'prefixLangTitle', 67 | 'label' => 'seo.db:pages.seo_title', 68 | 'config' => [ 69 | 'type' => 'input', 70 | 'size' => 40, 71 | 'max' => 255, 72 | 'eval' => 'trim', 73 | ], 74 | ], 75 | 'no_index' => [ 76 | 'exclude' => true, 77 | 'l10n_mode' => 'exclude', 78 | 'onChange' => 'reload', 79 | 'label' => 'seo.db:pages.no_index_formlabel', 80 | 'config' => [ 81 | 'type' => 'check', 82 | 'renderType' => 'checkboxToggle', 83 | 'items' => [ 84 | [ 85 | 'label' => '', 86 | 'invertStateDisplay' => true, 87 | ], 88 | ], 89 | ], 90 | ], 91 | 'no_follow' => [ 92 | 'exclude' => true, 93 | 'l10n_mode' => 'exclude', 94 | 'label' => 'seo.db:pages.no_follow_formlabel', 95 | 'config' => [ 96 | 'type' => 'check', 97 | 'renderType' => 'checkboxToggle', 98 | 'items' => [ 99 | [ 100 | 'label' => '', 101 | 'invertStateDisplay' => true, 102 | ], 103 | ], 104 | ], 105 | ], 106 | 'sitemap_changefreq' => [ 107 | 'exclude' => true, 108 | 'label' => 'seo.db:pages.sitemap_changefreq', 109 | 'config' => [ 110 | 'type' => 'select', 111 | 'renderType' => 'selectSingle', 112 | 'items' => [ 113 | ['label' => 'seo.db:pages.sitemap_changefreq.none', 'value' => ''], 114 | ['label' => 'seo.db:pages.sitemap_changefreq.always', 'value' => 'always'], 115 | ['label' => 'seo.db:pages.sitemap_changefreq.hourly', 'value' => 'hourly'], 116 | ['label' => 'seo.db:pages.sitemap_changefreq.daily', 'value' => 'daily'], 117 | ['label' => 'seo.db:pages.sitemap_changefreq.weekly', 'value' => 'weekly'], 118 | ['label' => 'seo.db:pages.sitemap_changefreq.monthly', 'value' => 'monthly'], 119 | ['label' => 'seo.db:pages.sitemap_changefreq.yearly', 'value' => 'yearly'], 120 | ['label' => 'seo.db:pages.sitemap_changefreq.never', 'value' => 'never'], 121 | ], 122 | 'dbFieldLength' => 10, 123 | ], 124 | ], 125 | 'sitemap_priority' => [ 126 | 'exclude' => true, 127 | 'label' => 'seo.db:pages.sitemap_priority', 128 | 'config' => [ 129 | 'type' => 'select', 130 | 'renderType' => 'selectSingle', 131 | 'default' => '0.5', 132 | 'items' => [ 133 | ['label' => '0.0', 'value' => '0.0'], 134 | ['label' => '0.1', 'value' => '0.1'], 135 | ['label' => '0.2', 'value' => '0.2'], 136 | ['label' => '0.3', 'value' => '0.3'], 137 | ['label' => '0.4', 'value' => '0.4'], 138 | ['label' => '0.5', 'value' => '0.5'], 139 | ['label' => '0.6', 'value' => '0.6'], 140 | ['label' => '0.7', 'value' => '0.7'], 141 | ['label' => '0.8', 'value' => '0.8'], 142 | ['label' => '0.9', 'value' => '0.9'], 143 | ['label' => '1.0', 'value' => '1.0'], 144 | ], 145 | ], 146 | ], 147 | 'canonical_link' => [ 148 | 'exclude' => true, 149 | 'label' => 'seo.db:pages.canonical_link', 150 | 'description' => 'seo.db:pages.canonical_link.description', 151 | 'displayCond' => 'FIELD:no_index:=:0', 152 | 'config' => [ 153 | 'type' => 'link', 154 | 'allowedTypes' => ['page', 'url', 'record'], 155 | 'size' => 50, 156 | 'appearance' => [ 157 | 'browserTitle' => 'seo.db:pages.canonical_link', 158 | 'allowedOptions' => ['params', 'rel'], 159 | ], 160 | ], 161 | ], 162 | 'og_title' => [ 163 | 'exclude' => true, 164 | 'l10n_mode' => 'prefixLangTitle', 165 | 'label' => 'seo.db:pages.og_title', 166 | 'config' => [ 167 | 'type' => 'input', 168 | 'size' => 40, 169 | 'max' => 255, 170 | 'eval' => 'trim', 171 | ], 172 | ], 173 | 'og_description' => [ 174 | 'exclude' => true, 175 | 'l10n_mode' => 'prefixLangTitle', 176 | 'label' => 'seo.db:pages.og_description', 177 | 'config' => [ 178 | 'type' => 'text', 179 | 'cols' => 40, 180 | 'rows' => 3, 181 | ], 182 | ], 183 | 'og_image' => [ 184 | 'exclude' => true, 185 | 'label' => 'seo.db:pages.og_image', 186 | 'config' => [ 187 | 'type' => 'file', 188 | 'allowed' => 'common-image-types', 189 | 'behaviour' => [ 190 | 'allowLanguageSynchronization' => true, 191 | ], 192 | 'overrideChildTca' => [ 193 | 'columns' => [ 194 | 'crop' => $openGraphCropConfiguration, 195 | ], 196 | ], 197 | ], 198 | ], 199 | 'twitter_title' => [ 200 | 'exclude' => true, 201 | 'l10n_mode' => 'prefixLangTitle', 202 | 'label' => 'seo.db:pages.twitter_title', 203 | 'config' => [ 204 | 'type' => 'input', 205 | 'size' => 40, 206 | 'max' => 255, 207 | 'eval' => 'trim', 208 | ], 209 | ], 210 | 'twitter_description' => [ 211 | 'exclude' => true, 212 | 'l10n_mode' => 'prefixLangTitle', 213 | 'label' => 'seo.db:pages.twitter_description', 214 | 'config' => [ 215 | 'type' => 'text', 216 | 'cols' => 40, 217 | 'rows' => 3, 218 | ], 219 | ], 220 | 'twitter_image' => [ 221 | 'exclude' => true, 222 | 'label' => 'seo.db:pages.twitter_image', 223 | 'config' => [ 224 | 'type' => 'file', 225 | 'allowed' => 'common-image-types', 226 | 'behaviour' => [ 227 | 'allowLanguageSynchronization' => true, 228 | ], 229 | 'overrideChildTca' => [ 230 | 'columns' => [ 231 | 'crop' => $openGraphCropConfiguration, 232 | ], 233 | ], 234 | ], 235 | ], 236 | 'twitter_card' => [ 237 | 'exclude' => true, 238 | 'label' => 'seo.db:pages.twitter_card', 239 | 'config' => [ 240 | 'type' => 'select', 241 | 'renderType' => 'selectSingle', 242 | 'default' => '', 243 | 'items' => [ 244 | ['label' => '', 'value' => ''], 245 | ['label' => 'seo.db:pages.twitter_card.summary', 'value' => 'summary'], 246 | ['label' => 'seo.db:pages.twitter_card.summary_large_image', 'value' => 'summary_large_image'], 247 | ], 248 | 'dbFieldLength' => 255, 249 | ], 250 | ], 251 | ], 252 | ]; 253 | 254 | $GLOBALS['TCA']['pages'] = array_replace_recursive($GLOBALS['TCA']['pages'], $tca); 255 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes( 256 | 'pages', 257 | ' 258 | --div--;core.form.tabs:seo, 259 | --palette--;;seo, 260 | --palette--;;robots, 261 | --palette--;;canonical, 262 | --palette--;;sitemap, 263 | --div--;core.form.tabs:socialmedia, 264 | --palette--;;opengraph, 265 | --palette--;;twittercards', 266 | (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_DEFAULT, 267 | 'after:title' 268 | ); 269 | 270 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToPalette('pages', 'seo', '--linebreak--, description', 'after:seo_title'); 271 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------