Skip to content

feat(apps-vtex): let the legacy PLP shrink its payload - #517

Open
aka-sacci-ccr wants to merge 2 commits into
mainfrom
vtex-plp-payload-patch
Open

feat(apps-vtex): let the legacy PLP shrink its payload#517
aka-sacci-ccr wants to merge 2 commits into
mainfrom
vtex-plp-payload-patch

Conversation

@aka-sacci-ccr

@aka-sacci-ccr aka-sacci-ccr commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Upstreams a patch that currently lives as a patches/ file on a live site (miess-01-tanstack). The fix belongs in blocks, not in a per-site patch.

The problem

leanVariants already existed on toProduct, and productDetailsPage (intelligent search) already exposed it — but the legacy PLP never plumbed it through. So a listing always paid a full nested toProduct per SKU.

Measured on a real store (36 products, 26 variants each): ~9.6 MB per page, 96% of each product sitting in isVariantOf.hasVariant, because every variant carries the whole 48-entry payment ladder plus a copy of the parent description — none of which a listing card reads.

What changed

LegacyPLPOptions now forwards leanVariants, variantPropertyNames, variantIncludeImage and variantIncludeInventory into toProduct, and ProductOptions gains two knobs (and is now exported):

displayedVariantId?: (items) => string | undefined — keep ONE variant on the full toProduct shape.

Why it's needed: leanVariants assumes the card renders the root sku, so buildOfferVariant empties priceSpecification on every entry. Cards that instead pick a representative variant out of hasVariant (e.g. "cheapest in stock") read that variant's own offer for list price and installments — with the ladder emptied, those render blank. This lets such a caller keep exactly the one entry it displays.

maxImages?: number — cap image[] to the first N entries. Listings render at most a couple of images per card, while the Catalog API returns every asset on the SKU (3 on average, up to 5 measured).

⚠️ It truncates by position. On a real listing page the vira (hover) image sits at index 1 in only 5 of the 19 products that have one — index 2 in 12 of them, index 3 in 2. A cap of 2 therefore drops the hover image on most cards that use one. Callers that select images by name should keep the named entries instead of using this option. This is documented on the option itself, not just here.

Compatibility

Every option is opt-in; undefined preserves current behaviour byte for byte. No existing caller passes any of them.

Verification

  • bun run typecheck — clean
  • bun run test — 268 tests, 25 files, all passing
  • biome check on both touched files — back to the same 6 pre-existing diagnostics as main (these two files are tab-indented while biome.json formats with spaces; that drift predates this PR)

Left out deliberately

