samdark/sitemap
A lightweight sitemap generator for Yii2 applications. Build XML sitemaps and sitemap indexes, add URLs with last-modified, change-frequency, and priority metadata, and write or cache the output. Supports gzip compression and straightforward integration with Yii2 projects.
samdark/sitemap is a dependency-free sitemap writer. It does not crawl, query your database, or know anything about Laravel — you hand it URLs and it writes spec-compliant XML. That makes it a small, predictable piece of infrastructure rather than a framework to adopt.
composer require samdark/sitemap
The smallest useful integration is an Artisan command that writes a static file:
use samdark\sitemap\Sitemap;
$sitemap = new Sitemap(public_path('sitemap.xml'));
$sitemap->addItem(url('/'), time(), Sitemap::DAILY, 1.0);
$sitemap->addItem(route('posts.index'), time(), Sitemap::WEEKLY, 0.8);
$sitemap->write();
Look first at:
src/Sitemap.php — addItem() signature and the ALWAYS/HOURLY/DAILY/WEEKLY/MONTHLY/YEARLY/NEVER constants.src/SitemapIndex.php — for splitting large sites across multiple files.src/extensions/ — image, video, news, mobile, and alternates support if your version ships them.Your first real use case should be a scheduled command, not a controller. Generate public/sitemap.xml (or public/sitemaps/sitemap.xml) at deploy or nightly, reference it from robots.txt, and submit it once to Search Console. From there, the main design question is whether you need one file or an index.
Command-driven generation. Keep generation out of the request cycle. A command is testable, schedulable, and safe to run during deploys.
class GenerateSitemap extends Command
{
protected $signature = 'sitemap:generate';
public function handle(): int
{
$dir = public_path('sitemaps');
File::ensureDirectoryExists($dir);
$sitemap = new Sitemap($dir . '/pages.xml');
$sitemap->addItem(url('/'), now()->timestamp, Sitemap::DAILY, 1.0);
$sitemap->addItem(route('about'), now()->timestamp, Sitemap::MONTHLY, 0.5);
$sitemap->write();
return self::SUCCESS;
}
}
$schedule->command('sitemap:generate')->dailyAt('03:15')->withoutOverlapping();
Sitemap index for anything large. Hard limits are 50,000 URLs and 50 MB uncompressed per sitemap, so chunk at ~45,000 to leave headroom. Use cursor() or chunkById() to avoid loading models into memory.
$index = new SitemapIndex($dir . '/sitemap.xml');
$chunk = 1;
$count = 0;
$sitemap = new Sitemap("$dir/posts-$chunk.xml");
Post::where('published', true)->orderBy('id')->cursor()->each(
function (Post $post) use (&$sitemap, &$chunk, &$count, $index, $dir) {
$sitemap->addItem(
route('posts.show', $post),
$post->updated_at->timestamp,
Sitemap::WEEKLY,
0.7
);
if (++$count % 45000 === 0) {
$sitemap->write();
$index->addSitemap(url("sitemaps/posts-$chunk.xml"));
$chunk++;
$sitemap = new Sitemap("$dir/posts-$chunk.xml");
}
}
);
if ($count % 45000 !== 0) {
$sitemap->write();
$index->addSitemap(url("sitemaps/posts-$chunk.xml"));
}
$index->write();
Gzip when bandwidth or file size matters. Pass true as the second constructor argument and use a .xml.gz filename. The index must reference the same .gz path.
$sitemap = new Sitemap($dir . '/products.xml.gz', true);
Serve static files from the web server. public/sitemaps/*.xml served by nginx/Apache is cheaper and more cacheable than a Laravel route. Use a route only when URLs must be generated per request:
Route::get('/sitemap.xml', fn () => response()->file(
public_path('sitemaps/sitemap.xml'),
['Content-Type' => 'application/xml']
));
Regenerate on content changes, debounced. Model events that synchronously regenerate the sitemap will punish bulk imports. Queue a unique, debounced job instead:
Post::saved(fn () => GenerateSitemap::dispatch()->delay(now()->addMinutes(5)));
Use ShouldBeUnique and WithoutOverlapping so a 10,000-row import produces one regeneration, not 10,000.
Use extensions for rich result schemas. Image, video, and news sitemaps require additional XML namespaces and per-URL metadata. The package extensions handle the namespace wiring; the exact addExtension()/payload signatures vary by version, so read src/extensions/ for the release you install rather than copying old blog posts.
Test the output, not the internals.
$this->artisan('sitemap:generate')->assertSuccessful();
$this->assertFileExists(public_path('sitemaps/sitemap.xml'));
$this->assertStringContainsString('<loc>', file_get_contents(public_path('sitemaps/sitemap.xml')));
Deployment tip. Run sitemap:generate in your deploy script after migrations and cache warmup. If generation fails, the old file remains and search engines keep working — which is usually preferable to serving an empty sitemap.
Sitemap accumulates items in memory. It is a builder, not a streaming writer. Chunking with cursor() reduces model memory, but the sitemap object itself still holds every entry until write(). Write every 45k rows and start a new instance.route() depends on APP_URL; behind a proxy or load balancer, add URL::forceScheme('https') in a service provider or your sitemap will contain http:// URLs from the wrong host.lastModified. Newer versions may accept DateTimeInterface — check the installed signature. Passing a formatted date string where an integer is expected produces a 1970 timestamp or invalid XML.APP_TIMEZONE=UTC or accept that the offset is emitted; do not mix server-local timestamps with UTC expectations.<url> entries. Deduplicate with a seen set or unique('url') before adding.& are handled. Invalid UTF-8 in slugs will still break the document — sanitize or skip those rows.write() needs the directory to exist and be writable by the PHP/deploy user. A failed open can leave you with an empty or stale file. Write to a temp path and rename() for atomic replacement if readers may fetch during generation.ext-zlib. Name the file .xml.gz and reference that name in the index. Don't serve gzipped bytes under a .xml name, and don't let nginx apply gzip again to an already-gzipped sitemap.noindex pages, authenticated areas, and faceted-filter permutations. A sitemap full of redirects is worse than no sitemap.robots.txt. Sitemap: https://example.com/sitemaps/sitemap.xml — and submit the index, not every child sitemap, to Search Console./sitemap.xml, regenerating the origin file does nothing until the cache expires or is purged. Set a short TTL on sitemap paths.addItem() API is stable; extension payload shapes have changed across releases. Pin the package version and re-read the extension source when upgrading.xmllint --noout public/sitemaps/sitemap.xml in CI catches malformed output from bad slugs or timestamps far faster than Search Console does.How can I help you explore Laravel packages today?