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 78 79 80 81 82 83 84 85 86 87 88 89 90 | 1x 1x 13x 13x 13x 4x 19x 17x 16x 15x 7x 6x 13x | // In Astro 5, `Astro.response` is only available on AstroGlobal (the `Astro`
// object in .astro frontmatter), not on APIContext/AstroSharedContext.
// We accept the minimal structural shape needed so callers pass `Astro` directly.
export interface AstroResponseContext {
response: {
headers: Pick<Headers, "set" | "get">;
};
}
export type CachePreset =
| "static"
| "city-hub"
| "listing"
| "detail"
| "blog-post"
| "faq-hub"
| "sitemap"
| "search"
| "account"
| "calculator";
const CACHE_CONTROL: Record<CachePreset, string> = {
static: "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800",
"city-hub":
"public, max-age=300, s-maxage=3600, stale-while-revalidate=86400",
listing: "public, max-age=60, s-maxage=600, stale-while-revalidate=3600",
detail: "public, max-age=60, s-maxage=600, stale-while-revalidate=86400",
"blog-post":
"public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800",
"faq-hub":
"public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800",
sitemap: "public, s-maxage=172800, stale-while-revalidate=604800",
calculator:
"public, max-age=300, s-maxage=3600, stale-while-revalidate=86400",
search: "private, no-cache",
account: "private, no-store",
};
const ROBOTS_TAG: Partial<Record<CachePreset, string>> = {
search: "noindex",
account: "noindex, nofollow",
};
export function setCacheHeaders(
ctx: AstroResponseContext,
preset: CachePreset,
): void {
ctx.response.headers.set("Cache-Control", CACHE_CONTROL[preset]);
const robotsTag = ROBOTS_TAG[preset];
if (robotsTag !== undefined) {
ctx.response.headers.set("X-Robots-Tag", robotsTag);
}
}
// Fail closed: only `production` is indexable. Any other value (or undefined)
// gets noindex. Misconfigured prod that loses the env var stays uncrawled —
// vastly preferable to dev/preview leaking into the index.
//
// Exception: Lighthouse audits bypass the noindex header so they see
// prod-equivalent SEO scores. Three opt-in signals (any one suffices):
// 1. Query param `_lh` on the URL — set by our `lighthouse:audit`
// runner as a per-run cache-buster, so it's already on every URL
// we audit. Lighthouse 13 strips "HeadlessChrome" from the network
// UA so a UA-only check can't detect it from inside the worker.
// 2. UA contains "Chrome-Lighthouse" (Lighthouse <13 default).
// 3. UA contains "HeadlessChrome" — covers other headless tools (the
// official Lighthouse 13 only shows this in hostUserAgent, not
// networkUserAgent, so this is a fallback).
// 4. Custom header "x-lighthouse-audit: 1" — explicit opt-in for
// manual curl audits or alternative tools.
// Canonical URLs are rendered server-side from the pathname (no query
// string), so a leaked `?_lh=...` URL still won't be indexed by Google.
// Real crawlers (Googlebot, Bingbot, etc.) never match any of these.
export function setEnvRobotTag(
ctx: AstroResponseContext,
envName: string | undefined,
userAgent?: string | null,
auditHeader?: string | null,
hasAuditQueryParam?: boolean,
): void {
if (envName === "production") return;
if (hasAuditQueryParam) return;
if (auditHeader === "1") return;
if (userAgent) {
if (userAgent.includes("Chrome-Lighthouse")) return;
if (userAgent.includes("HeadlessChrome")) return;
}
ctx.response.headers.set("X-Robots-Tag", "noindex, nofollow");
}
|