Skip to content

05 — Storefront Widgets + Public Read API (Screen 8 & 8.1)

Cursor-ready implementation plan: admin Widget Settings, two theme blocks (home + PDP), and storefront GET API for published testimonials.

05 — Storefront Widgets + Public Read API

Section titled “05 — Storefront Widgets + Public Read API”

Source: 02-Implementation-Blueprint.mdScreen 8 (/app/testimonials/widget) and Screen 8.1 (two storefront widgets + API + theme extension).

This document is a build spec only. Do not treat it as permission to edit unrelated legacy chat code unless a task explicitly references this plan.


Merchants configure testimonial widget appearance and behavior in admin; the storefront renders (A) a home strip of up to N published video testimonials store-wide and (B) a PDP section of published testimonials for the current product only, via a public read API and a Shopify theme app extension.


Implement this plan after (or in parallel with, using fixtures) the following exist; otherwise the API returns empty arrays and the admin preview is stubbed.

DependencyWhy
TestimonialSubmission, TestimonialMediaAsset (and linkage to shopifyProductId) per blueprint §5.7 / §5.8Read API must query published rows with playback URLs / thumbnails.
Shop row per installTenant scoping; widget settings storage (see §4).
Optional: Product mirrorNot required for read API if shopifyProductId on submission matches Liquid product.id format you standardize (see §6.3).

If submissions are not built yet, still implement routes, CORS, auth-free GET, and theme blocks that call the API and render an empty state using merchant copy from settings.


  • Persist Screen 8 and Screen 8.1 settings (fields listed in §4).
  • Public GET endpoint(s) for published testimonials only, with placement=home|pdp, limits, media filter, and PDP product_id.
  • Theme app extension: Option A from blueprint — two blocks (testimonial-widget-home, testimonial-widget-pdp) + shared JS (recommended).
  • CORS for storefront origin(s): allow the shop’s storefront to read JSON (mirror patterns used by existing public routes in the boilerplate, e.g. api.widget-settings style headers).
  • PDP fallback when zero product-scoped results: show_global_featured | show_recent | hide_widget per merchant setting.
  • Home behavior: default 5 items, video_only, ranking featured_then_newest unless settings say otherwise.
  • Video upload, transcoding, moderation inbox, campaign emails (§4 Email, Screens 5–7).
  • Authenticated write APIs from the storefront (submissions use token page Screen 13).
  • Collection-page widget (blueprint mentions toggle; v1 can return 404 or empty for placement=collection unless explicitly added).
  • Full live preview in admin with real iframe of merchant theme (§3.4 can be phase 2: start with JSON preview or static mock cards).

3) Admin UI — app/routes/app.testimonial-widget.jsx (Screen 8)

Section titled “3) Admin UI — app/routes/app.testimonial-widget.jsx (Screen 8)”
  • authenticate.admin(request).
  • Load current shop’s testimonial widget settings (from DB — see §4).
  • Pass defaults where fields are null.
  • Validate and save widget settings (single form or grouped saves).
  • Use Shopify admin toast / banner patterns consistent with app.settings.jsx or app.appearance.jsx in this repo.
  • Idempotent saves; no duplicate rows for singleton shop settings.
  1. Global — master enable, title, subtitle, logo, trust badges, header alignment/spacing.
  2. Layout & cards — carousel/grid/masonry, columns, card style, radii, shadow, toggles (rating, name, product name, date, transcript snippet + max length, autoplay/mute/loop).
  3. Home widget (8.1) — enabled, max items (default 5, max cap e.g. 12), media filter (default video_only), ranking, show product title on cards.
  4. PDP widget (8.1) — enabled, max items, media filter, fallback enum.
  5. Theme — colors, light/dark toggle.
  6. Preview (v1) — device toggle + render sample cards from last N published testimonials or placeholder cards if none.
  • Either fetch recent published testimonials server-side in the loader for preview or call the same public GET with a server-side shop context (avoid exposing secrets). Showing real data in admin preview is ideal when submissions exist.

4) Data model for widget settings (storage strategy)

Section titled “4) Data model for widget settings (storage strategy)”

Choose one approach and document it in code comments.

Option A — Columns on Shop (or TestimonialShopSettings 1:1 table)
Add nullable columns / JSON blobs grouped by concern, e.g.:

  • testimonialWidgetEnabled, titles, layout enums, numeric caps, color strings, trust badge list (JSON array), home/PDP subsection fields from Screen 8.1.

Option B — Single JSON column on Shop, e.g. testimonialWidgetSettings Json, with a Zod or manual validator in the action.

Required fields to persist (must map 1:1 to blueprint Screen 8 + 8.1):

  • All bullet fields under Screen 8 Fields, Layout and card behavior, Customer-facing copy, Theme/brand styling, Preview can be stubbed.
  • Home: enabled, max items (default 5), home_media_filter, home_ranking, show product title on home cards.
  • PDP: enabled, max items, pdp_media_filter, pdp_fallback (show_global_featured | show_recent | hide_widget).
  • Legacy/global toggles from blueprint Placement options may be stored but PDP product binding is always on for the PDP block.

Migration: one Prisma migration adding columns or JSON; no destructive changes to unrelated tables.


5) Public read API — app/routes/api.testimonials.public.jsx (name flexible)

Section titled “5) Public read API — app/routes/api.testimonials.public.jsx (name flexible)”
  • GET only. No session cookie required. Do not return draft/pending/rejected rows.
