Search by

siberfx / google-indexing

siberfx

Notify Google (Indexing API + Search Console sitemaps) and IndexNow search engines (Bing, Yandex, Naver, Seznam, Yep) about new, updated and deleted URLs. Reads your sitemap.xml, auto-hooks into Eloquent models, queues everything.

Package info

github.com/siberfx/google-indexing

pkg:composer/siberfx/google-indexing

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.1 2026-10-29 18:52 UTC

This package is auto-updated.

Last update: 2026-10-29 18:52:46 UTC


README

Latest Version on Packagist Tests Total Downloads PHP Version Laravel License

Tell search engines about your pages the moment they change.

  • Google — Indexing API (URL_UPDATED / URL_DELETED) and sitemap submission through the Search Console API.
  • Bing, Yandex, Naver, Seznam, Yep … — one call via IndexNow.
  • Reads your sitemap.xml (sitemap indexes, .xml.gz, <lastmod> filtering) and submits every URL.
  • Auto-injecting — add one trait to an Eloquent model and it notifies engines on create / update / restore / delete. The IndexNow key file route and the scheduler entry register themselves.
  • Queued, de-duplicated and retried. No Google SDK needed — OAuth is done with openssl + Laravel's HTTP client.

Requires PHP 8.3+ (8.3, 8.4 and 8.5 are tested) and Laravel 12 / 13.

Heads-up about "pinging". Google (2023) and Bing retired their …/ping?sitemap= URLs, so pinging them does nothing. This package does what works today: it submits the sitemap file via the Search Console API, and the individual URLs via the Indexing API and IndexNow. Google officially supports the Indexing API for JobPosting and BroadcastEvent pages; other page types are accepted by the API but Google gives no ranking guarantee.

Installation

composer require siberfx/google-indexing
php artisan indexing:install

indexing:install generates an IndexNow key, appends the env keys to .env, and checks your Google credentials. Everything is auto-discovered — there is no provider to register. Publish the config only if you want to change it:

php artisan indexing:install --publish-config

Environment

GOOGLE_INDEXING_ENABLED=true                 # defaults to true only in production
GOOGLE_INDEXING_CREDENTIALS=storage/app/google-service-account.json   # path, or the raw JSON
INDEXNOW_KEY=0f1e2d3c4b5a69788796a5b4c3d2e1f0                         # generated by indexing:install
GOOGLE_INDEXING_ENGINES=google,indexnow      # drop one to disable it
GOOGLE_INDEXING_SCHEDULE=true                # auto-run indexing:sitemap daily
GOOGLE_INDEXING_HISTORY=false                # store every submission (needs the migration)
BING_WEBMASTER_API_KEY=                      # optional, only if you add `bing` to GOOGLE_INDEXING_ENGINES

Run php artisan indexing:doctor --live after setup. It checks credentials, Search Console ownership, the IndexNow key file and the sitemap.

Where to get the credentials

Service What you need Where to get it Env variable
Google Indexing API Service-account JSON key Google Cloud Console GOOGLE_INDEXING_CREDENTIALS
Google sitemap submission Same service account + Search Console ownership Search Console GOOGLE_INDEXING_SITE_URL
IndexNow (Bing, Yandex, Naver, Seznam, Yep) A self-generated key — no account php artisan indexing:install INDEXNOW_KEY

Google (Indexing API + Search Console) — step by step

  1. Create or pick a project at https://console.cloud.google.com/projectcreate.
  2. Enable the APIs (APIs & Services → Library):
  3. Create a service account: IAM & Admin → Service Accounts → Create service account. No project roles are needed.
  4. Create the key: open the service account → Keys → Add key → Create new key → JSON. A file such as my-project-1234abcd.json downloads. This is your credentials file.
  5. Store it outside the web root, e.g. storage/app/google-service-account.json (add it to .gitignore), and set:
    GOOGLE_INDEXING_CREDENTIALS=storage/app/google-service-account.json
    You may paste the raw JSON into the variable instead (useful on platforms without file storage).
  6. Grant access in Search Console — this step is required, otherwise every call returns 403 Permission denied:
    • Open https://search.google.com/search-console → select your property → Settings → Users and permissions → Add user.
    • Paste the service account's e-mail (the client_email value in the JSON, …@…iam.gserviceaccount.com; php artisan indexing:install prints it) and set permission to Owner.
  7. Domain property? If you verified sc-domain:example.com rather than a URL-prefix property, set:
    GOOGLE_INDEXING_SITE_URL=sc-domain:example.com
    Otherwise it defaults to your APP_URL.

Quota: the Indexing API allows 200 publish requests/day by default; request more under IAM & Admin → Quotas.

IndexNow (Bing, Yandex, Naver, Seznam, Yep)

