Fix Your Sitemap for Laravel
Laravel doesn't ship a built-in sitemap generator, so most teams use spatie/laravel-sitemap. Memory blowups from loading entire Eloquent collections, stale sitemaps, and missing multi-tenant domains are the typical failure modes.
The standard Laravel sitemap pattern: an Artisan command runs on cron, builds a sitemap index, writes per-entity files to public/sitemaps/. When it goes wrong, it goes wrong predictably: someone loads ::all() on a 400k-row table, PHP OOMs, the cron job stops firing, and the sitemap slowly rots until somebody notices rankings have tanked.
Debugged a Laravel marketplace app last month. GenerateSitemap command had been failing silently for six weeks because a developer swapped chunkById(500) for get() "to make it simpler". The sitemap was frozen at 38k URLs while the actual catalog grew to 62k. GSC coverage dropped 18% during that window. Switching back to chunkById fixed the OOM; running once caught everything up.
Working Artisan command
// app/Console/Commands/GenerateSitemap.php
use Spatie\Sitemap\Sitemap;
use Spatie\Sitemap\SitemapIndex;
use Spatie\Sitemap\Tags\Url;
public function handle(): int
{
$index = SitemapIndex::create();
$shardSize = 40000;
// Static pages
$static = Sitemap::create()
->add('/')
->add('/about')
->add('/pricing');
$static->writeToFile(public_path('sitemaps/static.xml'));
$index->add('/sitemaps/static.xml');
// Posts, chunked to avoid OOM
$shard = 0;
$sitemap = Sitemap::create();
$count = 0;
Post::where('published', true)
->whereNull('deleted_at')
->orderBy('id')
->chunkById(1000, function ($posts) use (&$sitemap, &$count, &$shard, &$index, $shardSize) {
foreach ($posts as $post) {
$sitemap->add(
Url::create(route('posts.show', $post))
->setLastModificationDate($post->updated_at)
->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY)
->setPriority(0.7)
);
if (++$count >= $shardSize) {
$sitemap->writeToFile(public_path("sitemaps/posts-{$shard}.xml"));
$index->add("/sitemaps/posts-{$shard}.xml");
$shard++;
$sitemap = Sitemap::create();
$count = 0;
}
}
});
if ($count > 0) {
$sitemap->writeToFile(public_path("sitemaps/posts-{$shard}.xml"));
$index->add("/sitemaps/posts-{$shard}.xml");
}
$index->writeToFile(public_path('sitemap.xml'));
return self::SUCCESS;
}Common Laravel Sitemap Issues
::all()or non-chunked->get()on large tables causing OOM- Soft-deleted records leaking in when SoftDeletes global scope is bypassed
APP_URLpointing at localhost in production.env- Single sitemap file over 50MB because pagination isn't used
- Multi-tenant apps serving one tenant's sitemap to every domain
- Sitemap regenerating on every request via a controller route, instead of being cached to a file
- Cron not running (or running as the wrong user) so regeneration silently stops
- Laravel's
route()helper returning localhost URLs in scheduled commands because of missingURL::forceRootUrl()
Scheduling and cron
// app/Console/Kernel.php (or routes/console.php in Laravel 11)
$schedule->command('sitemap:generate')
->hourly()
->onOneServer()
->withoutOverlapping()
->appendOutputTo(storage_path('logs/sitemap.log'));
// Verify scheduler is running
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1Multi-tenant sites
For stancl/tenancy or hyn/multi-tenant setups, wrap the generator in the tenant context and write the file to that tenant's public path. Don't try to share a single sitemap.xml across tenants - each tenant has different URLs, and Google treats each domain as a separate property. Cron the command once per tenant, or use a scheduled loop that iterates active tenants.
Step-by-Step Fix Guide
- Install
composer require spatie/laravel-sitemapand publish its config - Verify
APP_URLin.envmatches your production domain - Create a
GenerateSitemapcommand that builds an index and per-entity shards - Use
chunkById(1000)on any Eloquent query that might exceed 10k rows - Cap shards at 40k URLs so you stay under the 50k sitemap limit with headroom
- Schedule hourly with
->onOneServer()and->withoutOverlapping() - Verify cron is firing:
tail -f storage/logs/sitemap.log - Test with
curl https://yoursite.com/sitemap.xml- should return 200 and an index - Submit to Google Search Console