Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Sitemap Laravel Package

roumen/sitemap

Roumen/sitemap is a Laravel package for generating XML sitemaps with support for tags, caching, and multiple sitemap files. It integrates easily with Laravel apps to help search engines discover and index your content.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer:

composer require roumen/sitemap

On Laravel 5.5+ the service provider is auto-discovered. On older versions, register Roumen\Sitemap\SitemapServiceProvider in config/app.php and optionally alias the facade. Publish the config if you want file/cache settings:

php artisan vendor:publish --provider="Roumen\Sitemap\SitemapServiceProvider"

First use case — a scheduled command that writes a static file:

use Roumen\Sitemap\Sitemap;
use Roumen\Sitemap\Tags\Url;

$sitemap = new Sitemap();

$sitemap->add(
    Url::create(route('home'))
        ->setLastModificationDate(now())
        ->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY)
        ->setPriority(1.0)
);

$sitemap->add(
    Url::create(route('about'))
        ->setLastModificationDate(now()->subWeek())
        ->setChangeFrequency(Url::CHANGE_FREQUENCY_MONTHLY)
        ->setPriority(0.5)
);

$sitemap->writeToFile(public_path('sitemap.xml'));

Look first at the Roumen\Sitemap\Sitemap class, the Roumen\Sitemap\Tags\Url tag, and the published config (typically use_cache, cached_path, cache_key). Everything else is composition around those three.

Upgrade to v1.0.4 before you start. This release patches a path traversal flaw in Sitemap::store() — the filename argument is now sanitized. If you are on v1.0.3 or earlier and pass anything other than a hardcoded literal to store() (a tenant slug, a locale, a route parameter), upgrade first, then audit your call sites for any filename derived from request data.

Implementation Patterns

Static site map in a command. Put generation in app/Console/Commands/GenerateSitemap.php, schedule it nightly, and let Nginx/Apache serve public/sitemap.xml directly. This is the lowest-risk pattern: no per-request overhead, no runtime failures for users.

Database-driven generation with chunking. Never load Post::all() into Url objects. Iterate with chunkById and flush:

Post::query()->where('published', true)->chunkById(1000, function ($posts) use ($sitemap) {
    foreach ($posts as $post) {
        $sitemap->add(
            Url::create(route('posts.show', $post))
                ->setLastModificationDate($post->updated_at)
                ->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY)
                ->setPriority(0.7)
        );
    }
});

For sites over 50,000 URLs, build multiple sitemaps and a sitemap index using Roumen\Sitemap\Tags\Sitemap:

use Roumen\Sitemap\Tags\Sitemap as SitemapTag;

$index = new Sitemap();
$index->add(
    SitemapTag::create(route('sitemap.part', ['page' => 1]))
        ->setLastModificationDate(now())
);
$index->writeToFile(public_path('sitemap.xml'));

Filenames come from your code, never from input. store() and writeToFile() take a destination path. Treat that argument as a constant: build it with public_path() plus a literal, not with concatenated user data. The v1.0.4 patch rejects traversal sequences, but defense-in-depth is cheap — validate against an allowlist if you genuinely need dynamic names (e.g. per-tenant sitemaps):

$name = preg_match('/^[a-z0-9-]+$/', $tenant->slug) ? $tenant->slug : 'sitemap';
$sitemap->store('xml', 'sitemaps/' . $name);

Dynamic route for smaller sites. Generate on demand and return a response with the right content type:

Route::get('/sitemap.xml', function () {
    $sitemap = new Sitemap();
    // add URLs...

    return response($sitemap->render(), 200, [
        'Content-Type' => 'application/xml; charset=UTF-8',
    ]);
});

Wrap this in Cache::remember() and key it by a content version so the database isn't hit on every crawl.

Cache invalidation via model events. Register observers on Post, Page, etc., and bump a version key or forget the cache key on saved/deleted. This avoids staleness without regenerating on every write:

Cache::forget(config('sitemap.cache_key'));

Localized URLs. For multi-language sites, add one Url per locale with the correct absolute URL (including locale prefix) and set hreflang alternates if your version of the package supports them; otherwise generate per-locale sitemaps and one index.

Testing. Assert against the rendered XML in feature tests:

$response = $this->get('/sitemap.xml');
$response->assertOk();
$response->assertSee('<loc>'.route('home').'</loc>', false);

Also assert excluded content: drafts, noindex pages, and 404s should not appear. And add one test that a hostile filename (../../etc/foo) never escapes the target directory — that regression test will outlive the package's own patch.

Integration tip: wrap it. Create a small SitemapBuilder service that owns URL collection, then calls the package only for serialization. This isolates you from future API changes — including security patches to store() — and makes unit testing trivial.

Gotchas and Tips

  • Upgrade to v1.0.4 now. Prior versions accepted traversal sequences in the store() filename. If you generate sitemaps per tenant/brand into named files, that was remotely influenceable any time the name touched request data. Patch, then grep your codebase for store( and writeToFile( and confirm every path argument is a literal or a validated slug.
  • URL limits are hard limits. A single sitemap may contain at most 50,000 URLs and be at most 50 MB uncompressed. Split and emit an index before you approach those numbers.
  • Memory climbs fast. Each Url and image tag is a PHP object. On large catalogs, generate in chunks, write each part to disk, then discard the Sitemap instance. Don't accumulate 100k tags in one request.
  • Use absolute, canonical URLs. Pass route()/url() output, not relative paths. Exclude query-string variants, redirects, noindex pages, and non-200 responses — crawlers will complain about all of them.
  • Escape ampersands. URLs with query strings need proper escaping; the package will XML-escape values, but malformed or already-encoded URLs still produce invalid sitemaps. Validate with xmllint --noout public/sitemap.xml.
  • lastmod must be a real W3C datetime. Carbon works, strings are parsed. Don't set it to now() on every regeneration — it trains crawlers to ignore the field. Use actual content modification times.
  • changefreq and priority are largely ignored by Google. Set them for other crawlers if you like, but don't spend time tuning values; correctness of URLs and lastmod matters far more.
  • File writes need permissions. writeToFile() fails silently or throws depending on version if public/ isn't writable by the PHP/queue user. Write to a temp path and rename() for atomic deploys, and ensure your deploy pipeline doesn't overwrite a freshly generated file with a stale one.
  • Cache is the #1 source of stale sitemaps. If use_cache is enabled, forgetting to invalidate on content changes means Search Console shows old URLs for days. Version the cache key or disable package caching and manage it yourself.
  • Watch class-name collisions. Roumen\Sitemap\Sitemap and Roumen\Sitemap\Tags\Sitemap are different concepts (index vs. file). Alias the tag class as SitemapTag to keep code readable.
  • Serve it statically when possible. A generated file served by the web server beats a PHP route every time a bot requests it, especially under crawl pressure.
  • Don't forget robots.txt. Add Sitemap: https://example.com/sitemap.xml and submit the index in Search Console — a perfect sitemap nobody finds is worthless.
  • Octane/queue workers. If you use Laravel Octane or long-running workers, don't hold a Sitemap singleton across requests; instantiate per generation to avoid leaking URLs between jobs.
  • Pin the version and subscribe to releases. The package is on the 1.0.x line — constrain it as ^1.0. Security fixes here are shipping as patch releases (v1.0.4 is a CVE-style fix, not a feature bump), so composer update should be a routine, low-friction operation. Keep the SitemapBuilder wrapper so a breaking release or abandonment doesn't block upgrades, but don't freeze the constraint so tightly that you miss a traversal patch.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource