otas / sitemap
A laravel package to create and generate sitemaps
Requires
- laravel/framework: >=9.0
Requires (Dev)
- orchestra/testbench: ^7.0@dev
This package is auto-updated.
Last update: 2026-07-22 08:40:24 UTC
README
A Laravel package to generate multilingual XML sitemaps with support for static pages, dynamic model-based URLs, translated slugs, and multi-segment path templates.
Table of Contents
- Requirements
- Installation
- Configuration
- Static Links
- Dynamic Links
- Generating the Sitemap
- Serving the Sitemap
- Artisan Commands
- Full Examples
- Generated XML Output
- License
Requirements
- PHP 8.1+
- Laravel 9.0+
Installation
composer require otas/sitemap
Run the setup command to publish the configuration file:
php artisan setup:sitemap
Or publish the config manually:
php artisan vendor:publish --provider="Otas\Sitemap\SitemapServiceProvider" --tag="sitemap-config"
Configuration
After publishing, the configuration file is located at config/sitemap.php.
return [ // Base URL for all sitemap links (falls back to app.url if null) 'website_url' => null, // Subdomain prefix (e.g. 'www', 'shop') 'subdomain' => null, // Optional base path for multi-tenant sitemap file storage 'base_path' => null, // Default locale — used for x-default hreflang and locale prefixing 'default_locale' => 'en', // Static pages (see Static Links section) 'static_links' => [], // Model classes implementing SitemapContract (see Dynamic Links section) 'dynamic_links' => [], // Whether to load the package's built-in /sitemap route 'load_routes' => true, // Route prefix for the sitemap endpoint 'package_routes_prefix' => 'api', // Subdirectory for published route files 'routes_publish_subdirectory' => '', ];
Note: The package also reads
config('translatable.locales')to determine which locales to generate URLs for. Make sure this is configured in your application (e.g. via theastrotomic/laravel-translatablepackage).
Static Links
Define static pages directly in the config file. Each entry specifies its location, alternate locale variants, and priority.
'static_links' => [ // Homepage [ 'loc' => '', 'other_locs' => ['ar'], 'alternates' => [ ['hreflang' => 'en', 'href' => ''], ['hreflang' => 'ar', 'href' => 'ar'], ['hreflang' => 'x-default', 'href' => ''], ], 'priority' => '1.0', ], // About page [ 'loc' => 'en/about', 'other_locs' => [], 'alternates' => [ ['hreflang' => 'en', 'href' => 'en/about'], ['hreflang' => 'ar', 'href' => 'عن-الشركة'], ], 'priority' => '0.8', ], // Contact page [ 'loc' => 'en/contact', 'other_locs' => [], 'alternates' => [ ['hreflang' => 'en', 'href' => 'en/contact'], ['hreflang' => 'ar', 'href' => 'اتصل-بنا'], ], 'priority' => '0.7', ], ],
Dynamic Links
Dynamic links are generated from Eloquent model records. Register your models in the config:
'dynamic_links' => [ App\Models\Offer::class, App\Models\Blog::class, App\Models\Product::class, ],
The SitemapContract
Every model must implement Otas\Sitemap\Contracts\SitemapContract:
use Otas\Sitemap\Contracts\SitemapContract; class Offer extends Model implements SitemapContract { public static function sitemapTranslatedSegments(): array { // Path template per locale with :placeholders } public static function buildSitemapUrls(): array { // Build and return the URL array } }
Path Template Placeholders
The sitemapTranslatedSegments() method returns a path template per locale. Templates use :placeholder syntax for dynamic parts:
public static function sitemapTranslatedSegments(): array { return [ 'en' => 'offers/:modelIdentifier', 'ar' => ':modelIdentifier/العروض', ]; }
Reserved placeholder:
| Placeholder | Resolved by |
|---|---|
:modelIdentifier |
Automatically resolved from the translations table (buildLocalizedUrls only). For buildDefaultUrls, you must provide it via slugResolver. |
Custom placeholders (:categorySlug, :brandSlug, etc.) are resolved via the $slugResolver callback:
slugResolver: fn($modelItem) => [ ':modelIdentifier' => $modelItem->slug, ':categorySlug' => $modelItem->category->slug, ]
The $slugResolver receives only the model item and returns a flat [':placeholder' => 'value'] map. No locale logic is needed inside it — the locale-specific path structure is already defined in the template.
Non-Translated Slugs (buildDefaultUrls)
Use this when the model's slug is the same across all locales (e.g. offers, products).
Since the slug is the same, there's no automatic resolution of :modelIdentifier. You must provide ALL placeholders in the slugResolver.
HandleDynamicSitemapHelper::buildDefaultUrls( Collection $modelItems, // The Eloquent collection array $translatedSegments, // Path template per locale callable $slugResolver, // Required: provide all placeholders float $priority, // URL priority (default: 0.8) );
Simple example — single segment:
use Otas\Sitemap\Contracts\SitemapContract; use Otas\Sitemap\Helpers\HandleDynamicSitemapHelper; class Offer extends Model implements SitemapContract { public static function sitemapTranslatedSegments(): array { return [ 'en' => 'offers/:modelIdentifier', 'ar' => ':modelIdentifier/العروض', ]; } public static function buildSitemapUrls(): array { $offers = self::visible()->get(); return HandleDynamicSitemapHelper::buildDefaultUrls( modelItems: $offers, translatedSegments: self::sitemapTranslatedSegments(), slugResolver: fn($offer) => [ ':modelIdentifier' => $offer->slug, ] ); } }
Generated URLs for an offer with slug summer-sale:
en/offers/summer-sale
summer-sale/العروض
Translated Slugs (buildLocalizedUrls)
Use this when the model's slug is different per locale (e.g. blog posts with translated titles/slugs).
HandleDynamicSitemapHelper::buildLocalizedUrls( Collection $modelItems, // The Eloquent collection array $translatedSegments, // Path template per locale ?callable $slugResolver, // Optional: resolve extra placeholders string $routeKeyName, // Translatable attribute to pluck (default: 'slug') float $priority, // URL priority (default: 0.8) );
The :modelIdentifier placeholder is automatically resolved to the translated value for each locale from the model's translations table.
Simple example — single segment:
use Otas\Sitemap\Contracts\SitemapContract; use Otas\Sitemap\Helpers\HandleDynamicSitemapHelper; class Blog extends Model implements SitemapContract { public static function sitemapTranslatedSegments(): array { return [ 'en' => 'blogs/:modelIdentifier', 'ar' => ':modelIdentifier/المدونة', ]; } public static function buildSitemapUrls(): array { $blogs = self::visible()->hasVisibleBlogCategory()->get(); return HandleDynamicSitemapHelper::buildLocalizedUrls( modelItems: $blogs, translatedSegments: self::sitemapTranslatedSegments(), ); } }
Generated URLs for a blog with EN slug laravel-tips and AR slug نصائح-لارافل:
en/blogs/laravel-tips
نصائح-لارافل/المدونة
Multi-Segment URLs
For URLs with multiple dynamic parts, add custom placeholders to the template and provide a slugResolver.
Example: Offer with Category (non-translated)
URL structure: offers/{category}/{offer-slug}
class Offer extends Model implements SitemapContract { public static function sitemapTranslatedSegments(): array { return [ 'en' => 'offers/:categorySlug/details/:modelIdentifier', 'ar' => ':modelIdentifier/تفاصيل/:categorySlug/العروض', ]; } public static function buildSitemapUrls(): array { $offers = self::visible()->with('category')->get(); return HandleDynamicSitemapHelper::buildDefaultUrls( modelItems: $offers, translatedSegments: self::sitemapTranslatedSegments(), slugResolver: fn($offer) => [ ':modelIdentifier' => $offer->slug, ':categorySlug' => $offer->category->slug, ], ); } }
Generated URLs for offer summer-sale in category electronics:
en/offers/electronics/details/summer-sale
summer-sale/تفاصيل/electronics/العروض
Example: Blog with Category (translated)
URL structure: blogs/{category}/{post-slug}
class Blog extends Model implements SitemapContract { public static function sitemapTranslatedSegments(): array { return [ 'en' => 'blogs/:categorySlug/:modelIdentifier', 'ar' => ':modelIdentifier/:categorySlug/المدونة', ]; } public static function buildSitemapUrls(): array { $blogs = self::visible()->with('category')->get(); return HandleDynamicSitemapHelper::buildLocalizedUrls( modelItems: $blogs, translatedSegments: self::sitemapTranslatedSegments(), slugResolver: fn($blog) => [ ':categorySlug' => $blog->category->slug, ], ); } }
Generated URLs for blog with EN slug laravel-tips, AR slug نصائح-لارافل, in category tech:
en/blogs/tech/laravel-tips
نصائح-لارافل/tech/المدونة
Example: Product with Brand + Category (three levels)
URL structure: shop/{brand}/{category}/{product-slug}
class Product extends Model implements SitemapContract { public static function sitemapTranslatedSegments(): array { return [ 'en' => 'shop/:brandSlug/:categorySlug/:modelIdentifier', 'ar' => ':modelIdentifier/:categorySlug/:brandSlug/متجر', ]; } public static function buildSitemapUrls(): array { $products = self::visible()->with(['brand', 'category'])->get(); return HandleDynamicSitemapHelper::buildDefaultUrls( modelItems: $products, translatedSegments: self::sitemapTranslatedSegments(), slugResolver: fn($product) => [ ':modelIdentifier' => $product->slug, ':brandSlug' => $product->brand->slug, ':categorySlug' => $product->category->slug, ], ); } }
Generated URLs for product macbook-pro, brand apple, category laptops:
en/shop/apple/laptops/macbook-pro
macbook-pro/laptops/apple/متجر
Generating the Sitemap
Generate the sitemap XML file using the Artisan command:
php artisan app:generate-sitemap
The file is saved to the public disk at the path configured in sitemap.base_path + sitemaps/sitemap.xml. If a subdomain is configured, the filename becomes {subdomain}-sitemap.xml.
You can schedule this command in your app/Console/Kernel.php:
protected function schedule(Schedule $schedule) { $schedule->command('app:generate-sitemap')->daily(); }
Serving the Sitemap
When load_routes is true (default), the package registers a route that serves the generated sitemap:
GET /api/sitemap
The route prefix is controlled by package_routes_prefix in the config.
To publish and customize the route file:
php artisan sitemap:publish-routes
This copies the route file to your application's routes/ directory for full control.
Artisan Commands
| Command | Description |
|---|---|
php artisan setup:sitemap |
Publish the config file (with overwrite prompt if it already exists) |
php artisan app:generate-sitemap |
Generate the sitemap XML and save it to the public disk |
php artisan sitemap:publish-routes |
Publish the package route file to your application's routes/ directory |
Full Examples
Minimal Setup (Single Model, No Translations)
1. Config:
// config/sitemap.php 'default_locale' => 'en', 'dynamic_links' => [ App\Models\Offer::class, ],
2. Model:
use Otas\Sitemap\Contracts\SitemapContract; use Otas\Sitemap\Helpers\HandleDynamicSitemapHelper; class Offer extends Model implements SitemapContract { public static function sitemapTranslatedSegments(): array { return [ 'en' => 'offers/:modelIdentifier', 'ar' => ':modelIdentifier/العروض', ]; } public static function buildSitemapUrls(): array { return HandleDynamicSitemapHelper::buildDefaultUrls( modelItems: self::visible()->get(), translatedSegments: self::sitemapTranslatedSegments(), slugResolver: fn($offer) => [ ':modelIdentifier' => $offer->slug, ] ); } }
3. Generate:
php artisan app:generate-sitemap
Complete Setup (Static + Multiple Dynamic Models)
Config:
// config/sitemap.php 'website_url' => 'https://example.com', 'subdomain' => null, 'default_locale' => 'en', 'static_links' => [ [ 'loc' => '', 'other_locs' => ['ar'], 'alternates' => [ ['hreflang' => 'en', 'href' => ''], ['hreflang' => 'ar', 'href' => 'ar'], ['hreflang' => 'x-default', 'href' => ''], ], 'priority' => '1.0', ], [ 'loc' => 'en/about', 'other_locs' => [], 'alternates' => [ ['hreflang' => 'en', 'href' => 'en/about'], ['hreflang' => 'ar', 'href' => 'عن-الشركة'], ], 'priority' => '0.8', ], ], 'dynamic_links' => [ App\Models\Offer::class, App\Models\Blog::class, App\Models\Product::class, ],
Models:
// Simple — non-translated slug class Offer extends Model implements SitemapContract { public static function sitemapTranslatedSegments(): array { return [ 'en' => 'offers/:modelIdentifier', 'ar' => ':modelIdentifier/العروض', ]; } public static function buildSitemapUrls(): array { return HandleDynamicSitemapHelper::buildDefaultUrls( modelItems: self::visible()->get(), translatedSegments: self::sitemapTranslatedSegments(), slugResolver: fn($offer) => [ ':modelIdentifier' => $offer->slug, ] ); } } // Simple — translated slug class Blog extends Model implements SitemapContract { public static function sitemapTranslatedSegments(): array { return [ 'en' => 'blogs/:modelIdentifier', 'ar' => ':modelIdentifier/المدونة', ]; } public static function buildSitemapUrls(): array { return HandleDynamicSitemapHelper::buildLocalizedUrls( modelItems: self::visible()->get(), translatedSegments: self::sitemapTranslatedSegments(), ); } } // Multi-segment — with slugResolver class Product extends Model implements SitemapContract { public static function sitemapTranslatedSegments(): array { return [ 'en' => 'shop/:brandSlug/:categorySlug/:modelIdentifier', 'ar' => ':modelIdentifier/:categorySlug/:brandSlug/متجر', ]; } public static function buildSitemapUrls(): array { return HandleDynamicSitemapHelper::buildDefaultUrls( modelItems: self::visible()->with(['brand', 'category'])->get(), translatedSegments: self::sitemapTranslatedSegments(), slugResolver: fn($product) => [ ':modelIdentifier' => $product->slug, ':brandSlug' => $product->brand->slug, ':categorySlug' => $product->category->slug, ], ); } }
Generated XML Output
The generated sitemap follows the Sitemaps XML protocol with xhtml:link alternate tags for multilingual support:
<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> <!-- Static page --> <url> <loc>https://www.example.com</loc> <xhtml:link rel="alternate" hreflang="en" href="https://www.example.com"/> <xhtml:link rel="alternate" hreflang="ar" href="https://www.example.com/ar"/> <xhtml:link rel="alternate" hreflang="x-default" href="https://www.example.com"/> <lastmod>2026-07-21T12:00:00+00:00</lastmod> <changefreq>daily</changefreq> <priority>1.0</priority> </url> <!-- Dynamic page (offer: summer-sale) --> <url> <loc>https://www.example.com/en/offers/summer-sale</loc> <xhtml:link rel="alternate" hreflang="en" href="https://www.example.com/en/offers/summer-sale"/> <xhtml:link rel="alternate" hreflang="ar" href="https://www.example.com/summer-sale/%D8%A7%D9%84%D8%B9%D8%B1%D9%88%D8%B6"/> <xhtml:link rel="alternate" hreflang="x-default" href="https://www.example.com/en/offers/summer-sale"/> <lastmod>2026-07-21T12:00:00+00:00</lastmod> <changefreq>daily</changefreq> <priority>0.8</priority> </url> </urlset>
License
Otas Sitemap is open-sourced software licensed under the MIT license.