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

indexnowkit/sitemap

A Laravel package for generating XML sitemaps and integrating with IndexNow to notify search engines of URL changes. It helps automate sitemap creation and keep search indexes up to date with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the release and publish the config:

composer require indexnowkit/laravel:^0.9.0
php artisan vendor:publish --tag=indexnow-config

Set your IndexNow key, host, and adapter in config/indexnow.php, then do the smallest possible run first:

php artisan sitemap --dry-run   # see exactly which URLs would be announced
php artisan sitemap             # announce them

Your first real use case is the deploy hook: run php artisan sitemap after a release so search engines learn about the pages that changed. The command reads the sitemap adapter you configured, applies the host filter, and submits in batches.

Where to look first in the source, in this order:

  1. Console\SitemapOptions — the single object describing a run ($newOnly, plus the window options). Everything the command can do is visible here.
  2. SitemapServices::runner(...) — the seam you use when you want to invoke a submission from your own code instead of the CLI.
  3. Sitemap\SeenStoreInterface — new in 0.9.0, and the thing you will most likely have to implement yourself (see below).

Implementation Patterns

Seed the store with a full run, then schedule --new-only. The store is what makes --new-only meaningful, and remember() is called on every non-dry run, with or without the option. So the intended shape is:

// once, and after any structural change to the sitemap
Schedule::command('sitemap')->dailyAt('02:00');

// cheap incremental runs the rest of the day
Schedule::command('sitemap --new-only')
    ->everyFifteenMinutes()
    ->withoutOverlapping();

A full run followed by scheduled --new-only runs announces each change exactly once.

Implement SeenStoreInterface and bind it. The framework adapters ship without a store in this wave, so this is the pattern most Laravel apps will need:

final class EloquentSeenStore implements \Indexnowkit\Sitemap\SeenStoreInterface
{
    public function unseen(iterable $entries): iterable
    {
        $known = SeenUrl::query()
            ->whereIn('url', array_map(fn ($e) => $e->url, iterator_to_array($entries, false)))
            ->pluck('fingerprint', 'url');

        foreach ($entries as $entry) {
            if (($known[$entry->url] ?? null) !== $this->fingerprint($entry)) {
                yield $entry;
            }
        }
    }

    public function remember(iterable $entries): void
    {
        foreach ($entries as $entry) {
            SeenUrl::updateOrCreate(
                ['url' => $entry->url],
                ['fingerprint' => $this->fingerprint($entry)],
            );
        }
    }

    private function fingerprint($entry): string
    {
        return $entry->lastmod?->format(\DateTimeInterface::ATOM) ?? '';
    }
}

Bind it in a service provider so both the console command and SitemapServices::runner() pick it up, and pass it explicitly when you drive the runner yourself:

SitemapServices::runner(..., seen: app(SeenStoreInterface::class))->run();

Stack --changed-since with --new-only. They compose: the time window is applied first, then the store filters what survives. That gives you "only things modified in the last day, and only if I haven't already announced them" — the right default for a high-frequency schedule.

Use the summary line as a metric. The output gains , N new or changed. Log or alert on that number; a sudden jump usually means <lastmod> churn, not a genuine content change.

Keep --dry-run in CI and on staging. It never calls remember(), so it is safe to run repeatedly, and it answers "what would this deploy announce?" before the real submission goes out.

Drive it programmatically for multi-host or per-tenant sites. Use Console\SitemapOptions to construct the run ($newOnly, window) and SitemapServices::runner(..., seen: ...) to execute it, passing a store scoped to the tenant.

Gotchas and Tips

  • --new-only is declared on every adapter but only works where a store is bound. Console\Definitions::sitemap() registers the option unconditionally, so it appears in --help for the Laravel adapter even though the framework adapters ship no store in 0.9.0. Without one you get SitemapRunner::NO_SEEN_STORE — --new-only needs a store of seen URLs; this application has none... — and exit code 2, with nothing read. If your scheduler already passes --new-only, bind a store before deploying this release or the job starts failing loudly.

  • Exit 2 is not exit 1. Distinct code, distinct cause: no store. Don't fold it into a generic "command failed" alert, and don't retry it — it will fail identically every time until a store is bound.

  • Skipping full runs silently starves the store. Because remember() fires on full runs too, they are not "wasted" submissions — they are what populates the fingerprints. A deployment that only ever runs --new-only with an empty store treats everything as new on the first pass (fine) but never establishes ground truth if the store is later truncated.

  • --dry-run never remembers, even with --new-only. Good for previews, but it means a "successful" dry run does not advance the store; the next real run announces everything the dry run showed you.

  • remember() is per batch and only for batches that did not fail. Partial failures are retried naturally on the next run, but expect a batch-sized wobble in the "new or changed" count after an engine hiccup. Don't mark URLs as seen in your own code on the way in — let the runner own it.

  • Fingerprints are URL + <lastmod> in ISO 8601, or empty. Sitemaps that stamp lastmod with the current time on every render make every URL look new on every run, which defeats the entire feature. Normalise lastmod to real content modification times before this is worth scheduling. URLs with no <lastmod> fingerprint to empty and compare stably — a URL that gains a lastmod will read as changed once.

  • You own store hygiene. The interface is unseen() / remember() only — there is no pruning, eviction, or "forget deleted URLs" method. If URLs are removed from your sitemap they stay in your table forever; add a periodic cleanup keyed off the current sitemap.

  • Use withoutOverlapping() on the scheduled command. Two concurrent runs against the same store can both see an entry as unseen and announce it twice.

  • 0.9.0 requires indexnowkit/core ^0.13.1. If you had pinned core directly, bump it in the same commit; a stale core against the new adapter is the most likely source of odd errors after upgrading.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings