--- import "../styles/global.css"; import "../styles/catalog.css"; import "../styles/source-status.css"; import "../styles/data-passport.css"; import "../styles/data-legend.css"; import "../styles/coordinate-radar.css"; import "../styles/activity-timeline.css"; import "../styles/fish-silhouette.css"; import "../styles/tackle-glyph.css"; import "../styles/empty-states.css"; import "../styles/alpha-banner.css"; import "../styles/signal-pagination.css"; import "../styles/pagination.css"; import "../styles/dashboard-polish.css"; import "../styles/loading-states.css"; import "../styles/query-context.css"; import "../styles/theme.css"; import "../styles/media-catalog.css"; import FishingIcon from "../components/FishingIcon.astro"; import AlphaBanner from "../components/AlphaBanner.astro"; const { title = "RF4 Spotter — свежие точки клёва Russian Fishing 4", description = "Свежие точки, уловы и честная оценка активности в Russian Fishing 4 с обязательным указанием источников.", noindex = false, image = "/og-rf4spotter.png", structuredData = null, errorPage = false, } = Astro.props; const path = Astro.url.pathname; const storedTheme = Astro.cookies.get("rf4-theme")?.value; const theme = storedTheme === "light" || storedTheme === "dark" ? storedTheme : "system"; const siteUrl = import.meta.env.PUBLIC_SITE_URL || "https://rf4spotter.ru"; const canonical = new URL(path, siteUrl).toString(); const socialImage = new URL(image, siteUrl).toString(); const isAdminPath = path === "/admin" || path.startsWith("/admin/"); const preventIndexing = noindex || isAdminPath; if (isAdminPath) { Astro.response.headers.set("Cache-Control", "private, no-store"); Astro.response.headers.set("X-Robots-Tag", "noindex, nofollow"); } const websiteJsonLd = { "@type": "WebSite", name: "RF4 Spotter", url: siteUrl, inLanguage: "ru" }; // A08: Skip structuredData on error pages (explicit errorPage prop) // Don't infer error from noindex alone — main page can have noindex on 422 const jsonLdGraph = (structuredData && !errorPage) ? [websiteJsonLd, structuredData] : [websiteJsonLd]; const jsonLd = JSON.stringify({ "@context": "https://schema.org", "@graph": jsonLdGraph, }).replaceAll("<", "\\u003c"); const configuredFilesDomain = process.env.FILES_DOMAIN || "files.rf4spotter.ru"; const filesDomain = /^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/i.test(configuredFilesDomain) ? configuredFilesDomain : "files.rf4spotter.ru"; const isLoopback = (hostname: string) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]"; const safeOrigin = (value: string | undefined, fallback: string) => { try { const url = new URL(value || fallback); return url.protocol === "https:" || (url.protocol === "http:" && isLoopback(url.hostname)) ? url.origin : fallback; } catch { return fallback; } }; const apiOrigin = safeOrigin(import.meta.env.PUBLIC_API_URL, siteUrl); const filesFallback = `https://${filesDomain}`; const filesOrigin = (() => { try { const url = new URL(process.env.FILES_ORIGIN || filesFallback); if ((url.protocol === "https:" && url.hostname === filesDomain) || (url.protocol === "http:" && isLoopback(url.hostname))) { return url.origin; } } catch { // Invalid deployment input falls back to the validated production hostname. } return filesFallback; })(); const localOrigins = [apiOrigin, filesOrigin].filter((origin) => origin.startsWith("http://")); const cspNonce = crypto.randomUUID().replaceAll("-", ""); Astro.response.headers.set("Content-Security-Policy", [ "default-src 'self'", "base-uri 'self'", "object-src 'none'", "frame-ancestors 'none'", "form-action 'self'", `connect-src 'self'${apiOrigin === siteUrl ? "" : ` ${apiOrigin}`}`, `img-src 'self' data: ${filesOrigin}`, "font-src 'self'", "media-src 'self'", "manifest-src 'self'", `script-src 'self' 'nonce-${cspNonce}'`, "script-src-attr 'none'", "style-src 'self'", "style-src-attr 'none'", ...(localOrigins.length ? [] : ["upgrade-insecure-requests"]), ].join("; ")); ---
Свежие данные и честная оценка