Open source · MIT · zero dependencies

Test your site migration before Google does.

Check a redirect map or an old sitemap against the live site. Catch broken 301s, chains, loops and redirects that land on pages Google can't index, from your terminal or in CI.

npx redirect-guard redirects.csv --base https://www.example.com

Node.js 20.3 or later. No install step, no account, no telemetry.

redirect-guard 0.1.0 · 10 URLs from redirects.csv

 FAIL  /chi-siamo                  redirects.csv:2
         301  /chi-siamo
       →   /about/
       → 200  /about
         expected /team
         ✖ wrong-destination     Ends at /about, expected /team
         ✖ status-not-permanent  Hop 2 returned 302, expected 301 or 308
                 2 hops, limit is 1

 FAIL  /servizi/seo                redirects.csv:3
         301  /servizi/seo
       → 200  /services/seo
         ✖ target-noindex  <meta name="robots" content="noindex,follow">

 WARN  4 URLs end at /             redirect-collapse
         /blog/old-post, /shop/categoria-a, /p/123, /tag/seo
         Old pages redirected to the homepage may be treated as soft 404s.

──────────────────────────────────────────────────
 10 checked · 2 ok ·  · 3 fail · 0.1s
 ✖ 3 entries failed. Exit code 1.
Excerpt of npm run demo, which runs the CLI against a local test server.

Migrations lose traffic in quiet ways

After a rebuild, a re-platform or a domain move, old URLs keep their rankings only if every one of them lands, permanently and directly, on a page that can be indexed. These are the mistakes that slip through.

Missing redirects

An old URL answers 404, or still serves the old page when it should redirect.

/chi-siamo → 404

Temporary instead of permanent

A 302 or 307 where a 301 or 308 belongs, on the first hop or hidden later in the chain.

/prodotti → 302 → /shop

Chains and loops

Each extra hop slows crawling and users. Loops never resolve at all.

/a → /a/ → /b → /b/

Targets that can't be indexed

The redirect works, but the destination is noindex, canonicalised elsewhere, blocked by robots.txt or not 200.

→ 200 · noindex

Everything to the homepage

Dozens of old pages sent to /. Google may treat them as soft 404s and drop them.

12 URLs → /

Small inconsistencies

Lost query strings, http targets on an https site, www and non-www mixed together.

/p?id=3 → /product

How it works

One command, run before launch against staging and again after DNS cutover.

  1. Give it the old URLs

    A CSV, TSV or JSON redirect map, a plain list, or simply the sitemap you saved from the old site.

  2. It follows every hop

    Each redirect is requested by hand, so the full chain is recorded: status, Location and timing. Then it checks the final page's headers, meta robots, canonical and robots.txt.

  3. You get a verdict

    A readable report, plus JSON, Markdown, JUnit or GitHub annotations for CI. Exit code 1 on failures, so it can block a deploy.

Quickstart

No install needed. Write a two-column CSV, or point it at an old sitemap.

redirects.csv
from,to,status
/chi-siamo,/about,
/servizi/seo,/services/seo,
/vecchia-promo,,410
terminal
npx redirect-guard redirects.csv --base https://www.example.com

# no map? use the old sitemap
npx redirect-guard --sitemap old-sitemap.xml --base https://www.example.com

An empty to with status 410 or 404 means "removed on purpose": the check passes only if the page is really gone.

What it checks

Every rule has a stable id, a default severity you can change, and a section in the docs explaining why it matters and how to fix it.

RuleDefaultFires when
Redirects
no-redirecterrorAn old URL answers 2xx instead of redirecting
status-not-permanenterrorA hop uses 302/307 where 301/308 is expected
wrong-destinationerrorThe chain ends somewhere other than the expected target
redirect-looperrorA URL repeats in the chain
too-many-redirectserrorThe chain exceeds the hard limit
chain-too-longwarnMore hops than allowed
trailing-slash-hopwarnA hop only adds or removes a trailing slash
Destination
target-not-okerrorThe final page is not 200
target-noindexerrornoindex in X-Robots-Tag or meta robots
canonical-mismatcherrorThe canonical points to a different URL
robots-blockederrorThe new robots.txt disallows the final URL
robots-unreachablewarnrobots.txt returns 5xx or fails
unexpected-statuserrorA page marked as removed does not return 404/410
Migration
redirect-to-homepagewarnA deep URL ends at the homepage
redirect-collapsewarnMany old URLs end at the same page
query-droppedwarnThe query string is lost on the way
protocol-inconsistentwarnAn http target on an https site
host-inconsistentwarnwww and non-www are mixed
location-relativeoffInformational: a relative Location header
Sitemap diff
orphan-urlerrorAn old URL is gone from the sitemap and not redirected
sitemap-url-redirectserrorA URL in the new sitemap redirects
sitemap-url-not-indexableerrorA URL in the new sitemap is not 200, noindex or blocked
Network
request-failederrorDNS, TLS, reset or timeout after retries

Full reference: docs/rules.md

Staging first, then CI

Test your production redirect map against a staging host before DNS changes, then keep it as a regression test on every deploy.

before cutover
STAGING_AUTH="user:pass" npx redirect-guard redirects.csv \
  --host-rewrite https://www.example.com=https://staging.example.com \
  --auth-env STAGING_AUTH
.github/workflows/redirects.yml
- name: Check redirects
  run: >
    npx redirect-guard@0 redirects.csv --base "$PREVIEW_URL"
    --reporter text --reporter markdown:rg.md
    --reporter junit:rg.xml
- name: Summary
  if: always()
  run: cat rg.md >> "$GITHUB_STEP_SUMMARY"

Credentials and custom headers are sent only to the origins you are testing. If a redirect points to a CDN or a third-party domain, that request goes out without them, and header values never appear in reports.

Principles

  • Zero runtime dependencies. Node built-ins only, enforced by a test.
  • No telemetry, ever. It talks only to the URLs you give it, their redirects and the destination's robots.txt.
  • Polite by default. 4 parallel requests, optional per-host delay, retries that respect Retry-After.
  • Stable contract. Rule ids, exit codes and the JSON schema follow semantic versioning.
  • MIT licensed. Use it in client work, fork it, ship it.

Honest limits

  • It reads raw HTML. Meta tags injected by JavaScript are not seen.
  • Bot protection may answer 403 to non-browser clients. Allowlist your runner or test staging. It never tries to bypass challenges.
  • It checks the URLs you supply. It does not crawl to discover new ones.

Details: docs/limitations.md