Dynamic XML Sitemap Generation
Dynamic sitemap is generated on-the-fly from a database — correctly reflects current site state, requires no manual updates. Critically important for e-commerce, news portals, and any sites with constantly changing content.
Architecture for Large Catalogs
For sites with hundreds of thousands of URLs, sitemaps cannot be generated in one request. Chunked approach is used: each section — separate file.
// routes/web.php
Route::get('/sitemap.xml', [SitemapController::class, 'index']);
Route::get('/sitemap-products-{page}.xml', [SitemapController::class, 'products']);
Route::get('/sitemap-categories.xml', [SitemapController::class, 'categories']);
Route::get('/sitemap-articles.xml', [SitemapController::class, 'articles']);
Index Sitemap with Page Count
public function index(): Response
{
$productPages = ceil(Product::where('is_active', true)->count() / 1000);
$sitemaps = [];
for ($i = 1; $i <= $productPages; $i++) {
$sitemaps[] = [
'loc' => route('sitemap.products', ['page' => $i]),
'lastmod' => now()->format('Y-m-d'),
];
}
$sitemaps[] = ['loc' => route('sitemap.categories'), 'lastmod' => now()->format('Y-m-d')];
$sitemaps[] = ['loc' => route('sitemap.articles'), 'lastmod' => now()->format('Y-m-d')];
return response()
->view('sitemap.index', compact('sitemaps'))
->header('Content-Type', 'application/xml; charset=UTF-8');
}
Chunked Product Generation
public function products(int $page): Response
{
$products = Product::where('is_active', true)
->select(['slug', 'updated_at', 'main_image'])
->orderBy('id')
->forPage($page, 1000)
->get();
return response()
->view('sitemap.products', compact('products'))
->header('Content-Type', 'application/xml; charset=UTF-8')
->header('Cache-Control', 'public, max-age=3600');
}
{{-- resources/views/sitemap/products.blade.php --}}
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
@foreach ($products as $product)
<url>
<loc>{{ url('/products/' . $product->slug) }}</loc>
<lastmod>{{ $product->updated_at->format('Y-m-d') }}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
@if ($product->main_image)
<image:image>
<image:loc>{{ $product->main_image }}</image:loc>
</image:image>
@endif
</url>
@endforeach
</urlset>
Caching
public function products(int $page): Response
{
$cacheKey = "sitemap:products:{$page}";
$content = Cache::remember($cacheKey, now()->addHour(), function () use ($page) {
$products = Product::where('is_active', true)
->select(['slug', 'updated_at', 'main_image'])
->orderBy('id')
->forPage($page, 1000)
->get();
return view('sitemap.products', compact('products'))->render();
});
return response($content)->header('Content-Type', 'application/xml; charset=UTF-8');
}
Cache invalidation on product change:
// Product observer
public function saved(Product $product): void
{
$page = ceil($product->getKey() / 1000);
Cache::forget("sitemap:products:{$page}");
}
Sitemap for Multilingual Sites (hreflang)
<url>
<loc>https://example.ru/products/laptop</loc>
<xhtml:link rel="alternate" hreflang="ru" href="https://example.ru/products/laptop"/>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/products/laptop"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/products/laptop"/>
</url>
Automatic Submission to Search Console
After each deploy or on schedule:
// Artisan command
Http::get('https://www.google.com/ping', [
'sitemap' => url('/sitemap.xml')
]);
Yandex Webmaster — similarly via https://webmaster.yandex.ru/ping?sitemap=URL.
Exclusion from Sitemap
Pages that should not be indexed: noindex pages, pagination pages, filters with parameters, duplicates. Filtered at database query level through additional flags or URL patterns.
Setup timeline: 1–2 days with caching and support for large catalogs.







