From d54278969a46bb2655596dc6baedddcb1ccb99c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 07:27:07 +0000 Subject: [PATCH] fix(img-api): proxy image content instead of redirecting to fix Google indexing errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all NextResponse.redirect() calls with a proxyImage() helper that fetches the upstream URL server-side and streams the response body directly. This eliminates: - Redirect chains (API → Supabase signed URL → S3/CDN) - Overly long redirect URLs (Supabase JWT tokens) - Potential empty/invalid redirect targets Also adds X-Robots-Tag: noindex, nofollow on all responses from this technical route to prevent Google from crawling it directly. https://claude.ai/code/session_01PzA98VhLMmsHpzs7gnLHGs --- app/api/img/[key]/route.ts | 56 ++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/app/api/img/[key]/route.ts b/app/api/img/[key]/route.ts index 2c9a9fd..3655fe6 100644 --- a/app/api/img/[key]/route.ts +++ b/app/api/img/[key]/route.ts @@ -5,11 +5,34 @@ import { DEFAULT_IMAGES } from "@/lib/site-images"; export const dynamic = "force-dynamic"; const BUCKET = "private-gallery"; -// Signed URL valide 1h côté Supabase +// Signed URL valide 1h côté Supabase (sert uniquement pour le fetch interne) const SIGNED_URL_TTL = 3600; -// Le navigateur/CDN met en cache la redirection 55 min -// (légèrement inférieur au TTL pour éviter tout risque d'expiration) -const REDIRECT_CACHE_TTL = 3300; +// Le navigateur/CDN met en cache la réponse 55 min +const PROXY_CACHE_TTL = 3300; + +async function proxyImage( + url: string, + cacheMaxAge: number +): Promise { + const upstream = await fetch(url, { redirect: "follow" }); + + if (!upstream.ok) { + return new NextResponse(null, { status: 502 }); + } + + const contentType = + upstream.headers.get("content-type") ?? "application/octet-stream"; + + return new NextResponse(upstream.body, { + status: 200, + headers: { + "Content-Type": contentType, + "Cache-Control": `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}, stale-while-revalidate=60`, + // Empêche Google d'indexer cette route technique + "X-Robots-Tag": "noindex, nofollow", + }, + }); +} export async function GET( _req: NextRequest, @@ -45,38 +68,25 @@ export async function GET( return new NextResponse(null, { status: 404 }); } - // ── URL externe (Unsplash, etc.) ────────────────────────────────────────── + // ── URL externe (Unsplash, etc.) → proxy direct ─────────────────────────── if (!rawUrl.startsWith("storage:")) { - return NextResponse.redirect(rawUrl, { - status: 302, - headers: { - "Cache-Control": "public, max-age=86400, s-maxage=86400", - }, - }); + return proxyImage(rawUrl, 86400); } - // ── Chemin bucket privé → générer une Signed URL fraîche ───────────────── + // ── Chemin bucket privé → générer une Signed URL puis proxifier ─────────── const filePath = rawUrl.slice("storage:".length); const { data, error } = await adminClient.storage .from(BUCKET) .createSignedUrl(filePath, SIGNED_URL_TTL); if (error || !data?.signedUrl) { - // Fallback sur l'image par défaut Unsplash si la génération échoue + // Fallback sur l'image par défaut si la génération échoue const fallback = DEFAULT_IMAGES[key]?.url; if (fallback) { - return NextResponse.redirect(fallback, { - status: 302, - headers: { "Cache-Control": "public, max-age=60, s-maxage=60" }, - }); + return proxyImage(fallback, 60); } return new NextResponse(null, { status: 503 }); } - return NextResponse.redirect(data.signedUrl, { - status: 302, - headers: { - "Cache-Control": `public, max-age=${REDIRECT_CACHE_TTL}, s-maxage=${REDIRECT_CACHE_TTL}, stale-while-revalidate=60`, - }, - }); + return proxyImage(data.signedUrl, PROXY_CACHE_TTL); }