m-adamski/symfony-sitemap-bundle
Symfony bundle for generating XML sitemaps in your application. Integrates sitemap creation with Symfony's configuration and services, helping you expose URLs to search engines with a framework-native setup.
The first thirty minutes should be spent reading source, not docs — this package has effectively no documentation beyond a one-line changelog, a 0.3 score, and a last release of June 2021.
composer require m-adamski/symfony-sitemap-bundle, open the package's composer.json and confirm the declared php and symfony/* ranges against your runtime. A 2021-era bundle may not declare Symfony 6/7 support even if it happens to run. This is a five-minute check that decides whether the rest is worth doing.config/bundles.php (or AppKernel on older Symfony), then drop into the source and read four files in this order:
src/DependencyInjection/Configuration.php — the authoritative list of config keys (base URL, providers, caching, output settings).src/DependencyInjection/*Extension.php — which services are registered and which service tags wire providers into the generator.src/Controller/ — how sitemap.xml is actually served and with what Content-Type.src/Provider/ (or equivalent) — the interface your own providers must implement.GET /sitemap.xml returns 200 with Content-Type: application/xml and parses cleanly:curl -sI https://app.test/sitemap.xml
curl -s https://app.test/sitemap.xml | xmllint --noout -
Get that green before touching dynamic content. The bundle is thin; the failure surface is almost entirely "your provider or your config," so you want a known-good baseline first.
Framework note: if you are coming from Laravel, the mental model maps cleanly — bundle config extension ≈ a package config file, tagged services ≈ tagged bindings resolved by an interface, the controller ≈ a published route. The one Symfony-specific thing to learn is the tag name that registers a provider; that tag is the extension point for everything else.
One provider per content type. Do not build a single god-provider. Split into StaticPagesProvider, BlogPostsProvider, ProductsProvider, each implementing the bundle's provider interface and tagged in config/services.yaml:
App\Sitemap\BlogPostsProvider:
tags: ['<tag discovered in the bundle extension>']
This keeps each provider independently testable and lets you disable one (say, products during a migration) without touching the others.
Generate URLs through the router, never by string concatenation. Inside a provider, inject Symfony\Component\Routing\Generator\UrlGeneratorInterface and call generate($routeName, $params, UrlGeneratorInterface::ABSOLUTE_URL). This inherits your locale prefixes, host, scheme, and route-parameter encoding for free. Hand-built URLs are the source of 90% of malformed-sitemap bugs — wrong host, missing slash, & unescaped.
Keep providers read-only and bounded. A provider runs per sitemap request; a naive findAll() will OOM on any real dataset. Use repository methods with a cursor/iterate() (batch over IDs), and never trigger lazy-loaded associations per row — that is an N+1 hidden inside XML output. If the dataset can exceed 50,000 URLs, you must plan for a sitemap index (see Gotchas).
Cache the rendered output, not the provider calls. The bundle is described as cache-friendly; make it so at your layer. Wrap the generator in your own SitemapRenderer service that stores the final XML string in a Symfony cache pool under a content-version key:
$xml = $this->cache->get('sitemap_v' . $this->contentVersion, function (ItemInterface $item) {
$item->expiresAfter(3600);
return $this->inner->render();
});
Bump contentVersion on deploy or on a content-change event. This makes the sitemap a static file for 99.9% of requests and turns cache invalidation into a single knob you control.
Pre-generate on a schedule. For large or slow sitemaps, don't generate on request at all: add a console command (app:sitemap:warm) that renders and writes to cache or to public/sitemap.xml, and run it from cron or after deploy. The controller then just returns the cached string. This also gives you a place to log URL counts and diff them across deploys.
Test it as a functional endpoint. One WebTestCase asserting: status 200, Content-Type contains xml, document parses, expected canonical URLs are present, and no duplicate <loc> values. That last assertion is what catches providers accidentally registering overlapping route sets.
composer.json ("1.0.1", not "^1.0"), read the source diff of any future release, and evaluate during a spike whether vendoring the ~few-hundred lines in-house is cheaper than depending on it. For a bundle this small, forking is a realistic exit.http://internal-host/... unless you configure framework.router.request_context (or trusted proxies) with the public scheme and host. Sitemap URLs must be absolute and public-facing. This is the single most common production-only failure: fine locally, wrong host in staging.DOMDocument/XMLWriter you're fine; if any provider is expected to return raw markup, a URL containing & (query strings, filters) will produce invalid XML that browsers silently reject. Keep URLs parameter-free — a sitemap should never contain session IDs or tracking params.<sitemapindex>. Check whether the bundle supports an index out of the box; if not, generate the child maps with it and hand-write the index (it's ten lines of XML). Don't discover this at scale.<lastmod> must be W3C datetime, not a timestamp or Y-m-d. If your provider passes DateTime, confirm the bundle formats it; otherwise format explicitly with ->format(DATE_W3C). Incorrect lastmod is ignored at best, distrusted at worst.bin/console cache:clear and hit the endpoint with a cache-busting header before you debug any code. Document this in your runbook.curl and a browser is slow and hides which provider is at fault.vendor/ is not a strategy, it's a scheduled outage.hreflang alternates. The changelog's mention of "alternates" concerns generation, not multilingual SEO headers. If you need multilingual /en/, /fr/ cross-linking, verify in the XML output rather than assuming, and add <xhtml:link> yourself if absent.How can I help you explore Laravel packages today?