Unified Cherry City Landing Page — Implementation Plan
Status: done
Implements spec.md. Closes the divergence noted in the spec's Tracking + Additional sections, and folds in the ScrollDepth75 event flagged as a Known Gap in cherry-city-ppc-prospecting/spec.md.
Goal
Ship the unified /lp/cherry-city LP with new LandingFunnelChoice and ScrollDepth75 Pixel + CAPI events, and redirect /cherry-city/{tour,hourly} to it.
Status
- [x] 1. Funnel-choice CAPI helper + schema + test
- [x] 2. Funnel-choice API route
- [x] 3. Scroll-depth CAPI helper + schema + test
- [x] 4. Scroll-depth API route
- [x] 5. Pixel:
trackLandingFunnelChoice - [x] 6. Pixel:
trackScrollDepth75+useScrollDepth75hook - [x] 7. LP route scaffold (
/lp/cherry-city) - [x] 8. Hero + Fork + Shared proof bar
- [x] 9. Monthly full section
- [x] 10. Hourly full section
- [x] 11. Shared testimonials
- [x] 12. Shared FAQ
- [x] 13. Closing CTA + redirects
- [x] 14. Spec follow-up
- [x] 15. Resolve closing CTA anchor divergence from spec
Driving citations
- Spec Tracking spec — the new
LandingFunnelChoiceevent is the optimization target that makes the unified Meta campaign possible. - Spec URL and routing —
/lp/cherry-citybecomes the cold-traffic conversion target;/cherry-city/{tour,hourly}301 to it. - Spec Page structure — pain-led hero → fork → shared proof → full-width product sections → shared testimonials/FAQ → closing CTA.
- Companion PPC spec Engineering dependencies —
ScrollDepth75event ask, in addition toLandingFunnelChoice.
Work breakdown
1. Funnel-choice CAPI helper + schema + test
- Files touched:
packages/shared-schemas/src/api/meta.ts(new) —funnelChoiceSchemawithchoice: 'monthly' | 'hourly',eventId,locationId,attribution?,eventSourceUrl?.packages/shared-schemas/src/api/index.ts— re-export.apps/api/src/lib/meta/track-funnel-choice.ts(new) —trackFunnelChoiceEventToMetamirroringtrack-lead.ts.event_name = 'LandingFunnelChoice',custom_data.content_name = choice,content_category = 'lp-fork'.apps/api/src/lib/meta/track-funnel-choice.test.ts(new) — exercise the buildUserData + sendCapiEvents path.- Spec coverage: Tracking spec — CAPI side
2. Funnel-choice API route
- Files touched:
apps/api/src/app/api/meta/funnel-choice/route.ts(new) — POST,withErrorHandling,validateRequestBody(req, funnelChoiceSchema). PullsgetClientIpAddress/getClientUserAgent. FirestrackFunnelChoiceEventToMetaviaafter(). ReturnscreateSuccessResponse({}).- Spec coverage: Tracking spec — CAPI side
3. Scroll-depth CAPI helper + schema + test
- Files touched:
packages/shared-schemas/src/api/meta.ts— addscrollDepthSchemawitheventId,url,attribution?.apps/api/src/lib/meta/track-scroll-depth.ts(new) —trackScrollDepthEventToMeta.event_name = 'ScrollDepth75', custom_data carries the URL.apps/api/src/lib/meta/track-scroll-depth.test.ts(new).- Spec coverage: PPC Engineering dependencies. LP spec amended in step 14.
4. Scroll-depth API route
- Files touched:
apps/api/src/app/api/meta/scroll-depth/route.ts(new) — same shape as the funnel-choice route.- Spec coverage: PPC Engineering dependencies.
5. Pixel: trackLandingFunnelChoice
- Files touched:
apps/web/src/lib/tracking.ts— addtrackLandingFunnelChoice({ choice, locationId }). Generates aneventID, callstrackMetaEvent('LandingFunnelChoice', { content_name: choice, content_category: 'lp-fork' }, { eventID }), fire-and-forget POSTs the sameeventId+ attribution snapshot (fromgetStoredUtms()+_fbp/_fbccookies) to/api/meta/funnel-choice. Pattern:trackTourRequested(lines 91–104) +trackCheckoutInitiated(eventID dedup).- Spec coverage: Tracking spec — Pixel side
6. Pixel: trackScrollDepth75 + useScrollDepth75 hook
- Files touched:
apps/web/src/lib/tracking.ts—trackScrollDepth75({ url }). PixeltrackCustom('ScrollDepth75', { url }, { eventID })+ POST to/api/meta/scroll-depth.apps/web/src/lib/hooks/useScrollDepth75.ts(new) — firestrackScrollDepth75once per session per URL whenscrollY + innerHeight >= 0.75 * documentHeight. Guard with sessionStorage key.- Spec coverage: PPC Engineering dependencies. LP spec amended in step 14.
7. LP route scaffold (/lp/cherry-city)
- Files touched:
apps/web/src/app/(landing)/lp/cherry-city/page.tsx(new) — server component. Fetches the cherry-city location via/locations/public?slug=cherry-city+/locations/{id}/landing(matches existing/cherry-city/tour/page.tsxpattern). Also fetches hourly resources for the hourly section. BuildsMetadata,LocalBusiness/MusicVenueJSON-LD, merged FAQ schema. RendersUnifiedLandingContent.apps/web/src/app/(landing)/lp/cherry-city/UnifiedLandingContent.tsx(new) — client shell that callstrackLocationLandingViewed, mountsuseScrollDepth75, and composes the section components added in steps 8–13.- Spec coverage: URL and routing, Page structure
8. Hero + Fork + Shared proof bar
- Files touched:
apps/web/src/app/(landing)/lp/cherry-city/UnifiedLandingContent.tsx— render hero (full-bleedfrontman-jump.jpeg+ dark overlay +GrainOverlay, headline "Band practice at home sucks. We fixed that.", subline, no primary CTA, "See your options ↓" anchor link), fork section ("How do you want to play?"), shared proof bar (4 stats — uses "Salem bands", not "Cherry City bands").- Both fork CTAs (
Free tour →andSee times →) wire totrackLandingFunnelChoicethenscrollIntoViewto#monthly/#hourly. - Spec coverage: Hero, Fork section, Shared proof bar
9. Monthly full section
- Files touched:
apps/web/src/app/(landing)/lp/cherry-city/UnifiedLandingContent.tsx—<section id="monthly">with full-sizeBeforeAfterSlider(empty → stocked), email-onlyLeadCaptureFormembedded, CM intro card, "What you get with a lockout"CardCarouselSection(all 6 FeatureCards from current /tour), "How it works"StepsSection,PhotoGrid. Drops the /tour hero, stats bar, testimonials, FAQ, and "Not ready for a lockout?" cross-link section.- Spec coverage: Monthly full section
10. Hourly full section
- Files touched:
apps/web/src/app/(landing)/lp/cherry-city/UnifiedLandingContent.tsx—<section id="hourly">withHourlyBookingContent(Studio B,simplifiedprop) wrapped inModalWrapperfor auth, "All-Inclusive"CardCarouselSection, "Easy to Get Started"StepsSection,ResourceCarouselDisplayfor "Browse Other Hourly Studios", and the globalHourlyBookingModalfor non-featured studios. Drops /hourly hero, stats bar, testimonials, FAQ, "Playing here all the time?" cross-link, and the mid-page "Book a Studio" scroll-back button.- Spec coverage: Hourly full section
11. Shared testimonials
- Files touched:
apps/web/src/app/(landing)/lp/cherry-city/UnifiedLandingContent.tsx—<TestimonialsSection>carousel with the deduplicated set: Michael, Timothy "Hardhead", Joseph, Icarus.- Spec coverage: Shared testimonials
12. Shared FAQ
- Files touched:
apps/web/src/app/(landing)/lp/cherry-city/page.tsx— build a mergedfaqItemsarray (monthly questions first, hourly questions second, dedupe shared). Source-of-truth for the items lives in this server file (matches existing pattern).UnifiedLandingContent.tsx— render<FAQSection items={faqItems} />and feed the merged set into theFAQPageJSON-LD.- Spec coverage: Shared FAQ
13. Closing CTA + redirects
- Files touched:
apps/web/src/app/(landing)/lp/cherry-city/UnifiedLandingContent.tsx— closing CTA section ("You ready?" + two-button "Free tour" / "See times" both anchoring to#monthly/#hourlyand firingtrackLandingFunnelChoice).apps/web/next.config.tsredirects()— add{ source: '/cherry-city/tour', destination: '/lp/cherry-city', permanent: true }and{ source: '/cherry-city/hourly', destination: '/lp/cherry-city', permanent: true }.- Sweep
grep -rn "cherry-city/tour\|cherry-city/hourly"and update internal links (e.g., the "Not ready for a lockout?" cross-link in[locationSlug]pages, the LandingHeader, sitemap entries) to point at/lp/cherry-city. - Spec coverage: URL and routing, Closing CTA
14. Spec follow-up
- Files touched:
docs/features/unified-cherry-city-lp/spec.md— addScrollDepth75to the Tracking spec (event name, Pixel + CAPI mirroring pattern, fires once per session at 75% scroll, dedup viaeventID). Bumplast_updated.docs/features/cherry-city-ppc-prospecting/spec.md— drop theScrollDepth75"new ask" Known Gap.docs/features/README.md— refresh the row forunified-cherry-city-lp: status link to plan.md (in-progress).- Closes the spec/code divergence per Spec leads code.
15. Resolve closing CTA anchor divergence from spec
- Spec Closing CTA says: "Single button or two-button 'Free tour' / 'See times' — both anchor back to the fork."
- Current code (
apps/web/src/app/(landing)/lp/cherry-city/UnifiedLandingContent.tsxlines ~664–683): the closing CTA buttons reusehandleMonthlyChoice/handleHourlyChoice, which scroll to#monthly/#hourlydirectly and fireLandingFunnelChoice— they do not anchor back to#fork. - The item-13 reviewer noted the divergence and accepted it (rationale: keeps "Keep tight", second
LandingFunnelChoicesignal). Per protocol, "Never edit the spec to match drifted code — fix the code instead." - Resolution options for the next implementer:
- (a) Change both closing CTA buttons to anchor
#fork(literal spec compliance — drops the second-fire LandingFunnelChoice signal); OR - (b) Escalate to Aaron with the prior reviewer's reasoning so he can amend the spec — only proceed under explicit owner approval.
- Spec coverage: Closing CTA.