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);
|
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;
|
||||||
@ -67,17 +87,36 @@ export async function GET(
|
|||||||
backendRes = res;
|
backendRes = res;
|
||||||
break;
|
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 {
|
} catch {
|
||||||
// Continue
|
// 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');
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user