Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/site/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { Footer } from "@prisma-docs/ui/components/footer";
import { ThemeProvider } from "@prisma-docs/ui/components/theme-provider";
import { FontAwesomeScript as WebFA } from "@prisma/eclipse";
import { UtmPersistence } from "@/components/utm-persistence";

const inter = Inter({
subsets: ["latin"],
Expand Down Expand Up @@ -187,6 +188,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<div className="bg-background-default absolute inset-0 -z-1 overflow-hidden" />
<Provider>
<ThemeProvider defaultTheme="system" storageKey="theme">
<UtmPersistence />
<NavigationWrapper
links={baseOptions().links}
utm={{ source: "website" }}
Expand Down
40 changes: 37 additions & 3 deletions apps/site/src/components/navigation-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

import { WebNavigation } from "@prisma-docs/ui/components/web-navigation";
import { Footer } from "@prisma-docs/ui/components/footer";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { usePathname, useSearchParams } from "next/navigation";
import {
getUtmParams,
readStoredUtmParams,
type UtmParams,
writeStoredUtmParams,
} from "@/lib/utm";

interface Link {
text: string;
Expand All @@ -23,7 +30,7 @@ interface Link {
interface NavigationWrapperProps {
links: Link[];
utm: {
source: "website";
source: string;
};
}

Expand All @@ -49,6 +56,27 @@ function getUtmMedium(pathname: string) {

export function NavigationWrapper({ links, utm }: NavigationWrapperProps) {
const pathname = usePathname();
const searchParams = useSearchParams();
const [storedUtmParams, setStoredUtmParams] = useState<UtmParams>({
utm_source: utm.source,
});

useEffect(() => {
const currentUtmParams = getUtmParams(new URLSearchParams(searchParams.toString()));

if (currentUtmParams.utm_source) {
setStoredUtmParams(currentUtmParams);
writeStoredUtmParams(currentUtmParams);
return;
}

const persistedUtmParams = readStoredUtmParams();
setStoredUtmParams(
persistedUtmParams.utm_source
? persistedUtmParams
: { utm_source: utm.source },
);
}, [searchParams, utm.source]);

// Determine button variant based on pathname
const getButtonVariant = (): ColorType => {
Expand All @@ -62,7 +90,13 @@ export function NavigationWrapper({ links, utm }: NavigationWrapperProps) {
return (
<WebNavigation
links={links}
utm={{ source: utm.source, medium: getUtmMedium(pathname) }}
utm={{
source: storedUtmParams.utm_source || utm.source,
medium: storedUtmParams.utm_medium || getUtmMedium(pathname),
campaign: storedUtmParams.utm_campaign,
content: storedUtmParams.utm_content,
term: storedUtmParams.utm_term,
}}
buttonVariant={getButtonVariant()}
/>
);
Expand Down
94 changes: 94 additions & 0 deletions apps/site/src/components/utm-persistence.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"use client";

import { useEffect } from "react";
import { usePathname, useSearchParams } from "next/navigation";
import {
getUtmParams,
hasUtmParams,
mergeUtmParams,
readStoredUtmParams,
writeStoredUtmParams,
} from "@/lib/utm";

export function UtmPersistence() {
const pathname = usePathname();
const searchParams = useSearchParams();

useEffect(() => {
const currentUtmParams = getUtmParams(new URLSearchParams(searchParams.toString()));

if (hasUtmParams(currentUtmParams)) {
writeStoredUtmParams(currentUtmParams);
return;
}

const storedUtmParams = readStoredUtmParams();

if (!hasUtmParams(storedUtmParams)) {
return;
}

const currentUrl = new URL(window.location.href);

if (!mergeUtmParams(currentUrl, storedUtmParams)) {
return;
}

window.history.replaceState(
window.history.state,
"",
`${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`,
);
}, [pathname, searchParams]);

useEffect(() => {
function handleClick(event: MouseEvent) {
const anchor = (event.target as HTMLElement).closest<HTMLAnchorElement>(
"a[href]",
);

if (!anchor) {
return;
}

const href = anchor.getAttribute("href");

if (
!href ||
href.startsWith("#") ||
href.startsWith("mailto:") ||
href.startsWith("tel:") ||
anchor.target === "_blank" ||
anchor.hasAttribute("download")
) {
return;
}

const storedUtmParams = readStoredUtmParams();

if (!hasUtmParams(storedUtmParams)) {
return;
}

const targetUrl = new URL(anchor.href, window.location.href);

if (targetUrl.origin !== window.location.origin) {
return;
}

if (!mergeUtmParams(targetUrl, storedUtmParams)) {
return;
}

anchor.setAttribute(
"href",
`${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`,
);
}

document.addEventListener("click", handleClick, true);
return () => document.removeEventListener("click", handleClick, true);
}, []);

return null;
}
52 changes: 52 additions & 0 deletions apps/site/src/lib/utm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export const UTM_STORAGE_KEY = "site_utm_params";

export type UtmParams = Record<string, string>;

export function getUtmParams(searchParams: URLSearchParams): UtmParams {
const utmParams: UtmParams = {};

for (const [key, value] of searchParams.entries()) {
if (key.startsWith("utm_") && value) {
utmParams[key] = value;
}
}

return utmParams;
}

export function hasUtmParams(utmParams: UtmParams) {
return Object.keys(utmParams).length > 0;
}

export function mergeUtmParams(url: URL, utmParams: UtmParams) {
let updated = false;

for (const [key, value] of Object.entries(utmParams)) {
if (!url.searchParams.has(key)) {
url.searchParams.set(key, value);
updated = true;
}
}

return updated;
}

export function readStoredUtmParams() {
const storedUtmParams = window.sessionStorage.getItem(UTM_STORAGE_KEY);

if (!storedUtmParams) {
return {};
}

try {
return JSON.parse(storedUtmParams) as UtmParams;
} catch {
return {};
}
}

export function writeStoredUtmParams(utmParams: UtmParams) {
if (hasUtmParams(utmParams)) {
window.sessionStorage.setItem(UTM_STORAGE_KEY, JSON.stringify(utmParams));
}
}
41 changes: 34 additions & 7 deletions packages/ui/src/components/web-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,51 @@ export interface Link {
interface WebNavigationProps {
links: Link[];
utm?: {
source: "website";
source: string;
medium: string;
campaign?: string;
content?: string;
term?: string;
};
buttonVariant?: "ppg" | "orm" | undefined;
}

function buildConsoleHref(
pathname: "/login" | "/sign-up",
utm?: WebNavigationProps["utm"],
) {
if (!utm) {
return `https://console.prisma.io${pathname}`;
}

const href = new URL(`https://console.prisma.io${pathname}`);

href.searchParams.set("utm_source", utm.source);
href.searchParams.set("utm_medium", utm.medium);
href.searchParams.set(
"utm_campaign",
utm.campaign || (pathname === "/login" ? "login" : "signup"),
);

if (utm.content) {
href.searchParams.set("utm_content", utm.content);
}

if (utm.term) {
href.searchParams.set("utm_term", utm.term);
}

return href.toString();
}

export function WebNavigation({
links,
utm,
buttonVariant = "ppg",
}: WebNavigationProps) {
const [mobileView, setMobileView] = useState(false);
const loginHref = utm
? `https://console.prisma.io/login?utm_source=${utm.source}&utm_medium=${utm.medium}&utm_campaign=login`
: "https://console.prisma.io/login";
const signupHref = utm
? `https://console.prisma.io/sign-up?utm_source=${utm.source}&utm_medium=${utm.medium}&utm_campaign=signup`
: "https://console.prisma.io/sign-up";
const loginHref = buildConsoleHref("/login", utm);
const signupHref = buildConsoleHref("/sign-up", utm);

useEffect(() => {
if (mobileView) {
Expand Down
Loading