From 4fb374103c878880bd5ae54365f36dbe2b94f7d3 Mon Sep 17 00:00:00 2001 From: Edilio Escalona Almira Date: Mon, 20 Apr 2026 10:28:52 -0500 Subject: [PATCH] [ADD] Exclude URLs from sitemap.xml based on values defined in the configuration --- website_exclude_sitemap/README.rst | 194 +++++++ website_exclude_sitemap/__init__.py | 5 + website_exclude_sitemap/__manifest__.py | 20 + website_exclude_sitemap/i18n/es.po | 80 +++ website_exclude_sitemap/i18n/es_ES.po | 80 +++ .../i18n/website_exclude_sitemap.pot | 75 +++ website_exclude_sitemap/models/__init__.py | 5 + website_exclude_sitemap/models/website.py | 98 ++++ .../models/website_page.py | 37 ++ website_exclude_sitemap/pyproject.toml | 3 + website_exclude_sitemap/readme/CONFIGURE.md | 41 ++ website_exclude_sitemap/readme/CONTEXT.md | 7 + .../readme/CONTRIBUTORS.md | 2 + website_exclude_sitemap/readme/DESCRIPTION.md | 8 + website_exclude_sitemap/readme/USAGE.md | 40 ++ .../static/description/index.html | 528 ++++++++++++++++++ .../img/readme/config_sitemap_exclusions.png | Bin 0 -> 26641 bytes .../img/readme/reload_manual_sitemap.png | Bin 0 -> 30363 bytes website_exclude_sitemap/tests/__init__.py | 4 + .../tests/test_website_exclude_sitemap.py | 221 ++++++++ website_exclude_sitemap/wizards/__init__.py | 4 + .../wizards/res_config_settings.py | 30 + .../wizards/res_config_settings_views.xml | 35 ++ 23 files changed, 1517 insertions(+) create mode 100644 website_exclude_sitemap/README.rst create mode 100644 website_exclude_sitemap/__init__.py create mode 100644 website_exclude_sitemap/__manifest__.py create mode 100644 website_exclude_sitemap/i18n/es.po create mode 100644 website_exclude_sitemap/i18n/es_ES.po create mode 100644 website_exclude_sitemap/i18n/website_exclude_sitemap.pot create mode 100644 website_exclude_sitemap/models/__init__.py create mode 100644 website_exclude_sitemap/models/website.py create mode 100644 website_exclude_sitemap/models/website_page.py create mode 100644 website_exclude_sitemap/pyproject.toml create mode 100644 website_exclude_sitemap/readme/CONFIGURE.md create mode 100644 website_exclude_sitemap/readme/CONTEXT.md create mode 100644 website_exclude_sitemap/readme/CONTRIBUTORS.md create mode 100644 website_exclude_sitemap/readme/DESCRIPTION.md create mode 100644 website_exclude_sitemap/readme/USAGE.md create mode 100644 website_exclude_sitemap/static/description/index.html create mode 100644 website_exclude_sitemap/static/img/readme/config_sitemap_exclusions.png create mode 100644 website_exclude_sitemap/static/img/readme/reload_manual_sitemap.png create mode 100644 website_exclude_sitemap/tests/__init__.py create mode 100644 website_exclude_sitemap/tests/test_website_exclude_sitemap.py create mode 100644 website_exclude_sitemap/wizards/__init__.py create mode 100644 website_exclude_sitemap/wizards/res_config_settings.py create mode 100644 website_exclude_sitemap/wizards/res_config_settings_views.xml diff --git a/website_exclude_sitemap/README.rst b/website_exclude_sitemap/README.rst new file mode 100644 index 0000000000..25fe1a2f18 --- /dev/null +++ b/website_exclude_sitemap/README.rst @@ -0,0 +1,194 @@ +======================= +Website Exclude Sitemap +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1049cece6aa3ee8137e4dadb74031cbd2c45c3ce3cc52c600095d748f0770cad + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwebsite-lightgray.png?logo=github + :target: https://github.com/OCA/website/tree/18.0/website_exclude_sitemap + :alt: OCA/website +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/website-18-0/website-18-0-website_exclude_sitemap + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/website&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module lets website administrators exclude public website URLs from +``/sitemap.xml``. + +The module only affects sitemap generation. It does not unpublish pages, +change access rights, or block direct access to the excluded URLs. + +It also clears the cached sitemap when website pages are created, +deleted, or their URL changes, so sitemap URLs are refreshed in the next +sitemap generation. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +Odoo includes public website URLs in the generated sitemap. + +Some public URLs can be useful for direct navigation but add little or +no SEO value when exposed in ``/sitemap.xml``. + +This module was created to keep the sitemap focused on strategic public +pages while leaving normal website access untouched. + +Configuration +============= + +Configure sitemap exclusions: + +1. Go to **Website > Configuration > Settings**. +2. Locate the **Sitemap Exclusions** setting. +3. Enter one or more paths or glob patterns. +4. Save the settings. + +Values can be separated by: + +- line breaks; +- commas; +- semicolons. + +For example, these values are equivalent: + +.. code:: text + + /customers/ + /livechat + /blog/*/feed + /jobs/apply/ + /profile/ + +.. code:: text + + /customers/, /livechat, /blog/*/feed, /jobs/apply/, /profile/ + +.. code:: text + + /customers/; /livechat; /blog/*/feed + /jobs/apply/, /profile/ + +Paths without ``*`` are matched by equality after normalizing the +trailing slash. For example, ``/customers/`` excludes ``/customers``, +but it does not exclude ``/customers/other``. + +Patterns with ``*`` use wildcard matching. For example, ``/blog/*/feed`` +excludes ``/blog/other/feed``, but it does not exclude +``/blog/feed/other``. + +Use the **Reload Sitemap** button in the same settings block to clear +the cached sitemap manually. + +Usage +===== + +Sitemap exclusions: + +1. Create or publish website pages as usual. + +2. Configure the paths or patterns to exclude in **Website > + Configuration > Settings > Sitemap Exclusions**. + + |Sitemap exclusions| + +3. Open ``/sitemap.xml``. + +4. Confirm that excluded URLs are absent and non-excluded public pages + are still present. + +Example: + +- configure ``/customers/``; +- publish a page at ``/customers``; +- publish another page at ``/customers/other``; +- open ``/sitemap.xml``. + +The ``/customers`` URL is excluded. The ``/customers/other`` URL remains +visible because ``/customers/`` is an exact path, not a wildcard +pattern. + +For wildcard matching: + +- configure ``/blog/*/feed``; +- publish or expose ``/blog/other/feed``; +- publish or expose ``/blog/feed/other``; +- open ``/sitemap.xml``. + +The ``/blog/other/feed`` URL is excluded. The ``/blog/feed/other`` URL +remains visible because it does not match the configured pattern. + +Ways to update the sitemap.xml: + +1. The sitemap cache is automatically cleared when website pages are + created, deleted, or their URL changes. + +2. The manual **Reload Sitemap** button can be used when a forced + sitemap cache clear is needed. + + |Reload manual sitemap| + +.. |Sitemap exclusions| image:: https://raw.githubusercontent.com/OCA/website/18.0/website_exclude_sitemap/static/img/readme/config_sitemap_exclusions.png +.. |Reload manual sitemap| image:: https://raw.githubusercontent.com/OCA/website/18.0/website_exclude_sitemap/static/img/readme/reload_manual_sitemap.png + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Binhex + +Contributors +------------ + +- `Binhex `__: + + - Edilio Escalona Almira e.escalona@binhex.cloud + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/website `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_exclude_sitemap/__init__.py b/website_exclude_sitemap/__init__.py new file mode 100644 index 0000000000..ab9413bd19 --- /dev/null +++ b/website_exclude_sitemap/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models +from . import wizards diff --git a/website_exclude_sitemap/__manifest__.py b/website_exclude_sitemap/__manifest__.py new file mode 100644 index 0000000000..3a40d8a4d8 --- /dev/null +++ b/website_exclude_sitemap/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Website Exclude Sitemap", + "summary": """ + Exclude addresses from the sitemap.xml file according to + the values defined in the configuration + """, + "category": "Website", + "version": "18.0.1.0.0", + "author": "Binhex, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/website", + "license": "AGPL-3", + "depends": [ + "website", + ], + "data": [ + "wizards/res_config_settings_views.xml", + ], +} diff --git a/website_exclude_sitemap/i18n/es.po b/website_exclude_sitemap/i18n/es.po new file mode 100644 index 0000000000..b000de0750 --- /dev/null +++ b/website_exclude_sitemap/i18n/es.po @@ -0,0 +1,80 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_exclude_sitemap +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-04-17 22:50+0000\n" +"PO-Revision-Date: 2026-04-17 22:50+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de configuración" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Exclude specific URLs or wildcard patterns from sitemap.xml." +msgstr "Excluir URL específicas o patrones con comodín de sitemap.xml." + +#. module: website_exclude_sitemap +#: model:ir.model.fields,help:website_exclude_sitemap.field_res_config_settings__sitemap_excluded_paths +#: model:ir.model.fields,help:website_exclude_sitemap.field_website__sitemap_excluded_paths +msgid "" +"One path or glob pattern per line, comma, or semicolon. Examples: " +"/contactus, /shop*, /blog/*" +msgstr "" +"Una ruta o patrón glob por línea, coma o punto y coma. Ejemplos: /contactus," +" /shop*, /blog/*" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_website_page +msgid "Page" +msgstr "Página" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Reload Sitemap" +msgstr "Recargar sitemap" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "" +"Separate paths with line breaks, commas, or semicolons.\n" +" Use * for wildcard matches." +msgstr "" +"Separar rutas con saltos de línea, comas o punto y coma.\n" +" Usar * para coincidencias con comodín." + +#. module: website_exclude_sitemap +#: model:ir.model.fields,field_description:website_exclude_sitemap.field_res_config_settings__sitemap_excluded_paths +#: model:ir.model.fields,field_description:website_exclude_sitemap.field_website__sitemap_excluded_paths +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Sitemap Exclusions" +msgstr "Exclusiones del sitemap" + +#. module: website_exclude_sitemap +#. odoo-python +#: code:addons/website_exclude_sitemap/wizards/res_config_settings.py:0 +msgid "Sitemap reloaded" +msgstr "Sitemap recargado" + +#. module: website_exclude_sitemap +#. odoo-python +#: code:addons/website_exclude_sitemap/wizards/res_config_settings.py:0 +msgid "The cached sitemap has been cleared for the current website." +msgstr "Se ha limpiado el sitemap en caché para el sitio web actual." + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_website +msgid "Website" +msgstr "Sitio web" diff --git a/website_exclude_sitemap/i18n/es_ES.po b/website_exclude_sitemap/i18n/es_ES.po new file mode 100644 index 0000000000..b000de0750 --- /dev/null +++ b/website_exclude_sitemap/i18n/es_ES.po @@ -0,0 +1,80 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_exclude_sitemap +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-04-17 22:50+0000\n" +"PO-Revision-Date: 2026-04-17 22:50+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de configuración" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Exclude specific URLs or wildcard patterns from sitemap.xml." +msgstr "Excluir URL específicas o patrones con comodín de sitemap.xml." + +#. module: website_exclude_sitemap +#: model:ir.model.fields,help:website_exclude_sitemap.field_res_config_settings__sitemap_excluded_paths +#: model:ir.model.fields,help:website_exclude_sitemap.field_website__sitemap_excluded_paths +msgid "" +"One path or glob pattern per line, comma, or semicolon. Examples: " +"/contactus, /shop*, /blog/*" +msgstr "" +"Una ruta o patrón glob por línea, coma o punto y coma. Ejemplos: /contactus," +" /shop*, /blog/*" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_website_page +msgid "Page" +msgstr "Página" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Reload Sitemap" +msgstr "Recargar sitemap" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "" +"Separate paths with line breaks, commas, or semicolons.\n" +" Use * for wildcard matches." +msgstr "" +"Separar rutas con saltos de línea, comas o punto y coma.\n" +" Usar * para coincidencias con comodín." + +#. module: website_exclude_sitemap +#: model:ir.model.fields,field_description:website_exclude_sitemap.field_res_config_settings__sitemap_excluded_paths +#: model:ir.model.fields,field_description:website_exclude_sitemap.field_website__sitemap_excluded_paths +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Sitemap Exclusions" +msgstr "Exclusiones del sitemap" + +#. module: website_exclude_sitemap +#. odoo-python +#: code:addons/website_exclude_sitemap/wizards/res_config_settings.py:0 +msgid "Sitemap reloaded" +msgstr "Sitemap recargado" + +#. module: website_exclude_sitemap +#. odoo-python +#: code:addons/website_exclude_sitemap/wizards/res_config_settings.py:0 +msgid "The cached sitemap has been cleared for the current website." +msgstr "Se ha limpiado el sitemap en caché para el sitio web actual." + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_website +msgid "Website" +msgstr "Sitio web" diff --git a/website_exclude_sitemap/i18n/website_exclude_sitemap.pot b/website_exclude_sitemap/i18n/website_exclude_sitemap.pot new file mode 100644 index 0000000000..b5a49c3d4f --- /dev/null +++ b/website_exclude_sitemap/i18n/website_exclude_sitemap.pot @@ -0,0 +1,75 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_exclude_sitemap +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-04-17 22:50+0000\n" +"PO-Revision-Date: 2026-04-17 22:50+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Exclude specific URLs or wildcard patterns from sitemap.xml." +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model.fields,help:website_exclude_sitemap.field_res_config_settings__sitemap_excluded_paths +#: model:ir.model.fields,help:website_exclude_sitemap.field_website__sitemap_excluded_paths +msgid "" +"One path or glob pattern per line, comma, or semicolon. Examples: " +"/contactus, /shop*, /blog/*" +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_website_page +msgid "Page" +msgstr "" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Reload Sitemap" +msgstr "" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "" +"Separate paths with line breaks, commas, or semicolons.\n" +" Use * for wildcard matches." +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model.fields,field_description:website_exclude_sitemap.field_res_config_settings__sitemap_excluded_paths +#: model:ir.model.fields,field_description:website_exclude_sitemap.field_website__sitemap_excluded_paths +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Sitemap Exclusions" +msgstr "" + +#. module: website_exclude_sitemap +#. odoo-python +#: code:addons/website_exclude_sitemap/wizards/res_config_settings.py:0 +msgid "Sitemap reloaded" +msgstr "" + +#. module: website_exclude_sitemap +#. odoo-python +#: code:addons/website_exclude_sitemap/wizards/res_config_settings.py:0 +msgid "The cached sitemap has been cleared for the current website." +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_website +msgid "Website" +msgstr "" diff --git a/website_exclude_sitemap/models/__init__.py b/website_exclude_sitemap/models/__init__.py new file mode 100644 index 0000000000..5898d43284 --- /dev/null +++ b/website_exclude_sitemap/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import website +from . import website_page diff --git a/website_exclude_sitemap/models/website.py b/website_exclude_sitemap/models/website.py new file mode 100644 index 0000000000..2e0342dbf9 --- /dev/null +++ b/website_exclude_sitemap/models/website.py @@ -0,0 +1,98 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import re +from urllib.parse import urlsplit + +from odoo import api, fields, models + +SITEMAP_DEFAULT_EXCLUDED_PATHS = ( + "/customers/", + "/livechat", + "/blog/*/feed", + "/jobs/apply/", + "/profile/", + "/website/info", + "/create-container-error", + "/machine-creation", +) + +SITEMAP_DEFAULT_EXCLUDED_PATHS_TEXT = "\n".join(SITEMAP_DEFAULT_EXCLUDED_PATHS) + + +class Website(models.Model): + _inherit = "website" + + sitemap_excluded_paths = fields.Text( + string="Sitemap Exclusions", + default=SITEMAP_DEFAULT_EXCLUDED_PATHS_TEXT, + help=( + "One path or glob pattern per line, comma, or semicolon. " + "Examples: /contactus, /shop*, /blog/*" + ), + ) + + @staticmethod + def _normalize_sitemap_path(value): + value = (value or "").strip() + if not value: + return "/" + if "://" in value: + parsed = urlsplit(value) + value = parsed.path or "/" + if parsed.query: + value = f"{value}?{parsed.query}" + if not value.startswith("/"): + value = f"/{value}" + return "/" if value == "/" else value.rstrip("/") + + def _get_sitemap_excluded_patterns(self): + self.ensure_one() + return [ + self._normalize_sitemap_path(pattern) + for pattern in re.split(r"[\n,;]+", self.sitemap_excluded_paths or "") + if pattern.strip() and not pattern.lstrip().startswith("#") + ] + + def _is_sitemap_path_excluded(self, path): + self.ensure_one() + normalized_path = self._normalize_sitemap_path(path) + return any( + self._matches_sitemap_pattern(normalized_path, pattern) + for pattern in self._get_sitemap_excluded_patterns() + ) + + @staticmethod + def _matches_sitemap_pattern(path, pattern): + if "*" not in pattern: + return path == pattern + regex = f"^{'.*'.join(re.escape(part) for part in pattern.split('*'))}$" + return re.match(regex, path) is not None + + def _clear_sitemap_cache(self): + IrAttachment = self.env["ir.attachment"].sudo() + for website in self: + IrAttachment.search( + [ + ("type", "=", "binary"), + ("url", "=like", f"/sitemap-{website.id}-%"), + ] + ).unlink() + + @api.model_create_multi + def create(self, vals_list): + websites = super().create(vals_list) + websites.filtered("sitemap_excluded_paths")._clear_sitemap_cache() + return websites + + def write(self, vals): + result = super().write(vals) + if "sitemap_excluded_paths" in vals: + self._clear_sitemap_cache() + return result + + def _enumerate_pages(self, query_string=None, force=False): + self.ensure_one() + for page in super()._enumerate_pages(query_string=query_string, force=force): + if not self._is_sitemap_path_excluded(page.get("loc")): + yield page diff --git a/website_exclude_sitemap/models/website_page.py b/website_exclude_sitemap/models/website_page.py new file mode 100644 index 0000000000..6c5464f25e --- /dev/null +++ b/website_exclude_sitemap/models/website_page.py @@ -0,0 +1,37 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class WebsitePage(models.Model): + _inherit = "website.page" + + _SITEMAP_CACHE_FIELDS = { + "url", + } + + def _get_sitemap_cache_websites(self): + website_id = self.website_id + if any(not page.website_id for page in self): + website_id |= self.env["website"].search([]) + return website_id + + @api.model_create_multi + def create(self, vals_list): + pages = super().create(vals_list) + pages._get_sitemap_cache_websites()._clear_sitemap_cache() + return pages + + def write(self, vals): + websites = self._get_sitemap_cache_websites() + result = super().write(vals) + if self._SITEMAP_CACHE_FIELDS.intersection(vals): + (websites | self._get_sitemap_cache_websites())._clear_sitemap_cache() + return result + + def unlink(self): + websites = self._get_sitemap_cache_websites() + result = super().unlink() + websites._clear_sitemap_cache() + return result diff --git a/website_exclude_sitemap/pyproject.toml b/website_exclude_sitemap/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/website_exclude_sitemap/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/website_exclude_sitemap/readme/CONFIGURE.md b/website_exclude_sitemap/readme/CONFIGURE.md new file mode 100644 index 0000000000..b2e033ee61 --- /dev/null +++ b/website_exclude_sitemap/readme/CONFIGURE.md @@ -0,0 +1,41 @@ +Configure sitemap exclusions: + +1. Go to **Website > Configuration > Settings**. +2. Locate the **Sitemap Exclusions** setting. +3. Enter one or more paths or glob patterns. +4. Save the settings. + +Values can be separated by: + +- line breaks; +- commas; +- semicolons. + +For example, these values are equivalent: + +```text +/customers/ +/livechat +/blog/*/feed +/jobs/apply/ +/profile/ +``` + +```text +/customers/, /livechat, /blog/*/feed, /jobs/apply/, /profile/ +``` + +```text +/customers/; /livechat; /blog/*/feed +/jobs/apply/, /profile/ +``` + +Paths without `*` are matched by equality after normalizing the trailing slash. +For example, `/customers/` excludes `/customers`, but it does not exclude +`/customers/other`. + +Patterns with `*` use wildcard matching. For example, `/blog/*/feed` excludes +`/blog/other/feed`, but it does not exclude `/blog/feed/other`. + +Use the **Reload Sitemap** button in the same settings block to clear the +cached sitemap manually. diff --git a/website_exclude_sitemap/readme/CONTEXT.md b/website_exclude_sitemap/readme/CONTEXT.md new file mode 100644 index 0000000000..c8bddc83cd --- /dev/null +++ b/website_exclude_sitemap/readme/CONTEXT.md @@ -0,0 +1,7 @@ +Odoo includes public website URLs in the generated sitemap. + +Some public URLs can be useful for direct navigation but add little or no SEO value when exposed +in `/sitemap.xml`. + +This module was created to keep the sitemap focused on strategic public pages +while leaving normal website access untouched. diff --git a/website_exclude_sitemap/readme/CONTRIBUTORS.md b/website_exclude_sitemap/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..4c895949c9 --- /dev/null +++ b/website_exclude_sitemap/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Binhex](https://www.binhex.cloud): + - Edilio Escalona Almira diff --git a/website_exclude_sitemap/readme/DESCRIPTION.md b/website_exclude_sitemap/readme/DESCRIPTION.md new file mode 100644 index 0000000000..9efe28a477 --- /dev/null +++ b/website_exclude_sitemap/readme/DESCRIPTION.md @@ -0,0 +1,8 @@ +This module lets website administrators exclude public website URLs from +`/sitemap.xml`. + +The module only affects sitemap generation. It does not unpublish pages, change +access rights, or block direct access to the excluded URLs. + +It also clears the cached sitemap when website pages are created, deleted, or +their URL changes, so sitemap URLs are refreshed in the next sitemap generation. diff --git a/website_exclude_sitemap/readme/USAGE.md b/website_exclude_sitemap/readme/USAGE.md new file mode 100644 index 0000000000..ba9cf406c6 --- /dev/null +++ b/website_exclude_sitemap/readme/USAGE.md @@ -0,0 +1,40 @@ +Sitemap exclusions: + +1. Create or publish website pages as usual. +2. Configure the paths or patterns to exclude in **Website > Configuration > + Settings > Sitemap Exclusions**. + + ![Sitemap exclusions](../static/img/readme/config_sitemap_exclusions.png) + +3. Open `/sitemap.xml`. +4. Confirm that excluded URLs are absent and non-excluded public pages are still + present. + +Example: + +- configure `/customers/`; +- publish a page at `/customers`; +- publish another page at `/customers/other`; +- open `/sitemap.xml`. + +The `/customers` URL is excluded. The `/customers/other` URL remains visible +because `/customers/` is an exact path, not a wildcard pattern. + +For wildcard matching: + +- configure `/blog/*/feed`; +- publish or expose `/blog/other/feed`; +- publish or expose `/blog/feed/other`; +- open `/sitemap.xml`. + +The `/blog/other/feed` URL is excluded. The `/blog/feed/other` URL remains +visible because it does not match the configured pattern. + +Ways to update the sitemap.xml: +1. The sitemap cache is automatically cleared when website pages are created, + deleted, or their URL changes. + +2. The manual **Reload Sitemap** button can be used when a forced sitemap cache clear is + needed. + + ![Reload manual sitemap](../static/img/readme/reload_manual_sitemap.png) diff --git a/website_exclude_sitemap/static/description/index.html b/website_exclude_sitemap/static/description/index.html new file mode 100644 index 0000000000..7187acd0ac --- /dev/null +++ b/website_exclude_sitemap/static/description/index.html @@ -0,0 +1,528 @@ + + + + + +Website Exclude Sitemap + + + +
+

