@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/blocks",
3
- "version": "7.50.0",
3
+ "version": "7.51.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -1,5 +1,11 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { getOptimizedMediaUrl, getSrcSet } from "./Image";
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
+ });
@@ -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
  }
@@ -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";