diff --git a/frontend/application/app/api/media/[...path]/route.ts b/frontend/application/app/api/media/[...path]/route.ts index e92d452..1f84af8 100644 --- a/frontend/application/app/api/media/[...path]/route.ts +++ b/frontend/application/app/api/media/[...path]/route.ts @@ -32,6 +32,24 @@ function getCandidateUrls(filePath: string): string[] { return Array.from(set); } +function getMimeType(filePath: string, fallback: string): string { + const ext = filePath.split('.').pop()?.toLowerCase(); + switch (ext) { + case 'webp': return 'image/webp'; + case 'jpg': + case 'jpeg': return 'image/jpeg'; + case 'png': return 'image/png'; + case 'gif': return 'image/gif'; + case 'svg': return 'image/svg+xml'; + case 'avif': return 'image/avif'; + case 'mp4': return 'video/mp4'; + case 'webm': return 'video/webm'; + case 'mp3': return 'audio/mpeg'; + case 'pdf': return 'application/pdf'; + default: return fallback || 'application/octet-stream'; + } +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } @@ -42,9 +60,11 @@ export async function GET( return new NextResponse('Bad Request: Missing file path', { status: 400 }); } + const rawFilePath = path.join('/'); const filePath = path.map(segment => encodeURIComponent(segment)).join('/'); const candidates = getCandidateUrls(filePath); + // Forward streaming/caching headers from client const forwardedHeaders: HeadersInit = {}; const rangeHeader = request.headers.get('range'); if (rangeHeader) forwardedHeaders['range'] = rangeHeader; @@ -68,7 +88,25 @@ export async function GET( break; } } catch { - // Continue + // Continue trying next candidate + } + } + + // If 304 received without body or if not found with conditional headers, retry unconditionally + if (backendRes && backendRes.status === 304 && !backendRes.body) { + for (const targetUrl of candidates) { + try { + const freshRes = await fetch(targetUrl, { + method: 'GET', + cache: 'no-store', + }); + if (freshRes.ok) { + backendRes = freshRes; + break; + } + } catch { + // Continue + } } } @@ -77,7 +115,8 @@ export async function GET( } const responseHeaders = new Headers(); - const contentType = backendRes.headers.get('content-type') || 'application/octet-stream'; + const rawContentType = backendRes.headers.get('content-type') || ''; + const contentType = getMimeType(rawFilePath, rawContentType); responseHeaders.set('Content-Type', contentType); const contentLength = backendRes.headers.get('content-length'); diff --git a/frontend/application/app/api/uploads/[...path]/route.ts b/frontend/application/app/api/uploads/[...path]/route.ts index d2f738c..49e64da 100644 --- a/frontend/application/app/api/uploads/[...path]/route.ts +++ b/frontend/application/app/api/uploads/[...path]/route.ts @@ -34,6 +34,24 @@ function getCandidateUrls(filePath: string): string[] { return Array.from(set); } +function getMimeType(filePath: string, fallback: string): string { + const ext = filePath.split('.').pop()?.toLowerCase(); + switch (ext) { + case 'webp': return 'image/webp'; + case 'jpg': + case 'jpeg': return 'image/jpeg'; + case 'png': return 'image/png'; + case 'gif': return 'image/gif'; + case 'svg': return 'image/svg+xml'; + case 'avif': return 'image/avif'; + case 'mp4': return 'video/mp4'; + case 'webm': return 'video/webm'; + case 'mp3': return 'audio/mpeg'; + case 'pdf': return 'application/pdf'; + default: return fallback || 'application/octet-stream'; + } +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } @@ -44,6 +62,7 @@ export async function GET( return new NextResponse('Bad Request: Missing file path', { status: 400 }); } + const rawFilePath = path.join('/'); const filePath = path.map(segment => encodeURIComponent(segment)).join('/'); const candidates = getCandidateUrls(filePath); @@ -75,12 +94,31 @@ export async function GET( } } + // If 304 received without body or if not found with conditional headers, retry unconditionally + if (backendRes && backendRes.status === 304 && !backendRes.body) { + for (const targetUrl of candidates) { + try { + const freshRes = await fetch(targetUrl, { + method: 'GET', + cache: 'no-store', + }); + if (freshRes.ok) { + backendRes = freshRes; + break; + } + } catch { + // Continue + } + } + } + if (!backendRes) { return new NextResponse('Media file not found upstream', { status: 404 }); } const responseHeaders = new Headers(); - const contentType = backendRes.headers.get('content-type') || 'application/octet-stream'; + const rawContentType = backendRes.headers.get('content-type') || ''; + const contentType = getMimeType(rawFilePath, rawContentType); responseHeaders.set('Content-Type', contentType); const contentLength = backendRes.headers.get('content-length'); diff --git a/frontend/application/components/Hero.tsx b/frontend/application/components/Hero.tsx index ddf36fe..9fa76ce 100644 --- a/frontend/application/components/Hero.tsx +++ b/frontend/application/components/Hero.tsx @@ -198,7 +198,6 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne alt={banner.title || "Canina Pharma Germany"} fill priority={idx === 0} - unoptimized={true} sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px" className="object-cover" /> @@ -211,7 +210,6 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne alt={activeBanner?.title || "Canina Pharma Germany"} fill priority - unoptimized={true} sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px" className="object-cover" /> diff --git a/frontend/application/components/SafeImage.tsx b/frontend/application/components/SafeImage.tsx index 7686768..733da6f 100644 --- a/frontend/application/components/SafeImage.tsx +++ b/frontend/application/components/SafeImage.tsx @@ -104,12 +104,7 @@ export default function SafeImage({ unoptimized={ isBlobOrData || !isAllowedHost || - (typeof src === 'string' && ( - src.startsWith('/api/uploads/') || - src.startsWith('/api/media/') || - src.startsWith('/uploads/') || - src.toLowerCase().endsWith('.svg') - )) + (typeof src === 'string' && src.toLowerCase().endsWith('.svg')) } className={cn( "transition-all duration-500 w-full h-full object-contain",