feat(images): enable Next.js image optimization with complete upstream buffer retrieval and explicit MIME types
This commit is contained in:
parent
89e59c8427
commit
08ad51152f
@ -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;
|
||||
@ -67,17 +87,36 @@ export async function GET(
|
||||
backendRes = res;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user