Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 8x 8x 44x 44x 44x 1x 43x 43x 43x 7x 36x 8x 6x 6x 3x 3x | // Case-insensitive canonical URL lookup for Cloudflare Pages middleware.
// The site uses titles as slugs (e.g. "Jenkins Pipeline…"), which makes paths
// case-sensitive on Linux hosting. Some browsers/apps normalize URLs to
// lowercase, so a case-insensitive 301 to the canonical path avoids 404s.
// The mapping is derived from the sitemap, so new content is picked up
// automatically after each build.
export type CanonicalPathMap = Map<string, string[]>;
/** Build a lowercase-path → canonical-path list from a sitemap XML body. */
export function buildCanonicalPathMap(sitemapXml: string): CanonicalPathMap {
const map: CanonicalPathMap = new Map();
for (const match of sitemapXml.matchAll(/<loc>([^<]+)<\/loc>/g)) {
const url = match[1].trim();
let pathname: string;
try {
pathname = new URL(url).pathname;
} catch {
continue;
}
const key = pathname.toLowerCase();
const list = map.get(key);
if (list) {
Eif (!list.includes(pathname)) list.push(pathname);
} else {
map.set(key, [pathname]);
}
}
return map;
}
/**
* Return the canonical path for a request path, or null when the request
* should be served as-is:
* - exact match (avoid redirect loops)
* - unknown path
* - lowercase key with multiple distinct canonicals (case collision, e.g.
* distinct tags that differ only by case) — redirecting would be ambiguous
*/
export function findCanonicalPathname(
map: CanonicalPathMap,
requestPathname: string,
): string | null {
const candidates = map.get(requestPathname.toLowerCase());
if (candidates?.length !== 1) return null;
const canonical = candidates[0];
return canonical === requestPathname ? null : canonical;
}
|