Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Sitemap Laravel Package

thepixeldeveloper/sitemap

Thepixeldeveloper/sitemap is a PHP package for building XML sitemaps and sitemap indexes with a fluent API. It integrates with Laravel to generate, cache, and serve search-engine-friendly sitemaps.

Deep Wiki
Context7

Getting Started

The package is a small, dependency-light object model for building sitemap XML: you assemble Urlset / Url (and optionally SitemapIndex / Sitemap) objects, then hand them to Output to render a string. There is no Laravel service provider, no config file, and no facade — treat it as a plain library you wrap yourself.

Install and pin it, because the package is archived and will not receive fixes:

composer require thepixeldeveloper/sitemap:^4.0

Where to look first, in this order:

  1. src/ for the actual class names — the public surface is tiny (Urlset, Url, Sitemap, SitemapIndex, Output, plus Image / Link for extensions). Confirm names against source before trusting any blog post; the API shifted between v2 and v4.
  2. tests/ for runnable usage examples — usually more accurate than the README, which lags releases.
  3. composer.json for the PHP constraint, so you know whether it runs clean on your PHP 8.x.

Minimal working example — this is the whole mental model:

use Thepixeldeveloper\Sitemap\Output;
use Thepixeldeveloper\Sitemap\Url;
use Thepixeldeveloper\Sitemap\Urlset;

$urlset = new Urlset();

$url = new Url('https://example.com/blog/hello-world');
$url->setLastMod(new \DateTime('2024-01-15 09:00:00'));
$url->setChangeFreq('weekly');
$url->setPriority(0.8);

$urlset->addUrl($url);

file_put_contents(
    public_path('sitemap.xml'),
    (new Output())->output($urlset)
);

First real use case: an Artisan command that regenerates public/sitemap.xml from your posts / pages tables. Do not generate it in a controller on every request — build it on a schedule, write it to disk (or a storage disk), and let nginx / your web server serve the static file with normal caching headers.

Implementation Patterns

Wrap the library in one service class. This is the single most important pattern given the package is archived: put every Urlset / Url / Output reference behind App\Sitemap\SitemapBuilder so a future swap to spatie/laravel-sitemap or hand-rolled XMLWriter is a one-file change.

namespace App\Sitemap;

use Thepixeldeveloper\Sitemap\{Output, Url, Urlset};

class SitemapBuilder
{
    public function build(): string
    {
        $urlset = new Urlset();

        foreach ($this->entries() as $entry) {
            $url = new Url($entry->loc);

            if ($entry->lastmod) {
                $url->setLastMod($entry->lastmod);
            }

            $url->setChangeFreq($entry->changefreq);
            $url->setPriority($entry->priority);

            $urlset->addUrl($url);
        }

        return (new Output())->output($urlset);
    }
}

Keep entries() as a generator (yield) over a cursor / lazy() query so you never hydrate 50,000 Eloquent models at once.

Split into a sitemap index once you approach the hard limits. Search engines cap a single sitemap at 50,000 URLs and 50 MB uncompressed. Emit one Urlset per content type or per 40k-URL chunk, write sitemap-posts-1.xml, sitemap-posts-2.xml, etc., then build a SitemapIndex whose Sitemap entries point at the absolute URLs of those files. The index itself counts against the same limits, so a sitemap index of sitemap indexes is a legitimate (and often necessary) third tier on very large sites.

Artisan command + scheduler is the natural home. php artisan sitemap:generate, registered in app/Console/Kernel.php (or routes/console.php on Laravel 11+) with ->daily(), or ->hourly() if content churns. For anything over a few tens of thousands of URLs, dispatch the generation to a queued job so the scheduler isn't blocked, and write to a temp path then rename() atomically so crawlers never fetch a half-written file.

Storage::disk('public')->put('sitemap.xml', $builder->build());

Serve robots.txt consistently. A sitemap nobody finds is wasted work:

Sitemap: https://example.com/sitemap.xml

Generate that line from config('app.url') rather than hardcoding it in the static file, otherwise staging copies advertise production URLs.

