From 33f599fea62b6dac46d9b642b5a4fa210418431b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:31:52 +0000 Subject: [PATCH 1/6] fix(layout,splash): enforce global header/footer chrome and fix first-visit splash - Move FirstVisitSplash mount from HomePage to root layout so it fires on the user's very first landing regardless of entry URL (fixes the 'only shows on the second visit to /' bug). - Gate the splash behind a micro-saas entry-point check: suppress on /tools* and on any utm/ref/source/utm_campaign value matching /micro[-_ ]?saas|microsaas/i. Bump storage key to v2 so existing visitors evaluate the new rules once. - Bring /lighthouse in line with the global marketing chrome: its layout no longer renders BrandHeader/BrandFooter/SiteContactDrawer directly. Instead the lighthouse audit and report-viewer pages wrap their content in MarketingChrome, which guarantees the same sticky header, mobile nav, gold trim, reach-out dialog, and BrandFooter as the rest of the site. - Extend MarketingChrome with optional headerCurrentSection / footerBuildTag / footerPoweredBy / segmentClassName props so the lighthouse surface can keep its 'audit' chip and Scanner v2 / Powered-by attribution inside the shared chrome. - /lighthouse/report/[id]/print stays intentionally outside MarketingChrome to keep the PDF output clean. Co-Authored-By: Anthony Jones --- apps/web/src/app/layout.tsx | 12 ++- apps/web/src/app/lighthouse/layout.tsx | 34 +++---- apps/web/src/app/lighthouse/page.tsx | 46 ++++++---- .../src/app/lighthouse/report/[id]/page.tsx | 85 +++++++++++------- .../components/marketing/FirstVisitSplash.tsx | 90 ++++++++++++++++--- .../web/src/components/marketing/HomePage.tsx | 2 - .../components/marketing/MarketingChrome.tsx | 20 ++++- 7 files changed, 203 insertions(+), 86 deletions(-) diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 76dbd286..0e80b525 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,9 +1,10 @@ import "@/design-system/dba-global.css"; import { CrispBootstrap } from "@/components/CrispBootstrap"; import { JsonLd } from "@/components/JsonLd"; +import { FirstVisitSplash } from "@/components/marketing/FirstVisitSplash"; import "@/styles/layout-shell.css"; import type { Metadata, Viewport } from "next"; -import type { ReactNode } from "react"; +import { type ReactNode, Suspense } from "react"; /** Mobile-first: correct scaling on phones/tablets; safe areas for notched devices. */ export const viewport: Viewport = { @@ -94,6 +95,15 @@ export default function RootLayout({ children }: { children: ReactNode }) { /> {children} + {/* Global first-visit splash. Mounted in the root layout so + it fires on the user's very first landing regardless of + entry URL. The `useSearchParams` hook inside the splash + forces the subtree onto the client, so it is wrapped in + a Suspense boundary to keep the surrounding server tree + statically renderable. */} + + + diff --git a/apps/web/src/app/lighthouse/layout.tsx b/apps/web/src/app/lighthouse/layout.tsx index 6e8b4fa3..a0bf92db 100644 --- a/apps/web/src/app/lighthouse/layout.tsx +++ b/apps/web/src/app/lighthouse/layout.tsx @@ -1,11 +1,7 @@ -import { CookieConsentBanner } from "@lh/components/CookieConsentBanner"; import { LighthouseJsonLd } from "@lh/components/LighthouseJsonLd"; import { LighthouseTechFingerprints } from "@lh/components/LighthouseTechFingerprints"; import type { Metadata, Viewport } from "next"; import { Fraunces, Inter, Outfit } from "next/font/google"; -import { BrandFooter } from "@/components/brand/BrandFooter"; -import { BrandHeader } from "@/components/brand/BrandHeader"; -import { SiteContactDrawer } from "@/components/marketing/SiteContactDrawer"; import { absoluteSiteUrl, SITE_BRAND } from "@/design-system/site-config"; import "./lighthouse-globals.css"; @@ -96,6 +92,16 @@ export const metadata: Metadata = { }, }; +/** + * The lighthouse layout intentionally does NOT render BrandHeader / BrandFooter + * / SiteContactDrawer / CookieConsentBanner directly. Those chrome elements + * come from the global `MarketingChrome` wrapper applied at the page level + * (see `page.tsx` and `report/[id]/page.tsx`), which keeps header, footer, + * gold trim, and cookie consent 100 % consistent with the rest of the site. + * + * The `/lighthouse/report/[id]/print` sub-route deliberately opts out of + * the marketing chrome so the PDF output is clean. + */ export default function LighthouseLayout({ children, }: Readonly<{ children: React.ReactNode }>) { @@ -105,27 +111,7 @@ export default function LighthouseLayout({ > - - - {children} - - - - - - {/* CrispBootstrap is mounted globally in the root layout; no need - to duplicate it here. */} ); } diff --git a/apps/web/src/app/lighthouse/page.tsx b/apps/web/src/app/lighthouse/page.tsx index a367382e..5700fc4f 100644 --- a/apps/web/src/app/lighthouse/page.tsx +++ b/apps/web/src/app/lighthouse/page.tsx @@ -1,25 +1,39 @@ import { AuditForm } from "@lh/components/AuditForm"; import { LighthouseHero } from "@lh/components/LighthouseHero"; import { LighthouseValueStrip } from "@lh/components/LighthouseValueStrip"; +import { MarketingChrome } from "@/components/marketing/MarketingChrome"; + +const LIGHTHOUSE_POWERED_BY = [ + { label: "Gemini 2.0", href: "https://deepmind.google/technologies/gemini/" }, + { label: "ElysiaJS", href: "https://elysiajs.com" }, + { label: "Next.js", href: "https://nextjs.org" }, +] as const; export default function LighthouseHome() { return ( -
-
-
- - -
+ +
+
+
+ + +
-
- -
-
-
+
+ +
+ + + ); } diff --git a/apps/web/src/app/lighthouse/report/[id]/page.tsx b/apps/web/src/app/lighthouse/report/[id]/page.tsx index 224b1c5b..227ae549 100644 --- a/apps/web/src/app/lighthouse/report/[id]/page.tsx +++ b/apps/web/src/app/lighthouse/report/[id]/page.tsx @@ -19,8 +19,28 @@ import { AuditResults } from "@lh/components/AuditResults"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; +import { MarketingChrome } from "@/components/marketing/MarketingChrome"; import { buildPublicApiUrl } from "@/lib/publicApi"; +const LIGHTHOUSE_POWERED_BY = [ + { label: "Gemini 2.0", href: "https://deepmind.google/technologies/gemini/" }, + { label: "ElysiaJS", href: "https://elysiajs.com" }, + { label: "Next.js", href: "https://nextjs.org" }, +] as const; + +function LighthouseReportShell({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + function isObject(v: unknown): v is Record { return typeof v === "object" && v !== null && !Array.isArray(v); } @@ -155,50 +175,55 @@ export default function LighthouseReportViewerPage() { if (!id) { return ( -
-

Invalid report link.

- - Back to scanner - -
+ +
+

Invalid report link.

+ + Back to scanner + +
+
); } if (error) { return ( -
-

{error}

-

- Reports expire after 90 days. If you saved this email link, please - request a fresh scan to view the latest version. -

- - Run a new scan - -
+ +
+

{error}

+

+ Reports expire after 90 days. If you saved this email link, please + request a fresh scan to view the latest version. +

+ + Run a new scan + +
+
); } if (!data) { return ( -
-

Loading your report…

-
+ +
+

Loading your report…

+
+
); } return ( -
-
- router.push("/lighthouse")} - /> + +
+
+ router.push("/lighthouse")} + /> +
-
+ ); } diff --git a/apps/web/src/components/marketing/FirstVisitSplash.tsx b/apps/web/src/components/marketing/FirstVisitSplash.tsx index a71d1b92..6b95e4bf 100644 --- a/apps/web/src/components/marketing/FirstVisitSplash.tsx +++ b/apps/web/src/components/marketing/FirstVisitSplash.tsx @@ -1,31 +1,101 @@ "use client"; import Link from "next/link"; +import { usePathname, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; -const STORAGE_KEY = "dba_first_visit_shown_v1"; +/** + * Bumped from `_v1` to `_v2` when splash ownership moved from `HomePage` + * to the root layout (2026-04). Old v1 flags get ignored so existing + * visitors still see the splash once under the new global rules. + */ +const STORAGE_KEY = "dba_first_visit_shown_v2"; + +const MICRO_SAAS_PATH_PREFIXES = ["/tools"] as const; +const MICRO_SAAS_PARAM_KEYS = [ + "ref", + "source", + "utm_source", + "utm_campaign", + "utm_medium", + "utm_content", +] as const; +const MICRO_SAAS_PARAM_PATTERN = /micro[-_ ]?saas|microsaas/i; + +function isMicroSaasEntry( + pathname: string | null, + searchParams: URLSearchParams | null, +): boolean { + if (pathname) { + for (const prefix of MICRO_SAAS_PATH_PREFIXES) { + if (pathname === prefix || pathname.startsWith(`${prefix}/`)) { + return true; + } + } + } + if (searchParams) { + for (const key of MICRO_SAAS_PARAM_KEYS) { + const value = searchParams.get(key); + if (value && MICRO_SAAS_PARAM_PATTERN.test(value)) { + return true; + } + } + } + return false; +} + +function safeGetStorage(key: string): string | null { + try { + return window.localStorage.getItem(key); + } catch { + return null; + } +} + +function safeSetStorage(key: string, value: string): void { + try { + window.localStorage.setItem(key, value); + } catch { + // swallow — private-mode / storage-full / disabled cookies + } +} export function FirstVisitSplash() { + const pathname = usePathname(); + const searchParams = useSearchParams(); const [isOpen, setIsOpen] = useState(false); + // biome-ignore lint/correctness/useExhaustiveDependencies: intentional — evaluates only the initial landing URL on mount; re-running on client nav would re-fire the splash. useEffect(() => { - // Check if already shown - const hasShown = localStorage.getItem(STORAGE_KEY); - if (!hasShown) { - // Show after short delay for better UX - const timer = setTimeout(() => setIsOpen(true), 1500); - return () => clearTimeout(timer); + // Only evaluate once per session (on initial mount). After the first + // run we persist a flag so subsequent navigations never re-trigger. + if (safeGetStorage(STORAGE_KEY)) { + return; + } + + // Micro-SaaS entry points (e.g. direct /tools landings, or any URL + // tagged with a micro-saas utm/ref param) must NEVER show the splash, + // since those visitors are already arriving for that exact offer. + if (isMicroSaasEntry(pathname, searchParams)) { + safeSetStorage(STORAGE_KEY, "skipped-microsaas"); + return; } + + const timer = window.setTimeout(() => setIsOpen(true), 1500); + return () => window.clearTimeout(timer); }, []); + const markShown = () => { + safeSetStorage(STORAGE_KEY, "shown"); + }; + const handleClose = () => { - localStorage.setItem(STORAGE_KEY, "true"); + markShown(); setIsOpen(false); }; const handleContactClick = () => { - localStorage.setItem(STORAGE_KEY, "true"); - // Track the CTA + markShown(); if (typeof window !== "undefined") { const w = window as unknown as { dataLayer?: unknown[] }; w.dataLayer?.push({ diff --git a/apps/web/src/components/marketing/HomePage.tsx b/apps/web/src/components/marketing/HomePage.tsx index 6a535e64..ebaae68a 100644 --- a/apps/web/src/components/marketing/HomePage.tsx +++ b/apps/web/src/components/marketing/HomePage.tsx @@ -11,7 +11,6 @@ import { STANDARD_WEBSITE_STARTING_PRICE, STANDARD_WEBSITE_TYPICAL_RANGE, } from "@/lib/offers"; -import { FirstVisitSplash } from "./FirstVisitSplash"; import { FoundingPartnerSection } from "./FoundingPartnerSection"; import { PremiumPitchStrip } from "./PremiumPitchStrip"; import "@/app/home-page.css"; @@ -501,7 +500,6 @@ export function HomePage() { }); })();`} - ); } diff --git a/apps/web/src/components/marketing/MarketingChrome.tsx b/apps/web/src/components/marketing/MarketingChrome.tsx index 3fea9644..0fa007ed 100644 --- a/apps/web/src/components/marketing/MarketingChrome.tsx +++ b/apps/web/src/components/marketing/MarketingChrome.tsx @@ -26,11 +26,23 @@ export function MarketingChrome({ footerCta, hidePreFooterCta, minimalChrome, + headerCurrentSection, + footerBuildTag, + footerPoweredBy, + segmentClassName, }: { children: ReactNode; footerCta?: FooterCtaProps; hidePreFooterCta?: boolean; minimalChrome?: boolean; + /** Marks the current section on the BrandHeader (e.g. "audit" on /lighthouse). */ + headerCurrentSection?: "audit"; + /** Optional mono build tag shown next to legal links in the BrandFooter. */ + footerBuildTag?: string; + /** Optional "Powered by" stack shown in the BrandFooter. */ + footerPoweredBy?: ReadonlyArray<{ label: string; href: string }>; + /** Optional class applied to the outer wrapper — used to scope segment-specific CSS (e.g. `lighthouse-segment`). */ + segmentClassName?: string; }) { if (minimalChrome) { return <>{children}; @@ -104,7 +116,7 @@ window.__dbaRevokeAnalyticsConsent = function () {