Synthetic Industry

Inspectable example · updated 2026-10-11

Synthetic redirect test sheet: nine old addresses, a loop, a chain and an unrelated destination, before and after

A made-up old-to-new map, with its rules and a short test script printed in full, is checked before and after a fix. The sheet records status codes, hops and final address, and shows how each fault is named and fixed.

An example, not a customer case study. Scope and evidence limitations are described below.

A made-up site and the agreed map

This is a synthetic specification, not a client case and not a test of a real site. The site has six live pages (home, about, two service pages, one article and contact). Nine old addresses must be handled according to an agreed map: each goes to one named page, or is deliberately gone. The agreed destinations are: /about-us and /team to /about; /services/web-design and /services/design to /services/web; /services/seo-audit and /services/search to /services/seo; /blog/2019/tips to /blog/website-tips; /contact.html to /contact; and /products/blue-widget removed on purpose with status 410. The invented redirect rules for the site before and after the fix, and the script that follows them, are printed in the next section; the two sheets on this page are exactly that script's output.

  • A pass needs a final page that returns 200, one permanent redirect (301 or 308) and the agreed destination, or the agreed 410.
  • Codes list each hop in order, so 301>301 is a chain of two permanent redirects.

The rules and the script

Save this as a file and run it with Node; it needs nothing else. The rules are plain tables of address, status and destination, so you can edit one and see the sheet change. It follows each old address through the rules, records every status, and applies the pass rule above. It prints the sheet for the before rules under the word BEFORE, then the sheet for the after rules under AFTER; the two sheets below are those two blocks without the headings. It tests nothing real: no server, no network.

  • before is the invented current state; after is the corrected one.
  • An address with no rule is simply not redirected, so a missing address answers 404 unless it is one of the six live pages.

If the matrix is wider than the box, scroll horizontally to read every column. Keyboard: focus the matrix and use Left/Right.

// Invented site: six live pages return 200.
const live = ['/', '/about', '/services/web', '/services/seo', '/blog/website-tips', '/contact'];
// The agreed map: old address -> its destination, or 410 for "deliberately gone".
const agreed = {
  '/about-us': '/about',
  '/team': '/about',
  '/services/web-design': '/services/web',
  '/services/design': '/services/web',
  '/services/seo-audit': '/services/seo',
  '/services/search': '/services/seo',
  '/blog/2019/tips': '/blog/website-tips',
  '/contact.html': '/contact',
  '/products/blue-widget': 410,
};
// Rules: address -> [status, destination]. An address with no rule is not redirected.
const before = {
  '/about-us': [302, '/about'],
  '/team': [301, '/about-us'],
  '/services/web-design': [301, '/services/design'],
  '/services/design': [301, '/services/web'],
  '/services/seo-audit': [301, '/services/search'],
  '/services/search': [301, '/services/seo-audit'],
  '/blog/2019/tips': [301, '/'],
  '/contact.html': [301, '/contact'],
};
const after = {
  '/about-us': [301, '/about'],
  '/team': [301, '/about'],
  '/services/web-design': [301, '/services/web'],
  '/services/design': [301, '/services/web'],
  '/services/seo-audit': [301, '/services/seo'],
  '/services/search': [301, '/services/seo'],
  '/blog/2019/tips': [301, '/blog/website-tips'],
  '/contact.html': [301, '/contact'],
  '/products/blue-widget': [410, null],
};

// Follow one old address through the rules, recording each status.
function follow(rules, start) {
  const codes = [], seen = new Set([start]);
  let at = start, loop = false;
  while (rules[at]) {
    const [status, to] = rules[at];
    codes.push(status);
    if (status === 410) return { codes, hops: codes.length - 1, at, end: 410, loop, shown: 'gone (410)' };
    if (seen.has(to)) { loop = true; break; }
    seen.add(to);
    at = to;
  }
  const end = live.includes(at) ? 200 : 404;
  return { codes, hops: codes.length, at, end, loop, shown: loop ? 'loop' : at + ' ' + end };
}

// A pass: one permanent hop to the agreed page that answers 200, or the agreed 410 with no redirect.
function verdict(old, r) {
  const want = agreed[old], faults = [];
  if (r.loop) faults.push('loop');
  else if (want === 410) {
    if (r.end !== 410) faults.push('should be 410');
    if (r.hops > 0) faults.push('redirects before the 410');
  } else {
    if (r.codes.some((c) => [302, 303, 307].includes(c))) faults.push('temporary redirect');
    if (r.hops > 1) faults.push('chain of ' + r.hops);
    if (r.at !== want) faults.push(r.at === '/' ? 'unrelated destination' : 'wrong destination');
    if (r.end !== 200) faults.push('ends at ' + r.end);
  }
  return faults.length ? 'FAIL ' + faults.join(', ') : 'pass';
}

