evheniy/sitemap-xml-bundle
SitemapXmlBundle is a Symfony2 bundle that generates sitemap.xml files for your site. Install it via Composer, register it in AppKernel before your main bundle, then extend SiteMapDumpCommand and implement setEntities() to define your domains and page URLs.
First, confirm you can actually run it. This is a Symfony2 bundle, not a Laravel package — there is no service provider, no config file to publish, and no facade. If your app is Laravel, you cannot composer require your way to a working sitemap; the realistic path is to read the entity classes (they are thin fluent builders) and port the XML-serialisation shape into an Artisan command. Everything below assumes either a Symfony 2.x app, or a Laravel developer mining the package for its output contract.
Minimal steps (Symfony2):
composer require evheniy/sitemap-xml-bundle "1.*" — pin the major, because the last tagged release is from October 2015 and nothing upstream will ever fix a regression for you.AppKernel::registerBundles(), before your own bundle (new Evheniy\SitemapXmlBundle\SitemapXmlBundle() above new AppBundle\AppBundle()). The README calls this out explicitly; the ordering is what lets your subclass of the dump command win over the bundle's.AppBundle\Command\SiteMapDumpCommand extending Evheniy\SitemapXmlBundle\Command\SiteMapDumpCommand, and override setEntities().$this->dumpEntity->setDomain('site.com')) and attach locations to a site map entity.app/console sitemap:dump.Where to look first, in order:
Resources/docs/service_manager.md — the $this->serviceManager->create*Entity() factory is the only construction path you should use; skipping it means you bypass whatever wiring the bundle does.Resources/docs/dump_manager.md and dump_entity.md — these tell you where the file lands and how the domain is applied. The README never states the output path, so read this before wiring a web route or a cron.sitemap_entity.md / location_entity.md — the core addLocation(...setLocation(...)->setLastmod(...)) shape you will use 95% of the time.image_entity.md, video_entity.md, news_entity.md — only if you have those media types; each carries required fields Google validates.First use case — make it the boring one. Pull a list of canonical URLs and updated_at timestamps out of your ORM, loop, and emit a flat <urlset>. Do not start with a sitemap index, hreflang, or news; get one valid file validating in Google Search Console first.
protected function setEntities()
{
$this->siteMapEntity = $this->serviceManager->createSiteMapEntity();
$this->dumpEntity->setDomain('example.com');
foreach ($this->pageRepository->findIndexable() as $page) {
$this->siteMapEntity->addLocation(
$this->serviceManager->createLocationEntity()
->setLocation($page->getCanonicalUrl())
->setLastmod($page->getUpdatedAt())
);
}
}
Command-as-adapter, entities-as-DTOs. The pattern the package pushes you toward is: your setEntities() override is the only application-specific code, and everything it does is compose entities from the service manager. Keep repository queries, filtering, and business rules in your own services and inject them into the command; setEntities() should read like a translation layer, not like a data-access layer.
Fluent composition beats intermediate variables. The documented style is a single nested expression — index → site map(s) → location(s) → image(s). It is readable for small trees but becomes unmanageable past a dozen entries; when a location needs several child entities, assign the location to a variable, add the children to it, then addLocation(). Same output, far easier to debug.
Sitemap index as the scaling switch. The README's threshold is 50,000 links. The structural difference is small: swap createSiteMapEntity() for createSiteMapIndexEntity() at the root and addSiteMap() each site map instead of addLocation() each page. Because both root entities share the same fluent shape, build a helper that chunks your URL list into N-sized site maps and feed the chunker either mode — that keeps the 50k cutover a one-line change rather than a rewrite.
Regenerate on write, not on read. Nothing in this package invalidates anything. The durable pattern is a deploy hook or a queued job on content publish that runs sitemap:dump, with the web route serving the pre-built file. Per-request generation from a controller is a trap: you pay full enumeration cost on every crawl and inherit the 50k ceiling at runtime.
Segment deliberately. Multiple site maps (products, blog, static pages) mapped one-to-one onto your content types makes partial regeneration cheap — republish a blog post, rebuild only the blog site map — and makes it obvious which content class is missing from the index when crawl coverage drops.
Laravel adaptation. Port the shape, not the code: a SitemapService that streams <urlset> with XMLWriter (cheap on memory for large sets), an Artisan command wrapping it, and a route returning the file with Content-Type: application/xml. You already have the URL source in Eloquent, which is the part the bundle was abstracting away.
SitemapXmlBundle before your main bundle. Get it wrong and your SiteMapDumpCommand subclass is silently shadowed by the base command — you will see the base behaviour with no error, which is a miserable hour to debug.setDomain() goes on $this->dumpEntity, not the site map entity. It is easy to misplace because the site map entity is what you are actively building. Read the README example literally; the domain lives on the dump entity.config.yml section to tune. Everything is driven from setEntities() and the DumpManager. If you need a different output path or filename, look at dump_manager.md — and expect to subclass rather than configure.setEntities() so the failure is loud at build time, not quiet at crawl time.<urlset>, not just that one row.setLastmod(new \DateTime($page['date'])) is the documented form — pass a DateTimeInterface produced from real content timestamps, not new \DateTime() per row. A sitemap where every lastmod equals build time trains crawlers to distrust your dates, which is worse than omitting the field.repositories entry if you do.setEntities() body as the thing worth keeping — it is portable — and the bundle wiring as disposable.How can I help you explore Laravel packages today?