@inoo-ch/payload-image-optimizer 1.5.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_DOCS.md +207 -38
- package/README.md +80 -19
- package/dist/components/ImageBox.js +8 -3
- package/dist/components/ImageBox.js.map +1 -1
- package/dist/defaults.js +2 -1
- package/dist/defaults.js.map +1 -1
- package/dist/exports/client.d.ts +3 -0
- package/dist/exports/client.js +2 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/hooks/beforeChange.js +10 -0
- package/dist/hooks/beforeChange.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tasks/regenerateDocument.js +7 -0
- package/dist/tasks/regenerateDocument.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/getOptimizedImageProps.d.ts +44 -0
- package/dist/utilities/getOptimizedImageProps.js +43 -0
- package/dist/utilities/getOptimizedImageProps.js.map +1 -0
- package/dist/utilities/responsiveImage.d.ts +46 -0
- package/dist/utilities/responsiveImage.js +64 -0
- package/dist/utilities/responsiveImage.js.map +1 -0
- package/package.json +1 -1
- package/src/components/ImageBox.tsx +6 -3
- package/src/defaults.ts +1 -0
- package/src/exports/client.ts +3 -0
- package/src/hooks/beforeChange.ts +11 -0
- package/src/index.ts +1 -1
- package/src/tasks/regenerateDocument.ts +8 -0
- package/src/types.ts +15 -0
- package/src/utilities/getOptimizedImageProps.ts +53 -0
- package/src/utilities/responsiveImage.ts +91 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MediaResource } from '../types.js'
|
|
2
|
+
import { getImageOptimizerProps, type ImageOptimizerProps } from './getImageOptimizerProps.js'
|
|
3
|
+
import { createVariantLoader } from './responsiveImage.js'
|
|
4
|
+
|
|
5
|
+
type ImageLoaderProps = { src: string; width: number; quality?: number | undefined }
|
|
6
|
+
type ImageLoader = (props: ImageLoaderProps) => string
|
|
7
|
+
|
|
8
|
+
export type OptimizedImageProps = ImageOptimizerProps & {
|
|
9
|
+
loader?: ImageLoader
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns all optimization props for a Next.js `<Image>` component in a single
|
|
14
|
+
* spread-friendly object: ThumbHash blur placeholder, focal-point positioning,
|
|
15
|
+
* and a variant-aware responsive loader.
|
|
16
|
+
*
|
|
17
|
+
* Designed as a drop-in enhancement for the Payload website template's `ImageMedia`:
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* // In your ImageMedia component — just add the import and spread:
|
|
21
|
+
* import { getOptimizedImageProps } from '@inoo-ch/payload-image-optimizer/client'
|
|
22
|
+
*
|
|
23
|
+
* const optimizedProps = getOptimizedImageProps(resource)
|
|
24
|
+
*
|
|
25
|
+
* <NextImage
|
|
26
|
+
* {...optimizedProps}
|
|
27
|
+
* src={src}
|
|
28
|
+
* alt={alt}
|
|
29
|
+
* fill={fill}
|
|
30
|
+
* sizes={sizes}
|
|
31
|
+
* priority={priority}
|
|
32
|
+
* loading={loading}
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* What it returns:
|
|
37
|
+
* - `placeholder` / `blurDataURL` — per-image ThumbHash (replaces the template's hardcoded blur)
|
|
38
|
+
* - `style.objectPosition` — focal-point-based positioning
|
|
39
|
+
* - `loader` — hybrid loader that serves pre-generated Payload size variants directly,
|
|
40
|
+
* falling back to `/_next/image` when no close match exists (only present when
|
|
41
|
+
* `resource.sizes` has variants)
|
|
42
|
+
*/
|
|
43
|
+
export function getOptimizedImageProps(
|
|
44
|
+
resource: MediaResource | null | undefined,
|
|
45
|
+
): OptimizedImageProps {
|
|
46
|
+
const base = getImageOptimizerProps(resource)
|
|
47
|
+
|
|
48
|
+
if (!resource) return base
|
|
49
|
+
|
|
50
|
+
const loader = createVariantLoader(resource)
|
|
51
|
+
|
|
52
|
+
return loader ? { ...base, loader } : base
|
|
53
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { MediaResource, MediaSizeVariant } from '../types.js'
|
|
2
|
+
|
|
3
|
+
type ImageLoaderProps = { src: string; width: number; quality?: number | undefined }
|
|
4
|
+
type ImageLoader = (props: ImageLoaderProps) => string
|
|
5
|
+
|
|
6
|
+
type ValidVariant = { url: string; width: number }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts usable variants from a Payload media resource's `sizes` field.
|
|
10
|
+
* Filters out entries missing url or width and sorts by width ascending.
|
|
11
|
+
*/
|
|
12
|
+
function getValidVariants(media: MediaResource): ValidVariant[] {
|
|
13
|
+
if (!media.sizes) return []
|
|
14
|
+
|
|
15
|
+
return Object.values(media.sizes)
|
|
16
|
+
.filter((v): v is MediaSizeVariant & { url: string; width: number } =>
|
|
17
|
+
v != null && typeof v.url === 'string' && typeof v.width === 'number',
|
|
18
|
+
)
|
|
19
|
+
.sort((a, b) => a.width - b.width)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Finds the best pre-generated variant for a requested width.
|
|
24
|
+
*
|
|
25
|
+
* Strategy:
|
|
26
|
+
* 1. Pick the smallest variant with width >= requested (no quality loss from upscaling)
|
|
27
|
+
* 2. If none is large enough, use the largest variant — but only if it covers >= 80%
|
|
28
|
+
* of the requested width (minor downscale is acceptable, large gap is not)
|
|
29
|
+
* 3. Returns null when no suitable variant exists → caller should fall back to /_next/image
|
|
30
|
+
*/
|
|
31
|
+
export function findBestVariant(
|
|
32
|
+
variants: ValidVariant[],
|
|
33
|
+
requestedWidth: number,
|
|
34
|
+
): ValidVariant | null {
|
|
35
|
+
if (variants.length === 0) return null
|
|
36
|
+
|
|
37
|
+
// Smallest variant >= requested width
|
|
38
|
+
const larger = variants.find((v) => v.width >= requestedWidth)
|
|
39
|
+
if (larger) return larger
|
|
40
|
+
|
|
41
|
+
// No variant large enough — use the largest if it's close
|
|
42
|
+
const largest = variants[variants.length - 1]!
|
|
43
|
+
if (largest.width >= requestedWidth * 0.8) return largest
|
|
44
|
+
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a Next.js Image `loader` that maps requested widths to pre-generated
|
|
50
|
+
* Payload size variants when a close match exists, falling back to the default
|
|
51
|
+
* `/_next/image` optimization pipeline when no suitable variant is available.
|
|
52
|
+
*
|
|
53
|
+
* Returns `undefined` when the media has no usable size variants (i.e. no custom
|
|
54
|
+
* loader needed — let next/image use its default behavior).
|
|
55
|
+
*
|
|
56
|
+
* ```tsx
|
|
57
|
+
* import { createVariantLoader } from '@inoo-ch/payload-image-optimizer/client'
|
|
58
|
+
*
|
|
59
|
+
* const loader = createVariantLoader(media)
|
|
60
|
+
* <NextImage loader={loader} src={media.url} ... />
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function createVariantLoader(media: MediaResource): ImageLoader | undefined {
|
|
64
|
+
const variants = getValidVariants(media)
|
|
65
|
+
if (variants.length === 0) return undefined
|
|
66
|
+
|
|
67
|
+
const cacheBust = media.updatedAt ? `?${media.updatedAt}` : ''
|
|
68
|
+
|
|
69
|
+
return ({ src, width, quality }) => {
|
|
70
|
+
const match = findBestVariant(variants, width)
|
|
71
|
+
|
|
72
|
+
if (match) {
|
|
73
|
+
return `${match.url}${cacheBust}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fall back to next/image optimization for unmatched widths
|
|
77
|
+
return `/_next/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 80}`
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns a sensible default `sizes` attribute for responsive images.
|
|
83
|
+
*
|
|
84
|
+
* For `fill` mode images without an explicit `sizes` prop, this prevents the
|
|
85
|
+
* browser from assuming `100vw` (which causes it to always download the
|
|
86
|
+
* largest srcSet variant regardless of actual display area).
|
|
87
|
+
*/
|
|
88
|
+
export function getDefaultSizes(fill: boolean | undefined): string | undefined {
|
|
89
|
+
if (!fill) return undefined
|
|
90
|
+
return '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
|
91
|
+
}
|