Development Book V18: Managing Sitemaps for the Odoo Website

A sitemap is an essential tool that contributes significantly to a website’s usability and search engine visibility. It acts as a structured file that outlines important information about the website’s pages and associated resources. Search engines use this data to efficiently crawl, index, and display relevant pages in search results, enhancing user access and overall SEO performance.

This blog explores how to customize an existing sitemap in an Odoo website. The process involves using the sitemap_xml_index method, which allows developers to modify and extend the default sitemap structure according to specific business or technical requirements.

                       @http.route('/sitemap.xml', type='http', auth="public", website=True, multilang=False, sitemap=False)
def sitemap_xml_index(self, **kwargs):
   current_website = request.website
   Attachment = request.env['ir.attachment'].sudo()
   View = request.env['ir.ui.view'].sudo()
   mimetype = 'application/xml;charset=utf-8'
   content = None
   url_root = request.httprequest.url_root
   # For a same website, each domain has its own sitemap (cache)
   hashed_url_root = md5(url_root.encode()).hexdigest()[:8]
   sitemap_base_url = '/sitemap-%d-%s' % (current_website.id, hashed_url_root)


   def create_sitemap(url, content):
       return Attachment.create({
           'raw': content.encode(),
           'mimetype': mimetype,
           'type': 'binary',
           'name': url,
           'url': url,
       })
   dom = [('url', '=', '%s.xml' % sitemap_base_url), ('type', '=', 'binary')]
   sitemap = Attachment.search(dom, limit=1)
   if sitemap:
       # Check if stored version is still valid
       create_date = fields.Datetime.from_string(sitemap.create_date)
       delta = datetime.datetime.now() - create_date
       if delta < SITEMAP_CACHE_TIME:
           content = base64.b64decode(sitemap.datas)


   if not content:
       # Remove all sitemaps in ir.attachments as we're going to regenerated them
       dom = [('type', '=', 'binary'), '|', ('url', '=like', '%s-%%.xml' % sitemap_base_url),
              ('url', '=', '%s.xml' % sitemap_base_url)]
       sitemaps = Attachment.search(dom)
       sitemaps.unlink()


       pages = 0
       locs = request.website.with_user(request.website.user_id)._enumerate_pages()
       while True:
           values = {
               'locs': islice(locs, 0, LOC_PER_SITEMAP),
               'url_root': url_root[:-1],
           }
           urls = View._render_template('website.sitemap_locs', values)
           if urls.strip():
               content = View._render_template('website.sitemap_xml', {'content': urls})
               pages += 1
               last_sitemap = create_sitemap('%s-%d.xml' % (sitemap_base_url, pages), content)
           else:
               break


       if not pages:
           return request.not_found()
       elif pages == 1:
           # rename the -id-page.xml => -id.xml
           last_sitemap.write({
               'url': "%s.xml" % sitemap_base_url,
               'name': "%s.xml" % sitemap_base_url,
           })
       else:
           # TODO: in master/saas-15, move current_website_id in template directly
           pages_with_website = ["%d-%s-%d" % (current_website.id, hashed_url_root, p) for p in range(1, pages + 1)]


           # Sitemaps must be split in several smaller files with a sitemap index
           content = View._render_template('website.sitemap_index_xml', {
               'pages': pages_with_website,
               # URLs inside the sitemap index have to be on the same
               # domain as the sitemap index itself
               'url_root': url_root,
           })
           create_sitemap('%s.xml' % sitemap_base_url, content)


   return request.make_response(content, [('Content-Type', mimetype)])

When a sitemap file is generated in Odoo, it includes a list of URLs representing the pages available on the website. This is typically done using the following line of code:

locs = request.website.with_user(request.website.user_id)._enumerate_pages()

This line calls the _enumerate_pages() method on the website object, executed under the context of the website's public user (request.website.user_id). The method returns a collection of URLs (locations) that are used to build the sitemap. These URLs represent the different accessible pages on the website, which are then indexed for search engines through the sitemap.

This is a key step in dynamically creating a comprehensive and accurate sitemap for better SEO and page discoverability.

   def _enumerate_pages(self, query_string=None, force=False):
   """ Available pages in the website/CMS. This is mostly used for links
       generation and can be overridden by modules setting up new HTML
       controllers for dynamic pages (e.g. blog).
       By default, returns template views marked as pages.
       :param str query_string: a (user-provided) string, fetches pages
                                matching the string
       :returns: a list of mappings with two keys: ``name`` is the displayable
                 name of the resource (page), ``url`` is the absolute URL
                 of the same.
       :rtype: list({name: str, url: str})
   """
   # ==== WEBSITE.PAGES ====
   # '/' already has a http.route & is in the routing_map so it will already have an entry in the xml
   domain = [('url', '!=', '/')]
   if not force:
       domain += [('website_indexed', '=', True), ('visibility', '=', False)]
       # is_visible
       domain += [
           ('website_published', '=', True), ('visibility', '=', False),
           '|', ('date_publish', '=', False), ('date_publish', '<=', fields.Datetime.now())
       ]
   if query_string:
       domain += [('url', 'like', query_string)]
   pages = self._get_website_pages(domain)