Images and hreflang. The package exposes Image and Link objects that you attach to a Url. Use Link with hreflang for localised variants and Image for the image sitemap extension — both are the reason people choose this package over a naive <?php echo "<urlset>" string builder. Attach them per-Url; they serialise as children of that <url> node.

Testing. Don't assert on the whole XML string — it's brittle and whitespace-sensitive. Assert structurally:

$xml = simplexml_load_string($builder->build());

$this->assertSame('urlset', $xml->getName());
$this->assertCount(3, $xml->url);
$this->assertSame('https://example.com/blog/hello-world', (string) $xml->url[0]->loc);

Better still, run xmllint --noout --schema sitemap.xsd in CI against the generated file — schema validation catches the escaping and date-format bugs that string assertions miss.

Gotchas and Tips

The package is archived — plan the exit. Last release April 2020 means: no PHP 8.2+ deprecation fixes, no security patches, no upstream support. On PHP 8.1+ expect dynamic-property and DateTime-related deprecation notices from transitive code; suppress them narrowly or pin to an older PHP. Set a calendar reminder to evaluate a replacement. It will keep working for years, but "keep working" is not the same as "supported."

Memory blows up before the URL count does. Each Url is a real object with DateTime, priority, changefreq, images, and links. 50,000 objects with attached images will exhaust a 256 MB memory_limit. Generate in chunks: build a Urlset, render it, write it, unset() it, next chunk. Never keep the whole site in one array.

Escape your URLs, then let the library escape them once. & in a query string must become &amp; in the XML. If you pre-escape (htmlspecialchars before new Url(...)) you get &amp;amp;. Pass raw URLs with proper percent-encoding in the path (rawurlencode each segment), and rely on the writer for XML escaping. Same trap with titles if you use the image extension.

lastmod must be a valid W3C datetime with a timezone. new DateTime() is fine; new DateTime('2024-01-15') gives you midnight in the app timezone, which can drift a day at the boundary. Use ->setTimezone(new DateTimeZone('UTC')) before serialising, and if you store timestamps in UTC (you should), convert once at the edge. A malformed lastmod is silently ignored by Google rather than erroring — you won't notice in testing.

Don't lie with lastmod / priority / changefreq. Google ignores priority and changefreq entirely now and treats an unreliable lastmod as a signal to distrust the whole sitemap. Populate lastmod from updated_at and leave the other two unset unless you have a specific reason.

All <loc> and index <sitemap><loc> values must be absolute. Relative paths produce a sitemap that validates against nothing and gets silently dropped. Build them from url() / config('app.url'), never from '/blog/x'.

Empty <urlset> is technically valid but pointless. If you have zero published URLs, skip writing the file (or write it and accept the Search Console "no URLs" warning) rather than shipping a broken-looking sitemap.

Trailing slashes and duplicate URLs. /blog/hello and /blog/hello/ are separate entries to a crawler. Canonicalise (lowercase host, strip default port, consistent trailing slash, drop tracking query params) in your entries() generator — the library will happily emit duplicates forever.

Extension points are thin, so make your own. There's no plugin registry. If you need a custom element the package doesn't model (video, news), either subclass the relevant class or — cleaner given the archive status — bypass the object model for that one sitemap and write it with XMLWriter directly. Keep both paths behind the same SitemapBuilder interface so callers never know the difference.

Debugging checklist when a crawler rejects the file: (1) curl -sI the URL and confirm Content-Type: application/xml and no gzip mismatch; (2) xmllint --noout public/sitemap.xml; (3) check the file starts with <?xml and has no PHP warning output above it — a single stray newline before the declaration invalidates the document; (4) confirm robots.txt points at the canonical host; (5) validate in Search Console, which reports per-URL errors the XSD won't.

Gzip if you can. Serving sitemap.xml.gz with Content-Encoding: gzip cuts bandwidth substantially on large indexes; the 50 MB limit applies to the uncompressed size, so compression buys transfer cost, not headroom.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource