diff --git a/README.md b/README.md index 6389b757..c11b56ca 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ _⚠️ READ BEFORE PROCEEDING_ Application behavior can be changed by configuring the following environment variables: - `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_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 - `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e555d6d1..60a7bd31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 3.564.0 '@next/bundle-analyzer': specifier: 14.2.3 - version: 14.2.3(bufferutil@4.0.8)(utf-8-validate@6.0.3) + version: 14.2.3(bufferutil@4.0.8) '@radix-ui/react-dropdown-menu': specifier: ^2.0.6 version: 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -103,7 +103,7 @@ importers: version: 29.7.0(@types/node@20.12.7) jest-environment-jsdom: specifier: ^29.7.0 - version: 29.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) + version: 29.7.0(bufferutil@4.0.8) nanoid: specifier: ^5.0.7 version: 5.0.7 @@ -4185,10 +4185,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 - utf-8-validate@6.0.3: - resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==} - engines: {node: '>=6.14.2'} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5452,9 +5448,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@next/bundle-analyzer@14.2.3(bufferutil@4.0.8)(utf-8-validate@6.0.3)': + '@next/bundle-analyzer@14.2.3(bufferutil@4.0.8)': dependencies: - webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.3) + webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.8) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -7987,7 +7983,7 @@ snapshots: jest-util: 29.7.0 pretty-format: 29.7.0 - jest-environment-jsdom@29.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.3): + jest-environment-jsdom@29.7.0(bufferutil@4.0.8): dependencies: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 @@ -7996,7 +7992,7 @@ snapshots: '@types/node': 20.12.7 jest-mock: 29.7.0 jest-util: 29.7.0 - jsdom: 20.0.3(bufferutil@4.0.8)(utf-8-validate@6.0.3) + jsdom: 20.0.3(bufferutil@4.0.8) transitivePeerDependencies: - bufferutil - supports-color @@ -8225,7 +8221,7 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@20.0.3(bufferutil@4.0.8)(utf-8-validate@6.0.3): + jsdom@20.0.3(bufferutil@4.0.8): dependencies: abab: 2.0.6 acorn: 8.11.3 @@ -8251,7 +8247,7 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) + ws: 8.16.0(bufferutil@4.0.8) xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -9390,11 +9386,6 @@ snapshots: dependencies: react: 18.3.1 - utf-8-validate@6.0.3: - dependencies: - node-gyp-build: 4.8.0 - optional: true - util-deprecate@1.0.2: {} uuid@9.0.1: {} @@ -9431,7 +9422,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-bundle-analyzer@4.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.3): + webpack-bundle-analyzer@4.10.1(bufferutil@4.0.8): dependencies: '@discoveryjs/json-ext': 0.5.7 acorn: 8.11.3 @@ -9445,7 +9436,7 @@ snapshots: opener: 1.5.2 picocolors: 1.0.0 sirv: 2.0.4 - ws: 7.5.9(bufferutil@4.0.8)(utf-8-validate@6.0.3) + ws: 7.5.9(bufferutil@4.0.8) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -9529,15 +9520,13 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@7.5.9(bufferutil@4.0.8)(utf-8-validate@6.0.3): + ws@7.5.9(bufferutil@4.0.8): optionalDependencies: bufferutil: 4.0.8 - utf-8-validate: 6.0.3 - ws@8.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.3): + ws@8.16.0(bufferutil@4.0.8): optionalDependencies: bufferutil: 4.0.8 - utf-8-validate: 6.0.3 xml-name-validator@4.0.0: {} diff --git a/src/app/api/route.ts b/src/app/api/route.ts index 19484bd4..a3c74dc5 100644 --- a/src/app/api/route.ts +++ b/src/app/api/route.ts @@ -6,6 +6,8 @@ import { SITE_TITLE, } from '@/site/config'; +export const dynamic = 'force-dynamic'; + export async function GET() { if (PUBLIC_API_ENABLED) { const photos = await getPhotosCached({ limit: API_PHOTO_REQUEST_LIMIT }); diff --git a/src/app/p/[photoId]/image/route.tsx b/src/app/p/[photoId]/image/route.tsx index 965db7df..9ff57e29 100644 --- a/src/app/p/[photoId]/image/route.tsx +++ b/src/app/p/[photoId]/image/route.tsx @@ -4,6 +4,19 @@ import PhotoImageResponse from '@/image-response/PhotoImageResponse'; import { getIBMPlexMonoMedium } from '@/site/font'; import { ImageResponse } from 'next/og'; import { getImageResponseCacheControlHeaders } from '@/image-response/cache'; +import { STATICALLY_OPTIMIZED_OG_IMAGES } from '@/site/config'; +import { GENERATE_STATIC_PARAMS_LIMIT, getPhotoIds } from '@/photo/db'; +import { isNextImageReadyBasedOnPhotos } from '@/photo'; + +export let generateStaticParams: + (() => Promise<{ photoId: string }[]>) | undefined = undefined; + +if (STATICALLY_OPTIMIZED_OG_IMAGES) { + generateStaticParams = async () => { + const photos = await getPhotoIds({ limit: GENERATE_STATIC_PARAMS_LIMIT }); + return photos.map(photoId => ({ photoId })); + }; +} export async function GET( _: Request, @@ -22,9 +35,19 @@ export async function GET( if (!photo) { return new Response('Photo not found', { status: 404 }); } const { width, height } = IMAGE_OG_DIMENSION; + + // Make sure next/image can be reached from absolute urls, + // which may not exist on first pre-render + const isNextImageReady = await isNextImageReadyBasedOnPhotos([photo]); return new ImageResponse( - , + , { width, height, fonts, headers }, ); } diff --git a/src/app/p/[photoId]/layout.tsx b/src/app/p/[photoId]/layout.tsx index 4167b64c..20380d6e 100644 --- a/src/app/p/[photoId]/layout.tsx +++ b/src/app/p/[photoId]/layout.tsx @@ -12,6 +12,18 @@ import { } from '@/site/paths'; import PhotoDetailPage from '@/photo/PhotoDetailPage'; import { getPhotosNearIdCachedCached } from '@/photo/cache'; +import { STATICALLY_OPTIMIZED_PAGES } from '@/site/config'; +import { GENERATE_STATIC_PARAMS_LIMIT, getPhotoIds } from '@/photo/db'; + +export let generateStaticParams: + (() => Promise<{ photoId: string }[]>) | undefined = undefined; + +if (STATICALLY_OPTIMIZED_PAGES) { + generateStaticParams = async () => { + const photos = await getPhotoIds({ limit: GENERATE_STATIC_PARAMS_LIMIT }); + return photos.map(photoId => ({ photoId })); + }; +} interface PhotoProps { params: { photoId: string } diff --git a/src/auth/cache.ts b/src/auth/cache.ts index 84ac9f5d..e1079046 100644 --- a/src/auth/cache.ts +++ b/src/auth/cache.ts @@ -1,6 +1,4 @@ import { cache } from 'react'; import { auth } from '@/auth'; -import { screenForPPR } from '@/utility/ppr'; -export const authCachedSafe = cache(() => auth() - .catch(e => screenForPPR(e, null, 'auth'))); +export const authCachedSafe = cache(() => auth().catch(() => null)); diff --git a/src/components/ExperimentalBadge.tsx b/src/components/ExperimentalBadge.tsx index 0e4b60d9..d3d51a0d 100644 --- a/src/components/ExperimentalBadge.tsx +++ b/src/components/ExperimentalBadge.tsx @@ -12,6 +12,7 @@ export default function ExperimentalBadge({ className={clsx( 'text-pink-500 dark:text-white', 'bg-pink-100 dark:bg-pink-600', + 'pt-0.5', className, )}> Experimental diff --git a/src/image-response/PhotoImageResponse.tsx b/src/image-response/PhotoImageResponse.tsx index f633e513..2175add2 100644 --- a/src/image-response/PhotoImageResponse.tsx +++ b/src/image-response/PhotoImageResponse.tsx @@ -12,11 +12,13 @@ export default function PhotoImageResponse({ width, height, fontFamily, + isNextImageReady = true, }: { photo: Photo width: NextImageSize height: number fontFamily: string + isNextImageReady: boolean }) { const model = photo.model ? formatCameraModelTextShort(cameraFromPhoto(photo)) @@ -25,7 +27,7 @@ export default function PhotoImageResponse({ return ( ( try { result = await callback(); } catch (e: any) { - screenForPPR(e, undefined, 'neon postgres'); if (MIGRATION_FIELDS_01.some(field => new RegExp( `column "${field}" of relation "photos" does not exist`, 'i', diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 2a37576c..0f3e008e 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -27,7 +27,6 @@ import { isUrlFromCloudflareR2, } from './cloudflare-r2'; import { PATH_API_PRESIGNED_URL } from '@/site/paths'; -import { screenForPPR } from '@/utility/ppr'; export const generateStorageId = () => generateNanoid(16); @@ -193,15 +192,15 @@ const getStorageUrlsForPrefix = async (prefix = '') => { if (HAS_VERCEL_BLOB_STORAGE) { urls.push(...await vercelBlobList(prefix) - .catch(e => screenForPPR(e, [], 'vercel blob'))); + .catch(() => [])); } if (HAS_AWS_S3_STORAGE) { urls.push(...await awsS3List(prefix) - .catch(e => screenForPPR(e, [], 'aws blob'))); + .catch(() => [])); } if (HAS_CLOUDFLARE_R2_STORAGE) { urls.push(...await cloudflareR2List(prefix) - .catch(e => screenForPPR(e, [], 'cloudflare blob'))); + .catch(() => [])); } return urls diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index 9b9fcfcd..546ce113 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -41,6 +41,9 @@ export default function SiteChecklistClient({ showFilmSimulations, showExifInfo, isProModeEnabled, + isStaticallyOptimized, + arePagesStaticallyOptimized, + areOGImagesStaticallyOptimized, isBlurEnabled, isGeoPrivacyEnabled, isPriorityOrderEnabled, @@ -119,7 +122,8 @@ export default function SiteChecklistClient({ > @@ -137,10 +141,13 @@ export default function SiteChecklistClient({ const renderSubStatus = ( type: ComponentProps['type'], label: ReactNode, + iconClassName?: string, ) =>
- - + + + + {label}
; @@ -350,6 +357,26 @@ export default function SiteChecklistClient({ higher quality image storage: {renderEnvVars(['NEXT_PUBLIC_PRO_MODE'])} + + 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-[4.5px]', + )} + {renderSubStatus( + areOGImagesStaticallyOptimized ? 'checked' : 'optional', + renderEnvVars(['NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES']), + 'translate-y-[4.5px]', + )} + 0, @@ -160,6 +165,12 @@ export const CONFIG_CHECKLIST_STATUS = { showFilmSimulations: SHOW_FILM_SIMULATIONS, showExifInfo: SHOW_EXIF_DATA, isProModeEnabled: PRO_MODE_ENABLED, + isStaticallyOptimized: ( + STATICALLY_OPTIMIZED_PAGES || + STATICALLY_OPTIMIZED_OG_IMAGES + ), + arePagesStaticallyOptimized: STATICALLY_OPTIMIZED_PAGES, + areOGImagesStaticallyOptimized: STATICALLY_OPTIMIZED_OG_IMAGES, isBlurEnabled: BLUR_ENABLED, isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED, isAiTextGenerationEnabled: AI_TEXT_GENERATION_ENABLED, @@ -184,3 +195,4 @@ export const IS_SITE_READY = CONFIG_CHECKLIST_STATUS.hasStorageProvider && CONFIG_CHECKLIST_STATUS.hasAuthSecret && CONFIG_CHECKLIST_STATUS.hasAdminUser; + \ No newline at end of file diff --git a/src/utility/ppr.ts b/src/utility/ppr.ts deleted file mode 100644 index e59340ef..00000000 --- a/src/utility/ppr.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const screenForPPR = ( - error: any, - fallback: T, - sourceToLog?: string, - debug?: boolean -): T => { - // PPR errors, if caught, must be re-thrown in order to - // postpone rendering - if (error.sourceError?.message?.includes('ppr-caught-error')) { - if (debug) { - console.log(`${sourceToLog}: throwing error.sourceError`); - } - throw error.sourceError; - } else if (error.message?.includes('ppr-caught-error')) { - if (debug) { - console.log(`${sourceToLog}: throwing error`); - } - throw error; - } - return fallback; -};