Missing redirects
An old URL answers 404, or still serves the old page when it should redirect.
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
redirect-guard 0.1.0 · 10 URLs from redirects.csv FAIL /chi-siamo redirects.csv:2 301 /chi-siamo → 302 /about/ → 200 /about expected /team ✖ wrong-destination Ends at /about, expected /team ✖ status-not-permanent Hop 2 returned 302, expected 301 or 308 ▲ chain-too-long 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 · 5 warn · 3 fail · 0.1s ✖ 3 entries failed. Exit code 1.
npm run demo, which runs the CLI against a local test server.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.
An old URL answers 404, or still serves the old page when it should redirect.
A 302 or 307 where a 301 or 308 belongs, on the first hop or hidden later in the chain.
Each extra hop slows crawling and users. Loops never resolve at all.
The redirect works, but the destination is noindex, canonicalised elsewhere, blocked by robots.txt or not 200.
Dozens of old pages sent to /. Google may treat them as soft 404s and drop them.
Lost query strings, http targets on an https site, www and non-www mixed together.
One command, run before launch against staging and again after DNS cutover.
A CSV, TSV or JSON redirect map, a plain list, or simply the sitemap you saved from the old site.
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.
A readable report, plus JSON, Markdown, JUnit or GitHub annotations for CI. Exit code 1 on failures, so it can block a deploy.
No install needed. Write a two-column CSV, or point it at an old sitemap.
from,to,status /chi-siamo,/about, /servizi/seo,/services/seo, /vecchia-promo,,410
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.
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.
| Rule | Default | Fires when |
|---|---|---|
| Redirects | ||
no-redirect | error | An old URL answers 2xx instead of redirecting |
status-not-permanent | error | A hop uses 302/307 where 301/308 is expected |
wrong-destination | error | The chain ends somewhere other than the expected target |
redirect-loop | error | A URL repeats in the chain |
too-many-redirects | error | The chain exceeds the hard limit |
chain-too-long | warn | More hops than allowed |
trailing-slash-hop | warn | A hop only adds or removes a trailing slash |
| Destination | ||
target-not-ok | error | The final page is not 200 |
target-noindex | error | noindex in X-Robots-Tag or meta robots |
canonical-mismatch | error | The canonical points to a different URL |
robots-blocked | error | The new robots.txt disallows the final URL |
robots-unreachable | warn | robots.txt returns 5xx or fails |
unexpected-status | error | A page marked as removed does not return 404/410 |
| Migration | ||
redirect-to-homepage | warn | A deep URL ends at the homepage |
redirect-collapse | warn | Many old URLs end at the same page |
query-dropped | warn | The query string is lost on the way |
protocol-inconsistent | warn | An http target on an https site |
host-inconsistent | warn | www and non-www are mixed |
location-relative | off | Informational: a relative Location header |
| Sitemap diff | ||
orphan-url | error | An old URL is gone from the sitemap and not redirected |
sitemap-url-redirects | error | A URL in the new sitemap redirects |
sitemap-url-not-indexable | error | A URL in the new sitemap is not 200, noindex or blocked |
| Network | ||
request-failed | error | DNS, TLS, reset or timeout after retries |
Full reference: docs/rules.md
Test your production redirect map against a staging host before DNS changes, then keep it as a regression test on every deploy.
STAGING_AUTH="user:pass" npx redirect-guard redirects.csv \ --host-rewrite https://www.example.com=https://staging.example.com \ --auth-env STAGING_AUTH
- 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.
Details: docs/limitations.md