← back to Open Seo

badseo/src/fixtures/redirects.ts

109 lines

import type { Fixture } from "./types";
import { redirect, htmlResponse, renderPage } from "../lib";
import { article } from "./helpers";

const CAT = "Redirects";

/**
 * Canonical path for the trailing-slash fixture. The SLASH form is the
 * canonical 200; the non-slash form 301-redirects to it (see the intercept in
 * index.ts). Exported so the Worker and the fixture agree on the path.
 */
export const TRAILING_SLASH_CANONICAL = "/redirect/trailing-slash";

// 22 — redirect chain: /chain-1 -> /chain-2 -> / (homepage) ----------------
// Two hops before content is a chain. The crawler records each hop as its own
// row and flags the head of the chain.
const redirectChain: Fixture = {
  path: "/redirect/chain-1",
  category: CAT,
  name: "Redirect chain (3 hops)",
  summary:
    "/redirect/chain-1 redirects to chain-2, which redirects to the homepage. Two hops to reach content.",
  lesson:
    "Every extra hop adds a little delay, loses a little link strength, and uses crawl budget. Point the first URL straight at the final page.",
  expectedIssues: ["redirect-chain"],
  inSitemap: false,
  handler: () => redirect("/redirect/chain-2", 301),
};

// Support: the middle hop. Not featured anywhere; found via chain-1.
const redirectChainMid: Fixture = {
  path: "/redirect/chain-2",
  category: CAT,
  name: "Redirect chain (middle hop)",
  summary: "The middle of the redirect chain. Redirects on to the homepage.",
  lesson: "",
  expectedIssues: [],
  support: true,
  linkedFromCatalog: false,
  inSitemap: false,
  handler: () => redirect("/", 301),
};

// 23 — redirect loop: a URL that redirects to itself ----------------------
const redirectLoop: Fixture = {
  path: "/redirect/loop",
  category: CAT,
  name: "Redirect loop",
  summary: "/redirect/loop redirects to itself, so it never resolves.",
  lesson:
    "A redirect that points back to itself never reaches a real page. Browsers and crawlers give up with an error.",
  expectedIssues: ["redirect-loop"],
  inSitemap: false,
  // Location resolves to this same URL, a self-redirect the crawler can never
  // satisfy, which is a redirect loop.
  handler: () => redirect("/redirect/loop", 302),
};

// 24 — trailing-slash canonical (redirect-cycle trap) ---------------------
// The canonical URL ends in a slash (/redirect/trailing-slash/ = 200); the
// non-slash form 301-redirects to it, exactly like WordPress and most CMSes.
// A crawler that normalizes away trailing slashes turns /redirect/trailing-slash/
// back into /redirect/trailing-slash, follows the 301 to the slash form, strips
// it again, and loops — the 508 "Loop Detected" class of bug from
// https://github.com/every-app/open-seo/pull/61. The audit must crawl the
// canonical page once as a 200 and NOT report a redirect loop, so this fixture
// expects zero issues. If it ever comes back with redirect-loop (or an error),
// the trailing-slash handling has regressed.
const trailingSlashCanonical: Fixture = {
  path: TRAILING_SLASH_CANONICAL,
  category: CAT,
  name: "Trailing-slash canonical (redirect-cycle trap)",
  summary:
    "The canonical URL ends in a slash; the non-slash form 301-redirects to it, like WordPress. This page is correct — it's a trap for crawlers that strip trailing slashes.",
  lesson:
    "A crawler that normalizes /foo/ to /foo will follow the 301 back to /foo/, strip it again, and loop forever (508 Loop Detected). The audit must crawl the canonical page once as a 200 and not report a false redirect loop.",
  expectedIssues: [],
  handler: () =>
    htmlResponse(
      renderPage({
        fixture: trailingSlashCanonical,
        title: "Trailing-slash canonical page",
        metaDescription:
          "The canonical version of this page ends in a trailing slash, and the non-slash form redirects to it, which trips up crawlers that strip slashes.",
        bodyHtml: article({
          h1: "This is the canonical, trailing-slash version",
          lede: "You reached this via a 301 from the non-slash URL. That is normal, and a crawler must handle it without looping.",
          sections: [
            {
              h2: "Why the trailing slash matters",
              body: "Most content management systems treat the trailing-slash form of a URL as canonical and 301-redirect the non-slash form to it. So /services and /services/ are not two pages; one is a permanent redirect to the other. This is correct behaviour, and every crawler has to deal with it.",
            },
            {
              h2: "Where crawlers go wrong",
              body: "A crawler that quietly strips trailing slashes to deduplicate URLs turns the canonical /services/ back into /services, fetches it, gets a 301 to /services/, strips the slash again, and queues /services once more. That cycle repeats until a loop detector gives up with a 508. The fix is to preserve the trailing slash, or to follow the redirect to the canonical form and mark it visited, so the page is crawled once and never re-queued.",
            },
          ],
        }),
      }),
    ),
};

export const redirectFixtures: Fixture[] = [
  redirectChain,
  redirectChainMid,
  redirectLoop,
  trailingSlashCanonical,
];