Site Migration Auditor
Compares a pre-migration crawl against the live post-migration site to verify that every indexable old URL 301-redirect…
Use this skill
$ npx skills add seoskills.sh/site-migration-auditorSKILL.md
---
name: Site Migration Auditor
description: Compares a pre-migration crawl against the live post-migration site to verify that every indexable old URL 301-redirects exactly once to a live, self-canonical equivalent, catching chains, loops, 302s, and 404s while diffing titles, canonicals, structured data, and indexability to score parity and rank traffic-at-risk URLs by GSC clicks. Use when the user is replatforming, launched a migration, sees post-launch traffic drops, or wants to validate redirects and SEO parity around a cutover.
category: audit
---
# Site Migration Auditor
AGENT ROLE: Autonomous migration-integrity agent. Walk each old URL's redirect chain (never auto-follow), fetch the live destination, diff SEO signals against the pre-migration record, score parity, join clicks to size traffic-at-risk, and emit the JSON in `references/output.schema.json`.
## OBJECTIVE
For a replatform, prove that each old indexable URL lands on the correct new page via a single clean 301, and that the new page preserves the title, canonical, structured data, and indexability that earned its traffic. Quantify what is broken and rank fixes by clicks at stake.
## INPUTS
- `old_crawl` (REQUIRED): JSON list of pre-migration page records `{url, title, canonical, jsonld_types|structured_data, internal_links}` (a crawler export). `results`-wrapped exports are unwrapped automatically.
- `new_crawl` (OPTIONAL): a post-migration crawl in the same shape; when present the destination signals are read from it instead of re-fetched.
- `gsc` (OPTIONAL): local JSON map `url -> clicks` for traffic weighting.
- `site` (OPTIONAL): GSC property (`sc-domain:example.com` or `https://example.com/`) to fetch clicks live instead of a file.
- `start` / `end` (OPTIONAL): ISO dates for the live GSC pull. Default = last 28 complete days ending 3 days ago (GSC lag).
- `max_urls` (OPTIONAL, default 5000), `max_hops` (OPTIONAL, default 10), `risk_threshold` (OPTIONAL, default 70): parity score below which a URL is at-risk, `concurrency` (OPTIONAL, default 6), `timeout` (OPTIONAL, default 15s).
## AUTHENTICATION (Search Console API — only if fetching clicks live)
1. Crawling needs NO credentials. IF neither `--gsc` nor `--site` is given THEN run without traffic weighting (`has_traffic_data=false`).
2. IF `--site` is given THEN REQUIRE env `GSC_OAUTH_TOKEN` (an OAuth 2.0 access token with scope `https://www.googleapis.com/auth/webmasters.readonly`). IF unset THEN STOP `error.code="AUTH_MISSING_CREDENTIALS"`.
3. Endpoint: `POST https://searchconsole.googleapis.com/webmasters/v3/sites/{urlEncoded site}/searchAnalytics/query` with `dimensions=["page"]`, `dataState="final"`. IF `403` THEN STOP `error.code="AUTH_NO_SITE_ACCESS"`.
## EXPECTED TOOL CALLS
- Run `scripts/migration_audit.py --old-crawl old.json [--new-crawl new.json] [--gsc clicks.json | --site {property}] [--max-urls 5000] [--risk-threshold 70]`.
- Per old URL: manual per-hop HEAD walk (redirects disabled), then one GET of the final 200 URL when its signals are not already in `--new-crawl`.
## PROCEDURE (deterministic)
STEP 1 — WALK each old URL hop-by-hop with redirects DISABLED; record `{url, status, location}` per hop; resolve relative `Location`; stop on a non-3xx status, a repeat (LOOP), or `max_hops` (TOO_LONG).
STEP 2 — CLASSIFY the redirect into exactly one `redirect_class`: `OK_301` (single 301/308 → 200), `PERSISTED_200` (URL unchanged, still 200), `CHAIN` (≥2 hops), `TEMPORARY_REDIRECT` (302/303/307), `LOOP`, `BROKEN_404`, `REDIRECT_TO_404`, `REDIRECT_TO_ERROR`, `FETCH_FAILED`, `OTHER`. Note `PROTOCOL_DOWNGRADE` (https→http) as a flag.
STEP 3 — PARITY DIFF (only when the destination is 200): read the new page's title, canonical, meta robots, and JSON-LD `@type` set (from `--new-crawl` or a live GET parsed with `html.parser`). Start at 100 and subtract: fatal redirect → 0; CHAIN/TEMPORARY −15; title token-Jaccard < 0.6 → −15 (`TITLE_CHANGED`); canonical not self-referential → −15 (`CANONICAL_DRIFT`); pre-migration `@type` set not a subset of new → −15 (`STRUCTURED_DATA_LOST`); `noindex` on target → −20 (`NOINDEX_ON_TARGET`); internal links collapsed >60% → −10. Floor at 0.
STEP 4 — TRAFFIC-AT-RISK: join clicks by URL; a URL is at-risk when `parity_score < risk_threshold` OR the redirect is fatal. `clicks_at_risk = clicks` for at-risk URLs.
STEP 5 — EMIT the per-URL ledger, a redirect distribution, average parity, and the `at_risk` list sorted by `clicks_at_risk` then parity deficit.
## RATE LIMITS & ERROR HANDLING
- Per-hop HEAD (GET fallback on 405/501). On network error retry ≤3 with backoff `2^attempt`; persistent → `redirect_class="FETCH_FAILED"` for that URL, batch continues.
- Concurrency capped at `--concurrency` (default 6) worker threads across URLs.
- GSC `429`/`5xx` → backoff `2^attempt` (max 5) then STOP `error.code="RATE_LIMITED"`.
## MISSING / INSUFFICIENT DATA
- No `--gsc`/`--site` → parity is still scored; `has_traffic_data=false` and every `clicks_at_risk=0` (ranking falls back to parity deficit). NEVER invent click counts.
- Destination 200 but signals unreadable → flag `TARGET_UNVERIFIABLE` (do not assume parity).
- `internal_links` diff only runs when both crawls carry integer counts; otherwise it is skipped, not guessed.
## OUTPUT
One JSON object per `references/output.schema.json`. No prose.
## FILES
- `scripts/migration_audit.py` — redirect walk, live signal fetch, parity diff, clicks join, risk ranking.
- `references/output.schema.json` — output contract.
Not using the CLI? Copy the SKILL.md and paste it straight into ChatGPT, Claude, or any agent.
Embed a badge
More in SEO Audits
seo-audit
When the user wants to audit, review, or diagnose SEO issues on their site. Also use when the user mentions "SEO audit," "technical SEO," "why am I not ranking," "SEO issues," "on-page SEO," "meta tags review," "SEO health check," "my traffic dropped," "lost rankings," "not showing up in Google," "site isn't ranking," "Google update hit me," "page speed," "core web vitals," "crawl errors," or "indexing issues." Use this even if the user just says something vague like "my SEO is bad" or "help with SEO" — start with an audit. For building pages at scale to target keywords, see programmatic-seo.
audit
Run a comprehensive SEO audit on a website covering technical health, on-page optimization, content quality, and backlink profile. Use when the user asks for an SEO audit, site review, SEO health check, "what's wrong with my SEO", website analysis, or a full diagnostic of their site's search performance. For speed-specific issues, see audit-speed. For technical crawl/index issues only, see diagnose-seo.
seoul-subway-arrival
Look up Seoul real-time subway arrival information with the official Seoul Open Data API. Use when the user asks when a train arrives, which trains are approaching a station, or how crowded Seoul subway timing looks right now.
seo-coach
Enter a friendly OpenSEO coach mode that explains workflows, recommends next steps, and helps users use agents, web search, scraping, and MCP data effectively.
paseo
Paseo reference for managing projects, workspaces, workspace scripts, agents, schedules, and heartbeats.
paseo-handoff
Hand off the current task to another agent with full context. Use when the user says "handoff", "hand off", "hand this to", or wants to pass work to another agent.