laravelium/sitemap
Framework-agnostic PHP package for generating XML, TXT, HTML, and Google News sitemaps. Works with Laravel, Symfony, or any PHP project, with support for images, videos, translations, alternates, and Google News. Includes extensible adapters and high test coverage.
Installation (pin the patched release):
composer require rumendamyanov/php-sitemap "^1.0.4"
v1.0.4 is a security release — it rejects path traversal in Sitemap::store() filenames. Any project that passes user-influenced or config-driven filenames to store() should upgrade from v1.0.3 or earlier before doing anything else. There are no API changes, so composer update is safe.
For Laravel, publish the config (optional):
php artisan vendor:publish --provider="Laravelium\Sitemap\SitemapServiceProvider"
Basic Usage: Define a route to generate the sitemap:
use Laravelium\Sitemap\Facades\Sitemap;
Route::get('/sitemap.xml', function () {
return Sitemap::create()
->add(url('/'), 1.0, 'daily')
->add(url('/about'), 0.8, 'weekly')
->render('xml');
});
First Use Case:
Generate a simple XML sitemap for core routes. Test locally by visiting /sitemap.xml.
Dynamic Sitemap Generation: Use closures to fetch dynamic URLs (e.g., blog posts):
Sitemap::create()
->add(url('/posts'), 0.7, 'monthly')
->add(url('/posts/{id}'), 0.9, 'weekly', function ($sitemap) {
return Post::all()->map(fn ($post) => $post->slug);
});
Multi-Language Support:
Add hreflang tags for translated content:
$sitemap->add(url('/post'), 1.0, 'daily')
->setLanguage('en')
->addAlternate(url('/post-es'), 'es');
Writing to Disk with store() (the code path hardened in v1.0.4):
// Safe: fixed, developer-controlled filename
Sitemap::create()
->add(url('/'), 1.0, 'daily')
->store('sitemap', 'xml');
// Safe: tenant slug validated before it reaches the generator
$tenant = Tenant::where('slug', $request->route('tenant'))->firstOrFail();
Sitemap::create()
->add(url("/{$tenant->slug}"), 1.0, 'daily')
->store("sitemaps/{$tenant->slug}", 'xml');
Since v1.0.4 the library rejects traversal sequences (..), absolute paths, and other escapes in the filename. Treat that rejection as a validation layer, not a substitute for validating the slug yourself — a ValidationException from the generator is much harder to map to a 422 than a Rule::alphaDash() check.
Image/Video Sitemaps: Extend the base class for rich media:
use Laravelium\Sitemap\SitemapGenerator;
use Laravelium\Sitemap\Items\Image;
class VideoSitemap extends SitemapGenerator {
public function addVideo($url, $title, $description) {
return $this->add($url, 1.0, 'daily')
->addVideo($title, $description, url('/video-thumbnail.jpg'));
}
}
Caching: Cache generated sitemaps for performance:
Route::get('/sitemap.xml', function () {
return Cache::remember('sitemap-xml', now()->addHours(6), function () {
return Sitemap::create()->add(...)->render('xml');
});
});
Route::get() for public sitemaps or Route::middleware(['auth']) for private ones.render() over store() in request handlers. render() returns a string you hand to the response and never touches the filesystem, so the v1.0.4 traversal surface doesn't apply. Reach for store() only when you genuinely need a file on disk (CDN upload, static hosting, cron-generated sitemaps).dispatch(new GenerateSitemapJob());
Sitemap facade in unit tests:
$sitemap = Mockery::mock(Laravelium\Sitemap\Facades\Sitemap::class);
$sitemap->shouldReceive('render')->andReturn('<xml>...</xml>');
Add a regression test for any store() call that receives interpolated input — assert the generated file lands inside the expected directory and that a ../ payload is refused.Path Traversal in store() (fixed in v1.0.4):
v1.0.3 or earlier, a filename containing ../ could write outside the intended output directory. Upgrade first, then audit every call site where the filename is built from a route parameter, tenant slug, locale, or DB column.v1.0.4, sanitise upstream:
$request->validate(['name' => ['required', 'alpha_dash', 'max:100']]);
try/catch that silently swallows it — a refused filename usually means untrusted input reached the generator, which is worth an alert.URL Validation:
url('/path') instead of /path). Relative URLs may break validation.str()->startsWith('http') to validate URLs before adding them.Change Frequency:
always for large collections (e.g., product pages). Use weekly or monthly instead.changefreq if lastmod is missing.Memory Limits:
Sitemap::create()->setLimit(3000)->render('xml'); // Max 3000 URLs per file
Caching Headers:
Cache-Control headers to prevent proxy caching issues:
return response(Sitemap::render('xml'))
->header('Cache-Control', 'public, max-age=86400');
try-catch to log issues:
try {
return Sitemap::create()->add(...)->render('xml');
} catch (\Exception $e) {
Log::error('Sitemap generation failed: ' . $e->getMessage());
return response('Error generating sitemap.', 500);
}
composer show rumendamyanov/php-sitemap should report v1.0.4 or later. Security patches are easy to lose behind a stale composer.lock in CI or a Docker layer cache.Custom Items:
Extend Laravelium\Sitemap\Items\Url for custom metadata:
class CustomUrl extends Url {
public function setCustomTag($name, $value) {
$this->customTags[$name] = $value;
}
}
Adapters:
Override the default XmlWriter adapter for custom formats:
use Laravelium\Sitemap\Adapters\AdapterInterface;
class JsonAdapter implements AdapterInterface {
public function render(array $items) {
return json_encode($items);
}
}
Service Provider: Bind custom sitemap classes in the service provider:
$this->app->bind('custom.sitemap', function () {
return new CustomSitemap();
});
Wrapping store(): If you subclass the generator or add your own writer, mirror the v1.0.4 behaviour — resolve the path with realpath()/Path::canonicalize() and assert the result is still inside the target directory before writing.
0.5 if not specified. Explicitly set priorities for critical pages (e.g., 1.0 for homepages).lastmod timestamps use UTC to avoid timezone-related issues:
$sitemap->add(url('/post'), 1.0, 'daily', now()->tz('UTC'));
composer.json allows ^1.0, add a conflict rule so nobody installs back below the patch:
"conflict": { "rumendamyanov/php-sitemap": "<1.0.4" }
How can I help you explore Laravel packages today?