@decocms/blocks 7.50.0 → 7.51.0
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/package.json +1 -1
- package/src/hooks/Image.test.ts +86 -1
- package/src/hooks/Image.tsx +56 -0
- package/src/hooks/index.ts +3 -0
package/package.json
CHANGED
package/src/hooks/Image.test.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getImageQuality,
|
|
4
|
+
getOptimizedMediaUrl,
|
|
5
|
+
getSrcSet,
|
|
6
|
+
type ImageQuality,
|
|
7
|
+
registerImageQuality,
|
|
8
|
+
} from "./Image";
|
|
3
9
|
|
|
4
10
|
describe("getOptimizedMediaUrl", () => {
|
|
5
11
|
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
@@ -98,3 +104,82 @@ describe("getSrcSet", () => {
|
|
|
98
104
|
expect(result).toContain("foo.jpg");
|
|
99
105
|
});
|
|
100
106
|
});
|
|
107
|
+
|
|
108
|
+
describe("registerImageQuality", () => {
|
|
109
|
+
// Module-level setter, so every test has to put it back or it leaks into
|
|
110
|
+
// the rest of the file.
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
registerImageQuality(undefined);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("emits no quality param by default", () => {
|
|
116
|
+
// The guarantee that makes this safe to land: every site that does not
|
|
117
|
+
// opt in keeps byte-identical URLs, so no CDN cache is invalidated.
|
|
118
|
+
expect(getImageQuality()).toBeUndefined();
|
|
119
|
+
const result = getOptimizedMediaUrl({
|
|
120
|
+
originalSrc: "https://cdn.example.com/foo.jpg",
|
|
121
|
+
width: 200,
|
|
122
|
+
fit: "cover",
|
|
123
|
+
});
|
|
124
|
+
expect(result).not.toContain("quality");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("emits the registered quality for CDN-routed images", () => {
|
|
128
|
+
registerImageQuality("high");
|
|
129
|
+
const result = getOptimizedMediaUrl({
|
|
130
|
+
originalSrc: "https://cdn.example.com/foo.jpg",
|
|
131
|
+
width: 200,
|
|
132
|
+
fit: "cover",
|
|
133
|
+
});
|
|
134
|
+
expect(result).toContain("quality=high");
|
|
135
|
+
});
|
|
136
|
+
it("pins the exact URL, param order included", () => {
|
|
137
|
+
// Param ORDER is part of the CDN cache key, and `toContain` cannot see
|
|
138
|
+
// it: reordering the params would keep every other assertion here green
|
|
139
|
+
// while cold-caching every image on every site. It is also the property
|
|
140
|
+
// that lets a site swap its node_modules patch for this setter without a
|
|
141
|
+
// cache flush, so it needs a real equality check.
|
|
142
|
+
registerImageQuality("high");
|
|
143
|
+
expect(
|
|
144
|
+
getOptimizedMediaUrl({
|
|
145
|
+
originalSrc: "https://cdn.example.com/foo.jpg",
|
|
146
|
+
width: 200,
|
|
147
|
+
height: 300,
|
|
148
|
+
fit: "cover",
|
|
149
|
+
}),
|
|
150
|
+
).toBe(
|
|
151
|
+
"https://decoims.com/image?fit=cover&width=200&height=300&quality=high&src=https://cdn.example.com/foo.jpg",
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("carries the quality into every srcset entry", () => {
|
|
156
|
+
registerImageQuality("high");
|
|
157
|
+
const result = getSrcSet("https://cdn.example.com/foo.jpg", 100);
|
|
158
|
+
const entries = result?.split(", ") ?? [];
|
|
159
|
+
expect(entries.length).toBeGreaterThan(1);
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
expect(entry).toContain("quality=high");
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("leaves VTEX sources alone — they resize via their own path syntax", () => {
|
|
166
|
+
registerImageQuality("high");
|
|
167
|
+
const result = getOptimizedMediaUrl({
|
|
168
|
+
originalSrc:
|
|
169
|
+
"https://acme.vtexassets.com/arquivos/ids/123456/product.jpg?v=1",
|
|
170
|
+
width: 200,
|
|
171
|
+
height: 300,
|
|
172
|
+
fit: "cover",
|
|
173
|
+
});
|
|
174
|
+
expect(result).toContain("/arquivos/ids/123456-200-300/");
|
|
175
|
+
expect(result).not.toContain("quality");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("treats an empty string as unset, for untyped callers", () => {
|
|
179
|
+
// Unreachable from TypeScript now that the parameter is a union, but a
|
|
180
|
+
// value read from env or CMS config arrives as a plain string, so the
|
|
181
|
+
// runtime guard still earns its keep.
|
|
182
|
+
registerImageQuality("" as unknown as ImageQuality);
|
|
183
|
+
expect(getImageQuality()).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
});
|
package/src/hooks/Image.tsx
CHANGED
|
@@ -31,6 +31,61 @@ export function getImageCdnDomain(): string {
|
|
|
31
31
|
return imageCdnDomain;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// -------------------------------------------------------------------------
|
|
35
|
+
// Configurable image quality
|
|
36
|
+
// -------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Quality levels the Deco image CDN accepts as a site-wide default.
|
|
40
|
+
*
|
|
41
|
+
* The CDN maps these to 60% / 70% / 80%. It also accepts `original` (100%),
|
|
42
|
+
* deliberately excluded here: a global 100% default hurts performance on
|
|
43
|
+
* every page, which is the same call `DefaultQualityOptions` makes in the
|
|
44
|
+
* Fresh implementation this file was ported from (deco-cx/apps,
|
|
45
|
+
* website/components/Image.tsx).
|
|
46
|
+
*
|
|
47
|
+
* A union rather than `string` because the CDN **fails silently** on
|
|
48
|
+
* anything else: it answers 200 and serves its 60% default. So the obvious
|
|
49
|
+
* `registerImageQuality("80")` — quality means 1-100 in Cloudflare Images,
|
|
50
|
+
* next/image and imgix — would quietly serve the LOWEST quality while
|
|
51
|
+
* changing the cache key on every image. Measured against production
|
|
52
|
+
* decoims.com on one asset: no param 2911 B, `low` 2911, `medium` 3235,
|
|
53
|
+
* `high` 5189, and `"80"` / `"HIGH"` / garbage all 2911.
|
|
54
|
+
*/
|
|
55
|
+
export type ImageQuality = "low" | "medium" | "high";
|
|
56
|
+
|
|
57
|
+
let imageQuality: ImageQuality | undefined;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register the quality level `getOptimizedMediaUrl` asks the image CDN for.
|
|
61
|
+
*
|
|
62
|
+
* Call once at module scope in your site's setup, NOT from a loader, action or
|
|
63
|
+
* anything else on the request path. This is module-level state shared by every
|
|
64
|
+
* request in a Worker isolate: setting it per-request would leak across
|
|
65
|
+
* concurrent requests, and setting it on only one of the SSR/hydration paths
|
|
66
|
+
* would produce mismatched `src`/`srcSet` and re-download every image.
|
|
67
|
+
*
|
|
68
|
+
* Unset by default, which emits no `quality` param and leaves the CDN on its
|
|
69
|
+
* 60% default — so existing sites are byte-for-byte unaffected and no CDN
|
|
70
|
+
* cache is invalidated. Set it when a site's art direction needs fidelity over
|
|
71
|
+
* bytes: on fashion/editorial catalogues the default compression visibly
|
|
72
|
+
* softens fabric texture and print detail.
|
|
73
|
+
*
|
|
74
|
+
* Applies to every URL built by `getOptimizedMediaUrl` — `Image`, `getSrcSet`
|
|
75
|
+
* and also `Video` when it is given `forceOptimizedSrc`. It does NOT apply to
|
|
76
|
+
* VTEX- or Shopify-hosted sources: those are resized through their own native
|
|
77
|
+
* URL syntax (`optimizeVTEX` / `optimizeShopify`), which returns before the
|
|
78
|
+
* query-param block. On a VTEX storefront that means product imagery is
|
|
79
|
+
* untouched and only CMS/banner assets are affected.
|
|
80
|
+
*/
|
|
81
|
+
export function registerImageQuality(quality: ImageQuality | undefined) {
|
|
82
|
+
imageQuality = quality || undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getImageQuality(): ImageQuality | undefined {
|
|
86
|
+
return imageQuality;
|
|
87
|
+
}
|
|
88
|
+
|
|
34
89
|
// -------------------------------------------------------------------------
|
|
35
90
|
// Fit options & optimization types
|
|
36
91
|
// -------------------------------------------------------------------------
|
|
@@ -126,6 +181,7 @@ export function getOptimizedMediaUrl(opts: OptimizationOptions): string {
|
|
|
126
181
|
params.set("fit", fit);
|
|
127
182
|
params.set("width", `${width}`);
|
|
128
183
|
if (height) params.set("height", `${height}`);
|
|
184
|
+
if (imageQuality) params.set("quality", imageQuality);
|
|
129
185
|
|
|
130
186
|
return `https://${imageCdnDomain}/image?${params}&src=${imageSource}`;
|
|
131
187
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -8,10 +8,13 @@ export {
|
|
|
8
8
|
default as Image,
|
|
9
9
|
registerImageCdnDomain,
|
|
10
10
|
getImageCdnDomain,
|
|
11
|
+
registerImageQuality,
|
|
12
|
+
getImageQuality,
|
|
11
13
|
getOptimizedMediaUrl,
|
|
12
14
|
getSrcSet,
|
|
13
15
|
FACTORS,
|
|
14
16
|
type ImageProps,
|
|
17
|
+
type ImageQuality,
|
|
15
18
|
type FitOptions,
|
|
16
19
|
} from "./Image";
|
|
17
20
|
export { Picture, Source, type PictureProps, type SourceProps } from "./Picture";
|