function sheet(rules) {
  const head = ['old address', 'codes', 'hops', 'final', 'verdict'];
  const rows = Object.keys(agreed).map((old) => {
    const r = follow(rules, old);
    return [old, r.codes.length ? r.codes.join('>') : '-', String(r.hops), r.shown, verdict(old, r)];
  });
  const w = head.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
  const line = (row) => row.map((c, i) => c.padEnd(w[i])).join(' | ').trimEnd();
  console.log([head, ...rows].map(line).join('\n'));
  console.log('pass ' + rows.filter((row) => row[4] === 'pass').length + ' of ' + rows.length);
}

console.log('BEFORE');
sheet(before);
console.log('\nAFTER');
sheet(after);

Before: what the test finds

Read the verdict column. The old about address uses a temporary redirect (302), so that move is not signalled as permanent. The old team address goes through a permanent hop and then that temporary one, so it fails twice over: a temporary redirect and a chain of two. The old web-design address takes two permanent hops to reach its page: a chain. The two search-related service addresses send each other round in a loop, so visitors never reach a page. One article goes to the home page, an unrelated destination, which Google warns can be treated as a soft error. The discontinued product has no rule and returns a plain not-found instead of the deliberate removal. Only two of the nine addresses pass.

  • Temporary redirect: change the code to permanent.
  • Chain: point the old address straight at the final page, and update the intermediate rule too.
  • Loop: break it by setting the true destination.
  • Unrelated destination: use the page that replaces the content.

If the matrix is wider than the box, scroll horizontally to read every column. Keyboard: focus the matrix and use Left/Right.

old address           | codes   | hops | final                     | verdict
/about-us             | 302     | 1    | /about 200                | FAIL temporary redirect
/team                 | 301>302 | 2    | /about 200                | FAIL temporary redirect, chain of 2
/services/web-design  | 301>301 | 2    | /services/web 200         | FAIL chain of 2
/services/design      | 301     | 1    | /services/web 200         | pass
/services/seo-audit   | 301>301 | 2    | loop                      | FAIL loop
/services/search      | 301>301 | 2    | loop                      | FAIL loop
/blog/2019/tips       | 301     | 1    | / 200                     | FAIL unrelated destination
/contact.html         | 301     | 1    | /contact 200              | pass
/products/blue-widget | -       | 0    | /products/blue-widget 404 | FAIL should be 410
pass 2 of 9

After: the same test on the corrected rules

The corrected rules send every old address, including the intermediate ones, to its final destination in one permanent hop, and return 410 for the removed product. The same script now passes all nine. Re-running it after each release is the point: a later routing change can bring a fault back, and a sheet with the previous result makes the difference obvious.

  • Keep the sheet with the rules as a regression test.
  • Add a sample of addresses outside the list, so a broad rule does not catch more than intended.

If the matrix is wider than the box, scroll horizontally to read every column. Keyboard: focus the matrix and use Left/Right.

old address           | codes | hops | final                  | verdict
/about-us             | 301   | 1    | /about 200             | pass
/team                 | 301   | 1    | /about 200             | pass
/services/web-design  | 301   | 1    | /services/web 200      | pass
/services/design      | 301   | 1    | /services/web 200      | pass
/services/seo-audit   | 301   | 1    | /services/seo 200      | pass
/services/search      | 301   | 1    | /services/seo 200      | pass
/blog/2019/tips       | 301   | 1    | /blog/website-tips 200 | pass
/contact.html         | 301   | 1    | /contact 200           | pass
/products/blue-widget | 410   | 0    | gone (410)             | pass
pass 9 of 9

Limits of this example

The rules, pages and results are invented, and the sheet only follows a small rule table; it does not test a server, a content network or a search engine, and it says nothing about ranking or indexing. Real sites also need canonical tags and sitemap entries to agree with the map, covered in the canonical guide. Nothing here means any site has been repaired for a client. The matching paid job is our address job (posted test price from £395, untested, up to 300 agreed addresses). Send the site, what changed and a rough count of addresses in your first enquiry, not logins.

Sources and limits