Merge pull request #164 from sambecker/static-categories

Offer static optimization for photo categories
This commit is contained in:
Sam Becker 2025-01-15 19:16:05 -06:00 committed by GitHub
commit 0bb0647608
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 120 additions and 38 deletions

View File

@ -104,9 +104,10 @@ Application behavior can be changed by configuring the following environment var
#### Site behavior
- `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage
- `NEXT_PUBLIC_DEFAULT_THEME = light | dark` sets preferred initial theme (defaults to `system` when not configured)
- `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage (results in increased storage usage)
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES = 1` enables static optimization for pages, i.e., renders pages at build time (results in increased project usage)—⚠️ _Experimental_
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES = 1` enables static optimization for OG images, i.e., renders images at build time (results in increased project usage)—⚠️ _Experimental_
- `NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS = 1` do not optimize photo uploads before storing (⚠️ results in increased storage usage)
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTOS = 1` enables static optimization for photo pages (`p/[photoId]`), i.e., renders pages at build time (⚠️ results in increased project usage)
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_OG_IMAGES = 1` enables static optimization for OG images, i.e., renders images at build time (⚠️ results in increased project usage)
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORIES = 1` enables static optimization for photo categories (`tag/[tag]`, `shot-on/[make]/[model]`, etc.), i.e., renders pages at build time (⚠️ results in increased project usage)
- `NEXT_PUBLIC_MATTE_PHOTOS = 1` constrains the size of each photo, and enables a surrounding border (potentially useful for photos with tall aspect ratios)
- `NEXT_PUBLIC_BLUR_DISABLED = 1` prevents image blur data being stored and displayed (potentially useful for limiting Postgres usage)
- `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data (⚠️ re-compresses uploaded images in order to remove GPS information)

View File