Website Exclude Sitemap

+ + +

Beta License: AGPL-3 OCA/website Translate me on Weblate Try me on Runboat

+

This module lets website administrators exclude public website URLs from +/sitemap.xml.

+

The module only affects sitemap generation. It does not unpublish pages, +change access rights, or block direct access to the excluded URLs.

+

It also clears the cached sitemap when website pages are created, +deleted, or their URL changes, so sitemap URLs are refreshed in the next +sitemap generation.

+

Table of contents

+ +
+

Use Cases / Context

+

Odoo includes public website URLs in the generated sitemap.

+

Some public URLs can be useful for direct navigation but add little or +no SEO value when exposed in /sitemap.xml.

+

This module was created to keep the sitemap focused on strategic public +pages while leaving normal website access untouched.

+
+
+

Configuration

+

Configure sitemap exclusions:

+
    +
  1. Go to Website > Configuration > Settings.
  2. +
  3. Locate the Sitemap Exclusions setting.
  4. +
  5. Enter one or more paths or glob patterns.
  6. +
  7. Save the settings.
  8. +
+

Values can be separated by:

+
    +
  • line breaks;
  • +
  • commas;
  • +
  • semicolons.
  • +
+

For example, these values are equivalent:

+
+/customers/
+/livechat
+/blog/*/feed
+/jobs/apply/
+/profile/
+
+
+/customers/, /livechat, /blog/*/feed, /jobs/apply/, /profile/
+
+
+/customers/; /livechat; /blog/*/feed
+/jobs/apply/, /profile/
+
+

Paths without * are matched by equality after normalizing the +trailing slash. For example, /customers/ excludes /customers, +but it does not exclude /customers/other.

+

Patterns with * use wildcard matching. For example, /blog/*/feed +excludes /blog/other/feed, but it does not exclude +/blog/feed/other.

+

Use the Reload Sitemap button in the same settings block to clear +the cached sitemap manually.

+
+
+

Usage

+

Sitemap exclusions:

+
    +
  1. Create or publish website pages as usual.

    +
  2. +
  3. Configure the paths or patterns to exclude in Website > +Configuration > Settings > Sitemap Exclusions.

    +

    Sitemap exclusions

    +
  4. +
  5. Open /sitemap.xml.

    +
  6. +
  7. Confirm that excluded URLs are absent and non-excluded public pages +are still present.

    +
  8. +
+

Example:

+
    +
  • configure /customers/;
  • +
  • publish a page at /customers;
  • +
  • publish another page at /customers/other;
  • +
  • open /sitemap.xml.
  • +
+

The /customers URL is excluded. The /customers/other URL remains +visible because /customers/ is an exact path, not a wildcard +pattern.

+

For wildcard matching:

+
    +
  • configure /blog/*/feed;
  • +
  • publish or expose /blog/other/feed;
  • +
  • publish or expose /blog/feed/other;
  • +
  • open /sitemap.xml.
  • +
+

The /blog/other/feed URL is excluded. The /blog/feed/other URL +remains visible because it does not match the configured pattern.

+

Ways to update the sitemap.xml:

+
    +
  1. The sitemap cache is automatically cleared when website pages are +created, deleted, or their URL changes.

    +
  2. +
  3. The manual Reload Sitemap button can be used when a forced +sitemap cache clear is needed.

    +

    Reload manual sitemap

    +
  4. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Binhex
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/website project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/website_exclude_sitemap/static/img/readme/config_sitemap_exclusions.png b/website_exclude_sitemap/static/img/readme/config_sitemap_exclusions.png new file mode 100644 index 0000000000000000000000000000000000000000..422fae99c795cb918a0e5b583ad86767e7265da9 GIT binary patch literal 26641 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 I?CXI40SDqO0{{R3 literal 0 HcmV?d00001 diff --git a/website_exclude_sitemap/static/img/readme/reload_manual_sitemap.png b/website_exclude_sitemap/static/img/readme/reload_manual_sitemap.png new file mode 100644 index 0000000000000000000000000000000000000000..632790b98b69cc799b504ade2fdbb44cc5732e84 GIT binary patch literal 30363 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 +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_website_exclude_sitemap diff --git a/website_exclude_sitemap/tests/test_website_exclude_sitemap.py b/website_exclude_sitemap/tests/test_website_exclude_sitemap.py new file mode 100644 index 0000000000..5a7a277144 --- /dev/null +++ b/website_exclude_sitemap/tests/test_website_exclude_sitemap.py @@ -0,0 +1,221 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo.addons.base.tests.common import BaseCommon + +from ..models.website import SITEMAP_DEFAULT_EXCLUDED_PATHS_TEXT + + +class TestWebsiteExcludeSitemap(BaseCommon): + def setUp(self): + super().setUp() + self.website = self.env["website"].create({"name": "Sitemap Test Website"}) + + def _create_cached_sitemap(self): + sitemap_xml = f"/sitemap-{self.website.id}-test.xml" + return self.env["ir.attachment"].create( + { + "name": sitemap_xml, + "type": "binary", + "url": sitemap_xml, + "raw": b"", + "mimetype": "application/xml;charset=utf-8", + } + ) + + def _create_website_page(self, url="/test-sitemap-page"): + key_suffix = (url.strip("/") or "home").replace("/", "_").replace("-", "_") + key = f"website_exclude_sitemap.test_sitemap_page_{key_suffix}" + view = self.env["ir.ui.view"].create( + { + "name": f"Test Sitemap Page {url}", + "type": "qweb", + "key": key, + "arch": ( + "
" + "" + ), + "website_id": self.website.id, + } + ) + return self.env["website.page"].create( + { + "url": url, + "view_id": view.id, + "website_published": True, + "website_indexed": True, + } + ) + + def test_config_settings_updates_website_exclusions(self): + settings = self.env["res.config.settings"].create( + { + "website_id": self.website.id, + "sitemap_excluded_paths": "/contactus\n/shop*", + } + ) + + self.assertEqual( + self.website.sitemap_excluded_paths, settings.sitemap_excluded_paths + ) + self.assertTrue(self.website._is_sitemap_path_excluded("/contactus")) + self.assertTrue(self.website._is_sitemap_path_excluded("/shop/category")) + self.assertFalse(self.website._is_sitemap_path_excluded("/blog")) + + def test_default_exclusions_on_new_website(self): + website = self.env["website"].create({"name": "Default Sitemap Exclusions"}) + + self.assertEqual( + website.sitemap_excluded_paths, SITEMAP_DEFAULT_EXCLUDED_PATHS_TEXT + ) + + def test_normalize_sitemap_path(self): + normalize = self.website._normalize_sitemap_path + + self.assertEqual(normalize(None), "/") + self.assertEqual(normalize(" "), "/") + self.assertEqual(normalize("/"), "/") + self.assertEqual(normalize("customers/"), "/customers") + self.assertEqual( + normalize("https://www.example.com/blog/example?feed=1"), + "/blog/example?feed=1", + ) + self.assertEqual(normalize("https://www.example.com"), "/") + + def test_patterns_without_wildcards_match_exactly(self): + self.website.sitemap_excluded_paths = "/customers/\n/jobs/apply/\n/profile/" + + self.assertTrue(self.website._is_sitemap_path_excluded("/customers")) + self.assertTrue(self.website._is_sitemap_path_excluded("/customers/")) + self.assertTrue(self.website._is_sitemap_path_excluded("/jobs/apply")) + self.assertTrue(self.website._is_sitemap_path_excluded("/jobs/apply/")) + self.assertTrue(self.website._is_sitemap_path_excluded("/profile")) + self.assertTrue(self.website._is_sitemap_path_excluded("/profile/")) + self.assertFalse(self.website._is_sitemap_path_excluded("/customers/acme")) + self.assertFalse(self.website._is_sitemap_path_excluded("/customers-other")) + self.assertFalse(self.website._is_sitemap_path_excluded("/jobs/apply/engineer")) + self.assertFalse(self.website._is_sitemap_path_excluded("/jobs/application")) + self.assertFalse(self.website._is_sitemap_path_excluded("/profile/demo")) + + def test_patterns_can_be_separated_by_commas_semicolons_or_newlines(self): + self.website.sitemap_excluded_paths = ( + "/customers/, /livechat; /blog/*/feed\n/jobs/apply/; /profile/" + ) + + self.assertEqual( + self.website._get_sitemap_excluded_patterns(), + [ + "/customers", + "/livechat", + "/blog/*/feed", + "/jobs/apply", + "/profile", + ], + ) + self.assertTrue(self.website._is_sitemap_path_excluded("/customers")) + self.assertFalse(self.website._is_sitemap_path_excluded("/customers/other")) + self.assertTrue(self.website._is_sitemap_path_excluded("/livechat")) + self.assertTrue(self.website._is_sitemap_path_excluded("/blog/other/feed")) + self.assertFalse(self.website._is_sitemap_path_excluded("/blog/feed/other")) + self.assertTrue(self.website._is_sitemap_path_excluded("/jobs/apply")) + self.assertTrue(self.website._is_sitemap_path_excluded("/profile")) + + def test_only_star_is_treated_as_wildcard(self): + self.website.sitemap_excluded_paths = ( + "/docs?/page\n/category/[abc]\n/files/*/download" + ) + + self.assertTrue(self.website._is_sitemap_path_excluded("/docs?/page")) + self.assertFalse(self.website._is_sitemap_path_excluded("/docsx/page")) + self.assertTrue(self.website._is_sitemap_path_excluded("/category/[abc]")) + self.assertFalse(self.website._is_sitemap_path_excluded("/category/a")) + self.assertTrue(self.website._is_sitemap_path_excluded("/files/a/download")) + self.assertFalse( + self.website._is_sitemap_path_excluded("/files/a/download/extra") + ) + + def test_requirement_default_patterns(self): + self.website.sitemap_excluded_paths = SITEMAP_DEFAULT_EXCLUDED_PATHS_TEXT + + self.assertTrue(self.website._is_sitemap_path_excluded("/customers")) + self.assertFalse(self.website._is_sitemap_path_excluded("/customers/acme")) + self.assertTrue(self.website._is_sitemap_path_excluded("/livechat")) + self.assertTrue(self.website._is_sitemap_path_excluded("/blog/example/feed")) + self.assertFalse(self.website._is_sitemap_path_excluded("/blog/feed/example")) + self.assertTrue(self.website._is_sitemap_path_excluded("/jobs/apply")) + self.assertFalse(self.website._is_sitemap_path_excluded("/jobs/apply/engineer")) + self.assertTrue(self.website._is_sitemap_path_excluded("/profile")) + self.assertFalse(self.website._is_sitemap_path_excluded("/profile/demo")) + self.assertTrue(self.website._is_sitemap_path_excluded("/website/info")) + self.assertTrue( + self.website._is_sitemap_path_excluded("/create-container-error") + ) + self.assertTrue(self.website._is_sitemap_path_excluded("/machine-creation")) + self.assertFalse(self.website._is_sitemap_path_excluded("/blog/example-post")) + self.assertFalse(self.website._is_sitemap_path_excluded("/product/example")) + + def test_write_clears_cached_sitemaps(self): + attachment = self._create_cached_sitemap() + + self.website.write({"sitemap_excluded_paths": "/contactus"}) + + self.assertFalse(attachment.exists()) + + def test_reload_sitemap_button_clears_cached_sitemaps(self): + attachment = self._create_cached_sitemap() + settings = self.env["res.config.settings"].create( + { + "website_id": self.website.id, + } + ) + + result = settings.action_reload_sitemap() + + self.assertFalse(attachment.exists()) + self.assertEqual(result["tag"], "display_notification") + + def test_website_page_create_clears_cached_sitemaps(self): + attachment = self._create_cached_sitemap() + + self._create_website_page() + + self.assertFalse(attachment.exists()) + + def test_website_page_write_clears_cached_sitemaps(self): + page = self._create_website_page() + attachment = self._create_cached_sitemap() + + page.write({"url": "/renamed-sitemap-page"}) + + self.assertFalse(attachment.exists()) + + def test_website_page_write_only_clears_cached_sitemaps_on_url_change(self): + page = self._create_website_page() + attachment = self._create_cached_sitemap() + + page.write({"website_indexed": False}) + + self.assertTrue(attachment.exists()) + + def test_website_page_unlink_clears_cached_sitemaps(self): + page = self._create_website_page() + attachment = self._create_cached_sitemap() + + page.unlink() + + self.assertFalse(attachment.exists()) + + def test_enumerate_pages_skips_excluded_urls(self): + self._create_website_page("/qa-excluded") + self._create_website_page("/qa-visible") + self.website.sitemap_excluded_paths = "/qa-excluded" + + pages = self.website.with_context(website_id=self.website.id)._enumerate_pages( + force=True + ) + locs = {page["loc"] for page in pages} + + self.assertNotIn("/qa-excluded", locs) + self.assertIn("/qa-visible", locs) diff --git a/website_exclude_sitemap/wizards/__init__.py b/website_exclude_sitemap/wizards/__init__.py new file mode 100644 index 0000000000..c54f5395ec --- /dev/null +++ b/website_exclude_sitemap/wizards/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import res_config_settings diff --git a/website_exclude_sitemap/wizards/res_config_settings.py b/website_exclude_sitemap/wizards/res_config_settings.py new file mode 100644 index 0000000000..d8d89c71c7 --- /dev/null +++ b/website_exclude_sitemap/wizards/res_config_settings.py @@ -0,0 +1,30 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import _, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + sitemap_excluded_paths = fields.Text( + related="website_id.sitemap_excluded_paths", + readonly=False, + ) + + def action_reload_sitemap(self): + self.ensure_one() + website = self.website_id or self.env["website"].get_current_website() + website._clear_sitemap_cache() + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Sitemap reloaded"), + "message": _( + "The cached sitemap has been cleared for the current website." + ), + "type": "success", + "sticky": False, + }, + } diff --git a/website_exclude_sitemap/wizards/res_config_settings_views.xml b/website_exclude_sitemap/wizards/res_config_settings_views.xml new file mode 100644 index 0000000000..8c316d1fc8 --- /dev/null +++ b/website_exclude_sitemap/wizards/res_config_settings_views.xml @@ -0,0 +1,35 @@ + + + + website.exclude.sitemap.res.config.settings + res.config.settings + + + + +
+
+ Separate paths with line breaks, commas, or semicolons. + Use * for wildcard matches. +
+
+ +
+
+
+
+
+
+
+
+