All files / src/lib url-case-normalizer.ts

100% Statements 18/18
87.5% Branches 7/8
100% Functions 2/2
100% Lines 16/16

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;
}