Acceptance Spec — Pro Website: Hero Type (video / slideshow / video-then-slideshow)
- Status: APPROVED (founder, 2026-06-02) — build in progress
- Owner: ChurchWiseAI (CWA)
- Surface: churchwiseai.com/admin/[token] (Website → Hero → "Your own photos") + public
/s/[slug](UnifiedTemplate / VideoHero) - Feature Registry id:
cwa-pro-website-hero-media-mode - Plan gating: Pro Website — same as the rest of the Website editor
- Critical path: touches
src/app/s/[slug]/page.tsx+src/app/api/premium/update/route.ts(bothcritical_path: true) → deployed Playwright verification mandatory.
Why
A church (Kevin Soodsma, Beckwith Hills CRC) had a custom-rendered hero video AND
uploaded 8 hero photos. The old behavior silently replaced the video with the
photo slideshow the moment any custom photo existed (UnifiedTemplate.tsx:
videoSrc={useCustomSlideshow ? undefined : …}), so his MP4 stopped showing and
only one photo appeared. Founder intent: play the hero video first, then the
photos — and give the church an explicit choice.
Audit (2026-06-02): exactly 1 church had both a custom video and custom photos; 0 had photos-only. So the new auto-default changes behavior for that one church only (the fix), and is a no-op for the other ~3,164 premium rows.
Data model (additive — no data migration)
premium_churches.hero_media_mode (+ draft_hero_media_mode), nullable text,
CHECK ∈ {video, slideshow, video_then_slideshow} or NULL.
Migration: churchwiseai-web/migrations/20260602_add_hero_media_mode.sql.
Draft twin registered in pro-website-draft.ts (DRAFT_TO_CANONICAL + select list).
hero_media_mode:
'video' — loop the hero video only (no slideshow)
'slideshow' — customer photos only (Ken Burns, no video/sound)
'video_then_slideshow' — play the hero video once, then cycle the photos
NULL — auto (see resolveHeroMediaMode)
Resolution (single source of truth)
resolveHeroMediaMode({ stored, hasCustomHeroVideo, hasCustomPhotos }) in
src/lib/premium-shared.ts (pure, shared by renderer + editor):
- a stored value always wins;
- NULL →
video_then_slideshowwhen both a custom hero video AND photos exist; - NULL →
slideshowwhen only photos exist; - NULL →
videootherwise.
Public render (UnifiedTemplate → VideoHero)
- video_then_slideshow:
videoSrcset,slideshowStandalone=false,slideshowImages=customPhotos. VideoHero plays the video once (loop={!hasSlideshow}), thenonEndedhands off to the Ken Burns slideshow. - slideshow:
videoSrc=undefined,slideshowStandalone=true, first photo is the static base before the slideshow starts; while the slideshow cycles, the base layer tracks the INCOMING slide so the crossfade never ghosts a third image (stutter fix, 2026-06-10). Photos cycle (Ken Burns + crossfade; the zoom runs continuously per layer — staggered infinite-alternate — so the outgoing slide no longer snaps back to scale(1) mid-fade). - video:
videoSrcset,slideshowImages=[]→ the video loops, no slideshow. - No custom photos (unchanged): legacy behavior preserved — the video plays,
then any curated library-poster slideshow (
hero_slideshow_keys) cycles after. - Reduced motion: per existing VideoHero behavior, autoplay + slideshow are suppressed and a single static hero image shows (accessibility — by design).
Admin editor (Website → Hero → "Your own photos")
- When ≥1 photo is uploaded, a "How should the hero play?" radio group shows the 3 modes with helper text. The active selection reflects the resolved effective mode (so it matches reality even before the church picks).
- While the church hasn't explicitly chosen, the effective mode tracks the current photo count (first upload defaults to "video, then photos" when a video exists, else "photos only"). An explicit pick is submitted as a concrete value.
- Draft-aware (
DraftAwareSaveForm→draft_hero_media_mode); Publish copies it to canonical via DRAFT_TO_CANONICAL. Test ids:ps-admin-hero-mode-{mode}. - Copy corrected: the old "they replace the hero video" line is removed.
Server validation
api/premium/update, website section: hero_media_mode coerced to the
allowlist (else NULL); routed to draft_hero_media_mode when
target=website_draft via the existing CANONICAL_TO_DRAFT remap.
Graceful degradation / regression guards
- ~3,164 churches with no custom photos: identical hero behavior to before.
- A church that never opens the control keeps
hero_media_mode = NULLand the renderer resolves it live — Beckwith self-heals tovideo_then_slideshowwith zero data writes. - Library-poster slideshow (
hero_slideshow_keys) behavior is untouched.
Verification (evidence-or-nothing)
pnpm build+ scopedpnpm lintgreen.- Playwright on the deployed preview against the Beckwith demo (or a demo church
with a hero video + ≥2 photos): set each of the 3 modes, Publish, load
/s/[slug], and confirm — (a)video_then_slideshowshows the video then cycles photos; (b)slideshowcycles photos with no video; (c)videoloops the video with no slideshow. Confirm a church with no custom photos is unchanged.