All files / src/lib facet-guard.ts

100% Statements 9/9
100% Branches 2/2
100% Functions 4/4
100% Lines 6/6

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                                    1x 1x                   17x 15x 5x               4x    
// Bot facet-crawl guard for the faceted listing pages (/projects, /blog).
//
// AI crawlers (CF bot score 99, rotating ASNs that ignore robots.txt) walk
// *unbounded unique* filter permutations. Each unique URL is a cache miss that
// runs a D1-heavy list query (correlated EXISTS subqueries, 1-4s); in bursts
// they saturate D1 and 5xx the API. Real users rarely combine more than a few
// filters, and these pages are `noindex` whenever any filter param is present
// (computeCanonical Rule 2), so short-circuiting degenerate combos costs no SEO
// and (almost) no legitimate UX.
//
// Heuristic — short-circuit (skip the API call, render the empty "refine your
// filters" state) when EITHER:
//   - any single facet has more than MAX_VALUES_PER_FACET values — the clearest
//     bot tell (no human selects 4+ rooms, or 7 blog categories); or
//   - the combined value count exceeds MAX_VALUES_COMBINED.
// These are tuned to spare realistic filtering (≤3 per facet, ≤5 total) while
// catching the degenerate permutations; the edge WAF rate-limit bounds the rest.
 
export const MAX_VALUES_PER_FACET = 3;
export const MAX_VALUES_COMBINED = 5;
 
/**
 * Decide whether a faceted request looks like a crawler permutation and should
 * skip the (expensive) API list call.
 * @param facetGroups one array of selected values per facet (e.g. rooms,
 *   propertyTypes, budgetRanges, or [category…]). Pass `[search]` as its own
 *   group if a search term should count toward the combined total.
 */
export function shouldShortCircuitFacets(facetGroups: string[][]): boolean {
	if (facetGroups.some((g) => g.length > MAX_VALUES_PER_FACET)) return true;
	const total = facetGroups.reduce((sum, g) => sum + g.length, 0);
	return total > MAX_VALUES_COMBINED;
}
 
/**
 * Sort + dedupe a facet's values so permutations that differ only by order
 * (`a,b` vs `b,a`) collapse onto a single API/cache key, improving L2 hit rate.
 */
export function canonicalizeFacet(values: string[]): string[] {
	return [...new Set(values)].sort();
}