Skip to content

fix(storefront): Show cash discount price in product JSON-LD - #790

Closed
vitorrgg wants to merge 1 commit into
mainfrom
feat/product-jsonld-cash-discount
Closed

fix(storefront): Show cash discount price in product JSON-LD#790
vitorrgg wants to merge 1 commit into
mainfrom
feat/product-jsonld-cash-discount

Conversation

@vitorrgg

Copy link
Copy Markdown
Member

Product JSON-LD offers now apply the store's configured payment discount (settings.modules.list_payments.discount_option), reusing getPriceWithDiscount from use-prices. Falls back to the regular price when no discount is configured, so behavior is unchanged for stores that don't set it.

Product JSON-LD offers now apply the store's configured payment
discount (settings.modules.list_payments.discount_option), reusing
getPriceWithDiscount from use-prices. Falls back to the regular price
when no discount is configured, so behavior is unchanged for stores
that don't set it.
@vitorrgg

Copy link
Copy Markdown
Member Author

Review

Overview

This PR makes the Product JSON-LD offers.price in packages/storefront/src/lib/layouts/BaseHead.astro reflect the store's configured payment (cash) discount, by wrapping getPrice(product) with the already-exported getPriceWithDiscount helper from use-prices.ts, fed by settings.modules?.list_payments?.discount_option. Small, focused diff (1 file, +5/−1).

What works well

  • Good reuse: getPriceWithDiscount is already exported and is the exact same logic driving the visible "price with discount" on product pages (usePrices().priceWithDiscount), so the JSON-LD now matches the price the storefront actually advertises. No duplicated math.
  • Safe fallback: the || {} default is correct — getPriceWithDiscount returns the input price unchanged when value is falsy, so stores without discount_option see zero behavior change.
  • Types line up: settings.modules.list_payments.discount_option is Partial<ListPaymentsResponse['discount_option']> (all-optional fields), which is assignable to the helper's parameter type.
  • Consistent data source: modules-info-preset.ts seeds the client-side discountOption state from the same settings.modules object, so SSR JSON-LD and the hydrated UI start from identical data.
  • Commit message follows Conventional Commits and the repo's convention (business-focused subject, technical detail in the body); scope is minimal and no lock/dependency files are touched.

Suggestions

  1. Round the resulting price. Percentage discounts produce floating-point artifacts (e.g. 99.9 * 0.95 = 94.90499999999999), which will be serialized raw into the JSON-LD. Structured-data consumers expect a clean monetary value — consider:

    price: Math.round(getPriceWithDiscount(
      getPrice(product),
      settings.modules?.list_payments?.discount_option || {},
    ) * 100) / 100,

    Precedent in this repo: packages/feeds/src/firebase/render-catalog.ts uses .toFixed(2) when applying a discount to feed prices.

  2. Consider restricting to type: 'percentage', or at least guarding the zero case. For type: 'fixed', the discount value is an order-level amount (especially with apply_at: 'total'), and applying the full fixed amount to a single item's price overstates the per-product discount for multi-item carts. Worse, getPriceWithDiscount clamps to 0 when the fixed value exceeds the price — a price: 0 with availability: InStock in JSON-LD is a plausible trigger for rich-result/Merchant Center disapprovals on cheap products. The on-page UI has the same behavior, so this isn't a new inconsistency, but JSON-LD is higher-stakes since it feeds search engines directly.

Risks worth being aware of (not blockers)

  • Behavior change for existing stores: any store that already has discount_option configured gets a lower advertised JSON-LD price on next deploy — this is not opt-in. Since the storefront already displays the discounted "à vista" price prominently, aligning JSON-LD with it arguably reduces Google's page-vs-markup price mismatch; but it's worth flagging in the changelog/release notes.
  • Feed divergence: packages/feeds only applies a discount when the merchant passes an explicit ?discount= query param to the catalog endpoint. Stores using Merchant Center feeds without that param will now have JSON-LD price ≠ feed price, which Google may flag. Might be worth a follow-up to derive the feed discount from the same settings.modules config.
  • apply_at semantics are ignored: apply_at: 'total' means the discount is computed over the checkout total (with freight), while here it's applied to the bare item price. For percentage discounts the per-item rate is the same, so this is fine in practice; for fixed discounts see suggestion 2. min_amount is checked against the single-unit price (conservative — a multi-quantity cart could still reach the threshold at checkout), which errs on the safe side of not over-advertising.
  • Staleness: settings.modules is a static settings snapshot; the live list_payments module response (from payment apps) can differ, and SSR pages are cached. This is a pre-existing characteristic of the preset mechanism, not introduced here.

Tests

packages/storefront has no test files today (nothing for use-prices.ts or BaseHead.astro), so no coverage regression — but getPriceWithDiscount is a pure function and would be a cheap, high-value unit-test target (percentage/fixed, min_amount boundary, clamp-to-zero, empty object) if you want to lock this behavior down.

Security

No concerns: a numeric value derived from trusted store settings, serialized via JSON.stringify into an inline script — no user-controlled input, no injection surface.

Verdict: solid, minimal change that fixes a real SEO price-accuracy gap. Suggest addressing rounding (1) before merge; (2) is a judgment call.

@leomp12

leomp12 commented Jul 31, 2026

Copy link
Copy Markdown
Member

duplicado de #788

@leomp12 leomp12 closed this Jul 31, 2026
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.

2 participants