Files
vue-journal-layer/app/utils/srcset.ts
Jakub Zych 1080a3f0c4 feat: WebP and srcset for featured images
- JournalFeaturedImage renders <picture> from featured_images[].variants:
  a WebP source and the original format as srcset fallback, the
  original's width and height, alt; lazy in cards, eager with
  fetchpriority high on the post page; unchanged without variants
- app/utils/srcset.ts (imageSrcsets) with node tests
- acme fixtures and SSR expectations for both cases
2026-11-04 17:10:30 +01:00

82 lines
3.1 KiB
TypeScript

// srcset strings of a featured image from the Journal API's
// featured_images[].variants (D-85, D-71): one for the WebP <source> and
// one for the <img> in the original format, the original itself added
// when it is wider than every variant, plus the size the <img> reserves.
// Pure, no Vue or Nuxt imports, so `node --test` drives it.
/** One size of a featured image (featured_images[].variants). */
export interface JournalImageVariant {
width: number
height: number
/** The thumbnail in the original's format. */
url: string
/** Its WebP copy. */
webp_url: string
/** The original's size. */
source_width?: number
source_height?: number
}
/** What <picture> renders from the variants. */
export interface JournalImageSrcsets {
/** srcset of the type="image/webp" <source>. */
webp: string
/** srcset of the <img>, in the original format. */
original: string
/** The size the <img> reserves: the original's, else the largest variant's. */
width: number
height: number
}
/** A URL safe inside a srcset candidate (no spaces or commas). */
function candidateURL(url: string): string {
return url.replace(/ /g, '%20').replace(/,/g, '%2C')
}
function validVariant(v: unknown): v is JournalImageVariant {
if (!v || typeof v !== 'object') return false
const o = v as Record<string, unknown>
return (
Number.isInteger(o.width) && (o.width as number) > 0 &&
Number.isInteger(o.height) && (o.height as number) > 0 &&
typeof o.url === 'string' && o.url !== '' &&
typeof o.webp_url === 'string' && o.webp_url !== ''
)
}
/**
* The srcsets of an image from its variants, narrowest first and each
* width once. originalUrl and originalWidth (the variants' source_width
* when not given) add the original to the <img> srcset when it is wider
* than every variant. No usable variant answers null: render the original
* as before.
*/
export function imageSrcsets(
variants: readonly unknown[] | null | undefined,
originalUrl?: string | null,
originalWidth?: number | null,
): JournalImageSrcsets | null {
if (!Array.isArray(variants)) return null
const byWidth = new Map<number, JournalImageVariant>()
for (const v of variants) {
if (validVariant(v) && !byWidth.has(v.width)) byWidth.set(v.width, v)
}
const list = [...byWidth.values()].sort((a, b) => a.width - b.width)
const largest = list[list.length - 1]
if (!largest) return null
const webp = list.map((v) => `${candidateURL(v.webp_url)} ${v.width}w`)
const original = list.map((v) => `${candidateURL(v.url)} ${v.width}w`)
const sourceWidth = originalWidth ?? largest.source_width ?? null
const sourceHeight = largest.source_height ?? null
if (originalUrl && sourceWidth && Number.isInteger(sourceWidth) && sourceWidth > largest.width) {
original.push(`${candidateURL(originalUrl)} ${sourceWidth}w`)
}
const hasSource = !!sourceWidth && !!sourceHeight && Number.isInteger(sourceWidth) && Number.isInteger(sourceHeight)
return {
webp: webp.join(', '),
original: original.join(', '),
width: hasSource ? (sourceWidth as number) : largest.width,
height: hasSource ? (sourceHeight as number) : largest.height,
}
}