jeffersongoncalves/laravel-google-search-console
A Laravel package for integrating Google Search Console into your applications, making it easy to pull search performance data, manage sites, and query analytics directly from your Laravel codebase.
This is the initial release, so treat the assessment as a baseline.
Install and publish config
composer require <vendor>/<package>
php artisan vendor:publish --tag=<package>-config
Pick an auth path. Two options are typical for Search Console:
Critically, the service account email must be added as a user on the Search Console property (Settings → Users and permissions → Add user), not just granted IAM roles in Cloud. This is the single most common "why am I getting 403" cause.
Verify the property string. Search Console has two property flavours:
https://example.com/sc-domain:example.comThe string you pass to the package must match exactly what appears in the property selector, including trailing slash and scheme.
First use case — a smoke test. Run the smallest possible query: last 7 days of search analytics grouped by query, limited to 10 rows. Confirm you get data back before wiring anything into a dashboard or database. If it returns empty, check the property string and the service-account permission before assuming the integration is broken — a brand-new or low-traffic property legitimately returns nothing.
Where to look first in the source: the config file (for auth and default property), the main client/service class, and any DTO/collection classes returned by the three feature areas. The three surfaces exposed in 1.0.0 are search analytics, URL inspection, and sitemaps — read those three method groups and you've read the whole package.
Never call the API on page render. Search Console is a reporting API with quotas and multi-day data lag; live calls in a controller are the fastest way to a slow app and a quota incident. The canonical pattern is a scheduled ingest into your own table:
Schedule::command('gsc:sync')->dailyAt('05:00');
→ for each configured property
→ for each date in the missing window
→ fetch search analytics (dimensions: date, query, page, country)
→ upsert into search_analytics table
Because the API is dimension-aggregated rather than raw-log, upserting on (property, date, query, page, country, search_type) is idempotent and safe to re-run. That makes backfills and late-arriving data trivial.
Backfill in date slices, not one big range. Loop day-by-day or week-by-week. You get resumability (a failure only costs one slice), you stay under per-request row caps, and you avoid re-pulling 16 months every time a job dies halfway.
Re-pull a rolling window, not just yesterday. Data for the last 2–3 days keeps changing as Google finalises it. Sync today-5 → today every night so yesterday's numbers self-heal. Store a synced_at so you can prove freshness.
Use searchType deliberately. The same page/query pair has different numbers for web, image, video, news, and discover. Decide whether you store one row per type or filter to web only — but decide it before you write the schema, because retrofitting a type dimension into a unique index is painful.
URL Inspection as a diagnostic job, not a bulk crawl. The inspection endpoint is for "why isn't this page indexed" checks. Wrap it in a queued job triggered on demand or for pages that recently changed, cache the verdict with a TTL (a few hours to a day), and surface verdict, coverageState, lastCrawlTime, and googleCanonical vs userCanonical. The canonical mismatch field catches more real bugs than anything else in the response.
Sitemaps fit naturally into the deploy pipeline. Submit on release (or when a sitemap.xml changes) and list on a schedule to alert when lastDownloaded goes stale or errors/warnings climb. Delete is useful for cleaning up environments or retiring URLs. Treat submit as fire-and-forget in a queued job — never block a deploy on Google's response.
Multi-tenant shape. If you run many properties, drive everything from a config array or a sites table, and put the property identifier into every method call rather than relying on a global default. Wrap the loop in a per-property try/catch so one misconfigured property doesn't abort the sync for the other fifty.
startRow, and sanity-check totals against a coarser query (dimension = date only) to detect truncation.429 should requeue with delay, not retry immediately.https://example.com/ and sc-domain:example.com return different data sets and neither errors if you ask for something plausible-but-wrong — you just get nothing. Validate the property format early and log the property string on every call.whereDate on a UTC timestamp column will be off by a day at the boundary. Store the GSC date string as a date column and compare like-for-like.LIKE, and remember filters apply before aggregation, so combining a query filter with a query dimension can behave unexpectedly.Http::fake) rather than mocking package internals, so upgrades don't break your test suite.SearchConsoleRepository) so that any eventual signature change in the package touches one file rather than every controller and job.How can I help you explore Laravel packages today?