Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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: 1 addition & 1 deletion apps/blog/src/components/navigation-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

interface Link {
text: string;
Expand Down
115 changes: 4 additions & 111 deletions apps/blog/src/components/utm-persistence.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,9 @@
"use client";

import { useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import { BLOG_PREFIX } from "@/lib/url";
import {
clearStoredUtmParams,
CONSOLE_HOST,
getUtmParams,
hasUtmParams,
syncUtmParams,
writeStoredUtmParams,
} from "@/lib/utm";
import { UtmPersistence as SharedUtmPersistence } from "@prisma-docs/ui/components/utm-persistence";

export function UtmPersistence() {
const pathname = usePathname();
const router = useRouter();

useEffect(() => {
const currentUtmParams = getUtmParams(
new URLSearchParams(window.location.search),
);

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

clearStoredUtmParams();
}, [pathname]);

useEffect(() => {
function handleClick(event: MouseEvent) {
if (event.defaultPrevented || event.button !== 0) {
return;
}

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.hasAttribute("download")
) {
return;
}

const activeUtmParams = getUtmParams(
new URLSearchParams(window.location.search),
);

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

const targetUrl = new URL(anchor.href, window.location.href);
const isInternalLink = targetUrl.origin === window.location.origin;
const isConsoleLink = targetUrl.hostname === CONSOLE_HOST;

if (!isInternalLink && !isConsoleLink) {
return;
}

if (!syncUtmParams(targetUrl, activeUtmParams)) {
return;
}

const nextHref = `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`;
const isBlogPath =
targetUrl.pathname === BLOG_PREFIX ||
targetUrl.pathname.startsWith(`${BLOG_PREFIX}/`);
const isModifiedClick =
event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;

if (
isInternalLink &&
isBlogPath &&
anchor.target !== "_blank" &&
!isModifiedClick
) {
const internalPathname =
targetUrl.pathname === BLOG_PREFIX
? "/"
: targetUrl.pathname.replace(
new RegExp(`^${BLOG_PREFIX}(?:/|$)`),
"/",
);
event.preventDefault();
router.push(
`${internalPathname}${targetUrl.search}${targetUrl.hash}`,
);
return;
}

anchor.setAttribute(
"href",
isInternalLink ? nextHref : targetUrl.toString(),
);
}

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

return null;
return (
<SharedUtmPersistence storageKey="blog_utm_params" basePath="/blog" />
);
}
29 changes: 27 additions & 2 deletions apps/docs/src/components/layout/link-item.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
'use client';
import type { ComponentProps, ReactNode } from 'react';
import { type ComponentProps, type ReactNode, useEffect, useState } from 'react';
import { usePathname } from 'fumadocs-core/framework';
import { isActive, isActiveAny } from '../../lib/urls';
import { getUtmParams, hasUtmParams } from '@prisma-docs/ui/lib/utm';
import Link from 'fumadocs-core/link';

function useUtmHref(base: string): string {
const [href, setHref] = useState(base);
useEffect(() => {
const utm = getUtmParams(new URLSearchParams(window.location.search));
if (!hasUtmParams(utm)) {
setHref(base);
return;
}
try {
const isAbsolute = base.startsWith('http');
const url = isAbsolute ? new URL(base) : new URL(base, 'https://n.co');
for (const [key, value] of Object.entries(utm)) {
url.searchParams.set(key, value);
}
setHref(isAbsolute ? url.toString() : `${url.pathname}${url.search}${url.hash}`);
} catch {
setHref(base);
}
}, [base]);
return href;
}

interface Filterable {
/**
* Restrict where the item is displayed
Expand Down Expand Up @@ -111,8 +134,10 @@ export function LinkItem({
? isActiveAny(item.activePaths, pathname)
: activeType !== 'none' && isActive(item.url, pathname, activeType === 'nested-url');

const href = useUtmHref(item.url);

return (
<Link ref={ref} href={item.url} external={item.external} {...props} data-active={active}>
<Link ref={ref} href={href} external={item.external} {...props} data-active={active}>
{props.children}
</Link>
);
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/src/components/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ReactNode } from "react";
import { source } from "@/lib/source";
import { TreeContextProvider } from "fumadocs-ui/contexts/tree";
import { TrackingProvider } from "@/components/tracking-provider";
import { UtmPersistence } from "@/components/utm-persistence";

const KAPA_INTEGRATION_ID = "1b51bb03-43cc-4ef4-95f1-93288a91b560";

Expand All @@ -29,6 +30,7 @@ export function Provider({ children }: { children: ReactNode }) {
}}
>
<TrackingProvider />
<UtmPersistence />
{children}
</RootProvider>
</KapaProvider>
Expand Down
9 changes: 9 additions & 0 deletions apps/docs/src/components/utm-persistence.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use client";

import { UtmPersistence as SharedUtmPersistence } from "@prisma-docs/ui/components/utm-persistence";

export function UtmPersistence() {
return (
<SharedUtmPersistence storageKey="docs_utm_params" basePath="/docs" />
);
}
2 changes: 1 addition & 1 deletion apps/site/src/components/console-cta-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useEffect, useState } from "react";
import { Button, type ButtonProps } from "@prisma/eclipse";
import { getUtmParams, hasUtmParams, type UtmParams } from "@/lib/utm";
import { getUtmParams, hasUtmParams, type UtmParams } from "@prisma-docs/ui/lib/utm";

interface ConsoleCtaButtonProps extends Omit<ButtonProps, "asChild"> {
consolePath: "/login" | "/sign-up";
Expand Down
2 changes: 1 addition & 1 deletion apps/site/src/components/navigation-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
getUtmParams,
hasUtmParams,
type UtmParams,
} from "@/lib/utm";
} from "@prisma-docs/ui/lib/utm";

interface Link {
text: string;
Expand Down
117 changes: 9 additions & 108 deletions apps/site/src/components/utm-persistence.tsx
Original file line number Diff line number Diff line change
@@ -1,113 +1,14 @@
"use client";

import { useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import {
clearStoredUtmParams,
CONSOLE_HOST,
getUtmParams,
hasUtmParams,
syncUtmParams,
writeStoredUtmParams,
} from "@/lib/utm";
import { UtmPersistence as SharedUtmPersistence } from "@prisma-docs/ui/components/utm-persistence";

export function UtmPersistence() {
const pathname = usePathname();
const router = useRouter();

useEffect(() => {
const currentUtmParams = getUtmParams(
new URLSearchParams(window.location.search),
);

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

clearStoredUtmParams();
}, [pathname]);

useEffect(() => {
function handleClick(event: MouseEvent) {
if (event.defaultPrevented || event.button !== 0) {
return;
}

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.hasAttribute("download")
) {
return;
}

const activeUtmParams = getUtmParams(
new URLSearchParams(window.location.search),
);

if (!hasUtmParams(activeUtmParams)) {
return;
}
const PROXIED_PATHS = ["/docs", "/blog"];

const targetUrl = new URL(anchor.href, window.location.href);
const isInternalLink = targetUrl.origin === window.location.origin;
const isConsoleLink = targetUrl.hostname === CONSOLE_HOST;

if (!isInternalLink && !isConsoleLink) {
return;
}

const updated = syncUtmParams(targetUrl, activeUtmParams);

if (!updated) {
return;
}

const nextHref = `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`;
const isModifiedClick =
event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;

// Paths proxied to other apps via rewrites — must use full navigation
// so the server-side rewrite kicks in instead of client-side routing.
const isProxiedPath =
targetUrl.pathname === "/docs" ||
targetUrl.pathname.startsWith("/docs/") ||
targetUrl.pathname === "/blog" ||
targetUrl.pathname.startsWith("/blog/");

if (
isInternalLink &&
!isProxiedPath &&
anchor.target !== "_blank" &&
!isModifiedClick
) {
event.preventDefault();
router.push(nextHref);
return;
}

anchor.setAttribute(
"href",
isInternalLink ? nextHref : targetUrl.toString(),
);
}

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

return null;
export function UtmPersistence() {
return (
<SharedUtmPersistence
storageKey="site_utm_params"
proxiedPaths={PROXIED_PATHS}
/>
);
}
Loading
Loading