sitemappilot/laravel

Official Laravel SDK for SitemapPilot — Automated Google Search Console, Bing, and IndexNow delivery.

v1.0.2 2026-08-31 20:06 UTC

This package is auto-updated.

Last update: 2026-08-31 20:11:30 UTC


README

Official Laravel SDK for SitemapPilot — automated Google Search Console, Bing and IndexNow delivery for your sitemaps.

Your app already knows the moment a page is published. This package tells SitemapPilot about it, so the sitemap is regenerated and resubmitted without anyone opening a dashboard.

  • Requires: PHP 8.2+ · Laravel 10, 11, 12 or 13
  • License: MIT — see LICENSE
  • Changes: CHANGELOG.md

Installation

composer require sitemappilot/laravel

The service provider and the SitemapPilot alias are registered by package discovery. Publish the config only if you want to edit it directly — every value is readable from the environment:

php artisan vendor:publish --tag=sitemappilot-config

Configuration

Mint a token on the API & SDK Integrations page of your SitemapPilot dashboard. It is shown once, at the moment you generate it: SitemapPilot stores only a SHA-256 hash of it and cannot show it to you again. Lose it and you regenerate — which immediately invalidates the old one.

SITEMAPPILOT_API_KEY=sp_live_your_workspace_api_key
SITEMAPPILOT_PROPERTY_ID=12
Variable Default Purpose
SITEMAPPILOT_API_KEY Workspace API token, sent as Authorization: Bearer …
SITEMAPPILOT_PROPERTY_ID Default property for every call; overridable per call
SITEMAPPILOT_BASE_URL https://sitemappilot.com/api/v1 Change this only if you run SitemapPilot yourself
SITEMAPPILOT_TIMEOUT 10 HTTP timeout in seconds
SITEMAPPILOT_QUEUE_CONNECTION / SITEMAPPILOT_QUEUE_NAME default queue Where SendSitemapPingJob is dispatched

A token is scoped to one workspace. Calling an endpoint for a property in another workspace answers 403, not 404.

Usage

1. Ping URLs as your models change

Add AutoPingsSitemap to a model and tell it the public URL:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use SitemapPilot\Laravel\Traits\AutoPingsSitemap;

class Article extends Model
{
    use AutoPingsSitemap;

    public function getSitemapUrl(): ?string
    {
        return route('articles.show', $this->slug);
    }
}

Every saved and deleted event queues a SendSitemapPingJob, so nothing is added to the request the user is waiting on. Override shouldPingSitemap() to skip drafts, and getSitemapPropertyId() to route a model at a specific property.

The URL has to be on the property's own host. The API rejects the whole batch with a 422 — it does not quietly drop the strays — because IndexNow would refuse it anyway: the key file only exists on that host.

2. Ping URLs by hand

use SitemapPilot\Laravel\Facades\SitemapPilot;

SitemapPilot::pingUrls([
    'https://example.com/blog/my-new-post',
    'https://example.com/pricing',
]);

3. Regenerate and submit from a deploy script

SitemapPilot::generate();   // crawl the site again, hash the result
SitemapPilot::submit();     // push the current sitemap to Google and Bing

generate() returns 202 as soon as the crawl is queued; it does not wait for it. The crawl only runs on a property whose domain ownership has been verified in the dashboard (a DNS TXT record) — on an unverified property the refusal is recorded against the generation instead, where you can read it in the panel.

submit() answers 422 when the property has no sitemap URL yet.

4. Read status back

$status = SitemapPilot::status();
[
    'property_id'       => 12,
    'host'              => 'example.com',
    'sitemap_hostname'  => 'sitemap.example.com',
    'cloudflare_status' => 'active',
    'latest_generation' => [
        'id' => 91, 'status' => 'success', 'finished_at' => '',
        'url_count' => 540, 'content_hash' => '', 'auto_submit_decision' => '',
    ],
    'gsc_status' => [
        'fetched_at' => '', 'last_downloaded' => '', 'is_pending' => false,
        'errors_count' => 0, 'warnings_count' => 0, 'contents' => [...],
    ],
]

latest_generation and gsc_status are null until the property has been generated and Search Console has reported back.

5. Several properties from one app

SitemapPilot::property(42)->pingUrls(['https://tenant-domain.com/new-page']);
SitemapPilot::property(42)->submit();

property() returns a clone, so the configured default is left alone.

6. Artisan commands

php artisan sitemappilot:ping https://example.com/new-page https://example.com/pricing
php artisan sitemappilot:generate
php artisan sitemappilot:submit
php artisan sitemappilot:status

Each accepts --property= to override the configured property. They print the API's error and exit 1 rather than throwing.

7. Testing

use SitemapPilot\Laravel\Facades\SitemapPilot;

public function test_publishing_an_article_notifies_search_engines(): void
{
    SitemapPilot::fake();

    Article::create(['title' => 'My New Article', 'slug' => 'my-new-article']);

    SitemapPilot::assertPinged('https://example.com/articles/my-new-article');
}
Assertion
assertPinged($urls|$callback, $propertyId = null) URLs were pushed
assertNotPinged($urls, $propertyId = null) URLs were not pushed
assertGenerated($propertyId = null) A crawl was requested
assertSubmitted($propertyId = null) A submission was requested
assertNothingSent() Nothing at all left the app

SitemapPilot::fake([...]) takes canned responses keyed generate, submit, ping_urls and status.

Errors and rate limits

Every failure raises a subclass of SitemapPilot\Laravel\Exceptions\SitemapPilotException, which extends RuntimeException — so one catch covers the package, and code that already caught RuntimeException keeps working.

Exception Raised on Carries
ConfigurationException before any request — no API key, or no property ID
AuthenticationException 401 — token missing, wrong or regenerated status(), payload()
AuthorizationException 403 — token belongs to another workspace status(), payload()
ValidationException 422 — URL off-host, or nothing to submit errors() keyed by field
RateLimitException 429 — over the limit retryAfter() in seconds
ConnectionFailedException DNS, TLS, refused, or timeout getPrevious(), status() is null
SitemapPilotException any other non-2xx status(), payload(), body()

The API allows 60 requests per minute per token, plus a looser per-address backstop. Over the limit it answers 429 with a JSON body and a Retry-After header; retryAfter() reads that header back so you can actually back off:

use SitemapPilot\Laravel\Exceptions\RateLimitException;

try {
    SitemapPilot::pingUrls($urls);
} catch (RateLimitException $e) {
    $this->release($e->retryAfter() ?? 60);
}

SendSitemapPingJob sets $tries = 3, logs a warning and rethrows, so a transient failure is retried by your queue rather than lost.

Endpoints this package calls

Nothing else. The SDK is a wrapper around these four routes:

Method Path Success
POST /api/v1/properties/{id}/generate 202
POST /api/v1/properties/{id}/submit 200
POST /api/v1/properties/{id}/ping-urls 200
GET /api/v1/properties/{id}/status 200

All four take Authorization: Bearer <token> and nothing else — there is no session and no CSRF token involved.

Contributing

composer install
composer test

The suite runs on Orchestra Testbench and fakes the HTTP layer; no network and no SitemapPilot account are needed. It is the gate on a release tag, so it has to be green on a clean checkout, not just on yours.

Releasing is PUBLISHING.md.

License

MIT. See LICENSE.