Skip to content

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 + useScrollDepth75 hook
  • [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 LandingFunnelChoice event is the optimization target that makes the unified Meta campaign possible.
  • Spec URL and routing/lp/cherry-city becomes 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 dependenciesScrollDepth75 event ask, in addition to LandingFunnelChoice.

Work breakdown

1. Funnel-choice CAPI helper + schema + test

  • Files touched:
  • packages/shared-schemas/src/api/meta.ts (new) — funnelChoiceSchema with choice: '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) — trackFunnelChoiceEventToMeta mirroring track-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). Pulls getClientIpAddress / getClientUserAgent. Fires trackFunnelChoiceEventToMeta via after(). Returns createSuccessResponse({}).
  • Spec coverage: Tracking spec — CAPI side

3. Scroll-depth CAPI helper + schema + test

  • Files touched:
  • packages/shared-schemas/src/api/meta.ts — add scrollDepthSchema with eventId, 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 — add trackLandingFunnelChoice({ choice, locationId }). Generates an eventID, calls trackMetaEvent('LandingFunnelChoice', { content_name: choice, content_category: 'lp-fork' }, { eventID }), fire-and-forget POSTs the same eventId + attribution snapshot (from getStoredUtms() + _fbp / _fbc cookies) 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.tstrackScrollDepth75({ url }). Pixel trackCustom('ScrollDepth75', { url }, { eventID }) + POST to /api/meta/scroll-depth.
  • apps/web/src/lib/hooks/useScrollDepth75.ts (new) — fires trackScrollDepth75 once per session per URL when scrollY + 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.tsx pattern). Also fetches hourly resources for the hourly section. Builds Metadata, LocalBusiness/MusicVenue JSON-LD, merged FAQ schema. Renders UnifiedLandingContent.
  • apps/web/src/app/(landing)/lp/cherry-city/UnifiedLandingContent.tsx (new) — client shell that calls trackLocationLandingViewed, mounts useScrollDepth75, 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-bleed frontman-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 → and See times →) wire to trackLandingFunnelChoice then scrollIntoView to #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-size BeforeAfterSlider (empty → stocked), email-only LeadCaptureForm embedded, 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"> with HourlyBookingContent (Studio B, simplified prop) wrapped in ModalWrapper for auth, "All-Inclusive" CardCarouselSection, "Easy to Get Started" StepsSection, ResourceCarouselDisplay for "Browse Other Hourly Studios", and the global HourlyBookingModal for 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 merged faqItems array (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 the FAQPage JSON-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 / #hourly and firing trackLandingFunnelChoice).
  • apps/web/next.config.ts redirects() — 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 — add ScrollDepth75 to the Tracking spec (event name, Pixel + CAPI mirroring pattern, fires once per session at 75% scroll, dedup via eventID). Bump last_updated.
  • docs/features/cherry-city-ppc-prospecting/spec.md — drop the ScrollDepth75 "new ask" Known Gap.
  • docs/features/README.md — refresh the row for unified-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.tsx lines ~664–683): the closing CTA buttons reuse handleMonthlyChoice / handleHourlyChoice, which scroll to #monthly / #hourly directly and fire LandingFunnelChoice — they do not anchor back to #fork.
  • The item-13 reviewer noted the divergence and accepted it (rationale: keeps "Keep tight", second LandingFunnelChoice signal). 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.