feat(images): enable Next.js image optimization with complete upstream buffer retrieval and explicit MIME types
Some checks failed
Deploy Canina / deploy (push) Successful in 2m4s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 7s

This commit is contained in:
parsa aghaei 2026-09-02 16:14:33 +03:30
parent 89e59c8427
commit 08ad51152f
4 changed files with 81 additions and 11 deletions

View File

@ -32,6 +32,24 @@ function getCandidateUrls(filePath: string): string[] {
return Array.from(set); 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( export async function GET(
request: NextRequest, request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> } { params }: { params: Promise<{ path: string[] }> }
@ -42,9 +60,11 @@ export async function GET(
return new NextResponse('Bad Request: Missing file path', { status: 400 }); return new NextResponse('Bad Request: Missing file path', { status: 400 });
} }
const rawFilePath = path.join('/');
const filePath = path.map(segment => encodeURIComponent(segment)).join('/'); const filePath = path.map(segment => encodeURIComponent(segment)).join('/');
const candidates = getCandidateUrls(filePath); const candidates = getCandidateUrls(filePath);
// Forward streaming/caching headers from client
const forwardedHeaders: HeadersInit = {}; const forwardedHeaders: HeadersInit = {};
const rangeHeader = request.headers.get('range'); const rangeHeader = request.headers.get('range');
if (rangeHeader) forwardedHeaders['range'] = rangeHeader; if (rangeHeader) forwardedHeaders['range'] = rangeHeader;
@ -68,7 +88,25 @@ export async function GET(
break; break;
} }
} catch { } 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 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); responseHeaders.set('Content-Type', contentType);
const contentLength = backendRes.headers.get('content-length'); const contentLength = backendRes.headers.get('content-length');

View File

@ -34,6 +34,24 @@ function getCandidateUrls(filePath: string): string[] {
return Array.from(set); 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( export async function GET(
request: NextRequest, request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> } { params }: { params: Promise<{ path: string[] }> }
@ -44,6 +62,7 @@ export async function GET(
return new NextResponse('Bad Request: Missing file path', { status: 400 }); return new NextResponse('Bad Request: Missing file path', { status: 400 });
} }
const rawFilePath = path.join('/');
const filePath = path.map(segment => encodeURIComponent(segment)).join('/'); const filePath = path.map(segment => encodeURIComponent(segment)).join('/');
const candidates = getCandidateUrls(filePath); 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) { if (!backendRes) {
return new NextResponse('Media file not found upstream', { status: 404 }); return new NextResponse('Media file not found upstream', { status: 404 });
} }
const responseHeaders = new Headers(); 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); responseHeaders.set('Content-Type', contentType);
const contentLength = backendRes.headers.get('content-length'); const contentLength = backendRes.headers.get('content-length');

View File

@ -198,7 +198,6 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
alt={banner.title || "Canina Pharma Germany"} alt={banner.title || "Canina Pharma Germany"}
fill fill
priority={idx === 0} priority={idx === 0}
unoptimized={true}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px"
className="object-cover" className="object-cover"
/> />
@ -211,7 +210,6 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
alt={activeBanner?.title || "Canina Pharma Germany"} alt={activeBanner?.title || "Canina Pharma Germany"}
fill fill
priority priority
unoptimized={true}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px"
className="object-cover" className="object-cover"
/> />

View File

@ -104,12 +104,7 @@ export default function SafeImage({
unoptimized={ unoptimized={
isBlobOrData || isBlobOrData ||
!isAllowedHost || !isAllowedHost ||
(typeof src === 'string' && ( (typeof src === 'string' && src.toLowerCase().endsWith('.svg'))
src.startsWith('/api/uploads/') ||
src.startsWith('/api/media/') ||
src.startsWith('/uploads/') ||
src.toLowerCase().endsWith('.svg')
))
} }
className={cn( className={cn(
"transition-all duration-500 w-full h-full object-contain", "transition-all duration-500 w-full h-full object-contain",