Skip to content

Commit c0a1870

Browse files
committed
fix(site): hydrate homepage console ctas safely
Avoid nav hydration mismatches by deferring exact UTM resolution until after mount and apply the same exact-or-default UTM handling to the homepage create database CTAs. Made-with: Cursor
1 parent 8e586db commit c0a1870

4 files changed

Lines changed: 90 additions & 18 deletions

File tree

apps/blog/src/components/navigation-wrapper.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { WebNavigation } from "@prisma-docs/ui/components/web-navigation";
4+
import { useEffect, useState } from "react";
45
import { getUtmParams, hasUtmParams, type UtmParams } from "@/lib/utm";
56

67
interface Link {
@@ -28,22 +29,26 @@ interface NavigationWrapperProps {
2829
}
2930

3031
export function NavigationWrapper({ links, utm }: NavigationWrapperProps) {
32+
const [mounted, setMounted] = useState(false);
3133
const defaultUtmParams = {
3234
utm_source: utm.source,
3335
utm_medium: utm.medium,
3436
};
37+
38+
useEffect(() => {
39+
setMounted(true);
40+
}, []);
41+
3542
const currentUtmParams: UtmParams =
36-
typeof window === "undefined"
37-
? {}
38-
: getUtmParams(new URLSearchParams(window.location.search));
39-
const hasExactUtm = hasUtmParams(currentUtmParams);
40-
const resolvedUtmParams = hasExactUtm ? currentUtmParams : defaultUtmParams;
43+
mounted ? getUtmParams(new URLSearchParams(window.location.search)) : {};
44+
const preserveExactUtm = hasUtmParams(currentUtmParams);
45+
const resolvedUtmParams = preserveExactUtm ? currentUtmParams : defaultUtmParams;
4146

4247
return (
4348
<WebNavigation
4449
links={links}
4550
utm={resolvedUtmParams}
46-
preserveExactUtm={hasExactUtm}
51+
preserveExactUtm={preserveExactUtm}
4752
/>
4853
);
4954
}

apps/site/src/app/(index)/page.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Button } from "@prisma/eclipse";
55
import { CopyCode } from "@/components/homepage/copy-btn";
66
import { Bento } from "@/components/homepage/bento";
77
import { CardSection } from "@/components/homepage/card-section/card-section";
8+
import { ConsoleCtaButton } from "@/components/console-cta-button";
89
import review from "../../data/homepage.json";
910
import Testimonials from "../../components/homepage/testimonials";
1011