legacyProductListingPage is a CMS-exposed loader (vtex/loaders/legacy/legacyProductListingPage, schema s24). Running bun run generate:schemas to pick up the new props produced 2230 lines of diff — almost all of it pre-existing drift unrelated to this change: schemas.gen.ts is stale against src and is missing whole loaders (e.g. the shelf's completeVariants, orderFormId/itemIndex, freeShippingTarget). I reverted the file rather than bury this change in that churn.

Consequence: the new props don't show up in the admin schema. That mirrors the site patch, which didn't touch schemas.gen.ts either, and the load-bearing one (displayedVariantId) is a function, so it isn't serializable regardless. Regenerating schemas.gen.ts deserves its own PR.

🤖 Generated with Claude Code


Summary by cubic

Legacy PLP now forwards leanVariants and related options into toProduct, adding two optional knobs to shrink listing payloads without breaking existing behavior.

  • LegacyPLPOptions gains leanVariants, variantPropertyNames, variantIncludeImage, variantIncludeInventory, displayedVariantId, and maxImages; all default to undefined and preserve current output.
  • displayedVariantId keeps one variant on the full shape for cards that read that variant's own offer (list price/installments).
  • maxImages caps image[] to the first N entries by position, ignoring values below 1; only toProduct honors it, and it's not for callers selecting images by name.
  • Schema generation (schemas.gen.ts) intentionally left out; regenerating it produces large unrelated drift and should be a separate PR.

Written for commit 6f49cb3. Summary will update on new commits.

Review in cubic

`leanVariants` existed on `toProduct` and was already exposed by
`productDetailsPage` (intelligent search), but the legacy PLP never
plumbed it through — so a listing always paid the full nested
`toProduct` per SKU. Measured on a real store (36 products, 26
variants each): ~9.6 MB per page, 96% of each product sitting in
`isVariantOf.hasVariant`, because every variant carries the whole
48-entry payment ladder plus a copy of the parent `description` —
none of which a listing card reads.

Forwards `leanVariants`, `variantPropertyNames`, `variantIncludeImage`
and `variantIncludeInventory` from `LegacyPLPOptions` into `toProduct`,
and adds two new `ProductOptions` knobs:

- `displayedVariantId(items) => itemId | undefined` — keep ONE variant
  on the full shape. `leanVariants` assumes the card renders the root
  sku, so `buildOfferVariant` empties `priceSpecification` on every
  entry; cards that instead pick a representative variant out of
  `hasVariant` (e.g. "cheapest in stock") read that variant's own offer
  for list price and installments, and render blank with the ladder
  emptied. This lets such a caller keep exactly the entry it displays.
- `maxImages` — cap `image[]` to the first N entries. Note it truncates
  by POSITION: on a real listing the `vira` (hover) image sits at index
  1 in only 5 of the 19 products that have one, index 2 in 12 of them,
  so a cap of 2 drops the hover image on most cards that use one.
  Callers selecting by name should keep the named entries instead.

Every option is opt-in — undefined preserves current behaviour byte for
byte. Originates as a local patch on a live site (miess); the fix
belongs here, not in a per-site patch file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aka-sacci-ccr
aka-sacci-ccr requested a review from a team August 31, 2026 14:49
Review follow-up on the two new toProduct options.

maxImages was unvalidated. `slice(0, 0)` returns [] rather than null, so
`maxImages: 0` slipped past the `?? [DEFAULT_IMAGE]` fallback and emitted
`image: []` — breaking any card that reads `image[0].url`. A negative
value dropped the LAST images instead of capping the first N, silently
contradicting the documented contract. Both now fall back to keeping
every image, matching how `undefined` behaves; documented as `>= 1`.

Also replaced the IIFE around the hasVariant map with a hoisted const.
The `level < 1 && options.leanVariants` guard preserves the original
call count — the callback still runs exactly once per product, and not
at all when leanVariants is off.

Documented that maxImages is honoured by toProduct only: toProductShelf
and toProductVariant build image[] through their own paths and ignore
it, so passing it via the intelligent-search shelf is a silent no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aka-sacci-ccr

Copy link
Copy Markdown
Contributor Author

Review follow-up pushed in 6f49cb3 — one correctness fix plus a readability pass. No new tests (deliberate, per request).

🔴 maxImages had no range guard

Confirmed by running the branch, not by reading it:

before:  maxImages: 0  → image: []
before:  maxImages: -1 → ["…/1.jpg", "…/2.jpg"]   // dropped the LAST image

slice(0, 0) returns [], not null, so it slipped past the ?? [DEFAULT_IMAGE] fallback that exists precisely to guarantee image[] is never empty — any card doing image[0].url would throw. The negative case silently inverted the documented contract ("cap to the first N").

Both now fall back to keeping every image, consistent with how undefined behaves, and the JSDoc states >= 1:

const cappedImages =
	typeof options.maxImages === "number" && options.maxImages > 0
		? images?.slice(0, options.maxImages)
		: images;

Verified after the fix: 0 and -1 both keep all 3 images; 1 keeps exactly the first; 2 caps to 2; undefined keeps all.

🟡 IIFE replaced with a hoisted const

The ((keepId) => …)(options.displayedVariantId?.(items)) wrapper was hard to read. Now:

const displayedId =
	level < 1 && options.leanVariants ? options.displayedVariantId?.(items) : undefined;

The level < 1 half of the guard is load-bearing — without it the callback would also fire inside the nested toProduct call the kept variant makes. Verified the call count is unchanged: exactly once per product, and zero times when leanVariants is off. The lean/full split still behaves as documented (displayedVariantId: () => "SKU2" → only SKU2 carries description).

🟡 Documented the shelf no-op

maxImages is honoured by toProduct only. toProductShelf and toProductVariant build image[] through their own paths, so passing it via the intelligent-search shelf loader is a silent no-op. Now stated on the option itself rather than being folk knowledge.

Still open, not addressed here

  • No tests. The "unchanged by default" invariant that makes this PR safe for the 8 loaders sharing toProduct is still verified only by hand, not in CI. I confirmed it empirically with throwaway tests during review, but nothing pins it.
  • schemas.gen.ts not regenerated, so leanVariants/maxImages remain unreachable from the admin for this CMS-exposed loader. Blocked on 2230 lines of pre-existing drift; needs its own PR.
  • LegacyPLPOptions still redeclares 6 fields rather than Pick-ing them from ProductOptions. Left as-is on purpose: the manual declarations carry PLP-specific JSDoc (the measured payload numbers) that a Pick would discard.

Verification: typecheck clean, 268 tests passing, biome check at the same 6 pre-existing diagnostics as main.

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