Redirects and canonicals Required
Keep every old address in one redirect map
Keep every address you have ever published in one redirect map, and check it before any other route. Never delete an entry.
How to do it
Store old and new paths as data, such as one JSON file. Normalise case and the trailing slash first, then look up the result and answer one 301 to the final address. Map every format of a page, such as its Markdown and JSON copies, not only the HTML. Add an entry in the same change that moves or renames a page. Resolve chains when you add an entry, so each one points straight at a live page. Keep technology out of addresses, such as .php or .aspx endings, so a new stack needs no new addresses. Redirect only to targets from the map, never to an address taken from the request, as in block open redirects.
Example
{
"/guides/old-name": "/guides/new-name",
"/blog/2023/caching": "/guides/caching",
"/products/discontinued-widget": null
}
// One hop: normalise, look up, redirect. null means gone for good.
app.use((req, res, next) => {
const query = req.originalUrl.slice(req.path.length);
// Collapse leading slashes, or "//evil.example" becomes an open redirect
const path = req.path.replace(/^\/+/, '/').replace(/(.)\/+$/, '$1');
const [, base, ext = ''] = /^(.*?)(\.md|\.json)?$/.exec(path.toLowerCase());
if (redirects.has(base)) {
const to = redirects.get(base);
return to ? res.redirect(301, to + ext + query) : res.status(410).send('This page was removed.');
}
if (path !== req.path) return res.redirect(301, path + query);
next();
});
# Flask: the same one-hop lookup, before any other route
import re
from flask import redirect, request
@app.before_request
def redirect_map():
path = re.sub(r"(.)/+$", r"\1", re.sub(r"^/+", "/", request.path))
base, ext = re.match(r"^(.*?)(\.md|\.json)?$", path.lower()).groups()
query = "?" + request.query_string.decode() if request.query_string else ""
if base in redirects:
to = redirects[base]
return redirect(to + (ext or "") + query, 301) if to else ("This page was removed.", 410)
if path != request.path:
return redirect(path + query, 301)
How to check
A test requests every entry and expects one 301 to an address that answers 200. The build fails if an entry points at a page that does not exist. No sitemap address ends in a server file type.
Measured
On a reference site, restructuring a guide moved 129 addresses in one change. Every old link kept working in one hop, which a test proves on every build.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers website playbook: https://agentwranglers.se/playbook/website/redirects-and-canonicals/redirect-map.