Created
July 8, 2026 11:39
-
-
Save moritzsalla/bd75fa1e91da96dbe8c23c6cf06f6abc to your computer and use it in GitHub Desktop.
image-focal-point.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import "server-only"; | |
| import { cache } from "react"; | |
| import type { FocalPoint } from "components/primitives/image/pixel-reveal"; | |
| import { getImageUrl, isVectorMimeType } from "lib/amplience/image"; | |
| import { computeFocalPoint } from "lib/amplience/image-saliency"; | |
| import sharp from "sharp"; | |
| // Contrast-saliency focal point for the pixel reveal. Server-side only: one | |
| // tiny thumb per unique asset, cached by the Next data cache, so the client | |
| // never pays a request for this. Enhancement tier: every failure path returns | |
| // null and callers fall back to the per-src default. | |
| const THUMB_WIDTH = 32; | |
| // Never let a decorative input hold a page render: the thumb fetch is a | |
| // third-party CDN call on the server render path (see enrichImageFocalPoints). | |
| const FETCH_TIMEOUT_MS = 1500; | |
| // Cross-request memo. The Next data cache dedupes the thumb fetch, but the | |
| // sharp decode would otherwise rerun on every request; this makes it once | |
| // per process per asset. Refresh-on-hit keeps hot assets resident. | |
| const FOCAL_POINT_CACHE_MAX = 500; | |
| const focalPointCache = new Map<string, FocalPoint | null>(); | |
| const cacheFocalPoint = (baseUrl: string, focalPoint: FocalPoint | null) => { | |
| if (focalPointCache.size >= FOCAL_POINT_CACHE_MAX) { | |
| const oldest = focalPointCache.keys().next().value; | |
| if (oldest !== undefined) focalPointCache.delete(oldest); | |
| } | |
| focalPointCache.set(baseUrl, focalPoint); | |
| }; | |
| export const getImageFocalPoint = cache( | |
| async (baseUrl: string): Promise<FocalPoint | null> => { | |
| const cached = focalPointCache.get(baseUrl); | |
| if (cached !== undefined) { | |
| focalPointCache.delete(baseUrl); | |
| focalPointCache.set(baseUrl, cached); | |
| return cached; | |
| } | |
| try { | |
| const res = await fetch(`${baseUrl}?w=${THUMB_WIDTH}&qlt=60&fmt=jpg`, { | |
| cache: "force-cache", | |
| signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), | |
| }); | |
| if (!res.ok) return null; | |
| const { data, info } = await sharp(Buffer.from(await res.arrayBuffer())) | |
| .greyscale() | |
| .raw() | |
| .toBuffer({ resolveWithObject: true }); | |
| // Cache the analysis result — including a legitimate null (flat image | |
| // with no salient region) so it isn't recomputed. Transient failures | |
| // above fall through uncached and retry on the next request. | |
| const focalPoint = computeFocalPoint(data, info.width, info.height); | |
| cacheFocalPoint(baseUrl, focalPoint); | |
| return focalPoint; | |
| } catch { | |
| return null; | |
| } | |
| }, | |
| ); | |
| const IMAGE_LINK_SCHEMA = | |
| "http://bigcontent.io/cms/schema/v1/core#/definitions/image-link"; | |
| type EnrichableImageLink = { | |
| _meta?: { schema?: typeof IMAGE_LINK_SCHEMA }; | |
| defaultHost: string; | |
| endpoint: string; | |
| name: string; | |
| id: string; | |
| mimeType?: string; | |
| focalPoint?: FocalPoint; | |
| }; | |
| const isImageLink = (value: object): value is EnrichableImageLink => { | |
| const link = value as Partial<EnrichableImageLink>; | |
| return ( | |
| link._meta?.schema === IMAGE_LINK_SCHEMA && | |
| typeof link.defaultHost === "string" && | |
| typeof link.endpoint === "string" && | |
| typeof link.name === "string" && | |
| typeof link.id === "string" | |
| ); | |
| }; | |
| /** | |
| * Walks resolved delivery content and attaches a computed contrast focalPoint | |
| * to every raster image link, in place. One awaited pass at the resolver | |
| * choke points (page, PDP content, navigation) gives every CMS surface a | |
| * saliency focalPoint with no adaptor changes. Misses leave links untouched | |
| * and the reveal's per-src fallback applies. | |
| */ | |
| export const enrichImageFocalPoints = async <T>(content: T): Promise<T> => { | |
| const links: EnrichableImageLink[] = []; | |
| const walk = (node: unknown) => { | |
| if (Array.isArray(node)) { | |
| for (const item of node) walk(item); | |
| return; | |
| } | |
| if (typeof node !== "object" || node === null) return; | |
| if (isImageLink(node)) { | |
| links.push(node); | |
| return; | |
| } | |
| for (const value of Object.values(node)) walk(value); | |
| }; | |
| walk(content); | |
| await Promise.all( | |
| links | |
| .filter((link) => !isVectorMimeType(link.mimeType)) | |
| .map(async (link) => { | |
| const focalPoint = await getImageFocalPoint(getImageUrl(link)); | |
| if (focalPoint) link.focalPoint = focalPoint; | |
| }), | |
| ); | |
| return content; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment