Optimize safe url generation for og images

This commit is contained in:
Sam Becker 2026-02-18 09:23:23 -06:00
parent af672dfcf4
commit 1994f3bfba
3 changed files with 34 additions and 28 deletions

View File

@ -3,7 +3,7 @@
import { Photo } from '@/photo'; import { Photo } from '@/photo';
import { NextImageSize } from '@/platforms/next-image'; import { NextImageSize } from '@/platforms/next-image';
import { IS_PREVIEW } from '@/app/config'; import { IS_PREVIEW } from '@/app/config';
import { getOptimizedUrlsForPhotos } from '@/photo/storage'; import { getDataUrlsForPhotos } from '@/photo/storage';
export default async function ImagePhotoGrid({ export default async function ImagePhotoGrid({
photos, photos,
@ -52,7 +52,7 @@ export default async function ImagePhotoGrid({
const cellHeight= height / rows - const cellHeight= height / rows -
(rows - 1) * gap / rows; (rows - 1) * gap / rows;
const photoUrls = await getOptimizedUrlsForPhotos( const photoUrls = await getDataUrlsForPhotos(
photos, photos,
optimizedSuffix, optimizedSuffix,
nextImageWidth, nextImageWidth,
@ -60,7 +60,7 @@ export default async function ImagePhotoGrid({
); );
const renderPhoto = ( const renderPhoto = (
{ id, url }: typeof photoUrls[number], { id, urlData }: typeof photoUrls[number],
width: number, width: number,
height: number, height: number,
) => ) =>
@ -75,7 +75,7 @@ export default async function ImagePhotoGrid({
}} }}
> >
<img {...{ <img {...{
src: url, src: urlData,
style: { style: {
...imageStyle, ...imageStyle,
width: '100%', width: '100%',

View File

@ -10,6 +10,7 @@ import {
uploadFileFromClient, uploadFileFromClient,
} from '@/platforms/storage'; } from '@/platforms/storage';
import { Photo } from '..'; import { Photo } from '..';
import { fetchBase64ImageFromUrl } from '@/utility/image';
const PREFIX_PHOTO = 'photo'; const PREFIX_PHOTO = 'photo';
const PREFIX_UPLOAD = 'upload'; const PREFIX_UPLOAD = 'upload';
@ -157,39 +158,35 @@ export const getStorageUrlsForPhoto = async ({ url }: Photo) => {
); );
}; };
export const getOptimizedUrlsForPhotos = async ( export const getDataUrlsForPhotos = async (
photos: Photo[], photos: Photo[],
optimizedSuffix: OptimizedSuffix, optimizedSuffix: OptimizedSuffix,
nextImageWidth: NextImageSize, nextImageWidth: NextImageSize,
addBypassSecret: boolean, addBypassSecret: boolean,
) => ) =>
Promise.all(photos Promise.all(photos
.map(async({ id, url: _url }) => { .map(async({ id, url }) => {
// Check for optimized image first // Check for optimized image first
const optimizedUrl =await getSignedUrlForUrl( const optimizedUrl =await getSignedUrlForUrl(
getOptimizedPhotoUrlForSuffix(_url, optimizedSuffix), getOptimizedPhotoUrlForSuffix(url, optimizedSuffix),
'GET', 'GET',
); );
const optimizedUrlExists = await fetch(optimizedUrl) const optimizedUrlData = await fetchBase64ImageFromUrl(optimizedUrl);
.then(res => res.ok)
.catch(() => false);
if (optimizedUrlExists) { if (optimizedUrlData) {
return { id, url: optimizedUrl }; return { id, urlData: optimizedUrlData };
} else { } else {
// Fall back on `next/image` if optimized image is not available // Fall back on `next/image` if optimized image is not available
const nextImageUrl = getOptimizedPhotoUrl({ const nextImageUrl = getOptimizedPhotoUrl({
imageUrl: _url, imageUrl: url,
size: nextImageWidth, size: nextImageWidth,
addBypassSecret, addBypassSecret,
}); });
const nextImageUrlExists = await fetch(nextImageUrl) const nextImageUrlData = await fetchBase64ImageFromUrl(nextImageUrl);
.then(res => res.ok) return { id, urlData: nextImageUrlData };
.catch(() => false);
return { id, url: nextImageUrlExists ? nextImageUrl : undefined };
} }
})) }))
.then(urls =>(urls.every(url => Boolean(url)) .then(urls =>(urls.every(({ urlData }) => Boolean(urlData))
? urls ? urls
// If any url is defined, return an empty array // If any url is undefined, return an empty array
: []) as { id: string, url: string }[]); : []) as { id: string, urlData: string }[]);

View File

@ -1,15 +1,24 @@
import { getFileNamePartsFromStorageUrl } from '@/platforms/storage';
export const removeBase64Prefix = (base64: string) => { export const removeBase64Prefix = (base64: string) => {
return base64.match(/^data:image\/[a-z]{3,4};base64,(.+)$/)?.[1] ?? base64; return base64.match(/^data:image\/[a-z]{3,4};base64,(.+)$/)?.[1] ?? base64;
}; };
export const fetchBase64ImageFromUrl = ( export const fetchBase64ImageFromUrl = async (
url: string, url: string,
contentType?: string,
fetchOptions?: RequestInit, fetchOptions?: RequestInit,
) => ) => {
fetch(url, fetchOptions) const { fileExtension } = getFileNamePartsFromStorageUrl(url);
const contentType = fileExtension === 'png' ? 'image/png' : 'image/jpeg';
return fetch(url, fetchOptions)
.then(async response => { .then(async response => {
const blob = await response.arrayBuffer(); if (response.ok) {
// eslint-disable-next-line max-len const blob = await response.arrayBuffer();
return `data:${contentType ?? response.headers.get('content-type')};base64,${Buffer.from(blob).toString('base64')}`; // eslint-disable-next-line max-len
}); return `data:${contentType};base64,${Buffer.from(blob).toString('base64')}`;
} else {
return undefined;
}
})
.catch(() => undefined);
};