for page in pages:
       record = {'loc': page['url'], 'id': page['id'], 'name': page['name']}
       if page.view_id and page.view_id.priority != 16:
           record['priority'] = min(round(page.view_id.priority / 32.0, 1), 1)
       if page['write_date']:
           record['lastmod'] = page['write_date'].date()
       yield record
   # ==== CONTROLLERS ====
   router = self.env['ir.http'].routing_map()
   url_set = set()
   sitemap_endpoint_done = set()
   for rule in router.iter_rules():
       if 'sitemap' in rule.endpoint.routing and rule.endpoint.routing['sitemap'] is not True:
           if rule.endpoint.func in sitemap_endpoint_done:
               continue
           sitemap_endpoint_done.add(rule.endpoint.func)


           func = rule.endpoint.routing['sitemap']
           if func is False:
               continue
           for loc in func(self.with_context(lang=self.default_lang_id.code).env, rule, query_string):
               yield loc
           continue
       if not self.rule_is_enumerable(rule):
           continue
       if 'sitemap' not in rule.endpoint.routing:
           logger.warning('No Sitemap value provided for controller %s (%s)' %
                          (rule.endpoint.original_endpoint, ','.join(rule.endpoint.routing['routes'])))
       converters = rule._converters or {}
       if query_string and not converters and (query_string not in rule.build({}, append_unknown=False)[1]):
           continue
       values = [{}]
       # converters with a domain are processed after the other ones
       convitems = sorted(
           converters.items(),
           key=lambda x: (hasattr(x[1], 'domain') and (x[1].domain != '[]'), rule._trace.index((True, x[0]))))
       for (i, (name, converter)) in enumerate(convitems):
           if 'website_id' in self.env[converter.model]._fields and (not converter.domain or converter.domain == '[]'):
               converter.domain = "[('website_id', 'in', (False, current_website_id))]"
           newval = []
           for val in values:
               query = i == len(convitems) - 1 and query_string
               if query:
                   r = "".join([x[1] for x in rule._trace[1:] if not x[0]])  # remove model converter from route
                   query = sitemap_qs2dom(query, r, self.env[converter.model]._rec_name)
                   if query == FALSE_DOMAIN:
                       continue
               for rec in converter.generate(self.env, args=val, dom=query):
                   newval.append(val.copy())
                   newval[-1].update({name: rec.with_context(lang=self.default_lang_id.code)})
           values = newval
       for value in values:
           domain_part, url = rule.build(value, append_unknown=False)
           pattern = query_string and '*%s*' % "*".join(query_string.split('/'))
           if not query_string or fnmatch.fnmatch(url.lower(), pattern):
               page = {'loc': url}
               if url in url_set:
                   continue
               url_set.add(url)
               yield page

This function enables smooth navigation between different pages on your website within the sitemap. It also allows for the inclusion of additional URLs, helping to ensure that your sitemap remains complete and up-to-date

Adding a Records Page to Your Sitemap

To include record pages in the sitemap in Odoo 18, you first need to import the necessary sitemap method. Here's how you can begin:

Import from odoo.addons.website.models.ir_http sitemap_qs2dom

The slug function is used to generate clean, user-friendly URLs. It works in conjunction with sitemap_qs2dom, a method commonly used to build domain filters based on routing paths and query parameters.

You can now define a new method to implement this functionality.

class Main(http.Controller):
   def sitemap_records(env, rule, qs):
       slug = request.env['ir.http']._slug
      records = env[your.model]
       dom = sitemap_qs2dom(qs, '/url', records._rec_name)
       for r in records.search(dom):
           loc = '/url/%s' % slug(r)
           if not qs or qs.lower() in loc:
               yield {'loc': loc}

The sitemap_records is a Python generator function that is triggered during sitemap generation. Inside this function, a domain is created using sitemap_qs2dom, which is then used to search for records. The slug() method is used to determine the location by generating a user-friendly URL.

Next, reference the sitemap_records function in the record detail root.

               @http.route('/url/', type='http', auth="user", website=True, sitemap=sitemap_records)
   def records_detail(self, record):

In this context, we linked the sitemap_records() function to the root using the sitemap keyword.

This feature allows record pages to be included in the sitemap.xml. If no filtering is needed and you want to include all records, you can replace the function reference with True.

The sitemap is refreshed every 12 hours. To view updates immediately, go to attachments, delete the existing sitemap.xml, and then open /sitemap.xml in your browser to see the changes.

whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message