@ -3,7 +3,10 @@
import PhotoUpload from '@/photo/PhotoUpload';
import { clsx } from 'clsx/lite';
import SiteGrid from '@/components/SiteGrid';
import { AI_TEXT_GENERATION_ENABLED, PRO_MODE_ENABLED } from '@/site/config';
import {
AI_TEXT_GENERATION_ENABLED,
PRESERVE_ORIGINAL_UPLOADS,
} from '@/site/config';
import AdminPhotosTable from '@/admin/AdminPhotosTable';
import AdminPhotosTableInfinite from '@/admin/AdminPhotosTableInfinite';
import PathLoaderButton from '@/components/primitives/PathLoaderButton';
@ -43,7 +46,7 @@ export default function AdminPhotosClient({
<div className="flex">
<div className="grow min-w-0">
<PhotoUpload
shouldResize={!PRO_MODE_ENABLED}
shouldResize={!PRESERVE_ORIGINAL_UPLOADS}
isUploading={isUploading}
setIsUploading={setIsUploading}
onLastUpload={onLastPhotoUpload}

View File

@ -1,13 +1,26 @@
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
import { getUniqueFilmSimulations } from '@/photo/db/query';
import { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation';
import FilmSimulationOverview from '@/simulation/FilmSimulationOverview';
import { IS_PRODUCTION } from '@/site/config';
import { getPhotosFilmSimulationDataCached } from '@/simulation/data';
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/site/config';
import { Metadata } from 'next/types';
import { cache } from 'react';
const getPhotosFilmSimulationDataCachedCached =
cache(getPhotosFilmSimulationDataCached);
export let generateStaticParams:
(() => Promise<{ simulation: FilmSimulation }[]>) | undefined = undefined;
if (STATICALLY_OPTIMIZED_PHOTO_CATEGORIES && IS_PRODUCTION) {
generateStaticParams = async () => {
const simulations = await getUniqueFilmSimulations();
return simulations.map(({ simulation }) => ({ simulation }));
};
}
interface FilmSimulationProps {
params: Promise<{ simulation: FilmSimulation }>
}

View File

@ -2,6 +2,9 @@ import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal';
import FocalLengthOverview from '@/focal/FocalLengthOverview';
import { getPhotosFocalLengthDataCached } from '@/focal/data';
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
import { IS_PRODUCTION } from '@/site/config';
import { getUniqueFocalLengths } from '@/photo/db/query';
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/site/config';
import { PATH_ROOT } from '@/site/paths';
import type { Metadata } from 'next';
import { redirect } from 'next/navigation';
@ -13,6 +16,16 @@ const getPhotosFocalDataCachedCached = cache((focal: number) =>
limit: INFINITE_SCROLL_GRID_INITIAL,
}));
export let generateStaticParams:
(() => Promise<{ focal: string }[]>) | undefined = undefined;
if (STATICALLY_OPTIMIZED_PHOTO_CATEGORIES && IS_PRODUCTION) {
generateStaticParams = async () => {
const focalLengths = await getUniqueFocalLengths();
return focalLengths.map(({ focal }) => ({ focal: focal.toString() }));
};
}
interface FocalLengthProps {
params: Promise<{ focal: string }>
}

View File

@ -4,7 +4,10 @@ import PhotoImageResponse from '@/image-response/PhotoImageResponse';
import { getIBMPlexMonoMedium } from '@/site/font';
import { ImageResponse } from 'next/og';
import { getImageResponseCacheControlHeaders } from '@/image-response/cache';
import { IS_PRODUCTION, STATICALLY_OPTIMIZED_OG_IMAGES } from '@/site/config';
import {
IS_PRODUCTION,
STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES,
} from '@/site/config';
import { getPhotoIds } from '@/photo/db/query';
import { GENERATE_STATIC_PARAMS_LIMIT } from '@/photo/db';
import { isNextImageReadyBasedOnPhotos } from '@/photo';
@ -12,7 +15,7 @@ import { isNextImageReadyBasedOnPhotos } from '@/photo';
export let generateStaticParams:
(() => Promise<{ photoId: string }[]>) | undefined = undefined;
if (STATICALLY_OPTIMIZED_OG_IMAGES && IS_PRODUCTION) {
if (STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES && IS_PRODUCTION) {
generateStaticParams = async () => {
const photos = await getPhotoIds({ limit: GENERATE_STATIC_PARAMS_LIMIT });
return photos.map(photoId => ({ photoId }));

View File

@ -12,7 +12,7 @@ import {
} from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotosNearIdCached } from '@/photo/cache';
import { IS_PRODUCTION, STATICALLY_OPTIMIZED_PAGES } from '@/site/config';
import { IS_PRODUCTION, STATICALLY_OPTIMIZED_PHOTOS } from '@/site/config';
import { getPhotoIds } from '@/photo/db/query';
import { GENERATE_STATIC_PARAMS_LIMIT } from '@/photo/db';
import { cache } from 'react';
@ -25,7 +25,7 @@ const getPhotosNearIdCachedCached = cache((photoId: string) =>
export let generateStaticParams:
(() => Promise<{ photoId: string }[]>) | undefined = undefined;
if (STATICALLY_OPTIMIZED_PAGES && IS_PRODUCTION) {
if (STATICALLY_OPTIMIZED_PHOTOS && IS_PRODUCTION) {
generateStaticParams = async () => {
const photos = await getPhotoIds({ limit: GENERATE_STATIC_PARAMS_LIMIT });
return photos.map(photoId => ({ photoId }));

View File

@ -5,6 +5,9 @@ import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
import { getPhotosCameraDataCached } from '@/camera/data';
import CameraOverview from '@/camera/CameraOverview';
import { cache } from 'react';
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/site/config';
import { IS_PRODUCTION } from '@/site/config';
import { getUniqueCameras } from '@/photo/db/query';
const getPhotosCameraDataCachedCached = cache((
make: string,
@ -15,6 +18,16 @@ const getPhotosCameraDataCachedCached = cache((
INFINITE_SCROLL_GRID_INITIAL,
));
export let generateStaticParams:
(() => Promise<{ make: string, model: string }[]>) | undefined = undefined;
if (STATICALLY_OPTIMIZED_PHOTO_CATEGORIES && IS_PRODUCTION) {
generateStaticParams = async () => {
const cameras = await getUniqueCameras();
return cameras.map(({ camera: { make, model } }) => ({ make, model }));
};
}
export async function generateMetadata({
params,
}: CameraProps): Promise<Metadata> {

View File

@ -1,4 +1,7 @@
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
import { getUniqueTags } from '@/photo/db/query';
import { IS_PRODUCTION } from '@/site/config';
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/site/config';
import { PATH_ROOT } from '@/site/paths';
import { generateMetaForTag } from '@/tag';
import TagOverview from '@/tag/TagOverview';
@ -10,6 +13,16 @@ import { cache } from 'react';
const getPhotosTagDataCachedCached = cache((tag: string) =>
getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL}));
export let generateStaticParams:
(() => Promise<{ tag: string }[]>) | undefined = undefined;
if (STATICALLY_OPTIMIZED_PHOTO_CATEGORIES && IS_PRODUCTION) {
generateStaticParams = async () => {
const tags = await getUniqueTags();
return tags.map(({ tag }) => ({ tag }));
};
}
interface TagProps {
params: Promise<{ tag: string }>
}

View File

@ -11,7 +11,7 @@ import { ExifData, ExifParserFactory } from 'ts-exif-parser';
import { PhotoFormData } from './form';
import { FilmSimulation } from '@/simulation';
import sharp, { Sharp } from 'sharp';
import { GEO_PRIVACY_ENABLED, PRO_MODE_ENABLED } from '@/site/config';
import { GEO_PRIVACY_ENABLED, PRESERVE_ORIGINAL_UPLOADS } from '@/site/config';
const IMAGE_WIDTH_RESIZE = 200;
const IMAGE_WIDTH_BLUR = 200;
@ -169,5 +169,5 @@ export const removeGpsData = async (image: ArrayBuffer) =>
GPSHPositioningError: GPS_NULL_STRING,
},
})
.toFormat('jpeg', { quality: PRO_MODE_ENABLED ? 95 : 80 })
.toFormat('jpeg', { quality: PRESERVE_ORIGINAL_UPLOADS ? 95 : 80 })
.toBuffer();

View File

@ -51,11 +51,12 @@ export default function SiteChecklistClient({
showFilmSimulations,
showExifInfo,
defaultTheme,
isProModeEnabled,
areOriginalUploadsPreserved,
isGridHomepageEnabled,
isStaticallyOptimized,
arePagesStaticallyOptimized,
areOGImagesStaticallyOptimized,
arePhotosStaticallyOptimized,
arePhotoOGImagesStaticallyOptimized,
arePhotoCategoriesStaticallyOptimized,
arePhotosMatted,
isBlurEnabled,
isGeoPrivacyEnabled,
@ -168,6 +169,16 @@ export default function SiteChecklistClient({
{label}
</span>
</div>;
const renderSubStatusWithEnvVar = (
type: ComponentProps<typeof StatusIcon>['type'],
variable: string,
) =>
renderSubStatus(
type,
renderEnvVars([variable]),
'translate-y-[5px]',
);
const renderError = ({
connection,
@ -452,13 +463,13 @@ export default function SiteChecklistClient({
{renderEnvVars(['NEXT_PUBLIC_DEFAULT_THEME'])}
</ChecklistRow>
<ChecklistRow
title="Pro mode"
status={isProModeEnabled}
title="Preserve original uploads"
status={areOriginalUploadsPreserved}
optional
>
Set environment variable to {'"1"'} to enable
higher quality image storage:
{renderEnvVars(['NEXT_PUBLIC_PRO_MODE'])}
Set environment variable to {'"1"'} to prevent
image uploads being optimized before storing:
{renderEnvVars(['NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS'])}
</ChecklistRow>
<ChecklistRow
title="Static optimization"
@ -468,15 +479,17 @@ export default function SiteChecklistClient({
>
Set environment variable to {'"1"'} to enable static optimization,
i.e., rendering pages and images at build time:
{renderSubStatus(
arePagesStaticallyOptimized ? 'checked' : 'optional',
renderEnvVars(['NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES']),
'translate-y-[3.5px]',
{renderSubStatusWithEnvVar(
arePhotosStaticallyOptimized ? 'checked' : 'optional',
'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTOS',
)}
{renderSubStatus(
areOGImagesStaticallyOptimized ? 'checked' : 'optional',
renderEnvVars(['NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES']),
'translate-y-[3.5px]',
{renderSubStatusWithEnvVar(
arePhotoOGImagesStaticallyOptimized ? 'checked' : 'optional',
'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_OG_IMAGES',
)}
{renderSubStatusWithEnvVar(
arePhotoCategoriesStaticallyOptimized ? 'checked' : 'optional',
'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORIES',
)}
</ChecklistRow>
<ChecklistRow

View File

@ -124,20 +124,28 @@ export const CURRENT_STORAGE: StorageType =
// SETTINGS
export const GRID_HOMEPAGE_ENABLED =
process.env.NEXT_PUBLIC_GRID_HOMEPAGE === '1';
export const DEFAULT_THEME =
process.env.NEXT_PUBLIC_DEFAULT_THEME === 'dark'
? 'dark'
: process.env.NEXT_PUBLIC_DEFAULT_THEME === 'light'
? 'light'
: 'system';
export const PRO_MODE_ENABLED =
export const PRESERVE_ORIGINAL_UPLOADS =
process.env.NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS === '1' ||
// Legacy environment variable name
process.env.NEXT_PUBLIC_PRO_MODE === '1';
export const GRID_HOMEPAGE_ENABLED =
process.env.NEXT_PUBLIC_GRID_HOMEPAGE === '1';
export const STATICALLY_OPTIMIZED_PAGES =
export const STATICALLY_OPTIMIZED_PHOTOS =
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTOS === '1' ||
// Legacy environment variable name
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES === '1';
export const STATICALLY_OPTIMIZED_OG_IMAGES =
export const STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES =
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_OG_IMAGES === '1' ||
// Legacy environment variable name
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES === '1';
export const STATICALLY_OPTIMIZED_PHOTO_CATEGORIES =
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORIES === '1';
export const MATTE_PHOTOS =
process.env.NEXT_PUBLIC_MATTE_PHOTOS === '1';
export const BLUR_ENABLED =
@ -208,15 +216,17 @@ export const CONFIG_CHECKLIST_STATUS = {
showSocial: SHOW_SOCIAL,
showFilmSimulations: SHOW_FILM_SIMULATIONS,
showExifInfo: SHOW_EXIF_DATA,
defaultTheme: DEFAULT_THEME,
isProModeEnabled: PRO_MODE_ENABLED,
isGridHomepageEnabled: GRID_HOMEPAGE_ENABLED,
defaultTheme: DEFAULT_THEME,
areOriginalUploadsPreserved: PRESERVE_ORIGINAL_UPLOADS,
isStaticallyOptimized: (
STATICALLY_OPTIMIZED_PAGES ||
STATICALLY_OPTIMIZED_OG_IMAGES
STATICALLY_OPTIMIZED_PHOTOS ||
STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES ||
STATICALLY_OPTIMIZED_PHOTO_CATEGORIES
),
arePagesStaticallyOptimized: STATICALLY_OPTIMIZED_PAGES,
areOGImagesStaticallyOptimized: STATICALLY_OPTIMIZED_OG_IMAGES,
arePhotosStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTOS,
arePhotoOGImagesStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES,
arePhotoCategoriesStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTO_CATEGORIES,
arePhotosMatted: MATTE_PHOTOS,
isBlurEnabled: BLUR_ENABLED,
isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED,