pecotamic/sitemap
Pecotamic Sitemap is a Laravel package for generating XML sitemaps. Define URLs or model-based entries, split large sitemaps into indexes, and serve them through routes or custom storage. Flexible configuration helps search engines discover and index your site's pages.
The 2.0 release is a hard cut: Statamic 5 or 6 and PHP 8.2+ only. If the project is still on Statamic 3/4, pin to the 1.x line and stop reading — there is no compat shim.
composer require <vendor>/statamic-sitemap:^2.0
Then publish the config and hit the route:
php artisan vendor:publish --tag=statamic-sitemap-config
Visit /sitemap.xml. On a default install you should get every entry in the collections the addon considers indexable, with 1.0 priority on / and 0.8 everywhere else. That out-of-the-box output is the fastest smoke test that the addon booted, the route registered, and the generator found your content.
First real use case: open the published config, list the collections (and any custom collection routes) that belong in the sitemap, and exclude the ones that don't (pages in, articles in, globals/drafts out). Everything else in this package is refinement on top of that one config edit.
Config-driven collections. Day-to-day, 90% of work is the config file: which collections appear, whether taxonomy term archives are included, and the expire value. Since 2.0 the default expire is one day rather than a minute — leave it there unless you have a specific reason, because invalidation is now event-driven rather than time-driven.
Custom entries via addEntries(). When you need per-entry control (priority by template, deeper paths, external URLs), the pattern is to build SitemapEntry objects. The 2.0 constructor takes a float priority and a nullable lastmod:
use Vendor\Sitemap\SitemapEntry;
$sitemap->addEntries(
$entries->map(fn ($entry) => new SitemapEntry(
url: $entry->absoluteUrl(),
priority: $entry->value('sitemap_priority') ?? 0.5, // float, not "0.5"
lastmod: $entry->lastModified(), // may be null
))
);
Cast at the boundary. (float) $entry->value('sitemap_priority') is cheap insurance against a string sneaking in from a YAML/field value.
Priority as a content concern. A common workflow is a sitemap_priority field on the blueprint (select: 1.0 / 0.8 / 0.5 / 0.3) and letting editors drive it, rather than hardcoding rules in PHP. Pair that with a fallback so entries without the field still get the 0.8 default and you never emit <priority></priority>.
Multisite. Site-prefix handling now lives in the service provider, so per-site sitemaps work without custom URL munging. If you were manually concatenating site roots onto paths, delete that code — you'll get doubled prefixes.
Template overrides. Copy the addon's Antlers/Blade sitemap template into resources/views/vendor/... if you need extra namespaces (<image:image>, <xhtml:link> alternates). Because priority whitespace is now cleaned up in PHP, don't re-add formatting logic in the template — just echo the value.
Caching in practice. With event-based invalidation, saving or deleting an entry or term flushes the sitemap cache immediately. That means the standard editorial loop (edit → publish → check sitemap) works without cache:clear. Keep it that way; don't disable caching "to be safe."
Testing. The 2.0 release ships unit + feature tests across PHP 8.2–8.4 / Statamic 5–6, so you can write assertions against generated XML in your own suite with reasonable confidence the addon contract is stable across versions.
The upgrade will bite you in three places. Audit for all three before bumping:
priority is a float. Any === '1.0', string comparison, or concatenation like $entry->priority . '.0' breaks silently.lastmod is nullable. The 1.x crash is fixed, but your templates must handle null too — {{ $entry->lastmod?->format('Y-m-d') }} or a conditional, not an unguarded ->format().SitemapEntry constructor signature changed. Positional arguments are the dangerous form; switch to named arguments and the migration is mostly mechanical.Generator no longer extends Facade. Any code calling Generator statically (or relying on the facade-ish behaviour) now fails. Inject the generator or resolve it from the container instead.
Event invalidation has edges. It fires on entry and term save/delete. It does not know about:
php artisan cache:clear or explicitly forget the sitemap key after a bulk job)expire becomes a real staleness window.The one-day expire is only safe with invalidation working. If you're on a shared cache store and something bypasses the events, you now wait up to 24 hours instead of 60 seconds for self-healing. When debugging "sitemap is stale," check cache:clear first — if that fixes it, the events aren't reaching your cache store; if it doesn't, look at static caching / CDN / nginx micro-caching in front of the route.
XML whitespace. The <priority> element is now clean because the value is computed in PHP. If you override the template and reintroduce loop whitespace, you'll get <priority>\n 0.8\n</priority> — technically parseable, ugly in diffs, and some strict consumers dislike it.
Extension points worth knowing. Custom SitemapEntry subclasses for exotic sources, the config file for collection scoping, the sitemap template for namespaced XML additions, and the container-resolved generator for injecting entries from an event listener or controller. Prefer addEntries() from a listener over forking the package — query-level filtering in 2.0 keeps the built-in path fast, and you want to stay on it.
Debugging checklist. /sitemap.xml 404 → route/config not published. Empty sitemap → collection not listed, or entries are drafts/unpublished (they're filtered at query level now, so they won't even be hydrated — don't go looking for them in PHP). Wrong URLs → check the site-prefix handling before writing your own URL logic. Wrong priorities → check the float cast at your addEntries() boundary.
How can I help you explore Laravel packages today?