Skip to content

Instantly share code, notes, and snippets.

@jacob-ebey
Last active February 29, 2024 05:25
Show Gist options
  • Save jacob-ebey/3a37a86307de9ef22f47aae2e593b56f to your computer and use it in GitHub Desktop.
Save jacob-ebey/3a37a86307de9ef22f47aae2e593b56f to your computer and use it in GitHub Desktop.
Remix Image Component
import { createHash } from "crypto";
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
import https from "https";
import { PassThrough } from "stream";
import type { Readable } from "stream";
import type { LoaderFunction } from "remix";
import sharp from "sharp";
import type { Request as NodeRequest } from "@remix-run/node";
import { Response as NodeResponse } from "@remix-run/node";
let badImageBase64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
function badImageResponse() {
let buffer = Buffer.from(badImageBase64, "base64");
return new Response(buffer, {
status: 500,
headers: {
"Cache-Control": "max-age=0",
"Content-Type": "image/gif;base64",
"Content-Length": buffer.length.toFixed(0),
},
});
}
function getIntOrNull(value: string | null) {
if (value === null) {
return null;
}
return Number.parseInt(value);
}
export let loader: LoaderFunction = async ({ request }) => {
let url = new URL(request.url);
let src = url.searchParams.get("src");
if (!src) {
return badImageResponse();
}
let width = getIntOrNull(url.searchParams.get("width"));
let height = getIntOrNull(url.searchParams.get("height"));
let fit: any = url.searchParams.get("fit") || "cover";
let hash = createHash("sha256");
hash.update("v1");
hash.update(request.method);
hash.update(request.url);
hash.update(width?.toString() || "0");
hash.update(height?.toString() || "0");
hash.update(fit);
let key = hash.digest("hex");
let cachedFile = path.resolve(path.join(".cache/images", key + ".webp"));
try {
let exists = await fsp
.stat(cachedFile)
.then((s) => s.isFile())
.catch(() => false);
if (exists) {
let fileStream = fs.createReadStream(cachedFile);
return new NodeResponse(fileStream, {
status: 200,
headers: {
"Content-Type": "image/webp",
"Cache-Control": "public, max-age=31536000, immutable",
},
}) as unknown as Response;
} else {
console.info("cache skipped for", src);
}
} catch (error) {
console.error(error);
}
try {
let imageBody: Readable | undefined;
let status = 200;
if (src.startsWith("/") && (src.length === 1 || src[1] !== "/")) {
imageBody = fs.createReadStream(path.resolve("public", src.slice(1)));
} else {
let imgRequest = new Request(src.toString()) as unknown as NodeRequest;
imgRequest.agent = new https.Agent({
rejectUnauthorized: false,
});
let imageResponse = await fetch(imgRequest as unknown as Request);
imageBody = imageResponse.body as unknown as PassThrough;
status = imageResponse.status;
}
if (!imageBody) {
return badImageResponse();
}
let sharpInstance = sharp();
sharpInstance.on("error", (error) => {
console.error(error);
});
if (width || height) {
sharpInstance.resize(width, height, { fit });
}
sharpInstance.webp({ reductionEffort: 6 });
let imageManipulationStream = imageBody.pipe(sharpInstance);
await fsp
.mkdir(path.dirname(cachedFile), { recursive: true })
.catch(() => {});
let cacheFileStream = fs.createWriteStream(cachedFile);
await new Promise<void>((resolve, reject) => {
imageManipulationStream.pipe(cacheFileStream);
imageManipulationStream.on("end", () => {
resolve();
imageBody!.destroy();
});
imageManipulationStream.on("error", async (error) => {
imageBody!.destroy();
await fsp.rm(cachedFile).catch(() => {});
});
});
let fileStream = fs.createReadStream(cachedFile);
return new NodeResponse(fileStream, {
status: status,
headers: {
"Content-Type": "image/webp",
"Cache-Control": "public, max-age=31536000, immutable",
},
}) as unknown as Response;
} catch (error) {
console.error(error);
return badImageResponse();
}
};
import type { ComponentPropsWithoutRef } from "react";
export function OptimizedImage({
optimizerUrl = "/resources/image",
responsive,
src,
...rest
}: ComponentPropsWithoutRef<"img"> & {
optimizerUrl?: string;
responsive?: {
maxWidth?: number;
size: { width: number; height?: number };
}[];
}) {
let url = src ? optimizerUrl + "?src=" + encodeURIComponent(src) : src;
let props: ComponentPropsWithoutRef<"img"> = {
src: url + `&width=${rest.width || ""}&height=${rest.height || ""}`,
};
let largestImageWidth = 0;
let largestImageSrc: string | undefined;
if (responsive && responsive.length) {
let srcSet = "";
let sizes = "";
for (let { maxWidth, size } of responsive) {
if (srcSet) {
srcSet += ", ";
}
let srcSetUrl =
url + `&width=${size.width}&height=${size.height || ""} ${size.width}w`;
srcSet += srcSetUrl;
if (maxWidth) {
if (sizes) {
sizes += ", ";
}
sizes += `(max-width: ${maxWidth}px) ${size.width}px`;
}
if (size.width > largestImageWidth) {
largestImageWidth = size.width;
largestImageSrc = srcSetUrl;
}
}
props.srcSet = srcSet;
props.sizes = sizes;
props.src = "";
}
if (largestImageSrc && (!rest.width || largestImageWidth > rest.width)) {
props.src = largestImageSrc;
}
return <img {...rest} {...props} />;
}
<OptimizedImage
loading={index === 0 ? "eager" : "lazy"}
className="w-full h-full object-contain"
src={image}
alt=""
height={480}
width={480}
responsive={[
{
size: {
height: 480,
width: 480,
},
maxWidth: 600,
},
{
size: {
height: 767,
width: 767,
},
maxWidth: 767,
},
{
size: {
height: 1024,
width: 1024,
},
},
]}
/>
@Josh-McFarlin
Copy link

Hi, I wanted to let you know I turned this gist into an npm package that enables implementing this functionality with a single resource route. I would love it if you could check over the implementation and possibly contribute any improvements since you wrote this gist. Thanks!

https://github.com/Josh-McFarlin/remix-image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment