Refactor og image generation

This commit is contained in:
Sam Becker 2023-09-17 19:46:07 -05:00
parent 0af79f491a
commit 9bdf0c3f09
22 changed files with 430 additions and 348 deletions

View File

@ -1,29 +1,27 @@
import { auth } from '@/auth';
import { getImageCacheHeadersForAuth } from '@/cache';
import {
IMAGE_OG_SMALL_SIZE,
MAX_PHOTOS_TO_SHOW_HOME,
} from '@/photo/image-response';
import HomeImageResponse from '@/photo/image-response/HomeImageResponse';
import { getPhotos } from '@/services/postgres';
import { IMAGE_OG_SMALL_HEIGHT, IMAGE_OG_SMALL_WIDTH } from '@/site';
import { ImageResponse } from '@vercel/og';
export const runtime = 'edge';
export async function GET(request: Request) {
const photos = await getPhotos();
const photos = await getPhotos(
undefined,
MAX_PHOTOS_TO_SHOW_HOME,
);
const headers = await getImageCacheHeadersForAuth(await auth());
const { width, height } = IMAGE_OG_SMALL_SIZE;
return new ImageResponse(
(
<HomeImageResponse {...{
photos,
request,
width: IMAGE_OG_SMALL_WIDTH,
height: IMAGE_OG_SMALL_HEIGHT,
}}/>
),
{
width: IMAGE_OG_SMALL_WIDTH,
height: IMAGE_OG_SMALL_HEIGHT,
headers,
},
<HomeImageResponse {...{ photos, request, width, height }}/>,
{ width, height, headers },
);
}

View File

@ -1,42 +1,29 @@
import { auth } from '@/auth';
import { getImageCacheHeadersForAuth } from '@/cache';
import PhotoOGImageResponse from '@/photo/image-response/PhotoOGImageResponse';
import { IMAGE_OG_SIZE } from '@/photo/image-response';
import PhotoImageResponse from '@/photo/image-response/PhotoImageResponse';
import { getPhoto } from '@/services/postgres';
import { IMAGE_OG_WIDTH, IMAGE_OG_HEIGHT } from '@/site';
import { FONT_FAMILY_IBM_PLEX_MONO, getIBMPlexMonoMedium } from '@/site/font';
import { getIBMPlexMonoMedium } from '@/site/font';
import { ImageResponse } from '@vercel/og';
export const runtime = 'edge';
export async function GET(request: Request, context: any) {
const photo = await getPhoto(context.params.photoId);
const fontData = await getIBMPlexMonoMedium();
const {
fontFamily,
fonts,
} = await getIBMPlexMonoMedium();
const headers = await getImageCacheHeadersForAuth(await auth());
if (!photo) { return null; }
const { width, height } = IMAGE_OG_SIZE;
return new ImageResponse(
(
<PhotoOGImageResponse
photo={photo}
requestOrPhotoPath={request}
width={IMAGE_OG_WIDTH}
height={IMAGE_OG_HEIGHT}
fontFamily={FONT_FAMILY_IBM_PLEX_MONO}
/>
),
{
width: IMAGE_OG_WIDTH,
height: IMAGE_OG_HEIGHT,
fonts: [
{
name: FONT_FAMILY_IBM_PLEX_MONO,
data: fontData,
weight: 500,
style: 'normal',
},
],
headers,
},
<PhotoImageResponse {...{ photo, request, width, height, fontFamily }} />,
{ width, height, fonts, headers },
);
}

View File

@ -1,30 +1,35 @@
import { auth } from '@/auth';
import { getImageCacheHeadersForAuth } from '@/cache';
import HomeImageResponse from '@/photo/image-response/HomeImageResponse';
import {
IMAGE_OG_SMALL_SIZE,
MAX_PHOTOS_TO_SHOW_PER_TAG,
} from '@/photo/image-response';
import TagImageResponse from '@/photo/image-response/TagImageResponse';
import { getPhotos } from '@/services/postgres';
import { IMAGE_OG_SMALL_HEIGHT, IMAGE_OG_SMALL_WIDTH } from '@/site';
import { getIBMPlexMonoMedium } from '@/site/font';
import { ImageResponse } from '@vercel/og';
export const runtime = 'edge';
export async function GET(request: Request, context: any) {
const photos = await getPhotos(
undefined, undefined, undefined, context.params.tag);
undefined,
MAX_PHOTOS_TO_SHOW_PER_TAG,
undefined,
context.params.tag,
);
const {
fontFamily,
fonts,
} = await getIBMPlexMonoMedium();
const headers = await getImageCacheHeadersForAuth(await auth());
const { width, height } = IMAGE_OG_SMALL_SIZE;
return new ImageResponse(
(
<HomeImageResponse {...{
photos,
request,
width: IMAGE_OG_SMALL_WIDTH,
height: IMAGE_OG_SMALL_HEIGHT,
}}/>
),
{
width: IMAGE_OG_SMALL_WIDTH,
height: IMAGE_OG_SMALL_HEIGHT,
headers,
},
<TagImageResponse {...{ photos, request, width, height, fontFamily }}/>,
{ width, height, fonts, headers },
);
}

View File

@ -53,13 +53,13 @@ export default async function TagPage({ params: { tag } }: TagProps) {
<SiteGrid
contentMain={<div className="space-y-8 mt-4">
<div className={cc(
'flex gap-2 sm:gap-0',
'sm:grid sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
'flex flex-col gap-y-0.5',
'xs:grid grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
)}>
<PhotoTag tag={tag} />
<span className={cc(
'uppercase text-gray-300 dark:text-gray-500',
'col-span-2 md:col-span-1 lg:col-span-2',
'sm:col-span-2 md:col-span-1 lg:col-span-2',
)}>
{descriptionForTaggedPhotos(photos)}
</span>

View File

@ -1,41 +1,45 @@
import { auth } from '@/auth';
import { getImageCacheHeadersForAuth } from '@/cache';
import DeployImageResponse from '@/photo/image-response/DeployImageResponse';
import {
IMAGE_OG_SIZE,
MAX_PHOTOS_TO_SHOW_TEMPLATE_TIGHT,
} from '@/photo/image-response';
import TemplateImageResponse from
'@/photo/image-response/TemplateImageResponse';
import { getPhotos } from '@/services/postgres';
import { IMAGE_OG_HEIGHT, IMAGE_OG_WIDTH } from '@/site';
import { FONT_FAMILY_IBM_PLEX_MONO, getIBMPlexMonoMedium } from '@/site/font';
import { getIBMPlexMonoMedium } from '@/site/font';
import { ImageResponse } from '@vercel/og';
export const runtime = 'edge';
export async function GET(request: Request) {
const photos = await getPhotos('priority');
const fontData = await getIBMPlexMonoMedium();
const photos = await getPhotos('priority', MAX_PHOTOS_TO_SHOW_TEMPLATE_TIGHT);
const {
fontFamily,
fonts,
} = await getIBMPlexMonoMedium();
const headers = await getImageCacheHeadersForAuth(await auth());
const { width, height } = IMAGE_OG_SIZE;
return new ImageResponse(
(
<DeployImageResponse {...{
<TemplateImageResponse {...{
photos,
request,
includeHeader: false,
outerMargin: 0,
width: IMAGE_OG_WIDTH,
height: IMAGE_OG_HEIGHT,
verticalOffset: -30,
fontFamily: FONT_FAMILY_IBM_PLEX_MONO,
width,
height,
fontFamily,
}}/>
),
{
width: IMAGE_OG_WIDTH,
height: IMAGE_OG_HEIGHT,
fonts: [
{
name: FONT_FAMILY_IBM_PLEX_MONO,
data: fontData,
style: 'normal',
},
],
width,
height,
fonts,
headers,
},
);

View File

@ -1,39 +1,37 @@
import { auth } from '@/auth';
import { getImageCacheHeadersForAuth } from '@/cache';
import DeployImageResponse from '@/photo/image-response/DeployImageResponse';
import {
GRID_OG_SIZE,
MAX_PHOTOS_TO_SHOW_TEMPLATE,
} from '@/photo/image-response';
import TemplateImageResponse from
'@/photo/image-response/TemplateImageResponse';
import { getPhotos } from '@/services/postgres';
import { GRID_OG_WIDTH, GRID_OG_HEIGHT } from '@/site';
import { FONT_FAMILY_IBM_PLEX_MONO, getIBMPlexMonoMedium } from '@/site/font';
import { getIBMPlexMonoMedium } from '@/site/font';
import { ImageResponse } from '@vercel/og';
export const runtime = 'edge';
export async function GET(request: Request) {
const photos = await getPhotos('priority');
const fontData = await getIBMPlexMonoMedium();
const photos = await getPhotos('priority', MAX_PHOTOS_TO_SHOW_TEMPLATE);
const {
fontFamily,
fonts,
} = await getIBMPlexMonoMedium();
const headers = await getImageCacheHeadersForAuth(await auth());
const { width, height } = GRID_OG_SIZE;
return new ImageResponse(
(
<DeployImageResponse {...{
<TemplateImageResponse {...{
photos,
request,
width: GRID_OG_WIDTH,
height: GRID_OG_HEIGHT,
fontFamily: FONT_FAMILY_IBM_PLEX_MONO,
width,
height,
fontFamily,
}}/>
),
{
width: GRID_OG_WIDTH,
height: GRID_OG_HEIGHT,
fonts: [
{
name: FONT_FAMILY_IBM_PLEX_MONO,
data: fontData,
style: 'normal',
},
],
headers,
},
{ width, height, fonts, headers },
);
}

View File

@ -9,9 +9,9 @@ import {
import { cc } from '@/utility/css';
import Link from 'next/link';
import { BiError } from 'react-icons/bi';
import { IMAGE_OG_HEIGHT, IMAGE_OG_RATIO, IMAGE_OG_WIDTH } from '@/site';
import { absolutePathForPhotoImage, pathForPhoto } from '@/site/paths';
import Spinner from '@/components/Spinner';
import { IMAGE_OG_SIZE } from './image-response';
export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed';
@ -44,6 +44,8 @@ export default function PhotoOGTile({
}
}, [loadingStateExternal, loadingStateInternal]);
const { width, height, ratio } = IMAGE_OG_SIZE;
return (
<Link
key={photo.id}
@ -58,7 +60,7 @@ export default function PhotoOGTile({
>
<div
className="relative"
style={{ aspectRatio: IMAGE_OG_RATIO }}
style={{ aspectRatio: ratio }}
>
{loadingState === 'loading' &&
<div className={cc(
@ -85,8 +87,8 @@ export default function PhotoOGTile({
'transition-opacity',
)}
src={absolutePathForPhotoImage(photo)}
width={IMAGE_OG_WIDTH}
height={IMAGE_OG_HEIGHT}
width={width}
height={height}
onLoad={() => {
if (onLoad) {
onLoad();

View File

@ -1,5 +1,6 @@
import { Photo } from '..';
import PhotoGridImageResponse from './PhotoGridImageResponse';
import ImageContainer from './components/ImageContainer';
import ImagePhotoGrid from './components/ImagePhotoGrid';
export default function HomeImageResponse({
photos,
@ -12,31 +13,16 @@ export default function HomeImageResponse({
width: number
height: number
}) {
const grid = photos.length >= 12
? { colCount: 4, rowCount: 3 }
: { colCount: 3, rowCount: 2 };
const photosPerGrid = grid.colCount * grid.rowCount;
return (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
background: 'transparent',
width,
height,
}}>
<PhotoGridImageResponse {...{
photos: photos.slice(0, photosPerGrid),
request,
nextImageWidth: 200,
...grid,
gap: 6,
width,
height,
}} />
</div>
<ImageContainer {...{ width, height }} >
<ImagePhotoGrid
{...{
photos,
request,
width,
height,
}}
/>
</ImageContainer>
);
}

View File

@ -1,87 +0,0 @@
import { getNextImageUrlForRequest } from '@/utility/image';
import { Photo, titleForPhoto } from '..';
export default function PhotoGridImageResponse({
photos,
request,
nextImageWidth = 400,
height,
width,
colCount,
rowCount,
gap = 12,
verticalOffset,
}: {
photos: Photo[]
request: Request
nextImageWidth?: 200 | 400,
height?: number
width: number
colCount: number
rowCount: number
gap?: number
verticalOffset?: number
}) {
const imageWidth = (width - ((colCount - 1) * gap)) / colCount;
const imageHeight = height
? (height - ((rowCount - 1) * gap)) / rowCount
: undefined;
return (
<div style={{
display: 'flex',
flexWrap: 'wrap',
...verticalOffset && { transform: `translateY(${verticalOffset}px)` },
}}>
{photos
.slice(0, colCount * rowCount)
.map((photo, index) => {
const photoWidth = imageHeight
? imageHeight * photo.aspectRatio
: imageWidth;
const photoHeight = imageHeight ?? imageWidth / photo.aspectRatio;
const horizontalOffset = imageHeight
? Math.abs((imageHeight * photo.aspectRatio - imageWidth) / 2)
: undefined;
return (
<div
key={photo.id}
style={{
display: 'flex',
position: 'relative',
width: imageWidth,
height: imageHeight ?? imageWidth / photo.aspectRatio,
...(index + 1) % colCount !== 0 && {
marginRight: gap,
},
...index < colCount * (rowCount - 1) && {
marginBottom: gap,
},
overflow: 'hidden',
}}
>
<img
src={getNextImageUrlForRequest(
photo.url,
request,
nextImageWidth,
)}
alt={titleForPhoto(photo)}
width={nextImageWidth}
height={nextImageWidth / photo.aspectRatio}
style={{
display: 'flex',
width: photoWidth,
height: photoHeight,
...horizontalOffset && {
marginLeft: `-${horizontalOffset}px`,
},
}}
/>
</div>
);
})}
</div>
);
}

View File

@ -0,0 +1,55 @@
import { Photo } from '..';
import { NextImageWidth } from '@/utility/image';
import { formatModelShort } from '@/utility/exif';
import { AiFillApple } from 'react-icons/ai';
import ImageCaption from './components/ImageCaption';
import ImagePhotoGrid from './components/ImagePhotoGrid';
export default function PhotoImageResponse({
photo,
request,
width,
height,
fontFamily,
}: {
photo: Photo
request: Request
width: NextImageWidth
height: number
fontFamily: string
}) {
return (
<div style={{
display: 'flex',
position: 'relative',
background: 'red',
width,
height,
}}>
<ImagePhotoGrid {...{
photos: [photo],
request,
width,
height,
}} />
<ImageCaption {...{ width, height, fontFamily }}>
{photo.make === 'Apple' &&
<div style={{ display: 'flex' }}>
<AiFillApple />
</div>}
<div style={{ display: 'flex' }}>
{formatModelShort(photo.model)}
</div>
<div style={{ display: 'flex' }}>
{photo.focalLengthFormatted}
</div>
<div style={{ display: 'flex' }}>
{photo.fNumberFormatted}
</div>
<div>
{photo.isoFormatted}
</div>
</ImageCaption>
</div>
);
};

View File

@ -1,75 +0,0 @@
import { Photo, titleForPhoto } from '..';
import { getNextImageUrlForRequest } from '@/utility/image';
import { formatModelShort } from '@/utility/exif';
import { AiFillApple } from 'react-icons/ai';
export default function PhotoOGImageResponse({
photo,
requestOrPhotoPath,
width,
height,
fontFamily,
}: {
photo: Photo
requestOrPhotoPath: Request | string
width: number
height: number
fontFamily: string
}) {
return (
<div style={{
display: 'flex',
position: 'relative',
background: 'red',
width,
height,
}}>
<img
src={typeof requestOrPhotoPath === 'string'
? requestOrPhotoPath
: getNextImageUrlForRequest(
photo.url,
requestOrPhotoPath,
width,
)}
width={width}
height={width / photo.aspectRatio}
alt={titleForPhoto(photo)}
/>
<div style={{
display: 'flex',
gap: 36,
position: 'absolute',
padding: '400px 56px 48px 56px',
color: 'white',
background:
'linear-gradient(to bottom, ' +
'rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0.7))',
backgroundBlendMode: 'multiply',
fontFamily,
fontSize: 60,
lineHeight: 1,
bottom: 0,
left: 0,
right: 0,
}}>
{photo.make === 'Apple' &&
<div style={{ display: 'flex' }}>
<AiFillApple />
</div>}
<div style={{ display: 'flex' }}>
{formatModelShort(photo.model)}
</div>
<div style={{ display: 'flex' }}>
{photo.focalLengthFormatted}
</div>
<div style={{ display: 'flex' }}>
{photo.fNumberFormatted}
</div>
<div>
{photo.isoFormatted}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,43 @@
import { Photo } from '..';
import { FaTag } from 'react-icons/fa';
import ImageCaption from './components/ImageCaption';
import ImagePhotoGrid from './components/ImagePhotoGrid';
import ImageContainer from './components/ImageContainer';
export default function TagImageResponse({
photos,
request,
width,
height,
fontFamily,
}: {
photos: Photo[]
request: Request
width: number
height: number
fontFamily: string
}) {
return (
<ImageContainer {...{
width,
height,
...photos.length === 0 && { background: 'black' },
}}>
<ImagePhotoGrid
{...{
photos,
request,
width,
height,
}}
/>
<ImageCaption {...{ width, height, fontFamily }}>
<FaTag
size={height * .067}
style={{ transform: `translateY(${height * .02}px)` }}
/>
<span>{'Door County'.toUpperCase()}</span>
</ImageCaption>
</ImageContainer>
);
}

View File

@ -1,9 +1,9 @@
import { Photo } from '..';
import PhotoGridImageResponse from './PhotoGridImageResponse';
import IconFullFrame from '@/icons/IconFullFrame';
import IconGrid from '@/icons/IconGrid';
import ImagePhotoGrid from './components/ImagePhotoGrid';
export default function DeployImageResponse({
export default function TemplateImageResponse({
photos,
request,
width,
@ -40,16 +40,16 @@ export default function DeployImageResponse({
height,
fontFamily,
}}>
<div style={{
display: 'flex',
alignItems: 'center',
fontSize: 40,
height: 80,
lineHeight: 1,
marginBottom: outerMargin,
width: '100%',
}}>
{includeHeader &&
{includeHeader &&
<div style={{
display: 'flex',
alignItems: 'center',
fontSize: 40,
height: 80,
lineHeight: 1,
marginBottom: outerMargin,
width: '100%',
}}>
<div style={{
display: 'flex',
justifyContent: 'flex-start',
@ -76,23 +76,29 @@ export default function DeployImageResponse({
<IconGrid includeTitle={false} width={80} />
</div>
</div>
</div>}
<div style={{
display: 'flex',
justifyContent: 'flex-end',
flexGrow: 1,
}}>
photos.sambecker.com
</div>
</div>
<div style={{
display: 'flex',
justifyContent: 'flex-end',
flexGrow: 1,
}}>
photos.sambecker.com
</div>
</div>}
<div style={{
display: 'flex',
...verticalOffset && { transform: `translateY(${verticalOffset}px)` },
}}>
<ImagePhotoGrid {...{
photos,
request,
width: innerWidth,
height: includeHeader
? height - 130 - outerMargin * 2
: height,
gap: 10,
}} />
</div>
<PhotoGridImageResponse {...{
photos,
request,
colCount: 4,
rowCount: 4,
width: innerWidth,
verticalOffset,
}} />
</div>
);
}

View File

@ -0,0 +1,35 @@
export default function ImageCaption({
height,
children,
fontFamily,
}: {
width: number
height: number
fontFamily: string
children: React.ReactNode
}) {
return (
<div style={{
display: 'flex',
gap: height * .053,
position: 'absolute',
paddingTop: height * .6,
paddingBottom: height * .075,
paddingLeft: height * .0875,
paddingRight: height * .0875,
color: 'white',
background:
'linear-gradient(to bottom, ' +
'rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0.7))',
backgroundBlendMode: 'multiply',
fontFamily,
fontSize: height *.089,
lineHeight: 1,
bottom: 0,
left: 0,
right: 0,
}}>
{children}
</div>
);
}

View File

@ -0,0 +1,27 @@
import { ReactNode } from 'react';
export default function ImageContainer({
width,
height,
background = 'transparent',
children,
}: {
width: number
height: number
background?: 'transparent' | 'black'
children: ReactNode
}) {
return (
<div style={{
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background,
width,
height,
}}>
{children}
</div>
);
}

View File

@ -0,0 +1,64 @@
/* eslint-disable jsx-a11y/alt-text */
import { Photo } from '@/photo';
import { getNextImageUrlForRequest } from '@/utility/image';
export default function ImagePhotoGrid({
photos,
request,
width,
height,
gap = 3,
}: {
photos: Photo[]
request: Request
width: number
height: number
gap?: number
}) {
let count = 1;
if (photos.length >= 12) { count = 12; }
else if (photos.length >= 6) { count = 6; }
else if (photos.length >= 4) { count = 4; }
else if (photos.length >= 2) { count = 2; }
const imageQuality = count <= 2 ? 1050 : 400;
let rows = 1;
if (count > 12) { rows = 4; }
else if (count > 6) { rows = 3; }
else if (count > 3) { rows = 2; }
const imagesPerRow = count / rows;
return (
<div style={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
justifyContent: 'center',
gap,
}}>
{photos.slice(0, count).map(photo =>
<div
key={photo.id}
style={{
display: 'flex',
width:
width / imagesPerRow -
(imagesPerRow - 1) * gap / (imagesPerRow),
height: height / rows - (rows - 1) * gap / rows,
}}
>
<img {...{
src: getNextImageUrlForRequest(photo.url, request, imageQuality),
style: {
width: '100%',
height: '100%',
objectFit: 'cover',
},
}} />
</div>)}
</div>
);
}

View File

@ -0,0 +1,35 @@
import { NextImageWidth } from '@/utility/image';
export const MAX_PHOTOS_TO_SHOW_HOME = 12;
export const MAX_PHOTOS_TO_SHOW_PER_TAG = 6;
export const MAX_PHOTOS_TO_SHOW_TEMPLATE = 16;
export const MAX_PHOTOS_TO_SHOW_TEMPLATE_TIGHT = 12;
// 16:9 og image ratio
const IMAGE_OG_RATIO = 16 / 9;
const IMAGE_OG_WIDTH: NextImageWidth = 1200;
const IMAGE_OG_HEIGHT = IMAGE_OG_WIDTH * (1 / IMAGE_OG_RATIO);
export const IMAGE_OG_SIZE = {
width: IMAGE_OG_WIDTH,
height: IMAGE_OG_HEIGHT,
ratio: IMAGE_OG_RATIO,
};
// 16:9 og image ratio, small
const IMAGE_OG_SMALL_WIDTH = 800;
const IMAGE_OG_SMALL_HEIGHT = IMAGE_OG_SMALL_WIDTH * (1 / IMAGE_OG_RATIO);
export const IMAGE_OG_SMALL_SIZE = {
width: IMAGE_OG_SMALL_WIDTH,
height: IMAGE_OG_SMALL_HEIGHT,
ratio: IMAGE_OG_RATIO,
};
// 3:2 og grid ratio
const GRID_OG_RATIO = 1.33;
const GRID_OG_WIDTH = 2000;
const GRID_OG_HEIGHT = GRID_OG_WIDTH * (1 / GRID_OG_RATIO);
export const GRID_OG_SIZE = {
width: GRID_OG_WIDTH,
height: GRID_OG_HEIGHT,
ratio: GRID_OG_RATIO,
};

View File

@ -1,8 +1,14 @@
import { BLOB_BASE_URL } from '@/site';
import { PATH_ADMIN_UPLOAD_BLOB_HANDLER } from '@/site/paths';
import { del, list, put } from '@vercel/blob';
import { upload } from '@vercel/blob/client';
const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
)?.[1].toLowerCase();
export const BLOB_BASE_URL =
`https://${STORE_ID}.public.blob.vercel-storage.com`;
export const ACCEPTED_PHOTO_FILE_TYPES = [
'image/jpg',
'image/jpeg',

View File

@ -1,7 +1,16 @@
export const FONT_FAMILY_IBM_PLEX_MONO = 'IBMPlexMono';
const FONT_FAMILY_IBM_PLEX_MONO = 'IBMPlexMono';
export const getIBMPlexMonoMedium = () => fetch(new URL(
'/public/fonts/IBMPlexMono-Medium.ttf',
import.meta.url
))
.then(res => res.arrayBuffer());
.then(res => res.arrayBuffer())
.then(data => ({
fontFamily: FONT_FAMILY_IBM_PLEX_MONO,
fonts: [{
name: FONT_FAMILY_IBM_PLEX_MONO,
data,
weight: 500,
style: 'normal',
} as const],
}));

View File

@ -6,26 +6,3 @@ export const IMAGE_SMALL_WIDTH = 300;
// Height determined by intrinsic photo aspect ratio
export const IMAGE_LARGE_WIDTH = 900;
// 16:9 og image ratio
export const IMAGE_OG_RATIO = 16 / 9;
export const IMAGE_OG_WIDTH = 1200;
export const IMAGE_OG_HEIGHT =
IMAGE_OG_WIDTH * (1 / IMAGE_OG_RATIO);
// 16:9 og image ratio, small
export const IMAGE_OG_SMALL_WIDTH = 800;
export const IMAGE_OG_SMALL_HEIGHT =
IMAGE_OG_SMALL_WIDTH * (1 / IMAGE_OG_RATIO);
// 3:2 og grid ratio
export const GRID_OG_RATIO = 1.33;
export const GRID_OG_WIDTH = 2000;
export const GRID_OG_HEIGHT = GRID_OG_WIDTH * (1 / GRID_OG_RATIO);
const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
)?.[1].toLowerCase();
export const BLOB_BASE_URL =
`https://${STORE_ID}.public.blob.vercel-storage.com`;

View File

@ -1,6 +1,7 @@
import Link from 'next/link';
import { pathForTag } from '@/site/paths';
import { FaTag } from 'react-icons/fa';
import { cc } from '@/utility/css';
export default function PhotoTag({
tag,
@ -15,7 +16,10 @@ export default function PhotoTag({
>
<FaTag
size={11}
className="text-gray-700 dark:text-gray-300 translate-y-[0.5px]"
className={cc(
'flex-shrink-0',
'text-gray-700 dark:text-gray-300 translate-y-[0.5px]',
)}
/>
<span className="uppercase">
{tag.replaceAll('-', ' ')}

View File

@ -1,7 +1,10 @@
// Must be explicity defined next.config.js `imageSizes`
export type NextImageWidth = 200 | 400 | 1050 | 1200;
export const getNextImageUrlForRequest = (
imageUrl: string,
request: Request,
width: number,
width: NextImageWidth,
quality = 75,
) => {
const protocol = (request.headers.get('x-forwarded-proto') || 'https')