From a4f412a10fa8c2b02855ce70032afc38d6749ef9 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Sat, 30 May 2026 19:42:31 +0800 Subject: [PATCH] refactor(images): extract client-safe variant tier constants Move the variant tier ladder (VARIANT_TIER_WIDTHS), format helpers (VARIANT_FORMATS, variantContentType), key builder (buildVariantKey) and tierWidthsForSource into a new client-safe module ~/lib/image/variant-tiers with no server-only dependencies. server/lib/image-variants.ts re-exports them (behavior-preserving), so the preprocessing pipeline keeps its existing import surface. The client-side gallery loader (lib/image/loader.ts) can now import the same module instead of mirroring the constants, eliminating the duplicated source of truth that FE-3 had to maintain by hand. Co-Authored-By: Claude Opus 4.8 --- lib/image/variant-tiers.ts | 51 ++++++++++++++++++++++++++++++ server/lib/image-variants.ts | 60 +++++++++++++----------------------- 2 files changed, 72 insertions(+), 39 deletions(-) create mode 100644 lib/image/variant-tiers.ts diff --git a/lib/image/variant-tiers.ts b/lib/image/variant-tiers.ts new file mode 100644 index 00000000..8544fecf --- /dev/null +++ b/lib/image/variant-tiers.ts @@ -0,0 +1,51 @@ +/** + * Client-safe variant tier constants and key helpers. + * + * This module is the single source of truth for the responsive-variant tier + * ladder and object-key naming convention. It is deliberately free of any + * server-only dependencies (no `sharp`, no AWS SDK, no `server-only`) so it can + * be imported from both the server-side preprocessing pipeline + * (`server/lib/image-variants.ts`, which re-exports these) and the client-side + * gallery image loader (`lib/image/loader.ts`) — eliminating the previously + * mirrored constants. + */ + +/** + * Responsive width ladder for generated image variants. + * + * Kept in sync with `next.config` `imageSizes` (320, 480) + `deviceSizes` + * (640..2560) so every width the custom image loader requests maps onto a + * width the pipeline actually generates. Always ascending — the preprocessing + * queue generates tiers small-to-large so `ready_max_width` is a monotonic + * watermark (a tier is ready iff its width <= ready_max_width). + */ +export const VARIANT_TIER_WIDTHS = [320, 480, 640, 800, 1080, 1280, 1920, 2560] as const + +export type VariantFormat = 'avif' | 'webp' + +/** Formats generated for every tier. Distinct immutable objects per format. */ +export const VARIANT_FORMATS: readonly VariantFormat[] = ['avif', 'webp'] + +export function variantContentType(format: VariantFormat): string { + return format === 'avif' ? 'image/avif' : 'image/webp' +} + +/** + * Variant object key: `{baseKey}_{width}.{format}`. + * + * Both the server pipeline and the client loader build this exact string, so it + * is the single source of truth for the naming convention. + */ +export function buildVariantKey(baseKey: string, width: number, format: VariantFormat): string { + return `${baseKey}_${width}.${format}` +} + +/** + * Tier widths to generate for a given source width, ascending. Never upscales: + * only tiers up to the source width are produced. Sources smaller than the + * smallest tier yield a single variant at their native width. + */ +export function tierWidthsForSource(sourceWidth: number): number[] { + const tiers = VARIANT_TIER_WIDTHS.filter((width) => width <= sourceWidth) + return tiers.length > 0 ? tiers : [sourceWidth] +} diff --git a/server/lib/image-variants.ts b/server/lib/image-variants.ts index eb33e698..a63773b6 100644 --- a/server/lib/image-variants.ts +++ b/server/lib/image-variants.ts @@ -6,21 +6,27 @@ import { PutObjectCommand, type S3Client } from '@aws-sdk/client-s3' import sharp from 'sharp' import { rgbaToThumbHash } from 'thumbhash' -/** - * Responsive width ladder for generated image variants. - * - * Kept in sync with `next.config` `imageSizes` (320, 480) + `deviceSizes` - * (640..2560) so every width the custom image loader requests maps onto a - * width we actually generated. Always ascending — the preprocessing queue - * generates tiers small-to-large so `ready_max_width` is a monotonic - * watermark (a tier is ready iff its width <= ready_max_width). - */ -export const VARIANT_TIER_WIDTHS = [320, 480, 640, 800, 1080, 1280, 1920, 2560] as const - -export type VariantFormat = 'avif' | 'webp' - -/** Formats generated for every tier. Distinct immutable objects per format. */ -export const VARIANT_FORMATS: readonly VariantFormat[] = ['avif', 'webp'] +import { + VARIANT_FORMATS, + VARIANT_TIER_WIDTHS, + buildVariantKey, + tierWidthsForSource, + variantContentType, + type VariantFormat, +} from '~/lib/image/variant-tiers' + +// The variant tier ladder + key/format helpers live in the client-safe +// `~/lib/image/variant-tiers` module (the single source of truth shared with +// the gallery loader). Re-export them so existing server-side imports from +// this module keep working. +export { + VARIANT_FORMATS, + VARIANT_TIER_WIDTHS, + buildVariantKey, + tierWidthsForSource, + variantContentType, +} +export type { VariantFormat } /** * Decompression-bomb guard for sharp. 100 MP comfortably covers high-end @@ -35,20 +41,6 @@ const THUMBHASH_MAX_EDGE = 100 /** Immutable cache header for content-addressed variant objects. */ export const VARIANT_CACHE_CONTROL = 'public, max-age=31536000, immutable' -export function variantContentType(format: VariantFormat): string { - return format === 'avif' ? 'image/avif' : 'image/webp' -} - -/** - * Variant object key: `{baseKey}_{width}.{format}`. - * - * The custom next/image loader builds the same string client-side, so this is - * the single source of truth for the naming convention. - */ -export function buildVariantKey(baseKey: string, width: number, format: VariantFormat): string { - return `${baseKey}_${width}.${format}` -} - /** * Content-addressed base key derived from the original bytes. Because the key * changes whenever the content changes, variant objects can be served with an @@ -59,16 +51,6 @@ export function computeImageKey(input: Buffer, prefix = 'variants'): string { return `${prefix}/${digest}` } -/** - * Tier widths to generate for a given source width, ascending. Never upscales: - * only tiers up to the source width are produced. Sources smaller than the - * smallest tier yield a single variant at their native width. - */ -export function tierWidthsForSource(sourceWidth: number): number[] { - const tiers = VARIANT_TIER_WIDTHS.filter((width) => width <= sourceWidth) - return tiers.length > 0 ? tiers : [sourceWidth] -} - /** * Resolve an image's *displayed* dimensions from sharp metadata. *