Skip to content

feat: @decocms/blocks/image (platform-neutral) with VTEX and Shopify image providers in their packages - #694

Draft
tlgimenes wants to merge 10 commits into
feat/draft-hosts-and-headersfrom
feat/blocks-image
Draft

tlgimenes wants to merge 10 commits into
feat/draft-hosts-and-headersfrom
feat/blocks-image

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #693 (P2 of the v8 framework-packages plan). Reworked after the product owner's review: @decocms/blocks keeps framework code only, and nothing that knows VTEX, Shopify or another commerce platform.

What

@decocms/blocks/image (platform-neutral): pure functions, no React, no module state.

  • decoCdn(options): the Deco image CDN (https://<domain>/image?fit=&width=…&src=). Options: domain, quality (tier), fit (default cover, as v7's getSrcSet/Image), legacyHosts (default ["decoims.com"] on the Cloudflare CDNs), sources ("deco" default, "all" for v7's "everything else goes through the CDN", or a predicate), stripAssetHosts ("leading", the default since the PO's 2026-10-15 decision: v7 from 7.71.4; or "anywhere": v7 up to 7.71.3 and two storefronts' vendored helpers), layout ("src-last" or "quality-last").
  • createImageUrl({ providers }): one ImageUrl per site, with isOptimizable and hosts(). Providers are tried in order; a provider's format may return null to leave a source to the next one.
  • srcSet, parseSrcSet, preloadLink, and the generic types (ImageProvider, ImageUrl, ResizeOptions, Quality, Fit, …). srcSet without a width writes v7's two width=NaN … NaNw candidates, as v7's getSrcSet did for an Image or Source without a width (a storefront's pages have 12 such <source> tags; before this, they lost their srcset).

@decocms/apps-vtex/image: VTEX's URL formats as providers. They are pure helpers over the blocks types: no fetch, no React, no env reads, no runtime imports.

  • vtexCatalog(): v7's Image rule. VTEX hosts, {id}-{w}-{h ?? w}, URL re-serialised.
  • vtexCatalogFrame({ frame? }): any host, a fixed frame or the call's box, rounded.
  • vtexCatalogWebp({ quality? }): the next/image loaders' rule. {id}-{w}-auto/{file}.webp?…&quality= on VTEX's 1-10 scale.
  • vtexFileManager({ via: "query" | "thumbor", quality? }).

@decocms/apps-shopify/image: shopifyCdn(), v7's rule (width, height when given, crop=center).

A site lists the platform providers before decoCdn. The bindings never depend on an apps-* package; sites wire the helper in. v7's registerImageCdnDomain/registerImageQuality do not come back: they were module state. Their values are decoCdn's domain and quality.

Golden URLs: only the approved changes

The fixtures were regenerated by running each v8 storefront's own helpers, unchanged:

  • the three vendored v7 getOptimizedMediaUrl/getSrcSet variants
  • two next/image loaders
  • a fixed-frame catalog loader

The corpus grew to cover edge cases: unencoded characters, an asset host inside another URL, already-resized CDN URLs, uppercase hosts, odd id suffixes, store-domain catalog URLs, and fractional widths. The fixtures are split per package:

Package Cases Sources
@decocms/blocks 1,508 Deco hosts, other hosts, paths, data:, ""
@decocms/apps-vtex 2,625 catalog + file manager, composed with decoCdn
@decocms/apps-shopify 144 Shopify CDN, composed with decoCdn

Every case reproduces today's URL byte for byte, except 192 approved @decocms/blocks cases:

  • 160 for approved change 2 below (the two already-resized CDN sources, in every config);
  • 32 for approved change 3 (the source https://images.example.com/https://assets.decocache.com/…, on the two configs that keep decoCdn's default with sources: "all").

Each of those carries before (today's URL) and reason. The test asserts that no other case changed, and that each change-3 before is exactly what stripAssetHosts: "anywhere" still writes. The apps-vtex and apps-shopify fixtures have no changed case and still assert that none carries a before. Where a helper throws today, the case is recorded as <throws>, and the provider still throws (v7's VTEX rule on a URL that only mentions a VTEX host in its query). The fixtures use no site names. The generator stays out of the repo because it reads the sites' paths.

Approved changes (PO, 2026-10-14 and 2026-10-15)

The product owner approved 1 and 2 on 2026-10-14 ("the image fixes are good") and 3 on 2026-10-15:

  1. File-manager images on the next/image loader that still uses VTEX's ?width=&aspect=true resizer. That resizer has answered 500 for raster files since 2026-10-07. That store switches to vtexFileManager({ via: "thumbor" }), which the other loader already uses. This is a site change only; no package changes.

  2. An already-resized Deco CDN URL pasted into content (https://assets.decocms.com/image?…, or the decoims.com, deco-assets.edgedeco.com and deco-assets.decoazn.com endpoints). Today it becomes src=image, or the whole /image URL without its query, and the CDN cannot serve either one. decoCdn now re-resizes it from its src by default (c80dd33, narrowed in 6b7e700). That covers an encoded src, a nested CDN URL, http and any host case. Resizing a result again gives the same URL. It applies only to the URLs decoCdn already resized (its domain and legacy hosts with the default sources: "deco"; every endpoint with sources: "all" or a predicate that takes it): a Deco CDN URL it passed through before, such as a deco-assets.edgedeco.com one under sources: "deco", still passes through unchanged, because it was served fine. Other URLs are unaffected, including a non-Deco /image path and an /image URL with no src. A differential fuzz against the previous commit (9 configs, 10 hosts, 6 path shapes) finds 108 changed URLs, every one of them src=image or src=<cdn>/image before.

  3. stripAssetHosts defaults to "leading" (afe430c). An asset host (assets.decocache.com/, the S3 bucket) is removed only where the source starts with it, as v7's Image does since 7.71.4. Two storefronts' vendored helpers (and v7 up to 7.71.3) removed it anywhere, so a source that only contains an asset URL (https://other.host/https://assets.decocache.com/x.png) used to lose that part and now keeps it. Sources that start with an asset host, which is every one the storefronts' content has in the corpus, give the same URL either way. "anywhere" stays an option. Follow-up in the migrator (feat: the v7-to-v8 migrator and skill use @decocms/tanstack and @decocms/nextjs #700): stop emitting "anywhere" and count and report the URLs this changes per site.

Proposals for the PO (not decided, not in the code)

  1. A mobile app's VTEX helper copies disagree with each other. One drops the source's query; the other leaves a pre-sized -W-auto id at its baked size. The app does not use these packages, so it is out of the golden suite. Proposal: point it at one helper.

Nuvemshop's image helper stays in the store's own code (PO, 2026-10-14). There is no apps-nuvemshop package.

Checks

Decision 3 (afe430c, on top of #693's decisions 1–3 merged in d654d9a): bun run check, bun run build, skills:check and skills:readme:check pass, and plain Node imports all 24 built exports (deco --help runs). bun run test (2 GB): 2150 passed, 3 failed: DS-2 (fails on main too; fixed separately) and two schema.test.ts timeouts under full-suite load (58/58 alone). Image tests 48/48 across the three packages; reverting the default fails three of them.

Adversarial pass (6b7e700): c80dd33 had also widened decoCdn's default match to every Deco CDN endpoint, which changed 168 URLs that used to pass through and were served fine; the match is back to the pre-fix sources, and a new unit test pins those pass-throughs. bun run check and bun run build pass; image tests 27/27; the golden fixtures are unchanged (160 approved cases). Full gates at the top of the stack (#700).

Fix 2 (c80dd33): bun run check, bun run build, skills:check and skills:readme:check pass. bun run test: 2133 passed, 2 failed: DS-2 (the known flake) and a schema.test.ts timeout under full-suite load, which passes 58/58 alone. The image tests pass 26/26, with 6 new fix-2 unit tests.

Review pass (2026-10-14): the srcSet fix above (ea0a6bf). Its golden markup cases (v7's own components, no width) are in #700's tests/tanstack-image-markup.golden.test.ts. Full gates at the top of the stack (#700).

First round (before the PO review):

  • bun run check passes. bun run build passes and emits dist/v8/image.*, apps-vtex/dist/image.* and apps-shopify/dist/image.*. Plain Node imports the built @decocms/apps-vtex/image.
  • bun run test: 2124 passed, 4 failed:
    • DS-2 in delivery.cli.test.ts, the known flake.
    • Three CLI conformance timeouts under full-suite load (cli-02, qs-01/in-03, saved-03/saved-11). Run alone, those files pass 179/179.
  • bun run skills:check and bun run skills:readme:check pass.
  • Bundle tests:
    • @decocms/blocks/image bundles for a browser and for workerd with no Node built-ins and no React (esbuild).
    • New tests/upstream-helpers.guardrail.test.ts: every apps-* helper subpath gets the same bundle check, plus a purity check (no network, React, env reads or runtime imports). Its comment records that the PO asked for helper subpaths in apps-*.
  • CLAUDE.md now says where platform helpers live.

For the PRs above (#696, #697, #700): the API changed:

  • vtexCatalog/vtexFileManager/shopify are no longer in @decocms/blocks/image.
  • parse/ParsedImage and proxyOtherHosts are gone (use sources: "all").
  • @decocms/tanstack's default ImageUrl cannot include VTEX/Shopify, because it must not depend on apps-*.

🤖 Generated with Claude Code
https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu

…et and preload data

Pure functions, no React, no module state: decoCdn, vtexCatalog,
vtexFileManager and shopify providers, createImageUrl, srcSet, parseSrcSet
and preloadLink. A site builds one ImageUrl; v7's registerImageCdnDomain and
registerImageQuality (module-wide state) do not come back.

Golden-URL tests (2,111 cases) generated from each v8 storefront's current
image helpers reproduce their URLs byte for byte. Intended changes: the
next/image loader still on VTEX's file-manager ?width= resizer moves to the
Thumbor route, and a mobile app's diverging hand copies follow its own
site-loader copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes and others added 2 commits October 14, 2026 15:19
Brings in 6a273bc: the draft names and Studio's origins leave @decocms/blocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…pify providers move to their packages

Product owner review: @decocms/blocks keeps framework code only.

- @decocms/blocks/image: decoCdn, createImageUrl, srcSet, parseSrcSet,
  preloadLink and the generic types. No VTEX, Shopify or other platform
  code, hosts or detection. decoCdn takes `sources` ("deco" | "all" | a
  predicate), `stripAssetHosts`, `fit`, `legacyHosts`, `layout`. A provider's
  format may return null to leave a source to the next provider.
- @decocms/apps-vtex/image: vtexCatalog, vtexCatalogFrame, vtexCatalogWebp,
  vtexFileManager. @decocms/apps-shopify/image: shopifyCdn. Pure functions
  over @decocms/blocks/image types.
- Every URL is what the sites' own helpers produce today: the golden
  fixtures (split per package, regenerated from the sites' unchanged
  helpers, edge cases added) have no changed case. Dropped from code: the
  file-manager Thumbor switch for the loader still on the query resizer,
  re-formatting already-resized CDN URLs, leading-only asset host stripping
  as the default, rounding and re-encoding differences, and the mobile app
  hand copies (not a consumer of these packages).
- tests/upstream-helpers.guardrail.test.ts: apps-* helper subpaths are pure
  (no network, React, env reads or runtime imports) and bundle for a browser
  and workerd. CLAUDE.md says where platform helpers live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
@tlgimenes tlgimenes changed the title feat: @decocms/blocks/image, image URL format and parse, srcset and preload data feat: @decocms/blocks/image (platform-neutral) with VTEX and Shopify image providers in their packages Oct 14, 2026
tlgimenes added a commit that referenced this pull request Oct 14, 2026
…draft names and Studio's origins

Brings in the PO review: @decocms/blocks no longer exports the draft names,
draftResponseHeaders or Studio's origins (#693), @decocms/blocks/image is
platform-neutral with the VTEX and Shopify helpers in @decocms/apps-vtex/image
and @decocms/apps-shopify/image (#694), StudioBridge takes its trusted origins
from the binding (#695), and @decocms/tanstack owns its own copy (#696).
CLAUDE.md conflict: tests/ (helper guardrails) and examples/ lines combined.

@decocms/nextjs, in the same commit so every package type-checks:
- src/draft.ts (DRAFT_PARAM/COOKIE/HEADER, draftResponseHeaders) and
  src/studio.ts (STUDIO_ORIGINS, STUDIO_FRAME_ANCESTORS, isStudioOrigin): the
  package's own copy, same values as @decocms/tanstack's, never imported from
  it. The proxy, drafts.ts, /server and config.test.ts use it; the root
  exports it.
- StudioBridge (src/StudioBridge.ts): blocks' bridge with trustedOrigins set
  to STUDIO_ORIGINS, so the layout still passes only the site.
- /image-loader stays platform-neutral; its next/image golden comparison
  moves to tests/nextjs-image-loader.golden.test.ts, which composes
  @decocms/apps-vtex/image with decoCdn as a Next storefront does: 1,212
  cases from blocks' and apps-vtex's fixtures, none changed. exports.test.ts
  pins that the package depends on no @decocms/tanstack or @decocms/apps-*.
- nextjs-binding-smoke: decoCdn({ fit: "contain" }) (blocks' default fit is
  now cover) and a check of the origins the bridge answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes added a commit that referenced this pull request Oct 14, 2026
Brings in the PO review down the stack: blocks without the draft names or
Studio's origins (#693), the platform-neutral @decocms/blocks/image with the
@decocms/apps-vtex/image and @decocms/apps-shopify/image helper subpaths
(#694), StudioBridge's required trustedOrigins (#695), and each binding's own
copy of the draft names and Studio's origins (#696, #697).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
v7's getSrcSet, given no width, wrote two candidates with width=NaN
("… NaNw, … NaNw"), and an Image or Source without a width rendered
that srcset. srcSet returned undefined instead, so such an <img> lost its
srcset and sizes and a <source> got one width=undefined URL. Found on a
storefront's pages (12 <source> tags). URLs must stay byte for byte, so
srcSet reproduces v7: no special case for a missing width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes and others added 4 commits October 14, 2026 19:44
…ts src

A Deco CDN URL pasted into content (https://assets.decocms.com/image?...&src=,
or the decoims.com, deco-assets.edgedeco.com and deco-assets.decoazn.com
endpoints) became src=image (or the whole /image URL without its query), which
the CDN cannot serve. decoCdn now unwraps it (encoded or nested src included)
and resizes the source it was made from. Default behaviour, approved by the
product owner (2026-10-14).

The golden fixture marks the 160 cases this changes (the two already-resized
CDN sources, every config) with `before` and `reason`; the test asserts no
other case changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
c80dd33 also widened decoCdn's default match (sources "deco") to every
Deco CDN endpoint, http and any host case. Those URLs passed through
unchanged before and were served fine, so 168 of them changed outside
the approved fix (e.g. https://deco-assets.edgedeco.com/image?…, an
http:// or uppercase-host assets.decocms.com URL). match is back to the
pre-fix prefixes; format still unwraps every Deco CDN endpoint, so the
URLs that became src=image or src=<cdn>/image (domain, legacy hosts,
sources "all", a predicate) are re-resized from their src. Differential
fuzz against ea0a6bf over 9 configs x 10 hosts x 6 paths: 108 changed,
all previously broken. Golden fixtures unchanged (160 approved cases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The product owner approved "leading" as stripAssetHosts' default
(2026-10-15), as v7's Image does since 7.71.4. "anywhere" stays an option.

Golden fixtures: the 32 cases this changes (the source that only contains
an asset URL, on the two configs that keep the default) are re-annotated
as approved, each with its old URL and the reason; a test checks that the
old URL is exactly what "anywhere" writes and that nothing else changed.
The apps-vtex and apps-shopify fixtures don't change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
tlgimenes added a commit that referenced this pull request Oct 15, 2026
…ass no dev flag

decoCdn strips only a leading asset host by default now (#694), so the
migrator leaves stripAssetHosts out for every site. A site whose v7 image
code stripped the asset hosts anywhere (7.x before 7.71.4, or such a copy)
moves to "leading" too, as the PO approved (2026-10-15): the report's
"image URLs" line counts and lists the URLs in .deco/blocks and src/ whose
image URL changes (changedByLeadingStrip, checked against decoCdn itself).

framework-code.md: Next.js sites pass no dev flag; createCMS reads NODE_ENV
(PO decision 1), and isNextDev is gone from @decocms/nextjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant