fix(img-api): proxy image content instead of redirecting to fix Google indexing errors
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
This commit is contained in:
@@ -5,11 +5,34 @@ import { DEFAULT_IMAGES } from "@/lib/site-images";
|
|||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const BUCKET = "private-gallery";
|
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;
|
const SIGNED_URL_TTL = 3600;
|
||||||
// Le navigateur/CDN met en cache la redirection 55 min
|
// Le navigateur/CDN met en cache la réponse 55 min
|
||||||
// (légèrement inférieur au TTL pour éviter tout risque d'expiration)
|
const PROXY_CACHE_TTL = 3300;
|
||||||
const REDIRECT_CACHE_TTL = 3300;
|
|
||||||
|
async function proxyImage(
|
||||||
|
url: string,
|
||||||
|
cacheMaxAge: number
|
||||||
|
): Promise<NextResponse> {
|
||||||
|
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(
|
export async function GET(
|
||||||
_req: NextRequest,
|
_req: NextRequest,
|
||||||
@@ -45,38 +68,25 @@ export async function GET(
|
|||||||
return new NextResponse(null, { status: 404 });
|
return new NextResponse(null, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── URL externe (Unsplash, etc.) ──────────────────────────────────────────
|
// ── URL externe (Unsplash, etc.) → proxy direct ───────────────────────────
|
||||||
if (!rawUrl.startsWith("storage:")) {
|
if (!rawUrl.startsWith("storage:")) {
|
||||||
return NextResponse.redirect(rawUrl, {
|
return proxyImage(rawUrl, 86400);
|
||||||
status: 302,
|
|
||||||
headers: {
|
|
||||||
"Cache-Control": "public, max-age=86400, s-maxage=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 filePath = rawUrl.slice("storage:".length);
|
||||||
const { data, error } = await adminClient.storage
|
const { data, error } = await adminClient.storage
|
||||||
.from(BUCKET)
|
.from(BUCKET)
|
||||||
.createSignedUrl(filePath, SIGNED_URL_TTL);
|
.createSignedUrl(filePath, SIGNED_URL_TTL);
|
||||||
|
|
||||||
if (error || !data?.signedUrl) {
|
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;
|
const fallback = DEFAULT_IMAGES[key]?.url;
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
return NextResponse.redirect(fallback, {
|
return proxyImage(fallback, 60);
|
||||||
status: 302,
|
|
||||||
headers: { "Cache-Control": "public, max-age=60, s-maxage=60" },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return new NextResponse(null, { status: 503 });
|
return new NextResponse(null, { status: 503 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.redirect(data.signedUrl, {
|
return proxyImage(data.signedUrl, PROXY_CACHE_TTL);
|
||||||
status: 302,
|
|
||||||
headers: {
|
|
||||||
"Cache-Control": `public, max-age=${REDIRECT_CACHE_TTL}, s-maxage=${REDIRECT_CACHE_TTL}, stale-while-revalidate=60`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user