lake-cimg 1.0.0 → 1.0.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/bin/cimg.js CHANGED
@@ -217,6 +217,20 @@ program
217
217
  }
218
218
  });
219
219
 
220
+ function collectString(val, memo) {
221
+ const arr = memo ?? [];
222
+ arr.push(val);
223
+ return arr;
224
+ }
225
+
226
+ function parseAliasPair(val) {
227
+ const i = val.indexOf("=");
228
+ if (i <= 0) {
229
+ throw new Error("格式须为 key=相对路径,例如 @=src 或 @assets=src/assets");
230
+ }
231
+ return [val.slice(0, i).trim(), val.slice(i + 1).trim()];
232
+ }
233
+
220
234
  program
221
235
  .command("scan-code [path]")
222
236
  .description(
@@ -231,17 +245,46 @@ program
231
245
  500
232
246
  )
233
247
  .option("--issues-only", "仅输出含 issues 的条目")
248
+ .option(
249
+ "--project-root <dir>",
250
+ "解析 /images/...、@/... 时的工程根目录(默认当前工作目录)"
251
+ )
252
+ .option(
253
+ "--public-dir <dir>",
254
+ "根路径下静态目录,可重复;用于解析以 / 开头的 URL 路径(默认 public、static)",
255
+ collectString
256
+ )
257
+ .option(
258
+ "--alias <pair>",
259
+ "路径别名 key=相对工程根的路径,可重复;默认 @=src。例: --alias @aaa=packages/aaa",
260
+ (v, prev) => {
261
+ const pair = parseAliasPair(v);
262
+ return [...(prev ?? []), pair];
263
+ },
264
+ []
265
+ )
234
266
  .action(async (pathArg, opts) => {
235
267
  const raw =
236
268
  pathArg != null && typeof pathArg === "string" ? pathArg.trim() : "";
237
269
  const root = resolveUserPath(raw !== "" ? raw : ".");
238
270
  const limit = Number.isNaN(opts.limit) ? 500 : opts.limit;
271
+ const aliasList = opts.alias ?? [];
272
+ const aliases =
273
+ aliasList.length > 0 ? Object.fromEntries(aliasList) : undefined;
274
+ const publicDirs = opts.publicDir;
239
275
  try {
240
276
  const result = await scanCodeReferences(root, {
241
277
  recursive: opts.recursive !== false,
242
278
  cwd: process.cwd(),
243
279
  limit,
244
280
  issuesOnly: !!opts.issuesOnly,
281
+ ...(opts.projectRoot != null && String(opts.projectRoot).trim() !== ""
282
+ ? { projectRoot: String(opts.projectRoot).trim() }
283
+ : {}),
284
+ ...(aliases != null ? { aliases } : {}),
285
+ ...(publicDirs != null && publicDirs.length > 0
286
+ ? { publicDirs }
287
+ : {}),
245
288
  });
246
289
  console.log(JSON.stringify(result, null, 2));
247
290
  process.exit(0);
package/lib/compress.js CHANGED
@@ -9,9 +9,13 @@ import sharp from "sharp";
9
9
  import {
10
10
  SUPPORTED_EXT,
11
11
  DEFAULT_QUALITY,
12
- DEFAULT_EFFORT,
13
12
  supportedFormatsLabel,
14
13
  } from "./constants.js";
14
+ import {
15
+ applyResizeInside,
16
+ encodeWebpBuffer,
17
+ encodeJpegBuffer,
18
+ } from "./sharpHelpers.js";
15
19
 
16
20
  /** Max concurrent tasks for processing images */
17
21
  const MAX_CONCURRENCY = Math.max(1, cpus().length);
@@ -59,23 +63,6 @@ function isSupported(filePath) {
59
63
  return SUPPORTED_EXT.includes(extname(filePath).toLowerCase());
60
64
  }
61
65
 
62
- /**
63
- * Encode pipeline to WebP buffer with shared sharp options.
64
- * @param {import("sharp").Sharp} pipeline
65
- * @param {number} quality
66
- * @param {{ lossless?: boolean }} [options]
67
- */
68
- async function encodeWebpBuffer(pipeline, quality, { lossless = false } = {}) {
69
- return pipeline
70
- .webp({
71
- quality,
72
- effort: DEFAULT_EFFORT,
73
- smartSubsample: true,
74
- lossless,
75
- })
76
- .toBuffer();
77
- }
78
-
79
66
  /**
80
67
  * Recursively collect image paths under dir (or return [dir] if dir is a file).
81
68
  * @param {string} inputPath - Absolute path to file or directory
@@ -135,15 +122,7 @@ export async function processOne(inputPath, options = {}) {
135
122
  let pipeline = sharp(inputBuffer, { animated: true });
136
123
  const inputMeta = await pipeline.metadata();
137
124
 
138
- if (size != null && size > 0) {
139
- pipeline = pipeline.resize({
140
- width: size,
141
- height: size,
142
- fit: "inside",
143
- withoutEnlargement: true,
144
- background: { r: 0, g: 0, b: 0, alpha: 0 },
145
- });
146
- }
125
+ pipeline = applyResizeInside(pipeline, size);
147
126
 
148
127
  const outputBuffer = toWebp
149
128
  ? await encodeWebpBuffer(pipeline, quality, { lossless: false })
@@ -172,7 +151,7 @@ async function toFormatBuffer(pipeline, ext, quality) {
172
151
  switch (ext) {
173
152
  case ".jpg":
174
153
  case ".jpeg":
175
- return pipeline.jpeg({ quality, mozjpeg: true }).toBuffer();
154
+ return encodeJpegBuffer(pipeline, quality);
176
155
  case ".png":
177
156
  return pipeline.png({ compressionLevel: 9 }).toBuffer();
178
157
  case ".webp":
@@ -5,10 +5,13 @@
5
5
  import { readFile, writeFile, mkdir, stat } from "fs/promises";
6
6
  import { dirname, join, resolve, basename, extname } from "path";
7
7
  import sharp from "sharp";
8
- import { DEFAULT_QUALITY, DEFAULT_EFFORT } from "./constants.js";
9
-
10
- /** AVIF effort 0–9 (higher = slower, often smaller) */
11
- const DEFAULT_AVIF_EFFORT = 4;
8
+ import { DEFAULT_QUALITY } from "./constants.js";
9
+ import {
10
+ applyResizeInside,
11
+ encodeAvifBuffer,
12
+ encodeWebpBuffer,
13
+ encodeJpegBuffer,
14
+ } from "./sharpHelpers.js";
12
15
 
13
16
  /**
14
17
  * @param {import("sharp").Sharp} pipeline - Configured pipeline (e.g. after resize)
@@ -16,22 +19,9 @@ const DEFAULT_AVIF_EFFORT = 4;
16
19
  */
17
20
  async function encodeTriple(pipeline, q) {
18
21
  const [avifBuf, webpBuf, jpegBuf] = await Promise.all([
19
- pipeline
20
- .clone()
21
- .avif({ quality: q.avifQuality, effort: DEFAULT_AVIF_EFFORT })
22
- .toBuffer(),
23
- pipeline
24
- .clone()
25
- .webp({
26
- quality: q.quality,
27
- effort: DEFAULT_EFFORT,
28
- smartSubsample: true,
29
- })
30
- .toBuffer(),
31
- pipeline
32
- .clone()
33
- .jpeg({ quality: q.jpegQuality, mozjpeg: true })
34
- .toBuffer(),
22
+ encodeAvifBuffer(pipeline.clone(), q.avifQuality),
23
+ encodeWebpBuffer(pipeline.clone(), q.quality),
24
+ encodeJpegBuffer(pipeline.clone(), q.jpegQuality),
35
25
  ]);
36
26
  return { avifBuf, webpBuf, jpegBuf };
37
27
  }
@@ -85,16 +75,10 @@ export async function processPictureStack(inputPath, options) {
85
75
  const inputBuffer = await readFile(absoluteIn);
86
76
  const sizeBefore = inputBuffer.length;
87
77
 
88
- let pipeline = sharp(inputBuffer, { animated: false });
89
- if (size != null && size > 0) {
90
- pipeline = pipeline.resize({
91
- width: size,
92
- height: size,
93
- fit: "inside",
94
- withoutEnlargement: true,
95
- background: { r: 0, g: 0, b: 0, alpha: 0 },
96
- });
97
- }
78
+ let pipeline = applyResizeInside(
79
+ sharp(inputBuffer, { animated: false }),
80
+ size
81
+ );
98
82
 
99
83
  const { avifBuf, webpBuf, jpegBuf } = await encodeTriple(pipeline, {
100
84
  quality,
@@ -40,6 +40,40 @@ const DEFAULT_EXCLUDE_DIRS = new Set([
40
40
  /** Relative ratio difference threshold (1% = 0.01) */
41
41
  const DEFAULT_RATIO_TOLERANCE = 0.01;
42
42
 
43
+ /** Try these folders under projectRoot for URL paths like `/images/a.png` (public 静态资源) */
44
+ const DEFAULT_PUBLIC_DIRS = ["public", "static"];
45
+
46
+ /**
47
+ * @param {string} ref
48
+ * @param {Record<string, string>} aliases
49
+ * @returns {object | null}
50
+ */
51
+ function matchAliasRef(ref, aliases) {
52
+ const keys = Object.keys(aliases)
53
+ .filter((k) => k !== "~")
54
+ .sort((a, b) => b.length - a.length);
55
+ for (const key of keys) {
56
+ const prefix = `${key}/`;
57
+ if (ref.startsWith(prefix)) {
58
+ return { key, rest: ref.slice(prefix.length) };
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * @param {string} abs
66
+ * @returns {Promise<boolean>}
67
+ */
68
+ async function isExistingFile(abs) {
69
+ try {
70
+ const s = await stat(abs);
71
+ return s.isFile();
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
43
77
  /**
44
78
  * @param {string} text
45
79
  * @param {number} index
@@ -313,11 +347,17 @@ function extractPugImgRefs(content) {
313
347
 
314
348
  /**
315
349
  * @param {string} ref
316
- * @param {string} hostDir
317
- * @returns {{ status: 'ok', path: string } | { status: 'skip', reason: string }}
350
+ * @param {string} hostDir - 源码文件所在目录(相对路径 ./ ../ 以此为基准)
351
+ * @param {{
352
+ * projectRoot: string,
353
+ * aliases: Record<string, string>,
354
+ * publicDirs: string[],
355
+ * }} opts
356
+ * @returns {Promise<{ status: 'ok', path: string } | { status: 'skip', reason: string }>}
318
357
  */
319
- function resolveLocalImageRef(ref, hostDir) {
320
- const trimmed = ref.trim();
358
+ async function resolveLocalImageRef(ref, hostDir, opts) {
359
+ const { projectRoot, aliases, publicDirs } = opts;
360
+ let trimmed = ref.trim();
321
361
  if (!trimmed) return { status: "skip", reason: "empty" };
322
362
  if (/^https?:\/\//i.test(trimmed)) {
323
363
  return { status: "skip", reason: "remote_url" };
@@ -325,14 +365,57 @@ function resolveLocalImageRef(ref, hostDir) {
325
365
  if (trimmed.startsWith("data:")) {
326
366
  return { status: "skip", reason: "data_uri" };
327
367
  }
328
- if (trimmed.startsWith("@/") || trimmed.startsWith("~/") || trimmed.startsWith("~@/")) {
329
- return { status: "skip", reason: "alias_path" };
368
+
369
+ if (trimmed.startsWith("~@/")) {
370
+ trimmed = `@/${trimmed.slice(3)}`;
330
371
  }
372
+
373
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
374
+ const relFromWebRoot = trimmed.replace(/^\/+/, "");
375
+ for (const pd of publicDirs) {
376
+ const candidate = resolve(projectRoot, pd, relFromWebRoot);
377
+ if (await isExistingFile(candidate)) {
378
+ return { status: "ok", path: candidate };
379
+ }
380
+ }
381
+ return { status: "skip", reason: "public_path_not_found" };
382
+ }
383
+
384
+ if (trimmed.startsWith("~/")) {
385
+ const tildeBase = aliases["~"];
386
+ if (tildeBase == null || tildeBase === "") {
387
+ return { status: "skip", reason: "alias_path" };
388
+ }
389
+ const rest = trimmed.slice(2);
390
+ const candidate = resolve(projectRoot, tildeBase, rest);
391
+ if (await isExistingFile(candidate)) {
392
+ return { status: "ok", path: candidate };
393
+ }
394
+ return { status: "skip", reason: "alias_target_not_found" };
395
+ }
396
+
397
+ const aliasHit = matchAliasRef(trimmed, aliases);
398
+ if (aliasHit) {
399
+ const baseRel = aliases[aliasHit.key];
400
+ if (baseRel == null || baseRel === "") {
401
+ return { status: "skip", reason: "unknown_alias" };
402
+ }
403
+ const candidate = resolve(projectRoot, baseRel, aliasHit.rest);
404
+ if (await isExistingFile(candidate)) {
405
+ return { status: "ok", path: candidate };
406
+ }
407
+ return { status: "skip", reason: "alias_target_not_found" };
408
+ }
409
+
410
+ if (trimmed.startsWith("@") && trimmed.includes("/")) {
411
+ return { status: "skip", reason: "unknown_alias" };
412
+ }
413
+
331
414
  if (!IMG_EXT_RE.test(trimmed)) {
332
415
  return { status: "skip", reason: "not_image_extension" };
333
416
  }
334
- const path = resolve(hostDir, trimmed);
335
- return { status: "ok", path };
417
+ const pathAbs = resolve(hostDir, trimmed);
418
+ return { status: "ok", path: pathAbs };
336
419
  }
337
420
 
338
421
  /**
@@ -515,6 +598,9 @@ async function runPool(tasks, concurrency) {
515
598
  * issuesOnly?: boolean,
516
599
  * ratioTolerance?: number,
517
600
  * metadataConcurrency?: number,
601
+ * projectRoot?: string,
602
+ * aliases?: Record<string, string>,
603
+ * publicDirs?: string[],
518
604
  * }} [options]
519
605
  */
520
606
  export async function scanCodeReferences(rootDir, options = {}) {
@@ -525,8 +611,22 @@ export async function scanCodeReferences(rootDir, options = {}) {
525
611
  issuesOnly = false,
526
612
  ratioTolerance = DEFAULT_RATIO_TOLERANCE,
527
613
  metadataConcurrency = 8,
614
+ projectRoot: projectRootOpt,
615
+ aliases: aliasesOpt,
616
+ publicDirs: publicDirsOpt,
528
617
  } = options;
529
618
 
619
+ const projectRoot = resolve(cwd, projectRootOpt != null ? projectRootOpt : ".");
620
+ const aliases = {
621
+ "@": "src",
622
+ ...(aliasesOpt && typeof aliasesOpt === "object" ? aliasesOpt : {}),
623
+ };
624
+ const publicDirs =
625
+ Array.isArray(publicDirsOpt) && publicDirsOpt.length > 0
626
+ ? publicDirsOpt
627
+ : DEFAULT_PUBLIC_DIRS;
628
+ const resolveOpts = { projectRoot, aliases, publicDirs };
629
+
530
630
  const sourceExts =
531
631
  options.sourceExts != null
532
632
  ? new Set(
@@ -602,7 +702,7 @@ export async function scanCodeReferences(rootDir, options = {}) {
602
702
  for (const { absHost, ref } of staged) {
603
703
  if (ref.parse?.dynamic || ref.parse?.missingSrc) continue;
604
704
  if (!ref.rawRef) continue;
605
- const res = resolveLocalImageRef(ref.rawRef, absHost);
705
+ const res = await resolveLocalImageRef(ref.rawRef, absHost, resolveOpts);
606
706
  if (res.status === "ok") uniquePaths.add(res.path);
607
707
  }
608
708
 
@@ -691,7 +791,7 @@ export async function scanCodeReferences(rootDir, options = {}) {
691
791
  continue;
692
792
  }
693
793
 
694
- const resolved = resolveLocalImageRef(r.rawRef, absHost);
794
+ const resolved = await resolveLocalImageRef(r.rawRef, absHost, resolveOpts);
695
795
  if (resolved.status === "skip") {
696
796
  const row = {
697
797
  file,
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Shared Sharp pipeline helpers (resize + encode) for compress and picture stack.
3
+ */
4
+ import { DEFAULT_EFFORT } from "./constants.js";
5
+
6
+ const RESIZE_BACKGROUND = { r: 0, g: 0, b: 0, alpha: 0 };
7
+
8
+ /** AVIF effort 0–9 (higher = slower, often smaller) */
9
+ export const DEFAULT_AVIF_EFFORT = 4;
10
+
11
+ /**
12
+ * Apply "fit inside" square resize when size > 0; otherwise return pipeline unchanged.
13
+ * @param {import("sharp").Sharp} pipeline
14
+ * @param {number | null | undefined} size
15
+ * @returns {import("sharp").Sharp}
16
+ */
17
+ export function applyResizeInside(pipeline, size) {
18
+ if (size == null || size <= 0) return pipeline;
19
+ return pipeline.resize({
20
+ width: size,
21
+ height: size,
22
+ fit: "inside",
23
+ withoutEnlargement: true,
24
+ background: RESIZE_BACKGROUND,
25
+ });
26
+ }
27
+
28
+ /**
29
+ * @param {import("sharp").Sharp} pipeline
30
+ * @param {number} quality
31
+ * @param {{ lossless?: boolean }} [options]
32
+ */
33
+ export async function encodeWebpBuffer(
34
+ pipeline,
35
+ quality,
36
+ { lossless = false } = {}
37
+ ) {
38
+ return pipeline
39
+ .webp({
40
+ quality,
41
+ effort: DEFAULT_EFFORT,
42
+ smartSubsample: true,
43
+ lossless,
44
+ })
45
+ .toBuffer();
46
+ }
47
+
48
+ /**
49
+ * @param {import("sharp").Sharp} pipeline
50
+ * @param {number} quality
51
+ */
52
+ export async function encodeJpegBuffer(pipeline, quality) {
53
+ return pipeline.jpeg({ quality, mozjpeg: true }).toBuffer();
54
+ }
55
+
56
+ /**
57
+ * @param {import("sharp").Sharp} pipeline
58
+ * @param {number} quality
59
+ * @param {number} [effort]
60
+ */
61
+ export async function encodeAvifBuffer(
62
+ pipeline,
63
+ quality,
64
+ effort = DEFAULT_AVIF_EFFORT
65
+ ) {
66
+ return pipeline.avif({ quality, effort }).toBuffer();
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lake-cimg",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Batch image compression to WebP — CLI plus Agent Skills for frontend image audit (scan-code, picture stack)",
5
5
  "keywords": [
6
6
  "image",
@@ -38,14 +38,14 @@ More flags: `npx lake-cimg@latest scan-code --help` (e.g. `--limit`, `--issues-o
38
38
  ## Workflow
39
39
 
40
40
  - [ ] **1. Scan (read-only):** `npx lake-cimg@latest scan-code [path]` — see **Invoke `scan-code`** above.
41
- - [ ] **2. Triage:** Prefer **`hints`** for what to change; use **`issues`** codes to group or filter. Common codes: `missing_dimensions`, `aspect_ratio_mismatch`, `suggest_modern_format` (consider AVIF/WebP + `<picture>` match product/browser support), `needs_manual_review`, `missing_src`, `cannot_resolve`, `cannot_read_metadata`.
42
- - [ ] **3. Fix then optimize:** Apply markup fixes using [reference.md](reference.md) templates (`<img>`, `<picture>`). For **all** raster refs that need format or responsive delivery, not only one hero row. Then run compression / stacks: `npx lake-cimg@latest <path> [options]`; for AVIF + WebP + JPEG from one source: `npx lake-cimg@latest picture <input> -O <outDir>` (details: package [README.md](../../README.md)).
41
+ - [ ] **2. Triage:** Prefer **`hints`** for what to change; use **`issues`** codes to group or filter. Common codes: `missing_dimensions`, `aspect_ratio_mismatch`, `suggest_modern_format` (default: **single WebP** — point `src` / `srcset` at `.webp` after exporting; use **`<picture>`** with AVIF/WebP + legacy fallback **only if the user explicitly asks** for multi-format markup or old-browser JPEG/PNG fallback), `needs_manual_review`, `missing_src`, `cannot_resolve`, `cannot_read_metadata`.
42
+ - [ ] **3. Fix then optimize:** Apply markup using [reference.md](reference.md) (`<img>` first; `<picture>` only when the user requires it). For **all** raster refs that need format or responsive delivery, not only one hero row. Compress / emit WebP: `npx lake-cimg@latest <path> [options]`. **Optional** full stack for `<picture>` when requested: `npx lake-cimg@latest picture <input> -O <outDir>` (details: package [README.md](../../README.md)).
43
43
 
44
44
  ## Rules of thumb
45
45
 
46
46
  - **Aspect ratio:** Match display ratio to intrinsic (w÷h), or use **`object-fit`** + explicit box / `aspect-ratio` for crop/letterbox.
47
47
  - **CLS:** Add `width` and `height` to `<img>`, or use CSS `aspect-ratio`. For `missing_dimensions`, fill in the exact `intrinsicWidth` / `intrinsicHeight`.
48
- - **Responsive:** `<picture>` and/or **`srcset` + `sizes`**; each `<source type="…">` must match the real file type; Generate width variants with `picture … -s <px>` or separate exports.
48
+ - **Responsive:** **`srcset` + `sizes`** on `<img>` (works with a single WebP). **`<picture>`** only when the user wants multiple formats in HTML; each `<source type="…">` must match the real file type. Width variants: separate files or `picture … -s <px>` when building a stack.
49
49
  - **LCP:** At most one hero per view: **`fetchPriority="high"`**, **`loading="eager"`**; lazy-load the rest.
50
50
  - **Alt:** Describe content and purpose; no keyword stuffing; **`alt=""`** only for decorative images.
51
51
 
@@ -60,4 +60,4 @@ If **`hints`** mention alias skip: use **`rawRef`** and map the alias via Vite/w
60
60
  | Command | Role |
61
61
  | --- | --- |
62
62
  | `npx lake-cimg@latest <path>` | Compress / WebP — see `npx lake-cimg@latest --help` (`-o`, `-s`, `-q`, `-r`). |
63
- | `npx lake-cimg@latest picture <input> -O <outDir>` | One raster → AVIF + WebP + JPEG for `<picture>`. |
63
+ | `npx lake-cimg@latest picture <input> -O <outDir>` | One raster → AVIF + WebP + JPEG **when the user wants `<picture>` / multi-format**; not the default if single WebP is enough. |
@@ -8,12 +8,15 @@ Supplement for [SKILL.md](SKILL.md). Copy-paste starting points; adjust paths, d
8
8
 
9
9
  ```html
10
10
  <img
11
- src="photo.jpg"
12
- width="1200"
13
- height="800"
14
- alt="Describe subject and purpose on this page"
15
- loading="lazy"
16
- decoding="async">
11
+ srcset="
12
+ maine-coon-nap-320w.webp 320w,
13
+ maine-coon-nap-480w.webp 480w,
14
+ maine-coon-nap-800w.webp 800w
15
+ "
16
+ sizes="(max-width: 320px) 280px, (max-width: 480px) 440px, 800px"
17
+ src="maine-coon-nap-800w.webp"
18
+ alt="A watercolor illustration of a maine coon napping leisurely in front of a fireplace"
19
+ />
17
20
  ```
18
21
 
19
22
  ## `<picture>` + `srcset` + `sizes`
@@ -24,26 +27,23 @@ Supplement for [SKILL.md](SKILL.md). Copy-paste starting points; adjust paths, d
24
27
  <picture>
25
28
  <source
26
29
  type="image/avif"
27
- srcset="hero-400.avif 400w,
28
- hero-800.avif 800w,
29
- hero-1200.avif 1200w"
30
- sizes="(max-width: 600px) 100vw, 50vw">
30
+ srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w"
31
+ sizes="(max-width: 600px) 100vw, 50vw"
32
+ />
31
33
  <source
32
34
  type="image/webp"
33
- srcset="hero-400.webp 400w,
34
- hero-800.webp 800w,
35
- hero-1200.webp 1200w"
36
- sizes="(max-width: 600px) 100vw, 50vw">
35
+ srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
36
+ sizes="(max-width: 600px) 100vw, 50vw"
37
+ />
37
38
  <img
38
39
  src="hero-800.jpg"
39
- srcset="hero-400.jpg 400w,
40
- hero-800.jpg 800w,
41
- hero-1200.jpg 1200w"
40
+ srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
42
41
  sizes="(max-width: 600px) 100vw, 50vw"
43
42
  width="1200"
44
43
  height="600"
45
44
  alt="Describe subject and purpose on this page"
46
45
  loading="lazy"
47
- decoding="async">
46
+ decoding="async"
47
+ />
48
48
  </picture>
49
49
  ```