muhammad-nawlo / filament-sitemap-generator
Filament plugin to generate XML sitemaps with splitting, news, images, and search-engine ping
Package info
github.com/Muhammad-Nawlo/filament-sitemap-generator
pkg:composer/muhammad-nawlo/filament-sitemap-generator
Fund package maintenance!
Requires
- php: ^8.2
- filament/filament: ^3.2 || ^4.0 || ^5.0
- spatie/laravel-package-tools: ^1.15.0
- spatie/laravel-sitemap: ^7.4
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-arch: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- pestphp/pest-plugin-livewire: ^3.0|^4.0
- rector/rector: ^2.0
- spatie/laravel-ray: ^1.26
README
A config-driven Filament plugin for Laravel that generates XML sitemaps with optional splitting, news, images, alternates, and search-engine ping. Built on spatie/laravel-sitemap.
Compatibility: Filament v3.2+, v4.x, and v5.x. The Filament page uses getter overrides only for navigation group, label, and title (no static property redeclaration), so it stays compatible with differing parent types across Filament versions.
Installation
composer require muhammad-nawlo/filament-sitemap-generator
Publish the config file:
php artisan vendor:publish --tag="filament-sitemap-generator-config"
Configure config/filament-sitemap-generator.php (path, static URLs, models, schedule, queue, news, ping) as needed.
Usage
- Filament: Open the Sitemap page (under Settings) and click Generate Sitemap.
- CLI:
php artisan filament-sitemap-generator:generate(runs synchronously or dispatches a job if queue is enabled). - Scheduler: Enable
schedule.enabledin config; the command is registered at your chosen frequency (e.g. daily).
Feature Matrix
| Feature | Status | Notes |
|---|---|---|
| Manual generation via Filament | ✅ Implemented | Single page under Settings; one "Generate Sitemap" action |
| CLI generation | ✅ Implemented | php artisan filament-sitemap-generator:generate |
| Queue support | ✅ Implemented | Optional; config-driven connection and queue name |
| Scheduler support | ✅ Implemented | Optional; config-driven frequency (e.g. daily, hourly) |
| Config-driven static URLs | ✅ Implemented | static_urls array with url, priority, changefreq, lastmod |
| Model-based URLs | ✅ Implemented | models config; route or getSitemapUrl() per model |
| Chunked model processing | ✅ Implemented | Configurable chunk_size; never loads full table |
| 50,000 URL file splitting | ✅ Implemented | Configurable max_urls_per_file; flush when limit reached |
| Sitemap index generation | ✅ Implemented | When multiple parts exist, main path becomes index |
| lastmod support | ✅ Implemented | Static: config key; models: getSitemapLastModified() or updated_at |
| changefreq support | ✅ Implemented | Per static entry and per model config |
| priority support | ✅ Implemented | Per static entry and per model config |
| Alternate URLs (hreflang) | ✅ Implemented | Model method getAlternateUrls(); locale => url |
| Image sitemap support | ✅ Implemented | Model method getSitemapImages(); url + caption |
| Google News sitemap | ✅ Implemented | Separate sitemap-news.xml; 48-hour window; config-driven |
| Search engine ping | ✅ Implemented | Google and Bing; main sitemap URL only; try/catch per engine |
| Multi-site support | 🚧 Planned | Single site only; no tenant or domain-specific sitemaps |
| Storage disk abstraction | ✅ Implemented | Output mode file (path) or disk (Laravel disk); config or Filament Settings |
| Optional URL crawling | ✅ Implemented | Spatie crawler; merge with static/model URLs; dedupe; optional JS execution |
| Event hooks | ❌ Not supported | No before/after or URL-collected events; extension via config/service binding only |
Performance Characteristics
-
Chunk-based model iteration: Models are read via
Model::query()->chunk($chunkSize, callback). Only one chunk of records is in memory at a time. This avoids loading entire tables and keeps memory usage bounded by chunk size and the size of the in-memory sitemap (see below). -
Memory usage: Peak memory is dominated by (1) one Spatie
Sitemapinstance holding up tomax_urls_per_fileURL tags (default 50,000), and (2) one chunk of Eloquent models (default 500 records). No full-table or full-sitemap accumulation in memory. -
Max URLs per sitemap file: When the number of URLs added reaches
max_urls_per_file(default 50,000), the current sitemap is written tositemap-{n}.xmland a new in-memory sitemap is started. No single file exceeds this limit. -
Index generation: If any part file is written, the main path (
sitemap.xml) is written as a sitemap index that references all part URLs. If the total URL count never reaches the limit, a single sitemap is written to the main path and no index is produced. -
Recommended queue usage for large sites: For sites with many thousands of URLs, run generation via the CLI with
queue.enabledset totrue, or trigger the command from the scheduler. Avoid running "Generate Sitemap" from the Filament page for large sites, as it runs in the web request and can hit time or memory limits. -
Recommended chunk_size tuning: Default is 500. Use a smaller value (e.g. 250) if model instances are large or memory is constrained; use a larger value (e.g. 1000) to reduce query round-trips when models are small and memory is sufficient.
Testing Strategy
The package uses Pest for tests and Orchestra Testbench for Laravel application bootstrapping in a package context.
What is tested (or should be covered by contributors):
- Command execution: The
filament-sitemap-generator:generatecommand runs and, when queue is disabled, invokes the service and returns the correct exit code; when queue is enabled, it dispatches the job and outputs the expected message. - Job dispatch: With queue enabled, the command dispatches
GenerateSitemapJobwith optional connection/queue from config; the job can be asserted as queued or run synchronously in tests. - Service generation:
SitemapGeneratorService::generate()reads config, writes sitemap file(s) to the configured path, and optionally builds an index and pings search engines without failing on ping errors. - Splitting behavior: When URL count exceeds
max_urls_per_file, multiple part files and an index are produced; when under the limit, a single sitemap file is written to the main path. - News sitemap logic: With news enabled,
sitemap-news.xmlis written in the same directory; only records with publication date within the last 48 hours are included.
How to run tests:
composer test
This runs the Pest test suite (typically ./vendor/bin/pest).
Contributors: Add tests in tests/ using Pest syntax. Use the base TestCase (which extends Orchestra Testbench’s package test case) so the Laravel application and package service provider are loaded. Prefer feature tests that run the command or service and assert on file output and exit codes; add unit tests for service methods where it helps guard against regressions.
Compatibility Table
| Laravel | Filament | PHP | Status |
|---|---|---|---|
| 10.x | 3.2+ / 4.x / 5.x | 8.2+ | Supported (via composer constraints) |
| 11.x | 3.2+ / 4.x / 5.x | 8.2+ | Supported (via composer constraints) |
Composer constraints: php: ^8.2, filament/filament: ^3.2 || ^4.0 || ^5.0. Laravel version is implied by Filament and other dependencies. CI may run on a subset of these; report issues for specific version combinations.
Architecture Diagram
┌─────────────────────┐
│ Filament Page │ (Settings → Sitemap → "Generate Sitemap")
│ SitemapGenerator │
└──────────┬──────────┘
│ calls generate()
▼
┌─────────────────────┐
│ Command │ filament-sitemap-generator:generate
│ GenerateSitemapCmd │ (sync) or dispatch job (queue)
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Queue Job │ GenerateSitemapJob (if queue.enabled)
│ (optional) │ handle() → service->generate()
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ SitemapGenerator │ buildStandardSitemaps → buildIndex (if needed)
│ Service │ → buildNewsSitemap (if enabled) → pingSearchEngines (if enabled)
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Spatie Sitemap │ Sitemap, SitemapIndex, Tags\Url (news, image, alternate)
│ Builder │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ XML Files │ sitemap.xml (single or index) + sitemap-1.xml, sitemap-2.xml, …
│ (single or index │ Optional: sitemap-news.xml
│ + parts) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Optional Search │ GET Google/Bing ping URLs (main sitemap URL); try/catch per engine
│ Engine Ping │
└─────────────────────┘
Extension Points
Optional model methods
Implement these on Eloquent models referenced in config('filament-sitemap-generator.models') or config('filament-sitemap-generator.news.models') to customize URL, lastmod, alternates, images, or news metadata. All methods are optional; fallbacks use config or standard attributes.
| Method | Return type | Description |
|---|---|---|
getSitemapUrl() |
string |
Canonical URL for this record. If absent, URL is built from models.*.route and the model for route(). |
getSitemapLastModified() |
\DateTimeInterface |
Last modification date for <lastmod>. If absent, updated_at is used when present. |
getAlternateUrls() |
array<string, string> |
Map of locale code => absolute URL for hreflang alternates (e.g. ['en' => 'https://...', 'fr' => 'https://...']). |
getSitemapImages() |
array<int, array{url: string, caption?: string}> |
List of image entries; each must have url; caption is optional. URLs are normalized with the configured base URL. |
getSitemapNewsTitle() |
string |
Title for Google News <title>. If absent, title or name attribute is used. Only relevant when the model is in news.models. |
getSitemapNewsPublicationDate() |
\DateTimeInterface |
Publication date for Google News. If absent, published_at, updated_at, or created_at is used. Only relevant when the model is in news.models. |
Extending via config
- Custom model configuration: Add entries to
config('filament-sitemap-generator.models')with model class as key andpriority,changefreq, androute(required if the model does not implementgetSitemapUrl()). Userouteto specify the named route used to build the URL (e.g.'posts.show'). - Override base URL: Set
config('filament-sitemap-generator.base_url')to a full base URL (e.g.https://example.com). All non-absolute URLs (static and model-generated) are prefixed with this value. Ifnull,config('app.url')is used.
Custom service implementation
The service is bound as a singleton in the package service provider:
$this->app->singleton(SitemapGeneratorService::class);
To use a custom implementation (e.g. to add URLs or change behavior), register your class in a service provider that runs after the package:
$this->app->singleton(SitemapGeneratorService::class, MyCustomSitemapGeneratorService::class);
Ensure your implementation is compatible with callers that type-hint SitemapGeneratorService (Filament page, command, job) or provide the same public generate(): bool contract.
Output modes (File vs Disk)
You can write the sitemap to a filesystem path or to a Laravel disk.
- File mode (default): Sitemap is written to a full path (e.g.
public_path('sitemap.xml')). Configureoutput.mode='file'andoutput.file_path. - Disk mode: Sitemap is written via
Storage::disk($disk)->put($path, $xml). Configureoutput.mode='disk',output.disk,output.disk_path, andoutput.visibility('public'or'private').
Config example:
'output' => [ 'mode' => 'file', 'file_path' => public_path('sitemap.xml'), 'disk' => 'public', 'disk_path' => 'sitemap.xml', 'visibility' => 'public', ],
You can also set output mode and paths from Filament → Settings → Sitemap Settings (Output section). Values are stored in the database and override config when present.
Crawling
Optional URL crawling discovers links by crawling a base URL and merges them with static and model URLs. Crawling is disabled by default.
- Enable via config:
crawl.enabled=trueandcrawl.url(e.g.https://example.com). - Or enable from Filament → Sitemap Settings → Crawling (toggle and URL).
- Crawled URLs are deduplicated with static and model URLs and respect splitting (
max_urls_per_file). - Options:
concurrency,max_count,maximum_depth,exclude_patterns(wildcards, e.g.*admin*).
Config example:
'crawl' => [ 'enabled' => false, 'url' => null, 'concurrency' => 10, 'max_count' => null, 'max_tags_per_sitemap' => 50000, 'exclude_patterns' => ['*admin*', '*?preview=*'], ],
Advanced crawler configuration
You can plug in Spatie crawler behaviour via config or Filament Settings:
- Crawl profile:
crawl.crawl_profile— class name implementingSpatie\Crawler\CrawlProfiles\CrawlProfile(used by Spatie asconfig('sitemap.crawl_profile')during the run only). - should_crawl:
crawl.should_crawl— invokable class:(UriInterface $url) => bool. If provided, only URLs for which this returnstrueare crawled. - has_crawled:
crawl.has_crawled— invokable class:(Url $url, ?ResponseInterface $response) => Url. Transform or filter the tag before it is added to the crawl result.
Example: custom crawl profile
use Spatie\Crawler\CrawlProfiles\CrawlProfile; use Psr\Http\Message\UriInterface; class MyCrawlProfile extends CrawlProfile { public function shouldCrawl(UriInterface $url): bool { return true; // or custom logic } }
Register in config: 'crawl_profile' => MyCrawlProfile::class.
Example: should_crawl invokable class
use Psr\Http\Message\UriInterface; class AllowOnlyBlog { public function __invoke(UriInterface $url): bool { return str_contains((string) $url, '/blog/'); } }
Set crawl.should_crawl to AllowOnlyBlog::class.
JavaScript execution requirements
Crawling can run with JavaScript execution so that client-rendered links are discovered. This is off by default and requires:
- spatie/browsershot (and thus Node.js and Chrome/Chromium) on the server.
- Config:
crawl.execute_javascript=true; optionallycrawl.chrome_binary_pathandcrawl.node_binary_path.
If execute_javascript is true but Browsershot is not installed, the plugin logs a warning and continues the crawl without JavaScript. If the JS crawl fails at runtime (e.g. Chrome not found), it retries once without JS, then continues generation. Config keys are applied temporarily and restored after the crawl so global config is not polluted.
getSitemapVideos() example
For Google Video sitemap support, implement getSitemapVideos() on your model. Return an array of entries with at least thumbnail_loc, title, description, and either content_loc or player_loc:
use Spatie\Sitemap\Contracts\Sitemapable; use Spatie\Sitemap\Tags\Url; class Post extends Model implements Sitemapable { public function getSitemapVideos(): array { return [ [ 'thumbnail_loc' => 'https://example.com/thumbs/1.jpg', 'title' => 'My video title', 'description' => 'Short description', 'content_loc' => 'https://example.com/videos/1.mp4', 'duration' => 120, 'publication_date' => $this->published_at?->toIso8601String(), ], ]; } }
Optional keys include duration, expiration_date, rating, view_count, publication_date, family_friendly, restriction, tags, allow, deny, etc.
Example Large-Site Configuration
Example production-style config for a site with 500,000+ URLs: queue and scheduler enabled, chunk size tuned, file splitting at 50,000 URLs, and ping enabled.
// config/filament-sitemap-generator.php (excerpt for large-site scenario) return [ 'path' => public_path('sitemap.xml'), 'chunk_size' => 500, 'max_urls_per_file' => 50000, 'base_url' => null, 'static_urls' => [ ['url' => '/', 'priority' => 1.0, 'changefreq' => 'daily'], // ... other static entries ], 'models' => [ App\Models\Post::class => [ 'priority' => 0.8, 'changefreq' => 'weekly', 'route' => 'posts.show', ], App\Models\Category::class => [ 'priority' => 0.7, 'changefreq' => 'weekly', 'route' => 'categories.show', ], // ... other models ], 'schedule' => [ 'enabled' => true, 'frequency' => 'daily', ], 'queue' => [ 'enabled' => true, 'connection' => null, // default 'queue' => 'sitemaps', // dedicated queue recommended for large runs ], 'news' => [ 'enabled' => true, 'publication_name' => 'Your Site Name', 'publication_language' => 'en', 'models' => [App\Models\Post::class], ], 'ping_search_engines' => [ 'enabled' => true, 'engines' => ['google', 'bing'], ], ];
With this setup, php artisan filament-sitemap-generator:generate (or the daily schedule) dispatches the job to the sitemaps queue. A worker processes it; the service produces sitemap-1.xml through sitemap-N.xml (each ≤ 50,000 URLs), sitemap.xml as the index, and optionally sitemap-news.xml. Google and Bing are then pinged with the main sitemap URL. Ensure a queue worker is running (e.g. php artisan queue:work --queue=sitemaps or your production worker config).
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.
Architectural Overview
Structured technical overview of the package for maintainers and contributors.
1. High-Level Purpose
1.1 Problem Solved
The package provides config-driven, Filament-backed XML sitemap generation for Laravel applications. It allows site owners or automation to produce sitemap(s) that comply with common limits (e.g. 50,000 URLs per file), support news/image/alternate hints, and optionally ping search engines—without writing custom generation code.
1.2 Filament Integration
- Plugin:
FilamentSitemapGeneratorPluginimplementsFilament\Contracts\Plugin, registers a single custom page, and is attached to the default panel only insideFilament::serving()(and only if not already registered). - UI: One Filament Page under the "Settings" group exposes a "Generate Sitemap" header action that invokes the service and shows success/failure via
Notification. No generation logic lives in the page. - Coupling: The package assumes a default Filament panel; it does not support selecting the panel or multi-panel registration.
1.3 Relationship to spatie/laravel-sitemap
- Builds on: Spatie’s
Sitemap,SitemapIndex, andTags\Url(withaddNews,addImage,addAlternate) for XML structure and writing. - Adds: Config-driven sources (static URLs + model lists), chunked model iteration, file splitting and index generation, news sitemap, ping, and Filament/CLI/queue/scheduler entry points. Spatie is used as the low-level sitemap builder, not as a drop-in replacement.
2. Architecture Breakdown
2.1 Service Provider
Class: FilamentSitemapGeneratorServiceProvider (extends Spatie\LaravelPackageTools\PackageServiceProvider)
Responsibilities:
- Package configuration: Name, config file, commands, install command (config publish, migrations prompt, GitHub star), optional migrations/translations/views based on existing paths.
- Registration: Binds
SitemapGeneratorServiceas a singleton inpackageRegistered(). - Boot: In
packageBooted(): registers the Filament plugin on the default panel (insideFilament::serving()), registers assets/icons (currently empty), publishes stubs whenstubs/exists, registers the schedule when enabled, and adds a testing mixin.
Notable details: Schedule registration uses $this->app->booted() and resolves Schedule from the container; frequency is applied via method_exists($event, $frequency). The stubs publish loop will error if stubs/ is missing. Install command still references migrations even though the package does not appear to ship sitemap DB tables.
2.2 Plugin
Class: FilamentSitemapGeneratorPlugin (Filament\Contracts\Plugin)
Responsibilities:
- Identity:
getId()returns'filament-sitemap-generator'. - Registration: Registers
SitemapGeneratoras a Filament page inregister(Panel $panel);boot()is empty. - Factory:
make()andget()resolve the plugin from the container or current panel.
Separation: No business logic; only Filament registration.
2.3 Service Layer
Class: SitemapGeneratorService
Responsibilities:
- Orchestration:
generate()reads config, then calls (in order)buildStandardSitemaps(), optionallybuildIndex(), optionallybuildNewsSitemap(), and optionallypingSearchEngines(). - Standard sitemaps:
buildStandardSitemaps()streams URLs (static + chunked models) into SpatieSitemapinstances, flushes tositemap-{n}.xmlwhen the count reachesmax_urls_per_file, and either writes a single file to the main path or returns part URLs for the index. - Index:
buildIndex()builds a SpatieSitemapIndexfrom part URLs and writes it to the main path. - News:
buildNewsSitemap()builds a separatesitemap-news.xmlfrom configured news models, filtering by publication date (last 48 hours) and usinggetSitemapUrl/getSitemapNewsTitle/getSitemapNewsPublicationDate(with fallbacks). - Ping:
pingSearchEngines()builds the main sitemap URL and GETs Google/Bing ping endpoints; failures are caught and do not affect generation. - URL/tag building:
normalizeUrl()/getBaseUrl(),buildModelUrlTag(),resolveModelUrl(), and helpers for lastmod, priority/changefreq, alternates, and images. All useconfig('filament-sitemap-generator.*')(andconfig('app.url')for base); the only other external dependency is optionalHttpClientFactoryfor ping.
Design: Single public entry point (generate()), small private methods, no facades except config. Logic is centralized in the service; Filament, command, and job only call generate().
2.4 Command and Job
Command: GenerateSitemapCommand (filament-sitemap-generator:generate)
- Resolves
SitemapGeneratorServicevia constructor. - If
queue.enabledis true: dispatchesGenerateSitemapJob(with optional connection/queue from config), prints "Sitemap generation dispatched.", returns 0. - Otherwise: calls
$sitemapGenerator->generate(), prints success or error, returns 0 or 1.
Job: GenerateSitemapJob (ShouldQueue)
handle(SitemapGeneratorService $sitemapGenerator)only calls$sitemapGenerator->generate(). No generation logic in the job.
Separation: Command and job are thin adapters; all behavior is in the service.
2.5 Config Structure
| Key | Purpose |
|---|---|
path |
Main sitemap path (default public_path('sitemap.xml')) |
chunk_size |
Model query chunk size (default 500) |
max_urls_per_file |
Max URLs per file before splitting (default 50,000) |
base_url |
Override for absolute URLs (default null → app.url) |
static_urls |
List of url, priority, changefreq, optional lastmod |
models |
Map of model class => priority, changefreq, route (for URL when no getSitemapUrl) |
schedule.enabled / frequency |
Whether to schedule the command and with which frequency |
queue.enabled / connection / queue |
Whether to queue and which connection/queue |
news.enabled / publication_name / publication_language / models |
Google News sitemap |
ping_search_engines.enabled / engines |
Whether to ping and which engines (e.g. google, bing) |
2.6 Separation of Concerns
- Strong: Generation, splitting, index, news, and ping are all in the service; UI and CLI only invoke the service. Config is the single source for behavior.
- Gaps: Facade alias points at an empty
FilamentSitemapGeneratorclass, not the service. Install command and migrations list suggest DB usage that the package does not implement. Stubs publish assumes astubs/directory.
3. Execution Flow
3.1 Manual Generation (Filament)
- User opens the Sitemap page and clicks "Generate Sitemap".
SitemapGenerator::runGeneration()runs in a try/catch, callsSitemapGeneratorService::generate(), then sends a success or danger notification with the exception message on failure.- No queue: generation runs in the current request. For large sitemaps this can hit time/memory limits.
3.2 CLI Generation
php artisan filament-sitemap-generator:generaterunsGenerateSitemapCommand::handle().- If
queue.enabled: command dispatchesGenerateSitemapJoband exits; the worker runs the job (see 3.3). - If not queued: command calls
SitemapGeneratorService::generate()synchronously and returns exit code 0 or 1 with console output.
3.3 Queue Generation
- Command dispatches
GenerateSitemapJob(optionally with connection/queue from config). - Worker runs the job; container injects
SitemapGeneratorServiceintohandle(); job callsgenerate()once. - Same flow as synchronous generation, but in a worker process.
3.4 Scheduler Flow
- When
schedule.enabledis true, the provider registers inapp->booted()a schedule entry forfilament-sitemap-generator:generatewith the configured frequency (e.g.daily()), using the container’sScheduleinstance. schedule:run(or cron) executes the command at that frequency; the command then behaves as in 3.2 (sync or queue depending on config).
3.5 File Splitting Logic
buildStandardSitemaps()keeps one in-memorySitemapand a URL count.- Each static URL and each model URL (from chunked queries) is added via a closure that: if
count >= max_urls_per_file, writes the current sitemap tositemap-{partNumber}.xmlin the same directory, appends its full URL to a list, creates a newSitemap, resets count, then adds the new URL. - After all URLs: if no part file was ever written and
count > 0, the single sitemap is written to the main path and an empty list is returned. If at least one part was written and the last chunk has URLs, that chunk is written as the next part and its URL is appended. - Part URLs use
getBaseUrl()plus filename (e.g.https://example.com/sitemap-1.xml).
3.6 Sitemap Index Logic
- If
buildStandardSitemaps()returns a non-empty list of part URLs,buildIndex()is called. - A Spatie
SitemapIndexis created, each part URL is added, and the index is written to the main path (overwriting the mainsitemap.xml). - Result: main path is the index; part files are
sitemap-1.xml,sitemap-2.xml, etc. If there is only one "part" (no split), the implementation still writessitemap-1.xmland then the index, so the main file is always an index when splitting occurs; single-file case is when no flush ever happens, and the only sitemap is written directly to the main path.
4. Scalability Analysis
4.1 Memory Safety
- Models: Uses
Model::query()->chunk($chunkSize, callback)so records are not all loaded at once; only the current chunk is in memory. - Standard sitemap: One Spatie
Sitemapis held in memory and written when the URL count hitsmax_urls_per_fileor at the end. So at most ~50,000Urltags in memory at once for the standard sitemap. - News: Same chunked pattern; news sitemap is built in one
Sitemapand written once; if news models are large, the 48-hour filter is applied per record in PHP (no DB-level date filter), so many old records can be loaded and then skipped.
4.2 Chunking Strategy
- Chunk size is configurable (default 500). Smaller chunks reduce peak memory and increase query round-trips; larger chunks do the opposite.
- Chunking is only for DB reads; the in-memory sitemap can still grow up to
max_urls_per_filebefore flush.
4.3 Large Database Handling
- For standard sitemaps, chunking prevents loading the full table. For news, all chunks are iterated and filtered by date in PHP; for tables with many historical rows, a query-level date filter (e.g.
where('published_at', '>=', $cutoff)) would be more scalable and is not implemented.
4.4 50,000 URL Compliance
- Flush is triggered when
count >= max_urls_per_file(default 50,000), so no sitemap file exceeds that limit. Index file only references part URLs, so it stays small.
4.5 Performance Bottlenecks
- Single request (Filament): Long-running and memory-heavy for large sites; no progress feedback or timeout handling.
- News: No DB-level 48-hour filter; all records are streamed and filtered in PHP.
- Ping: Sequential GETs; failure of one engine does not block the others (try/catch per engine).
- Base URL: Built from config on every URL normalize; negligible cost.
5. SEO Capabilities
| Feature | Support | Notes |
|---|---|---|
| Standard sitemap | Yes | Static URLs + models; optional splitting + index |
| lastmod | Yes | Static: lastmod in config. Models: getSitemapLastModified() or updated_at |
| changefreq & priority | Yes | From config per static entry and per model config |
| Alternate URLs | Yes | Model method getAlternateUrls() → [locale => url]; applied via Spatie addAlternate |
| Google News | Yes | Separate sitemap-news.xml; 48-hour window; publication name/language from config; title/date from model methods or attributes |
| Image sitemap | Yes | Model method getSitemapImages() → list of url/caption; applied via Spatie addImage |
| Search engine ping | Yes | Google and Bing; main sitemap URL only (index or single file); failures caught |
Model contracts (optional): getSitemapUrl(), getSitemapLastModified(), getAlternateUrls(), getSitemapImages(), getSitemapNewsTitle(), getSitemapNewsPublicationDate(). Fallbacks use attributes like title, updated_at, published_at, etc.
6. Configuration Flexibility
6.1 Customizable
- Output path, chunk size, max URLs per file, base URL.
- Static URLs (with priority, changefreq, lastmod).
- Models and their options (priority, changefreq, route).
- Schedule on/off and frequency (any method on the schedule event, e.g.
daily,hourly). - Queue on/off, connection, and queue name.
- News on/off, publication name/language, list of models.
- Ping on/off and list of engine names (google, bing).
6.2 Not Customizable (Without Code Changes)
- News sitemap path (fixed as
sitemap-news.xmlin the same dir as main path). - Part file naming (
sitemap-1.xml,sitemap-2.xml, …). - 48-hour window for news.
- Which engines are supported (only google/bing in the ping map).
- Filament panel (always default), page slug, navigation group/label.
- Schedule is only registered for the default app schedule (single environment).
6.3 Extensibility
- No events or hooks during generation; no way to add URLs or modify tags without forking or wrapping the service.
- No interface/contract for "sitemap source"; models are configured by class name and options only.
- Service is a concrete class; swapping implementation would require binding a different implementation in the container.
7. Strengths
- Single responsibility: Service owns all generation; Filament, command, and job are thin.
- Config-driven: One config file controls paths, limits, sources, schedule, queue, news, and ping.
- Strict types and DI: Service, command, and job use type hints and constructor injection; only config and optional HTTP factory are used as globals.
- Spatie reuse: Uses Spatie’s sitemap building and tags correctly (index, news, images, alternates).
- Safe splitting: Respects 50,000 URL limit and produces a valid index when multiple files are used.
- Resilient ping: Ping failures do not fail generation.
- Chunked models: Avoids loading full tables into memory for standard sitemaps.
- Backward compatible: Single-file behavior preserved when URL count stays under the limit.
8. Weaknesses and Limitations
- Facade:
FilamentSitemapGeneratorfacade resolves to an empty class; it does not delegate toSitemapGeneratorService, so it is misleading and unused. - Stubs: Provider calls
Filesystem::files(__DIR__ . '/../stubs/')without checking the directory exists; will throw ifstubs/is missing. - Install command: References migrations and "askToRunMigrations" although the package has no real migration (only a stub name); can confuse installs.
- News date filter: 48-hour filter is done in PHP after loading chunks; no
where('date_column', '>=', $cutoff)on the query, so large tables waste work and memory. - No events: No "before/after generate" or "url collected" events for logging, caching, or third-party extensions.
- Single panel: Plugin is only registered on the default Filament panel; no multi-panel or explicit panel choice.
- Filament sync: Manual generation runs in the web request; large sitemaps can time out or exhaust memory with no guidance to use queue/CLI.
- Ping scope: Only the main sitemap URL is pinged;
sitemap-news.xmlis not pinged. - Schedule coupling: Schedule registration assumes the app uses the default
Schedulefrom the container; custom scheduler setups may not see the entry.
9. Suggested Improvements for Enterprise Readiness
- Wire facade: Point
FilamentSitemapGeneratoratSitemapGeneratorService(or a small wrapper) soFilamentSitemapGenerator::generate()works and is documented. - Guard stubs publish: Check
is_dir(__DIR__ . '/../stubs/')before iterating, or remove the publish if no stubs are shipped. - Align install command: Remove or implement migrations; if no DB is used, drop migration steps from the install command.
- News query filter: Add an optional configurable date column (e.g.
published_at) and applywhere($column, '>=', $cutoff)in the news query so only recent rows are loaded. - Events: Dispatch events (e.g.
SitemapGenerating,SitemapGenerated) with path and part count so apps can log, invalidate caches, or extend. - Filament queue hint: When queue is disabled and the page is used, consider a warning or info notification suggesting queue/CLI for large sitemaps.
- Ping news sitemap: Optionally ping the news sitemap URL when news is enabled (or document that only the main index is pinged).
- Documentation: Document model contracts (method names and return shapes), config options, and recommended chunk_size / queue usage for large sites.
- Testing: Add unit tests for the service (e.g. splitting at 50k, index contents, news 48h filter) and integration tests for command and job.
- Optional interfaces: Define optional interfaces (e.g.
SitemapUrlProvider) for models so IDEs and static analysis can rely on a clear contract alongside the current convention-based methods.