Merge pull request #90 from sambecker/static-optimization
Add optional static optimization
This commit is contained in:
commit
cdb2ab5215
@ -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
|
||||
|
||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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(
|
||||
<PhotoImageResponse {...{ photo, width, height, fontFamily }} />,
|
||||
<PhotoImageResponse {...{
|
||||
photo,
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
isNextImageReady,
|
||||
}} />,
|
||||
{ width, height, fonts, headers },
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (
|
||||
<ImageContainer {...{ width, height }}>
|
||||
<ImagePhotoGrid {...{
|
||||
photos: [photo],
|
||||
photos: isNextImageReady ? [photo] : [],
|
||||
width,
|
||||
height,
|
||||
...OG_TEXT_BOTTOM_ALIGNMENT && { imagePosition: 'top' },
|
||||
|
||||
@ -16,7 +16,8 @@ import { parameterize } from '@/utility/string';
|
||||
import { TagsWithMeta } from '@/tag';
|
||||
import { FilmSimulation, FilmSimulations } from '@/simulation';
|
||||
import { SHOULD_DEBUG_SQL, PRIORITY_ORDER_ENABLED } from '@/site/config';
|
||||
import { screenForPPR } from '@/utility/ppr';
|
||||
|
||||
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
|
||||
|
||||
const PHOTO_DEFAULT_LIMIT = 100;
|
||||
|
||||
@ -310,7 +311,6 @@ const safelyQueryPhotos = async <T>(
|
||||
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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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({
|
||||
>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className={clsx(
|
||||
'text-medium',
|
||||
'text-xs font-medium tracking-wide',
|
||||
'px-0.5 py-0.5',
|
||||
'rounded-sm',
|
||||
'bg-gray-100 dark:bg-gray-800',
|
||||
)}>
|
||||
@ -137,10 +141,13 @@ export default function SiteChecklistClient({
|
||||
const renderSubStatus = (
|
||||
type: ComponentProps<typeof StatusIcon>['type'],
|
||||
label: ReactNode,
|
||||
iconClassName?: string,
|
||||
) =>
|
||||
<div className="flex gap-1 -translate-x-1">
|
||||
<StatusIcon {...{ type }} />
|
||||
<span>
|
||||
<span className={iconClassName}>
|
||||
<StatusIcon {...{ type }} />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
{label}
|
||||
</span>
|
||||
</div>;
|
||||
@ -350,6 +357,26 @@ export default function SiteChecklistClient({
|
||||
higher quality image storage:
|
||||
{renderEnvVars(['NEXT_PUBLIC_PRO_MODE'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Static Optimization"
|
||||
status={isStaticallyOptimized}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
experimental
|
||||
>
|
||||
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]',
|
||||
)}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Image Blur"
|
||||
status={isBlurEnabled}
|
||||
|
||||
@ -104,6 +104,10 @@ export const CURRENT_STORAGE: StorageType =
|
||||
|
||||
export const PRO_MODE_ENABLED =
|
||||
process.env.NEXT_PUBLIC_PRO_MODE === '1';
|
||||
export const STATICALLY_OPTIMIZED_PAGES =
|
||||
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES === '1';
|
||||
export const STATICALLY_OPTIMIZED_OG_IMAGES =
|
||||
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES === '1';
|
||||
export const BLUR_ENABLED =
|
||||
process.env.NEXT_PUBLIC_BLUR_DISABLED !== '1';
|
||||
export const GEO_PRIVACY_ENABLED =
|
||||
@ -143,10 +147,11 @@ export const CONFIG_CHECKLIST_STATUS = {
|
||||
hasVercelBlobStorage: HAS_VERCEL_BLOB_STORAGE,
|
||||
hasCloudflareR2Storage: HAS_CLOUDFLARE_R2_STORAGE,
|
||||
hasAwsS3Storage: HAS_AWS_S3_STORAGE,
|
||||
hasStorageProvider:
|
||||
hasStorageProvider: (
|
||||
HAS_VERCEL_BLOB_STORAGE ||
|
||||
HAS_CLOUDFLARE_R2_STORAGE ||
|
||||
HAS_AWS_S3_STORAGE,
|
||||
HAS_AWS_S3_STORAGE
|
||||
),
|
||||
hasMultipleStorageProviders: HAS_MULTIPLE_STORAGE_PROVIDERS,
|
||||
currentStorage: CURRENT_STORAGE,
|
||||
hasAuthSecret: (process.env.AUTH_SECRET ?? '').length > 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;
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
export const screenForPPR = <T>(
|
||||
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;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user