diff --git a/apps/web/__tests__/api/cookie-consent.test.ts b/apps/web/__tests__/api/cookie-consent.test.ts
new file mode 100644
index 00000000..530355ad
--- /dev/null
+++ b/apps/web/__tests__/api/cookie-consent.test.ts
@@ -0,0 +1,54 @@
+import { describe, expect, it } from "vitest";
+import { NextRequest } from "next/server";
+import { POST } from "@/app/api/cookie-consent/route";
+import {
+ COOKIE_CONSENT_COOKIE,
+ COOKIE_CONSENT_MAX_AGE,
+ serializeCookieConsent,
+} from "@/lib/cookie-consent";
+
+function makeRequest(body: unknown) {
+ return new NextRequest(new URL("/api/cookie-consent", "http://localhost"), {
+ method: "POST",
+ body: JSON.stringify(body),
+ headers: { "Content-Type": "application/json" },
+ });
+}
+
+describe("POST /api/cookie-consent", () => {
+ it("sets an essential-only consent cookie", async () => {
+ const response = await POST(makeRequest({ preference: "essential" }));
+ const json = await response.json();
+ const setCookie = response.headers.get("set-cookie") ?? "";
+
+ expect(response.status).toBe(200);
+ expect(json).toEqual({ preference: "essential", analytics: false });
+ expect(setCookie).toContain(
+ `${COOKIE_CONSENT_COOKIE}=${serializeCookieConsent("essential")}`,
+ );
+ expect(setCookie).toContain(`Max-Age=${COOKIE_CONSENT_MAX_AGE}`);
+ expect(setCookie).toContain("Path=/");
+ expect(setCookie.toLowerCase()).toContain("samesite=lax");
+ });
+
+ it("sets analytics consent when all cookies are accepted", async () => {
+ const response = await POST(makeRequest({ preference: "all" }));
+ const json = await response.json();
+ const setCookie = response.headers.get("set-cookie") ?? "";
+
+ expect(response.status).toBe(200);
+ expect(json).toEqual({ preference: "all", analytics: true });
+ expect(setCookie).toContain(
+ `${COOKIE_CONSENT_COOKIE}=${serializeCookieConsent("all")}`,
+ );
+ });
+
+ it("rejects invalid preferences", async () => {
+ const response = await POST(makeRequest({ preference: "marketing" }));
+ const json = await response.json();
+
+ expect(response.status).toBe(400);
+ expect(json.error).toBe("Invalid cookie preference");
+ expect(response.headers.get("set-cookie")).toBeNull();
+ });
+});
diff --git a/apps/web/__tests__/unit/cookie-consent.test.ts b/apps/web/__tests__/unit/cookie-consent.test.ts
new file mode 100644
index 00000000..51d7e8ae
--- /dev/null
+++ b/apps/web/__tests__/unit/cookie-consent.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, it } from "vitest";
+import {
+ COOKIE_CONSENT_COOKIE,
+ getCookieConsentFromCookieString,
+ parseCookieConsent,
+ serializeCookieConsent,
+} from "@/lib/cookie-consent";
+
+describe("cookie consent helpers", () => {
+ it("serializes and parses essential-only consent", () => {
+ const value = serializeCookieConsent("essential");
+
+ expect(parseCookieConsent(value)).toEqual({
+ preference: "essential",
+ analytics: false,
+ });
+ });
+
+ it("serializes and parses analytics consent", () => {
+ const value = serializeCookieConsent("all");
+
+ expect(parseCookieConsent(value)).toEqual({
+ preference: "all",
+ analytics: true,
+ });
+ });
+
+ it("extracts consent from a browser cookie string", () => {
+ const value = serializeCookieConsent("essential");
+
+ expect(
+ getCookieConsentFromCookieString(
+ `theme=dark; ${COOKIE_CONSENT_COOKIE}=${value}; ref=alice`,
+ ),
+ ).toEqual({
+ preference: "essential",
+ analytics: false,
+ });
+ });
+
+ it("ignores unknown consent values", () => {
+ expect(parseCookieConsent("v0-all")).toBeNull();
+ expect(getCookieConsentFromCookieString("theme=dark")).toBeNull();
+ });
+});
diff --git a/apps/web/app/(landing)/join/[username]/opengraph-image.tsx b/apps/web/app/(landing)/join/[username]/opengraph-image.tsx
index 00a3ed99..be4cecc4 100644
--- a/apps/web/app/(landing)/join/[username]/opengraph-image.tsx
+++ b/apps/web/app/(landing)/join/[username]/opengraph-image.tsx
@@ -141,7 +141,6 @@ export default async function Image({
flexDirection: "column",
alignItems: "center",
position: "relative",
- zIndex: 1,
}}
>
{/* Avatar */}
diff --git a/apps/web/app/(landing)/join/[username]/ref-cookie.tsx b/apps/web/app/(landing)/join/[username]/ref-cookie.tsx
index cde3005f..ed85e779 100644
--- a/apps/web/app/(landing)/join/[username]/ref-cookie.tsx
+++ b/apps/web/app/(landing)/join/[username]/ref-cookie.tsx
@@ -1,10 +1,25 @@
"use client";
import { useEffect } from "react";
+import {
+ COOKIE_CONSENT_EVENT,
+ getCookieConsentFromCookieString,
+} from "@/lib/cookie-consent";
export function RefCookie({ username }: { username: string }) {
useEffect(() => {
- document.cookie = `ref=${encodeURIComponent(username)}; path=/; max-age=${30 * 24 * 60 * 60}; samesite=lax`;
+ function setRefCookie() {
+ document.cookie = `ref=${encodeURIComponent(username)}; path=/; max-age=${
+ 30 * 24 * 60 * 60
+ }; samesite=lax`;
+ }
+
+ if (getCookieConsentFromCookieString(document.cookie)) {
+ setRefCookie();
+ }
+
+ window.addEventListener(COOKIE_CONSENT_EVENT, setRefCookie);
+ return () => window.removeEventListener(COOKIE_CONSENT_EVENT, setRefCookie);
}, [username]);
return null;
diff --git a/apps/web/app/(landing)/layout.tsx b/apps/web/app/(landing)/layout.tsx
index 2106dd72..26983436 100644
--- a/apps/web/app/(landing)/layout.tsx
+++ b/apps/web/app/(landing)/layout.tsx
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
+import { CookieConsentModal } from "@/components/landing/CookieConsentModal";
export const metadata: Metadata = {
title: { absolute: "Straude — Strava for Claude Code" },
@@ -11,5 +12,10 @@ export default function LandingLayout({
}: {
children: React.ReactNode;
}) {
- return <>{children}>;
+ return (
+ <>
+ {children}
+
- Last updated: April 8, 2026 + Last updated: June 2, 2026
We use essential cookies for authentication and session - management. We do not use third-party tracking cookies. + management through Supabase Auth, security, referral + attribution, and storing your cookie preference. Analytics + stays off unless you choose to accept all cookies. We do not use + third-party tracking cookies.
diff --git a/apps/web/app/api/cookie-consent/route.ts b/apps/web/app/api/cookie-consent/route.ts new file mode 100644 index 00000000..bf4c0c1b --- /dev/null +++ b/apps/web/app/api/cookie-consent/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + COOKIE_CONSENT_COOKIE, + COOKIE_CONSENT_MAX_AGE, + type CookieConsentPreference, + serializeCookieConsent, +} from "@/lib/cookie-consent"; + +const VALID_PREFERENCES = new Set