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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | 21x 21x 21x 16x 16x 21x 21x 21x 8x 13x 13x 12x 10x 13x 9x 13x 8x 5x 1x 1x 1x 1x 1x 1x 4x | export interface CanonicalInput {
baseUrl: string;
pathname: string;
searchParams: URLSearchParams;
allowedCanonicalParams?: string[];
}
export interface CanonicalOutput {
canonicalUrl: string;
robots: string;
}
export function computeCanonical(input: CanonicalInput): CanonicalOutput {
const { baseUrl, pathname, searchParams, allowedCanonicalParams = [] } = input;
const base = `${baseUrl}${pathname}`;
// A noindexed page MUST be self-canonical. Pointing its canonical at a
// *different* URL (e.g. the bare base) lets Google propagate the noindex back
// to that target and deindex it — which would knock /pros, /projects, or page 1
// out of the index. So every noindex branch below canonicals to the page
// itself (path + its own query), never to `base`.
const selfCanonical = (): string => {
const qs = searchParams.toString();
return qs ? `${base}?${qs}` : base;
};
// Rule 1 & 5: page param handling
const pageRaw = searchParams.get("page");
const pageNum = pageRaw !== null ? parseInt(pageRaw, 10) : 1;
if (pageNum > 1) {
// Rule 1: page 2+ → noindex, self-canonical (keep page in the canonical;
// canonical-to-page-1 + noindex risks deindexing page 1).
return { canonicalUrl: selfCanonical(), robots: "noindex, follow" };
}
// At this point page is absent, "1", or invalid (treat as 1).
// Collect non-page params to check for filters.
const nonPageParams: string[] = [];
for (const key of searchParams.keys()) {
if (key !== "page") {
nonPageParams.push(key);
}
}
// Rule 2: any param NOT in allowedCanonicalParams present (filter/search) →
// noindex, self-canonical. This drops faceted-filter and internal-search
// permutations from the index while still following links to detail pages.
const hasDisallowedParam = nonPageParams.some(
(key) => !allowedCanonicalParams.includes(key),
);
if (hasDisallowedParam) {
return { canonicalUrl: selfCanonical(), robots: "noindex, follow" };
}
// Rule 3: only allowedCanonicalParams present → preserve them in canonical
if (nonPageParams.length > 0) {
const preserved = new URLSearchParams();
for (const key of allowedCanonicalParams) {
const values = searchParams.getAll(key);
for (const v of values) {
preserved.append(key, v);
}
}
const qs = preserved.toString();
// When Rule 3 applies, nonPageParams holds allowed params that exist in
// searchParams — so `preserved` is always non-empty and the `:base` branch
// of the ternary below is unreachable in practice.
/* v8 ignore start -- qs is always truthy when Rule 3 applies */
return { canonicalUrl: qs ? `${base}?${qs}` : base, robots: "index, follow" };
/* v8 ignore stop */
}
// Rule 4: no params (or only page=1) → canonical is bare base
return { canonicalUrl: base, robots: "index, follow" };
}
|