No signup. IndexNow proves you own the site with a key file:

  1. Run php artisan indexing:install — it writes INDEXNOW_KEY=<random 32 chars> to .env. (You can also use any 8–128 character a-z A-Z 0-9 - string, or generate one at https://www.bing.com/indexnow/getstarted.)
  2. Deploy. The package serves the key at https://your.site/{INDEXNOW_KEY}.txt automatically — no file to upload.
  3. Check it opens in a browser and shows the key.

Submitting to the shared endpoint (api.indexnow.org) notifies all participating engines. Optionally, view your submissions in Bing Webmaster Tools.

Usage

Facade

use Siberfx\GoogleIndexing\Facades\GoogleIndexing;

// Created or updated
GoogleIndexing::publish('https://example.com/blog/hello-world');

// Several at once, only to IndexNow
GoogleIndexing::publish($urls, only: ['indexnow']);

// Removed
GoogleIndexing::delete('https://example.com/blog/old-post');

// Don't block the request — send via the queue
GoogleIndexing::queue(route('posts.show', $post));

// Inspect what happened
foreach (GoogleIndexing::publish($url) as $result) {
    $result->engine;     // "google" | "indexnow"
    $result->accepted;   // ['https://…']
    $result->failed;     // ['https://…' => 'Permission denied']
    $result->successful();
}

// When did Google last hear about this URL? (Indexing API — only URLs submitted through it)
GoogleIndexing::status('https://example.com/blog/hello-world');

// Is it actually indexed? (Search Console URL Inspection — works for any URL)
$check = GoogleIndexing::inspect('https://example.com/blog/hello-world');
$check['indexed'];   // true | false
$check['coverage'];  // e.g. "Submitted and indexed"

Engine results also expose what to do next: $result->retryable lists URLs that failed temporarily (429, 5xx, network), and $result->deferred lists URLs held back because the daily Google quota is used up. Neither is counted as a permanent failure.

Sitemap

// Uses config('google-indexing.sitemap.url') or {APP_URL}/sitemap.xml
GoogleIndexing::submitSitemap();

// Only pages modified in the last 24 hours, via the queue
GoogleIndexing::submitSitemap(since: '1 day', queue: true);

// A different sitemap, or a local file
GoogleIndexing::submitSitemap('https://example.com/news-sitemap.xml');

// Just read the URLs
GoogleIndexing::sitemapUrls(since: now()->subWeek());

submitSitemap() also registers the sitemap file itself with Google Search Console (when credentials are present). URLs with no <lastmod> are always included.

Eloquent models (auto-injecting)

use Illuminate\Database\Eloquent\Model;
use Siberfx\GoogleIndexing\Concerns\Indexable;

class Post extends Model
{
    use Indexable;

    public function indexableUrl(): ?string
    {
        return route('posts.show', $this);
    }

    // Optional: drafts are not announced
    public function shouldBeIndexed(): bool
    {
        return $this->published_at?->isPast() ?? false;
    }
}

That's it. created and updated send URL_UPDATED, restored too, deleted (including soft deletes) sends URL_DELETED. Writes that only touch updated_at are ignored (override ignoredIndexingAttributes() to change that). Jobs are queued after the DB transaction commits and identical bursts collapse into one job for 60 seconds.

Manual trigger: $post->requestIndexing(); or $post->requestIndexing(UrlAction::Deleted);

Artisan

php artisan indexing:sitemap                       # read sitemap, notify everyone
php artisan indexing:sitemap --dry-run             # list URLs, send nothing
php artisan indexing:sitemap --since="1 day" --queue
php artisan indexing:sitemap --url=https://example.com/news.xml --only=indexnow

php artisan indexing:publish https://example.com/a https://example.com/b
php artisan indexing:publish https://example.com/gone --delete
php artisan indexing:status https://example.com/a
php artisan indexing:inspect https://example.com/a          # is it indexed? (--json for raw output)
php artisan indexing:report --hours=24                     # submissions per engine + latest failures (needs history)
php artisan indexing:doctor [--live]                       # check the whole setup

Scheduling

Set GOOGLE_INDEXING_SCHEDULE=true and the package schedules indexing:sitemap itself (default dailyAt:03:00, configurable in sitemap.schedule.frequency, e.g. hourly, daily, dailyAt:05:30).

Two settings keep the daily run inside Google's 200 requests/day quota:

  • sitemap.dedupe (on by default) skips URLs already submitted that have not changed since. A URL is sent again after sitemap.resubmit_after (30 days), or when its <lastmod> is newer than the last submission.
  • GOOGLE_INDEXING_ONLY_CHANGED_WITHIN="1 day" sends only pages modified recently.

URLs that do not fit in today's Google quota are deferred and sent on a later run. The count is kept across runs and reset at midnight Pacific time.

Failed submissions are retried automatically, but only the URLs that failed temporarily (429, 5xx, network). Permanent rejections such as 403 Permission denied are not retried.

Events

Every engine submission dispatches Siberfx\GoogleIndexing\Events\UrlsSubmitted ($result, $action) — log it, alert on failures, store it:

Event::listen(UrlsSubmitted::class, function ($e) {
    if ($e->result->failed) {
        Log::warning('Indexing failed', $e->result->failed);
    }
});

History (optional)

GOOGLE_INDEXING_HISTORY=true
php artisan vendor:publish --tag=google-indexing-migrations
php artisan migrate

Every engine result is then stored in google_indexing_submissions (engine, action, url, status accepted|failed|deferred|skipped, error). Use indexing:report or query the table directly.

Custom engines

use Siberfx\GoogleIndexing\Contracts\Engine;

final class MyEngine implements Engine { /* name(), isConfigured(), submit() */ }

// config/google-indexing.php
'engines' => ['google' => …, 'indexnow' => …, 'mine' => MyEngine::class],
'use'     => ['google', 'indexnow', 'mine'],

Testing your app

Everything goes through Laravel's HTTP client and bus, so use the fakes:

Http::fake(['indexing.googleapis.com/*' => Http::response([]), 'oauth2.googleapis.com/*' => Http::response(['access_token' => 'x']), 'api.indexnow.org/*' => Http::response('')]);
Bus::fake();

$post->update(['title' => 'New']);

Bus::assertDispatched(SubmitUrlsJob::class, fn ($job) => $job->urls === [route('posts.show', $post)]);

Set GOOGLE_INDEXING_ENABLED=false in .env.testing to make the package a no-op.

Configuration reference

See config/google-indexing.php — every option is commented.

Testing this package

composer install && vendor/bin/pest

License

MIT