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;
-};