I’ve recently rewritten a React app in Next.js (version 14.1.4). It’s a website that has a page with a list of products, each product consisting of some text and an image. The content is managed by the owner through an admin panel, which is a separate application, so products can be changed, deleted and added at any time.
After building the app on my server, I noticed that newly added images weren’t being loaded by Next.js. This made sense to me as I was using the Image
component from next/image
. Since my images are already optimized in the backend with the appropriate formats and sizes, I decided to take care of it myself and avoid using the Image
component. However, this didn’t work as I still had the problem of new images not loading.
I couldn’t find any information in the official documentation on how to handle this properly, so I created a custom request handler that returns the appropriate image as a response:
// api/images/[itemId]/[imageName]/route.ts
import { type NextRequest } from "next/server";
import path from "path";
import fs from "fs";
export async function GET(
request: NextRequest,
{ params }: { params: { itemId: string; imageName: string; } }
) {
const searchParams = request.nextUrl.searchParams;
const size = searchParams.get("size");
const type = searchParams.get("type");
const productType = searchParams.get("productType");
const { itemId, imageName } = params;
const imagePath = path.join(
process.cwd(), "public", "uploads", `${productType}`, itemId, `${imageName}_${size}.${type}`
);
if (!fs.existsSync(imagePath)) {
return new Response("Image not found", { status: 404 });
}
let contentType: string;
switch (type) {
case "jpg":
contentType = "image/jpeg";
break;
case "webp":
contentType = "image/webp";
break;
default:
contentType = "image/jpeg";
}
const headers = {
"Content-Type": contentType
};
// Create a read stream from the image file and return it as the response
const stream = fs.createReadStream(imagePath);
// @ts-ignore
return new Response(stream, { headers });
}
I also created a custom Image component for this:
import clsx from "clsx";
type Props = React.ComponentPropsWithoutRef<"img"> & {
src: string;
alt: string;
productType: "products" | "gallery";
className?: string;
};
export default function CustomImage({
src,
alt,
productType,
className,
...rest
}: Props) {
return !src ? <p>No image</p> : (
<picture>
<source
type="image/webp"
srcSet={`${src}?size=sm&type=webp&productType=${productType} 450w, ${src}?size=md&type=webp&productType=${productType} 900w, ${src}?size=lg&type=webp&productType=${productType} 1350w`}
sizes="(min-width: 61.25em) 500px, 100vw" // 980px
/>
<img
loading="lazy"
src={`${src}?size=sm&type=jpg&productType=${productType}`}
alt={alt ?? ""}
width="400"
height="300"
className={clsx("product-image", className)}
{...rest}
/>
</picture>
);
}
This seemed to be working fine, but I noticed that the images were not always loading on my mobile device. When visiting the /products
web page, the images are displayed without any problems. However, when I navigate from the product detail page /products/slug
back to /products
using the link provided in the breadcrumbs (it’s basically just a Link
component), I’m back on the same page but without the images loading. Again, this behavior somehow only occurs on my mobile device, in all major browsers. Also worth noting is that I have the same Link
component in my main navigation and everything works fine when I click on the /products
link, very strange.
I find this kind of bug hard to track down, because I don’t know what’s really going on and what Next.js is doing under the hood that could be causing this error.
So, despite my attempts to gain control over the images, it seems that Next.js is still messing with my images, and I don’t want that.
Which brings me to my question: Am I on the right track with my approach to handling the images myself, or am I completely missing something and there is a much easier way to handle this?