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.md — Screen 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.
0) Goal (one sentence)
Section titled “0) Goal (one sentence)”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.
1) Prerequisites and dependencies
Section titled “1) Prerequisites and dependencies”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.
| Dependency | Why |
|---|---|
TestimonialSubmission, TestimonialMediaAsset (and linkage to shopifyProductId) per blueprint §5.7 / §5.8 | Read API must query published rows with playback URLs / thumbnails. |
Shop row per install | Tenant scoping; widget settings storage (see §4). |
Optional: Product mirror | Not 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.
2) Scope
Section titled “2) Scope”In scope
Section titled “In scope”- Persist Screen 8 and Screen 8.1 settings (fields listed in §4).
- Public
GETendpoint(s) for published testimonials only, withplacement=home|pdp, limits, media filter, and PDPproduct_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-settingsstyle headers). - PDP fallback when zero product-scoped results:
show_global_featured|show_recent|hide_widgetper merchant setting. - Home behavior: default 5 items, video_only, ranking
featured_then_newestunless settings say otherwise.
Out of scope (defer to other plans)
Section titled “Out of scope (defer to other plans)”- 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=collectionunless 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)”3.1 Loader
Section titled “3.1 Loader”authenticate.admin(request).- Load current shop’s testimonial widget settings (from DB — see §4).
- Pass defaults where fields are null.
3.2 Action(s)
Section titled “3.2 Action(s)”- Validate and save widget settings (single form or grouped saves).
- Use Shopify admin toast / banner patterns consistent with
app.settings.jsxorapp.appearance.jsxin this repo. - Idempotent saves; no duplicate rows for singleton shop settings.
3.3 Polaris layout (suggested sections)
Section titled “3.3 Polaris layout (suggested sections)”- Global — master enable, title, subtitle, logo, trust badges, header alignment/spacing.
- Layout & cards — carousel/grid/masonry, columns, card style, radii, shadow, toggles (rating, name, product name, date, transcript snippet + max length, autoplay/mute/loop).
- 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.
- PDP widget (8.1) — enabled, max items, media filter, fallback enum.
- Theme — colors, light/dark toggle.
- Preview (v1) — device toggle + render sample cards from last N published testimonials or placeholder cards if none.
3.4 Preview panel (v1 minimum)
Section titled “3.4 Preview panel (v1 minimum)”- Either fetch recent published testimonials server-side in the loader for preview or call the same public GET with a server-side
shopcontext (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)”5.1 Method and auth
Section titled “5.1 Method and auth”- GET only. No session cookie required. Do not return draft/pending/rejected rows.
5.2 Query parameters
Section titled “5.2 Query parameters”| Param | Required | Notes |
|---|---|---|
shop | Yes | Shop domain, e.g. store.myshopify.com, normalized to match Shop.shopDomain. |
placement | Yes | home | pdp (optional later: collection). |
product_id | If placement=pdp | Must match stored TestimonialSubmission.shopifyProductId after normalization (§6.3). |
limit | No | Capped server-side (e.g. min 1, max 24). Default from shop settings or 5 home / 12 PDP. |
media | No | video | 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: []andfallback: { "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.
5.4 Query logic (server)
Section titled “5.4 Query logic (server)”Filters:
shopIdresolved fromshopparam.published === true(andstatus === approvedif you use both flags — align with blueprint Screen 7).TestimonialMediaAsset.processingStatus === ready(or equivalent) whenmedia=video.- Home: no
shopifyProductIdfilter; applymediaType/media filter; sort perhome_ranking;take(limit). - PDP: filter
shopifyProductId == normalizedProductId; if empty, apply fallback query (featured store-wide or recent store-wide) or return empty perhide_widget.
Performance: index (shopId, published, shopifyProductId) already suggested in blueprint §5.7; use select minimal columns.
5.5 Security
Section titled “5.5 Security”- 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.jspatterns.
5.6 App Proxy vs direct app URL
Section titled “5.6 App Proxy vs direct app URL”- 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.tomlif 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)”6.1 Files (Option A)
Section titled “6.1 Files (Option A)”extensions/<name>/shopify.extension.toml— typetheme.blocks/testimonial-widget-home.liquidblocks/testimonial-widget-pdp.liquidassets/testimonial-widget.jslocales/en.default.json— merchant-visible block names and setting labels.
6.2 Liquid responsibilities
Section titled “6.2 Liquid responsibilities”- 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
templatesincludes product.
6.3 product_id normalization (critical)
Section titled “6.3 product_id normalization (critical)”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).
6.4 JS responsibilities
Section titled “6.4 JS responsibilities”- On
DOMContentLoaded, find containers, readdata-*, fetch GET URL, render skeleton → cards. - Respect autoplay settings from API or embed a read-only subset of settings in a
data-settingsJSON 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.
7) Navigation and wiring
Section titled “7) Navigation and wiring”- Register
app.testimonial-widget.jsxin embedded app nav (futureapp.jsxtestimonial 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=homereturns only published items for that shop; respects video filter and limit; sorts featured first when configured. - GET
placement=pdpwithproduct_idreturns only that product’s published items. - PDP fallback: with no product items,
show_global_featuredreturns featured items;hide_widgetreturns 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)”- Prisma: add widget settings storage (§4) + migrate.
- Admin route: loader/action + Polaris UI (§3).
- Public GET route: query + CORS + validation (§5).
- Theme extension: Liquid + JS (§6).
- End-to-end test on dev store with at least one published video submission (or seed script in dev).
10) References
Section titled “10) References”- 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.