Video Testimonial Collector - Implementation Blueprint
Screen-by-screen fields, workflows, and database design for building a photo + video testimonial app using this boilerplate.
Video Testimonial Collector - Implementation Blueprint
Section titled “Video Testimonial Collector - Implementation Blueprint”This is a build-focused blueprint for implementing the Post-Purchase Photo + Video Testimonial Collector using this repository as boilerplate.
It is designed to match patterns already used in this codebase:
- Shop-scoped multi-tenant data model (
ShopFK on tenant tables) - Prisma + Postgres with
@map("snake_case")columns - React Router loaders/actions for admin screens
- Theme extension for storefront rendering
- Webhook-driven automation for post-purchase workflows
1) Product Scope Recommendation
Section titled “1) Product Scope Recommendation”Use a video-first, photo-supported strategy.
- Primary CTA to customer: “Record a quick video testimonial”
- Secondary CTA: “Upload a photo + short text”
- Same moderation and publication workflow for both media types
This balances differentiation (video) and submission volume (photos).
2) App Architecture in This Boilerplate
Section titled “2) App Architecture in This Boilerplate”2.1 Existing project conventions to follow
Section titled “2.1 Existing project conventions to follow”- Keep merchant-level config in
Shopwhere possible. - New app entities should include:
id String @id @default(uuid())shopId String @map("shop_id")createdAt DateTime @default(now()) @map("created_at")updatedAt DateTime @updatedAt @map("updated_at")- relation
shop Shop @relation(... onDelete: Cascade)
- Add route files as
app/routes/app.<screen>.jsx. - Use
authenticate.admin(request)in loaders/actions. - Use webhook routes for automation (
app/routes/webhooks.*.jsx).
2.2 Core modules
Section titled “2.2 Core modules”- Campaign engine (who gets request + when)
- Request delivery (email/SMS)
- Public submission page (secure token URL)
- Media ingestion/processing
- Moderation queue
- Storefront widget publication
- Analytics/billing
Per-product collection rule (required):
- Create and send one testimonial request per delivered product line item.
- Example: one order with 3 delivered products -> 3 request records and 3 testimonial request emails/SMS messages.
- Each request token must map to exactly one Shopify product ID.
- Each approved testimonial must remain linked to that same product and render automatically on that product detail page (PDP).
3) Admin Screens and Fields (Detailed)
Section titled “3) Admin Screens and Fields (Detailed)”Recommended v1: 13 screens (12 admin + 1 public submission page).
Screen 1 - Dashboard (/app)
Section titled “Screen 1 - Dashboard (/app)”Purpose: At-a-glance performance.
Fields/components:
- Date range filter (
last 7/30/90 days) - KPI cards:
- Requests sent
- Request click rate
- Submission rate
- Approval rate
- Published testimonials
- Top products by testimonial count
- Pending moderation count CTA
- Recent submissions list (thumbnail, type, status)
Screen 2 - Campaigns List (/app/testimonial-campaigns)
Section titled “Screen 2 - Campaigns List (/app/testimonial-campaigns)”Purpose: Manage automation campaigns.
Fields/components:
- Campaign table:
- Name
- Trigger event (
order_paid,order_fulfilled) - Delay days
- Audience scope (all products / collection / product list)
- Channel (
email,sms,both) - Status (
active,paused) - Last sent at
- Actions: create, pause, duplicate, archive
Screen 3 - Campaign Create/Edit (/app/testimonial-campaigns/:id)
Section titled “Screen 3 - Campaign Create/Edit (/app/testimonial-campaigns/:id)”Purpose: Configure one post-purchase campaign.
Fields:
- Campaign name (text)
- Active toggle (boolean)
- Trigger event (select)
- Delay days (integer, min 0, max 60)
- Audience:
- All products toggle
- Collection selector (multi)
- Product selector (multi)
- Channel (select: email/sms/both)
- Reminder rules:
- Enable reminder (boolean)
- Reminder delay days (int)
- Max reminders (int)
- Incentive:
- Enabled (boolean)
- Incentive type (
discount_code,gift_card,none) - Incentive value/code
Screen 4 - Templates (/app/testimonial-templates)
Section titled “Screen 4 - Templates (/app/testimonial-templates)”Purpose: Edit request messages.
Fields:
- Template type (
initial_request,reminder_1,reminder_2) - Subject (email only)
- Body (rich/plain text)
- Variables help panel:
{{customer_first_name}}{{shop_name}}{{product_name}}{{submission_link}}{{incentive_text}}
- Preview pane
- Send test button
Screen 5 - Submissions Inbox (/app/testimonials)
Section titled “Screen 5 - Submissions Inbox (/app/testimonials)”Purpose: Review incoming content quickly.
Fields/components:
- Tabs:
pending,approved,rejected,archived - Filters:
- Media type (
video,photo,both) - Product
- Rating
- Date range
- Media type (
- Row cards:
- Thumbnail
- Customer display name
- Product title
- Media type
- Submitted at
- Current status
- Bulk actions: approve/reject/archive
Screen 6 - Submission Detail (/app/testimonials/:id)
Section titled “Screen 6 - Submission Detail (/app/testimonials/:id)”Purpose: Deep moderation + edit before publish.
Fields:
- Media preview player/image
- Submission metadata:
- Duration (video)
- Resolution
- File size
- Source (
upload,record)
- Customer info:
- Name
- Email (masked in UI if desired)
- Order number
- Testimonial content:
- Star rating
- Headline
- Quote/review text
- Consent:
- Accepted checkbox state
- Accepted timestamp
- Consent version
- Moderation actions:
- Approve
- Reject with reason
- Flag
- Publish toggle
- Merchandising controls:
- Assign products (multi)
- Featured toggle
- Sort priority
Screen 7 - Published Content (/app/testimonials/published)
Section titled “Screen 7 - Published Content (/app/testimonials/published)”Purpose: Manage what is live.
Fields:
- List/grid toggle
- Product assignment column
- Visibility by placement:
- PDP
- Home carousel
- Collection page
- Feature/pin
- Display order drag handle
- Unpublish action
Screen 8 - Widget Settings (/app/testimonials/widget)
Section titled “Screen 8 - Widget Settings (/app/testimonials/widget)”Purpose: Storefront look and behavior.
Fields:
- Enable widget (boolean)
- Widget title text (string)
- Default:
Loved by customers
- Default:
- Widget subtitle/trust copy (string)
- Default:
Real reviews from verified buyers
- Default:
- Show store logo (boolean)
- Store logo source (enum:
shop_logo,custom_upload) - Custom logo image upload (file, png/svg/webp)
- Logo position (enum:
left_of_title,above_title,hidden_on_mobile) - Trust badge row toggle (boolean)
- Trust badges (multi-select):
verified_purchasesecure_checkoutmoney_back_guaranteefast_shipping
- Header alignment (enum:
left,center) - Header-to-grid spacing (int px)
Layout and card behavior:
- Layout style (
carousel,grid,masonry) - Card style (
minimal,detailed) - Desktop columns (int: 2-5)
- Mobile cards per view (int: 1-2)
- Card corner radius (int px)
- Card shadow style (enum:
none,soft,medium) - Show rating (boolean)
- Show customer first name (boolean)
- Show customer location (boolean)
- Show verified purchase badge on card (boolean)
- Show product name on card (boolean)
- Show submission date (boolean)
- Show captions/transcript snippet (boolean)
- Transcript snippet max length (int chars)
- Autoplay video previews (boolean)
- Mute autoplay videos by default (boolean)
- Loop preview video (boolean)
- Max testimonials per section (int)
Customer-facing copy controls:
- Primary CTA label (string)
- Default:
Watch customer stories
- Default:
- Secondary CTA label (string)
- Default:
Read more reviews
- Default:
- Empty-state headline (string)
- Example:
Be the first to share your experience
- Example:
- Empty-state helper text (string)
- Example:
Customer stories will appear here after the first submission is approved.
- Example:
Theme/brand styling:
- Primary color
- Background color
- Text color
- Accent color (play icon, chips, CTA links)
- Star color
- Trust badge background color
- Trust badge text color
- Light/dark adaptation toggle (boolean)
Placement options (legacy / global toggles — see Screen 8.1 for the required v1 split):
- Product page position (enum:
below_price,below_description,below_recommendations,manual_block) - Home page section toggle (boolean) — superseded by Home widget block settings in 8.1; keep for backward compatibility if a single “master” toggle is desired
- Collection page section toggle (boolean)
- Product-page-only filter (show only testimonials linked to current product) — always on for the PDP widget defined in 8.1
- Fallback behavior if product has no testimonials (PDP widget only):
show_global_featuredshow_recenthide_widget
Home widget (store index) — admin defaults (pair with Theme block “Home”):
- Home widget enabled (boolean)
- Home max items (int, default
5) - Home media filter (enum:
video_only,video_and_photo) — defaultvideo_onlyfor the “top video reviews” strip - Home ranking (enum:
featured_then_newest,newest,manual_sort_only) — defaultfeatured_then_newest - Show product title on home cards (boolean, default
true) so shoppers know which product each clip refers to
PDP widget (product template) — admin defaults (pair with Theme block “PDP”):
- PDP widget enabled (boolean)
- PDP max items (int, nullable = no cap beyond performance limit, or default e.g.
12) - PDP media filter (enum:
video_only,video_and_photo,all_published) — default can match merchant preference; product binding is always enforced - PDP fallback when this product has zero published testimonials: same enum as above (
show_global_featured|show_recent|hide_widget)
Preview and QA controls:
- Live preview panel with device toggle (
desktop,mobile) - Preview sample density (
3,6,12cards) - Preview language selector (for localized storefronts)
- Reset to defaults button
- Publish changes button
Screen 8.1 — Two storefront widgets (v1 requirement)
Section titled “Screen 8.1 — Two storefront widgets (v1 requirement)”Merchants must be able to show two different widgets on the storefront. Treat this subsection as the canonical spec when you split work into separate .md sub-plans (suggested sub-plan ids below).
| Sub-plan id | Scope | One-line outcome |
|---|---|---|
SP-TW-01 | Home widget | Renders up to 5 published video testimonials store-wide (not tied to one product). |
SP-TW-02 | PDP widget | Renders only testimonials linked to the current product (shopifyProductId matches Liquid product.id). |
SP-TW-03 | Widget API + app proxy | One or two HTTP endpoints; query rules for placement=home vs placement=pdp + optional product_id. |
SP-TW-04 | Theme app extension | Liquid block(s) + shared JS; passes shop, placement, product id (PDP only), limits from block settings or app defaults. |
Widget A — Home page: “Top video reviews”
Section titled “Widget A — Home page: “Top video reviews””- Where it lives: Home page (index) template via Theme app extension block (merchant adds block where they want the strip).
- What it shows: Published testimonials with
mediaType = video, playback ready (e.g. assetprocessingStatus = readywhen applicable), not filtered by current product. - How many: 5 items maximum by default (
Home max items); merchant may raise/lower in Widget Settings within a sane cap (e.g. max 12) for future flexibility. - Sort order: Featured first, then by
publishedAt/sortOrderper admin Home ranking setting. - Empty state: Use Widget Settings empty-state copy; no fallback to “random product” reviews unless explicitly added later.
Widget B — Product detail page (PDP): “Reviews for this product only”
Section titled “Widget B — Product detail page (PDP): “Reviews for this product only””- Where it lives: Product template via Theme app extension block.
- What it shows: Only rows where
TestimonialSubmission.shopifyProductIdequals the current product’s Shopify GID or numeric id (match whatever the storefront passes consistently — document inSP-TW-04). - Media: Default can be video-first or mixed per PDP media filter; binding to product is non-negotiable.
- Fallback (no testimonials for this product): Use PDP fallback enum —
show_global_featured,show_recent, orhide_widget— so merchants control whether the section disappears or shows broader social proof.
Theme extension: one block vs two blocks (implementation choice)
Section titled “Theme extension: one block vs two blocks (implementation choice)”Document this decision in SP-TW-04 when you implement.
- Option A (recommended for sub-plan clarity): Two app blocks in the theme extension:
blocks/testimonial-widget-home.liquid— only home; embeds attributes: shop domain,placement=home,limit(default 5),media=video.blocks/testimonial-widget-pdp.liquid— only PDP; embedsplacement=pdp,product_id={{ product.id }}, optional limit, fallback behavior key if overridden per block.- Shared asset:
assets/testimonial-widget.js(fetch, render, carousel/grid behavior).
- Option B: Single block
blocks/testimonial-widget.liquidwith a schema settingwidget_mode:home_top_videos|product_reviews. Same JS; mode selects API query. Slightly fewer files; slightly more merchant confusion if labels are unclear.
Storefront API behavior (for SP-TW-03)
Section titled “Storefront API behavior (for SP-TW-03)”Define a single public read endpoint (App Proxy or approved pattern used elsewhere in this app), for example:
GET .../testimonials?shop={shop}&placement=home&limit=5&media=videoGET .../testimonials?shop={shop}&placement=pdp&product_id={id}&limit={n}&media={...}
Rules:
- Only return rows that are published (and any other merchant visibility flags from Published Content screen).
- Home placement: ignore
product_id; apply global ranking and video filter per query params / shop defaults. - PDP placement: require
product_id; filter strictly to that product unless fallback mode instructs otherwise (fallback may return a second “featured” payload — specify in implementation sub-plan).
Merchant UX summary
Section titled “Merchant UX summary”- Add both blocks in Theme Editor: one on Home, one on Product.
- Widget Settings screen supplies defaults; individual blocks may allow overrides (limit, title override) via Liquid schema if desired in a later sub-plan.
Screen 9 - Moderation Settings (/app/testimonials/moderation)
Section titled “Screen 9 - Moderation Settings (/app/testimonials/moderation)”Purpose: Reduce manual effort and keep brand safety.
Fields:
- Auto-approve photos only (boolean)
- Require minimum rating (int)
- Blocklist words (textarea list)
- Minimum video length (seconds)
- Maximum video length (seconds)
- Reject blurry videos/photos (future AI toggle placeholder)
- Team notification emails
Screen 10 - Requests Log (/app/testimonials/requests)
Section titled “Screen 10 - Requests Log (/app/testimonials/requests)”Purpose: Delivery observability and troubleshooting.
Fields:
- Request ID
- Shopify product ID
- Product title
- Customer
- Order
- Campaign
- Channel
- Status (
queued,sent,delivered,opened,clicked,failed) - Failure reason
- Sent timestamp
- Last event timestamp
Screen 11 - Analytics (/app/testimonials/analytics)
Section titled “Screen 11 - Analytics (/app/testimonials/analytics)”Purpose: Measure ROI.
Fields/charts:
- Funnel chart:
- Sent -> Opened -> Clicked -> Submitted -> Approved -> Published
- Product-level testimonial coverage
- Conversion comparison:
- Products with testimonials vs without
- Media type performance split:
- Video submit rate
- Photo submit rate
- Optional UTM/source analysis
Screen 12 - Billing & Usage (/app/testimonials/billing)
Section titled “Screen 12 - Billing & Usage (/app/testimonials/billing)”Purpose: Plan limits and overages.
Fields:
- Current plan
- Period usage:
- Requests used
- Storage used (GB)
- Processed video minutes
- Overage settings/toggles
- Upgrade CTA
Screen 13 - Public Submission Page (/t/:token)
Section titled “Screen 13 - Public Submission Page (/t/:token)”Purpose: Customer uploads testimonial.
Fields:
- Header text and trust copy
- Product context header (locked from token):
- Product title
- Product image thumbnail
- Optional variant title
- Media mode choice:
- Record video
- Upload video
- Upload photo
- Text fields:
- Headline (optional)
- Review text (optional)
- Star rating selector (optional/required based on settings)
- Consent checkbox (required)
- Submit button
- Success state with optional reward message
- Product binding note:
- Submission is automatically linked to the token’s product.
- Customer cannot switch product on this page.
4) Email/SMS Workflow (Operational Detail)
Section titled “4) Email/SMS Workflow (Operational Detail)”- Order event webhook received.
- Eligibility evaluated against campaign rules.
- Split eligible delivered order items into product-level request jobs.
- Create one
testimonial_requestrow per delivered product with its own token and scheduled send time. - Worker/cron sends one message per request row via provider.
- Track delivery/open/click events into request-event table.
- If no submission by reminder window, send reminder message.
- Stop reminders when submission is completed.
5) Database Design (Prisma Style)
Section titled “5) Database Design (Prisma Style)”The following tables are designed to match this repo conventions.
5.1 Extend Shop model
Section titled “5.1 Extend Shop model”Add testimonial config fields in Shop:
testimonialEnabled Boolean @default(true) @map("testimonial_enabled")testimonialWidgetLayout String? @default("carousel") @map("testimonial_widget_layout")testimonialPrimaryColor String? @map("testimonial_primary_color")testimonialAutoApprovePhotos Boolean @default(false) @map("testimonial_auto_approve_photos")testimonialConsentVersion String? @map("testimonial_consent_version")
5.2 New model: TestimonialCampaign
Section titled “5.2 New model: TestimonialCampaign”id String @id @default(uuid())shopId String @map("shop_id")name Stringstatus String @default("active")// active | paused | archivedtriggerEvent String @map("trigger_event")// order_paid | order_fulfilleddelayDays Int @default(7) @map("delay_days")channel String @default("email")// email | sms | bothallProducts Boolean @default(true) @map("all_products")reminderEnabled Boolean @default(true) @map("reminder_enabled")reminderDelayDays Int @default(3) @map("reminder_delay_days")maxReminders Int @default(1) @map("max_reminders")incentiveEnabled Boolean @default(false) @map("incentive_enabled")incentiveType String? @map("incentive_type")// discount_code | gift_cardincentiveValue String? @map("incentive_value")createdAt DateTime @default(now()) @map("created_at")updatedAt DateTime @updatedAt @map("updated_at")
Indexes:
@@index([shopId, status])
5.3 New model: TestimonialCampaignProduct
Section titled “5.3 New model: TestimonialCampaignProduct”Mapping table for product-targeted campaigns.
id String @id @default(uuid())campaignId String @map("campaign_id")shopId String @map("shop_id")shopifyProductId String @map("shopify_product_id")createdAt DateTime @default(now()) @map("created_at")
Unique:
@@unique([campaignId, shopifyProductId])
5.4 New model: TestimonialTemplate
Section titled “5.4 New model: TestimonialTemplate”id String @id @default(uuid())shopId String @map("shop_id")campaignId String? @map("campaign_id")channel String// email | smstemplateType String @map("template_type")// initial_request | reminder_1 | reminder_2subject String?body String @db.TextisDefault Boolean @default(false) @map("is_default")createdAt DateTime @default(now()) @map("created_at")updatedAt DateTime @updatedAt @map("updated_at")
Unique:
@@unique([shopId, campaignId, channel, templateType])
5.5 New model: TestimonialRequest
Section titled “5.5 New model: TestimonialRequest”One row per outreach attempt for a single product (customer/order/product/campaign).
id String @id @default(uuid())shopId String @map("shop_id")campaignId String @map("campaign_id")shopifyOrderId String @map("shopify_order_id")shopifyProductId String @map("shopify_product_id")shopifyVariantId String? @map("shopify_variant_id")shopifyCustomerId String? @map("shopify_customer_id")customerEmail String? @map("customer_email")customerPhone String? @map("customer_phone")submissionToken String @unique @map("submission_token")submissionUrl String @map("submission_url")status String @default("scheduled")// scheduled | sent | clicked | submitted | expired | failedscheduledFor DateTime @map("scheduled_for")sentAt DateTime? @map("sent_at")clickedAt DateTime? @map("clicked_at")submittedAt DateTime? @map("submitted_at")reminderCount Int @default(0) @map("reminder_count")lastError String? @map("last_error") @db.TextcreatedAt DateTime @default(now()) @map("created_at")updatedAt DateTime @updatedAt @map("updated_at")
Indexes:
@@index([shopId, status])@@index([shopId, scheduledFor])@@index([campaignId])@@index([shopId, shopifyProductId])
5.6 New model: TestimonialRequestEvent
Section titled “5.6 New model: TestimonialRequestEvent”Event stream for delivery analytics.
id String @id @default(uuid())shopId String @map("shop_id")requestId String @map("request_id")eventType String @map("event_type")// queued | sent | delivered | opened | clicked | failedeventAt DateTime @default(now()) @map("event_at")providerMessageId String? @map("provider_message_id")payload Json?
Indexes:
@@index([requestId, eventAt])
5.7 New model: TestimonialSubmission
Section titled “5.7 New model: TestimonialSubmission”Main UGC submission entity.
id String @id @default(uuid())shopId String @map("shop_id")requestId String? @map("request_id")shopifyOrderId String? @map("shopify_order_id")shopifyProductId String @map("shopify_product_id")shopifyVariantId String? @map("shopify_variant_id")shopifyCustomerId String? @map("shopify_customer_id")displayName String? @map("display_name")displayEmail String? @map("display_email")mediaType String @map("media_type")// video | photoheadline String?reviewText String? @map("review_text") @db.Textrating Int?status String @default("pending")// pending | approved | rejected | archivedconsentAccepted Boolean @default(false) @map("consent_accepted")consentAcceptedAt DateTime? @map("consent_accepted_at")consentVersion String? @map("consent_version")published Boolean @default(false)publishedAt DateTime? @map("published_at")featured Boolean @default(false)sortOrder Int? @map("sort_order")submittedAt DateTime @default(now()) @map("submitted_at")createdAt DateTime @default(now()) @map("created_at")updatedAt DateTime @updatedAt @map("updated_at")
Indexes:
@@index([shopId, status])@@index([shopId, published])@@index([requestId])@@index([shopId, shopifyProductId, published])
5.8 New model: TestimonialMediaAsset
Section titled “5.8 New model: TestimonialMediaAsset”Media processing and hosting details.
id String @id @default(uuid())shopId String @map("shop_id")submissionId String @map("submission_id")storageProvider String @map("storage_provider")// stream | mux | s3storageKey String @map("storage_key")playbackUrl String? @map("playback_url") @db.TextthumbnailUrl String? @map("thumbnail_url") @db.TextmimeType String? @map("mime_type")durationSec Int? @map("duration_sec")width Int?height Int?fileSizeBytes BigInt? @map("file_size_bytes")processingStatus String @default("uploaded") @map("processing_status")// uploaded | processing | ready | failedprocessingError String? @map("processing_error") @db.TexttranscriptText String? @map("transcript_text") @db.TextcreatedAt DateTime @default(now()) @map("created_at")updatedAt DateTime @updatedAt @map("updated_at")
Indexes:
@@index([submissionId])@@index([shopId, processingStatus])
5.9 New model: TestimonialProductLink
Section titled “5.9 New model: TestimonialProductLink”Optional extension for secondary merchandising links.
Primary product linkage should come from TestimonialSubmission.shopifyProductId.
id String @id @default(uuid())shopId String @map("shop_id")submissionId String @map("submission_id")shopifyProductId String @map("shopify_product_id")createdAt DateTime @default(now()) @map("created_at")
Unique:
@@unique([submissionId, shopifyProductId])
5.10 New model: TestimonialModerationLog
Section titled “5.10 New model: TestimonialModerationLog”Audit trail for moderation decisions.
id String @id @default(uuid())shopId String @map("shop_id")submissionId String @map("submission_id")action String// approve | reject | archive | unpublish | featurereason String? @db.TextactorType String @map("actor_type")// merchant | systemactorEmail String? @map("actor_email")createdAt DateTime @default(now()) @map("created_at")
Indexes:
@@index([submissionId, createdAt])
5.11 New model: TestimonialDailyAnalytics
Section titled “5.11 New model: TestimonialDailyAnalytics”Pre-aggregated daily metrics for dashboard speed.
id String @id @default(uuid())shopId String @map("shop_id")date DateTime @map("date")// UTC day bucketrequestsSent Int @default(0) @map("requests_sent")requestsClicked Int @default(0) @map("requests_clicked")submissionsCount Int @default(0) @map("submissions_count")approvedCount Int @default(0) @map("approved_count")publishedCount Int @default(0) @map("published_count")videoSubmissionsCount Int @default(0) @map("video_submissions_count")photoSubmissionsCount Int @default(0) @map("photo_submissions_count")createdAt DateTime @default(now()) @map("created_at")updatedAt DateTime @updatedAt @map("updated_at")
Unique:
@@unique([shopId, date])
6) Suggested Route/File Map (Boilerplate-Aligned)
Section titled “6) Suggested Route/File Map (Boilerplate-Aligned)”Admin routes:
app/routes/app.testimonial-dashboard.jsxapp/routes/app.testimonial-campaigns.jsxapp/routes/app.testimonial-campaign.$id.jsxapp/routes/app.testimonial-templates.jsxapp/routes/app.testimonial-submissions.jsxapp/routes/app.testimonial-submission.$id.jsxapp/routes/app.testimonial-published.jsxapp/routes/app.testimonial-widget.jsxapp/routes/app.testimonial-moderation.jsxapp/routes/app.testimonial-requests.jsxapp/routes/app.testimonial-analytics.jsxapp/routes/app.testimonial-billing.jsx
Public/customer routes:
app/routes/t.$token.jsx(public submission page)app/routes/api.testimonial-upload-url.jsxapp/routes/api.testimonial-submit.jsxapp/routes/api.testimonial-media-callback.jsx(optional if provider webhook)
Webhook routes:
app/routes/webhooks.orders.paid.jsxapp/routes/webhooks.orders.fulfilled.jsx
Theme extension (align with Screen 8.1):
- Recommended (two blocks):
extensions/<testimonial-extension>/blocks/testimonial-widget-home.liquid— home: top video reviews (default limit 5)extensions/<testimonial-extension>/blocks/testimonial-widget-pdp.liquid— PDP: this product only + fallback behavior
- Alternative (single block):
extensions/<testimonial-extension>/blocks/testimonial-widget.liquid— schemawidget_mode:home_top_videos|product_reviews
- Shared:
extensions/<testimonial-extension>/assets/testimonial-widget.js
7) Integration and Processing Plan
Section titled “7) Integration and Processing Plan”- On order webhook -> evaluate active campaigns.
- Expand delivered line items into product-level requests (one request per product).
- Create and schedule request rows with a unique per-product token.
- Scheduler job sends messages from due requests.
- Public page validates token and captures submission.
- Upload media via signed URL.
- Worker updates processing status and derived metadata.
- Submission appears in moderation queue.
- On approve, testimonial is automatically bound to its product PDP via stored
shopifyProductId. - Approved submissions are exposed by a storefront read API consumed by the theme extension:
- Home widget requests
placement=home,limit=5(default),media=video(default). - PDP widget requests
placement=pdpwithproduct_idmatching the current product; applies PDP fallback rules when the result set is empty.
- Home widget requests
8) Security and Compliance Requirements
Section titled “8) Security and Compliance Requirements”- Tokenized submission URLs with expiry.
- Rate limit public submission endpoints.
- Store consent acceptance and consent version per submission.
- Keep moderation log immutable for audit.
- Handle GDPR webhooks similarly to existing compliance route:
- On customer redact, remove testimonial rows by customer email/id.
- On shop redact, delete shop-scoped testimonial data via cascade.
9) Release Plan (Milestones)
Section titled “9) Release Plan (Milestones)”Milestone 1 (2 weeks)
Section titled “Milestone 1 (2 weeks)”- DB schema + migrations
- Campaign CRUD
- Request creation from order webhook
Milestone 2 (2 weeks)
Section titled “Milestone 2 (2 weeks)”- Public token page
- Media upload + storage
- Moderation inbox and detail page
Milestone 3 (2 weeks)
Section titled “Milestone 3 (2 weeks)”- Theme widget rendering (two placements: home top-5 video strip + PDP product-scoped widget per Screen 8.1)
- Widget storefront API (
SP-TW-03) + theme blocks (SP-TW-04) - Analytics + billing limits
- App Store readiness and QA
10) Build Notes for This Boilerplate
Section titled “10) Build Notes for This Boilerplate”- Reuse current nav pattern in
app/routes/app.jsxand add testimonial sections. - Reuse page composition style from
app.settings.jsxandapp.appearance.jsx. - Keep actions idempotent where webhook retries can happen.
- Use UTC date buckets for analytics (same convention already present in dashboard logic).
- Keep optional fields nullable in Prisma to avoid migration friction during phased rollout.