ParamRequiredNotes
shopYesShop domain, e.g. store.myshopify.com, normalized to match Shop.shopDomain.
placementYeshome | pdp (optional later: collection).
product_idIf placement=pdpMust match stored TestimonialSubmission.shopifyProductId after normalization (§6.3).
limitNoCapped server-side (e.g. min 1, max 24). Default from shop settings or 5 home / 12 PDP.
mediaNovideo | all — interpret per home/PDP media filter defaults.

Invalid combinations → 400 JSON { "error": "..." }.

5.3 Response JSON (stable contract for theme JS)

Section titled “5.3 Response JSON (stable contract for theme JS)”

Return a versioned shape Cursor can rely on, e.g.:

{
"version": 1,
"placement": "home",
"items": [
{
"id": "uuid",
"mediaType": "video",
"playbackUrl": "https://...",
"thumbnailUrl": "https://...",
"durationSec": 42,
"headline": "optional",
"reviewText": "optional",
"rating": 5,
"displayName": "Sam",
"productTitle": "Optional",
"shopifyProductId": "string",
"publishedAt": "ISO-8601",
"featured": true
}
],
"fallback": null
}

For PDP with pdp_fallback active and zero product hits:

  • Either items: [] and fallback: { "mode": "featured", "items": [...] } or omit fallback key and let JS hide section — pick one behavior and document it; blueprint allows second payload for featured/recent.

Filters:

  • shopId resolved from shop param.
  • published === true (and status === approved if you use both flags — align with blueprint Screen 7).
  • TestimonialMediaAsset.processingStatus === ready (or equivalent) when media=video.
  • Home: no shopifyProductId filter; apply mediaType/media filter; sort per home_ranking; take(limit).
  • PDP: filter shopifyProductId == normalizedProductId; if empty, apply fallback query (featured store-wide or recent store-wide) or return empty per hide_widget.

Performance: index (shopId, published, shopifyProductId) already suggested in blueprint §5.7; use select minimal columns.

  • Rate limit (per shop + IP) to reduce scraping — even a simple in-memory or DB counter is acceptable for v1 if documented.
  • Never return email, full order id, or internal moderation notes.
  • CORS: allow GET from storefront; mirror existing cors.server.js patterns.
  • Prefer the same pattern the boilerplate uses for storefront → app communication (App Proxy path or public app URL). Document the final URL in shopify.app.toml if proxy is required.

6) Theme app extension (Screen 8.1 / SP-TW-04)

Section titled “6) Theme app extension (Screen 8.1 / SP-TW-04)”
  • extensions/<name>/shopify.extension.toml — type theme.
  • blocks/testimonial-widget-home.liquid
  • blocks/testimonial-widget-pdp.liquid
  • assets/testimonial-widget.js
  • locales/en.default.json — merchant-visible block names and setting labels.
  • Output a single container element with data-* attributes: data-app-url, data-shop, data-placement, data-limit, data-media, PDP: data-product-id="{{ product.id }}".
  • Home block: must not require product.
  • PDP block: must be used on product templates; validate in schema templates includes product.

Shopify Liquid product.id may be a number; Admin/API may store GID strings. Pick one canonical format in DB (recommend: store numeric string or full GID consistently). The public API must normalize product_id query param and submission field the same way (strip gid://shopify/Product/ if present).

  • On DOMContentLoaded, find containers, read data-*, fetch GET URL, render skeleton → cards.
  • Respect autoplay settings from API or embed a read-only subset of settings in a data-settings JSON blob from Liquid (only non-sensitive keys).
  • Accessibility: play/pause, muted autoplay, focusable controls.
  • Empty state: render headline + helper from API response fields or static defaults.

  • Register app.testimonial-widget.jsx in embedded app nav (future app.jsx testimonial section — not part of this file’s code work, but the plan expects a link Testimonials → Widget or similar).
  • Ensure OAuth / session is not required for public GET.

8) Testing checklist (acceptance criteria)

Section titled “8) Testing checklist (acceptance criteria)”
  • Admin: save settings, reload page, values persist.
  • GET placement=home returns only published items for that shop; respects video filter and limit; sorts featured first when configured.
  • GET placement=pdp with product_id returns only that product’s published items.
  • PDP fallback: with no product items, show_global_featured returns featured items; hide_widget returns empty and JS hides section.
  • Theme: home block renders on index; PDP block renders on product page; wrong template shows Polaris/theme editor warning if possible.
  • No PII leakage in JSON; CORS works from storefront.

9) Implementation order (for Cursor agents)

Section titled “9) Implementation order (for Cursor agents)”
  1. Prisma: add widget settings storage (§4) + migrate.
  2. Admin route: loader/action + Polaris UI (§3).
  3. Public GET route: query + CORS + validation (§5).
  4. Theme extension: Liquid + JS (§6).
  5. End-to-end test on dev store with at least one published video submission (or seed script in dev).

  • Product context: 01-Post-Purchase-Video-Testimonial-Collector-Plan.md§6 MVP → Storefront Display.
  • Full schema: 02-Implementation-Blueprint.md§5.7 / §5.8, Screen 7 (published flags), Screen 8 / 8.1, §6 theme paths.

11) Open decisions (record in PR / ticket when closing)

Section titled “11) Open decisions (record in PR / ticket when closing)”
  • Exact App Proxy path vs public URL.
  • Whether collection placement is excluded in v1.
  • Whether transcript snippet is included in API v1 or deferred until transcript pipeline exists.