Leadtype
Documentation

Redirect renamed pages

Renaming a docs page breaks every link that pointed at it — search results, agent citations, other sites, llms.txt copies agents cached last week. Leadtype's redirect tracking notices renames at generate time, emits a machine-readable redirect map, and refuses to let a page silently disappear.

Enable it

Add a redirects block to docs.config.ts:

export default defineDocsConfig({
  product: { name: "My library", tagline: "..." },
  // ...
  redirects: {},
});

On the next leadtype generate, two things appear:

  • <docs source dir>/paths.lock.json — a lockfile recording every published path with a content hash. Commit this file. It is the record of previously published paths that future runs diff against.
  • <out>/docs/redirects.json — the artifact your app serves redirects from: { redirects: [{ from, to, status }] }.

Enabling redirects also turns on output pruning (see convertAllMdx prune) — rename detection diffs the emitted page set, so stale mirrors from renamed sources must be garbage-collected or the old path never disappears.

What happens on a rename

Generate compares this run's pages against the lockfile:

  • Pure move (same content, new path): detected automatically by content hash — the body is hashed with frontmatter excluded, so git-enrichment churn doesn't defeat the match. Emits a permanent 308 redirect. No authoring needed.

  • Move + edit in one commit: the hash no longer matches, so the build fails loudly listing the disappeared paths. Add the old path to the successor page's frontmatter:

    ---
    title: Script Loader
    redirectFrom:
      - /docs/guides/script-loader
    ---
  • Intentional deletion: acknowledge it in the config to serve 410 Gone instead of failing:

    redirects: {
      removed: ["/docs/guides/retired-page"],
    }

Redirects accumulate in the lockfile across generates: when /docs/a moved to /docs/b last month and /docs/b moves to /docs/c today, the old a → b entry collapses to a → c automatically, and entries whose target is later removed degrade to 410.

Ambiguity is never guessed: if two pages with identical content disappear and reappear, the build fails and asks for explicit redirectFrom instead of picking one.

Filtered generates (--include / --exclude) skip redirect tracking and pruning entirely — a partial page set would make every excluded page look deleted. Only full builds touch the lockfile.

Serve the redirects

The generate step emits data; serving is one helper call. For HTML routes, consult the map in your catch-all before rendering a 404:

import { resolveRedirect } from "leadtype/redirects";
import redirects from "./generated/redirects";

function serveRedirect(url: URL): Response | null {
  const redirect = resolveRedirect(url.pathname, redirects.redirects);
  if (!redirect) {
    return null; // fall through to normal routing / 404
  }
  return redirect.to
    ? Response.redirect(new URL(redirect.to, url.origin), redirect.status)
    : new Response("Gone", { status: 410 });
}

leadtype/redirects is edge-safe (no Node built-ins), so the resolver runs in Cloudflare Workers and Vercel Edge routes; the generate-time lockfile helpers live under leadtype/redirects/node.

The agent surface is covered for you: pass the entries to createAgentMarkdownResponse and agent-shaped requests for a renamed page — including its .md mirror (/docs/old.md/docs/new.md) — get the 308/410 directly, while browser requests still fall through to your HTML routing:

createAgentMarkdownResponse({
  urlPath: url.pathname,
  headers,
  manifest,
  redirects: redirects.redirects,
  readMarkdownFile,
});

Options

OptionDescription
lockfileLockfile path, resolved relative to the docs source directory. Default paths.lock.json.
removedPaths acknowledged as intentionally deleted; served as 410 Gone instead of failing the build.

CI

Because the lockfile is committed, a rename that lands without its lockfile update shows up as a dirty git diff after regeneration — the same "regenerate and assert clean" gate that guards the other generated artifacts covers redirects too.