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.
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"]
SitemapRefresherloads the settings and the Sylius channel, validates both and builds aChannelContext(hostname, default locale, locales, menu taxon, settings). All values are copied up front because the readers clear the entity manager between batches.- The per-channel lock is requested without blocking. If it is taken, the run ends immediately with a message.
SitemapGeneratorfirst validates the section names of all providers, before anything is written.- The providers run in the order of their tag priority. Every URL is checked for duplicates through a 60-bit hash of its address.
- 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. - Without any URL,
sitemap-empty-1.xmlis created. - The index is written last, then stale files matching the pattern
sitemap-<section>-<n>.xmlare deleted. Foreign files in the directory are left alone. - Every exception other than
SitemapGenerationExceptionis 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.