Markus Michalski FAQ

XML Sitemap: Technical Details

Technical details of the XML sitemap for Sylius 2: generation flow, plugin structure, storage layout with lock file, XML structure, database table, admin integration and readers.

Deutsche Version

Back to overview

XML Sitemap: Technical Details

This documentation always describes the latest published version.

Generation flow

graph TD
    A["Command or admin button"] --> B["SitemapRefresher"]
    B --> C{"Settings, channel and host OK?"}
    C -->|No| D["SitemapGenerationException"]
    C -->|Yes| E{"Channel lock free?"}
    E -->|No| D
    E -->|Yes| F["SitemapGenerator"]
    F --> G["Providers: products, categories, pages, custom"]
    G --> H["Split files by URL count and size"]
    H --> I["Write index last"]
    I --> J["Delete stale files"]
    J --> K["Record last run"]
  1. SitemapRefresher loads the settings and the Sylius channel, validates both and builds a ChannelContext (hostname, default locale, locales, menu taxon, settings). All values are copied up front because the readers clear the entity manager between batches.
  2. The per-channel lock is requested without blocking. If it is taken, the run ends immediately with a message.
  3. SitemapGenerator first validates the section names of all providers, before anything is written.
  4. The providers run in the order of their tag priority. Every URL is checked for duplicates through a 60-bit hash of its address.
  5. Each section is streamed into a temporary file. A new file starts before the next URL would exceed one of the two limits (max_urls_per_file, max_bytes_per_file). A single URL larger than the whole budget gets a file of its own.
  6. Without any URL, sitemap-empty-1.xml is created.
  7. The index is written last, then stale files matching the pattern sitemap-<section>-<n>.xml are deleted. Foreign files in the directory are left alone.
  8. Every exception other than SitemapGenerationException is converted into one, so a failure only affects the channel concerned. After the run, time and URL count are saved on the setting.

Plugin structure

src/
  Catalog/       Readers for products and categories (Doctrine, behind interfaces) and their records
  Command/       mmd:sitemap:generate, mmd:sitemap:setup
  Controller/    Serving the sitemap, admin action Generate now
  DependencyInjection/  Configuration and extension
  Entity/        ChannelSettings
  Exception/     SitemapGenerationException
  Form/Type/     Form of the channel settings
  Generator/     Generator, refresher, channel context
  Menu/          Entry in the admin sidebar
  Migrations/    Table of the channel settings
  Provider/      URL providers and UrlProviderInterface
  Settings/      Repository of the channel settings
  Sitemap/       Value objects and XML writer
  Storage/       File storage and lock
  Twig/          Function sitemap_plugin_version()
  Url/           URL building through the router, hreflang, image URLs
config/          services.php, routes, grid, Twig hooks
templates/       Title block and button of the admin page
translations/    German and English

Storage layout

<storage_dir>/
  <channel code>.lock                    lock file
  <channel code>/
    index.xml                            sitemap index, served as /sitemap-index.xml and /sitemap.xml
    sitemap-products-1.xml
    sitemap-categories-1.xml
    sitemap-pages-1.xml

Files are written to a temporary file in the target directory that is then renamed into place. File names must match [A-Za-z0-9_-]+.xml because the name comes from the URL and must not escape the directory. The lock is a flock() per channel that the operating system releases itself when the process ends.

XML structure

Every file is a urlset with the namespaces for sitemap, XHTML and Google images:

<url>
  <loc>https://shop.example.com/en/products/example</loc>
  <lastmod>2026-10-01T10:00:00+00:00</lastmod>
  <changefreq>weekly</changefreq>
  <priority>0.8</priority>
  <xhtml:link rel="alternate" hreflang="en" href="https://shop.example.com/en/products/example"/>
  <xhtml:link rel="alternate" hreflang="x-default" href="https://shop.example.com/en/products/example"/>
  <image:image>
    <image:loc>https://shop.example.com/media/image/ab/cd/picture.jpg</image:loc>
    <image:title>Example product</image:title>
  </image:image>
</url>

The index is a sitemapindex with loc and lastmod per file. The URL fragments are not indented because whitespace only costs file size.

Database

One table, mmd_sylius_sitemap_channel_settings, with at most one row per channel. The channel is matched by its code, not through a foreign key, so no Sylius core table has to be mapped.

Column Type Default
id int, primary key
channel_code varchar(255), unique
enabled bool false
include_products, include_taxons, include_static_pages, include_images, include_hreflang bool true
product_change_frequency, taxon_change_frequency varchar(16) weekly
static_change_frequency varchar(16) monthly
product_priority float 0.8
taxon_priority float 0.6
static_priority float 0.5
last_generated_at datetime, nullable
last_url_count int, nullable

Admin integration

The bundle wires the grid and the Twig hooks (title block with the plugin version, "Generate now" button) in by prepending configuration, because a plugin's config/config.yaml is not loaded in a real application. The admin routes use the parameter %sylius_admin.path_name% instead of a fixed admin segment, so they stay inside the firewall and access rule if the admin path is changed. The menu entry hangs in the shared group "MMKreativ Plugins".

Readers

The product reader first fetches a page of 500 IDs (key comparison instead of OFFSET), then the entities with translations and, if images are wanted, images, and clears the entity manager afterwards. The category reader reads all taxons from level 1 up in one query in nested-set order and skips, in PHP, the subtree of every disabled taxon.