@@ -114,17 +115,22 @@ export default function SiteHome() {
114115
ship faster.
115116
</p>
116117
<div className="flex flex-col md:flex-row gap-4 items-center justify-center">
117-
<Button
118+
<ConsoleCtaButton
118119
variant="ppg"
119-
href="https://console.prisma.io/sign-up?utm_source=website&utm_medium=index&utm_campaign=cta"
120+
consolePath="/sign-up"
121+
defaultUtm={{
122+
utm_source: "website",
123+
utm_medium: "index",
124+
utm_campaign: "cta",
125+
}}
120126
size="3xl"
121127
target="_blank"
122128
rel="noopener noreferrer"
123129
className="font-sans-display! font-[650]"
124130
>
125131
<span>Create database</span>
126132
<i className="fa-regular fa-database ml-2" />
127-
</Button>
133+
</ConsoleCtaButton>
128134
<CopyCode text="npx prisma init">
129135
<span className="text-foreground-neutral-reverse-weak">$</span>
130136
<span className="text-foreground-neutral-weak">
@@ -246,14 +252,19 @@ export default function SiteHome() {
246252
</p>
247253
</div>
248254
<div className="flex flex-col gap-6 md:flex-row">
249-
<Button
255+
<ConsoleCtaButton
250256
variant="ppg"
251257
size="2xl"
252-
href="https://console.prisma.io/sign-up?utm_source=website&utm_medium=index&utm_campaign=cta"
258+
consolePath="/sign-up"
259+
defaultUtm={{
260+
utm_source: "website",
261+
utm_medium: "index",
262+
utm_campaign: "cta",
263+
}}
253264
>
254265
<span>Create your first Database</span>
255266
<i className="fa-regular fa-arrow-right ml-2" />
256-
</Button>
267+
</ConsoleCtaButton>
257268
<Button variant="default-stronger" size="2xl" href="/pricing">
258269
<span>Explore Pricing</span>
259270
<i className="fa-regular fa-arrow-right ml-2" />
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import type { ComponentProps } from "react";
5+
import { Button } from "@prisma/eclipse";
6+
import { getUtmParams, hasUtmParams, type UtmParams } from "@/lib/utm";
7+
8+
interface ConsoleCtaButtonProps extends Omit<ComponentProps<typeof Button>, "href"> {
9+
consolePath: "/login" | "/sign-up";
10+
defaultUtm: UtmParams;
11+
}
12+
13+
function buildConsoleHref(consolePath: "/login" | "/sign-up", utmParams: UtmParams) {
14+
const href = new URL(`https://console.prisma.io${consolePath}`);
15+
16+
for (const [key, value] of Object.entries(utmParams)) {
17+
if (key.startsWith("utm_") && value) {
18+
href.searchParams.set(key, value);
19+
}
20+
}
21+
22+
return href.toString();
23+
}
24+
25+
export function ConsoleCtaButton({
26+
consolePath,
27+
defaultUtm,
28+
children,
29+
...props
30+
}: ConsoleCtaButtonProps) {
31+
const [href, setHref] = useState(() => buildConsoleHref(consolePath, defaultUtm));
32+
33+
useEffect(() => {
34+
const currentUtmParams = getUtmParams(
35+
new URLSearchParams(window.location.search),
36+
);
37+
38+
setHref(
39+
buildConsoleHref(
40+
consolePath,
41+
hasUtmParams(currentUtmParams) ? currentUtmParams : defaultUtm,
42+
),
43+
);
44+
}, [consolePath, defaultUtm]);
45+
46+
return (
47+
<Button {...props} href={href}>
48+
{children}
49+
</Button>
50+
);
51+
}

apps/site/src/components/navigation-wrapper.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { WebNavigation } from "@prisma-docs/ui/components/web-navigation";
44
import { Footer } from "@prisma-docs/ui/components/footer";
5+
import { useEffect, useState } from "react";
56
import { usePathname } from "next/navigation";
67
import {
78
getUtmParams,
@@ -54,16 +55,20 @@ function getUtmMedium(pathname: string) {
5455

5556
export function NavigationWrapper({ links, utm }: NavigationWrapperProps) {
5657
const pathname = usePathname();
58+
const [mounted, setMounted] = useState(false);
5759
const defaultUtmParams = {
5860
utm_source: utm.source,
5961
utm_medium: getUtmMedium(pathname),
6062
};
63+
64+
useEffect(() => {
65+
setMounted(true);
66+
}, []);
67+
6168
const currentUtmParams: UtmParams =
62-
typeof window === "undefined"
63-
? {}
64-
: getUtmParams(new URLSearchParams(window.location.search));
65-
const hasExactUtm = hasUtmParams(currentUtmParams);
66-
const resolvedUtmParams = hasExactUtm ? currentUtmParams : defaultUtmParams;
69+
mounted ? getUtmParams(new URLSearchParams(window.location.search)) : {};
70+
const preserveExactUtm = hasUtmParams(currentUtmParams);
71+
const resolvedUtmParams = preserveExactUtm ? currentUtmParams : defaultUtmParams;
6772

6873
// Determine button variant based on pathname
6974
const getButtonVariant = (): ColorType => {
@@ -78,7 +83,7 @@ export function NavigationWrapper({ links, utm }: NavigationWrapperProps) {
7883
<WebNavigation
7984
links={links}
8085
utm={resolvedUtmParams}
81-
preserveExactUtm={hasExactUtm}
86+
preserveExactUtm={preserveExactUtm}
8287
buttonVariant={getButtonVariant()}
8388
/>
8489
);

0 commit comments

Comments
 (0)