From cc038b9b32885c7862c32d86f9577c8e38fdf9dd Mon Sep 17 00:00:00 2001 From: biggest-littlest Date: Sat, 15 Aug 2026 18:45:36 -0400 Subject: [PATCH 01/12] fix(analytics): forward $raw_user_agent and $host for cookieless ingestion (#52) PostHog's cookieless server-hash step reads $raw_user_agent and $host straight off event.properties and drops the event with a cookieless_missing_user_agent/cookieless_missing_host ingestion warning if either is absent. createCommonProperties rebuilt an allowlisted properties object that dropped both, so every event was silently discarded at ingestion. Forward them through; never add $ip, which PostHog's capture service fills in server-side from the connection. Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com> --- frontend/lib/posthog-privacy.ts | 23 +++++++++++- frontend/test/posthog-source.test.mjs | 19 ++++++++++ frontend/test/posthog.test.ts | 52 +++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) diff --git a/frontend/lib/posthog-privacy.ts b/frontend/lib/posthog-privacy.ts index 1254d76..4809f28 100644 --- a/frontend/lib/posthog-privacy.ts +++ b/frontend/lib/posthog-privacy.ts @@ -70,11 +70,30 @@ function getRawPath(properties: Record): unknown { function createCommonProperties(properties: Record) { const token = properties.token; + // PostHog's cookieless server-hash ingestion step computes the anonymous + // distinct id from day + team + $ip + $host + $raw_user_agent. It reads + // $raw_user_agent/$host straight off event.properties (not headers) and + // silently drops the event with a cookieless_missing_user_agent / + // cookieless_missing_host ingestion warning if either is absent + // (PostHog/posthog nodejs/src/ingestion/common/cookieless/cookieless-manager.ts, + // getProperties()/doBatchInner()). posthog-js attaches both to every + // envelope by default (PostHog/posthog-js + // packages/browser-common/src/utils/event-utils.ts, getEventProperties()), + // so they must survive the allowlist rebuild below. $ip is deliberately + // NOT forwarded here: posthog-js never sends it, and PostHog's capture + // service fills it in from the request's own connection IP when absent โ€” a + // client-supplied $ip would only be able to make that worse, never better. + const rawUserAgent = properties.$raw_user_agent; + const host = properties.$host; if ( typeof token !== "string" || !PROJECT_TOKEN_PATTERN.test(token) || properties.$cookieless_mode !== true || - properties.$process_person_profile !== false + properties.$process_person_profile !== false || + typeof rawUserAgent !== "string" || + rawUserAgent === "" || + typeof host !== "string" || + host === "" ) { return null; } @@ -88,6 +107,8 @@ function createCommonProperties(properties: Record) { site: "codeswhat", surface: "marketing", path, + $raw_user_agent: rawUserAgent, + $host: host, }; if (properties.distinct_id === "$posthog_cookieless") { common.distinct_id = "$posthog_cookieless"; diff --git a/frontend/test/posthog-source.test.mjs b/frontend/test/posthog-source.test.mjs index b742812..7660607 100644 --- a/frontend/test/posthog-source.test.mjs +++ b/frontend/test/posthog-source.test.mjs @@ -62,3 +62,22 @@ test("public documentation does not advertise the retired provider badge", async const retiredBadgeText = ["Go", "Report", "Card"].join("\\s+"); assert.doesNotMatch(roadmap, new RegExp(retiredBadgeText, "i")); }); + +test("the cookieless envelope keeps the fields PostHog's server hash requires", async () => { + const privacy = await read("lib/posthog-privacy.ts"); + + // PostHog's cookieless server-hash ingestion step reads $raw_user_agent and + // $host straight off event.properties and drops the event โ€” with a + // cookieless_missing_user_agent / cookieless_missing_host ingestion warning + // and zero rows ingested โ€” if either is absent (PostHog/posthog + // nodejs/src/ingestion/common/cookieless/cookieless-manager.ts, + // getProperties()/doBatchInner()). posthog-js attaches both by default; + // createCommonProperties must allowlist them through, not silently strip + // them. Regression guard: if these keys ever disappear from the allowlist + // (or the comment explaining why they're there), every cookieless event on + // codeswhat.com drops with no PostHog-side error beyond the ingestion + // warning. + assert.match(privacy, /\$raw_user_agent/u); + assert.match(privacy, /\$host/u); + assert.match(privacy, /cookieless_missing_user_agent|cookieless server-hash/u); +}); diff --git a/frontend/test/posthog.test.ts b/frontend/test/posthog.test.ts index e894f1f..c2c064d 100644 --- a/frontend/test/posthog.test.ts +++ b/frontend/test/posthog.test.ts @@ -17,6 +17,19 @@ const { PostHog } = require("../node_modules/posthog-js/lib/src/posthog-core.js" }; }; +// posthog-js attaches these to every envelope by default (PostHog/posthog-js +// packages/browser-common/src/utils/event-utils.ts, getEventProperties()). +// sanitizeEvent must forward them: PostHog's cookieless server-hash +// ingestion step reads them straight off event.properties and drops the +// event with a cookieless_missing_user_agent / cookieless_missing_host +// ingestion warning if either is absent (PostHog/posthog +// nodejs/src/ingestion/common/cookieless/cookieless-manager.ts, +// getProperties() + doBatchInner()). +const COOKIELESS_HASH_PROPERTIES = { + $raw_user_agent: "Mozilla/5.0 (Test Runner)", + $host: "codeswhat.com", +}; + test("route sanitization only returns the finite public route manifest", () => { assert.deepEqual(ALLOWED_ROUTES, ["/"]); assert.equal(sanitizeRoute("/?utm_source=secret#private"), "/"); @@ -45,6 +58,7 @@ test("pageview events keep only the sanitized pathname", () => { distinct_id: "$posthog_cookieless", $cookieless_mode: true, $process_person_profile: false, + ...COOKIELESS_HASH_PROPERTIES, }, }), { @@ -59,6 +73,7 @@ test("pageview events keep only the sanitized pathname", () => { surface: "marketing", path: "/", $current_url: "https://codeswhat.com/", + ...COOKIELESS_HASH_PROPERTIES, }, }, ); @@ -76,6 +91,7 @@ test("CTA events are limited to the initial GitHub placements", () => { path: "/", cta_id: "github_org", placement: "hero", + ...COOKIELESS_HASH_PROPERTIES, }, }), { @@ -90,6 +106,7 @@ test("CTA events are limited to the initial GitHub placements", () => { path: "/", cta_id: "github_org", placement: "hero", + ...COOKIELESS_HASH_PROPERTIES, }, }, ); @@ -116,6 +133,7 @@ test("web vitals events keep only metric data", () => { $web_vitals_LCP_event: { attribution: "private" }, rating: "good", $current_url: "https://codeswhat.com/?private=1", + ...COOKIELESS_HASH_PROPERTIES, }, }), { @@ -129,6 +147,7 @@ test("web vitals events keep only metric data", () => { surface: "marketing", path: "/", $web_vitals_LCP_value: 123.4, + ...COOKIELESS_HASH_PROPERTIES, }, }, ); @@ -166,6 +185,7 @@ test("the pinned PostHog before_send pipeline keeps the required cookieless enve path: "/?secret=1#fragment", $set: { email: "private@example.com" }, $set_once: { referrer: "private" }, + ...COOKIELESS_HASH_PROPERTIES, }, $set: { email: "private@example.com" }, $set_once: { referrer: "private" }, @@ -188,6 +208,38 @@ test("the pinned PostHog before_send pipeline keeps the required cookieless enve surface: "marketing", path: "/", $current_url: "https://codeswhat.com/", + ...COOKIELESS_HASH_PROPERTIES, }, }); }); + +test("sanitizeEvent requires and forwards the cookieless server-hash fields", () => { + const validProperties = { + token: "phc_public-token_123", + distinct_id: "$posthog_cookieless", + $cookieless_mode: true, + $process_person_profile: false, + path: "/", + ...COOKIELESS_HASH_PROPERTIES, + }; + + const result = sanitizeEvent({ event: "$pageview", properties: validProperties }); + assert.ok(result); + assert.equal(result.properties.$raw_user_agent, COOKIELESS_HASH_PROPERTIES.$raw_user_agent); + assert.equal(result.properties.$host, COOKIELESS_HASH_PROPERTIES.$host); + assert.equal(result.properties.$ip, undefined); + + // Regression guard: if sanitizeEvent ever goes back to rebuilding + // properties from an allowlist that forgets these two keys, cookieless + // ingestion drops every event again with zero warning-free indication + // beyond cookieless_missing_user_agent / cookieless_missing_host. + for (const missingKey of Object.keys(COOKIELESS_HASH_PROPERTIES)) { + const withoutField = { ...validProperties }; + delete withoutField[missingKey as keyof typeof withoutField]; + assert.equal( + sanitizeEvent({ event: "$pageview", properties: withoutField }), + null, + `sanitizeEvent must drop events missing ${missingKey}`, + ); + } +}); From 3b6d9cd8eb95f40b3d585637d0373e3b8ee459c5 Mon Sep 17 00:00:00 2001 From: Scott Benson <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 16 Aug 2026 17:52:26 -0400 Subject: [PATCH 02/12] chore(config): remove the stale Cursor rules (#54) * fix(analytics): promote cookieless ingestion fix to production (#53) PostHog's cookieless server-hash step reads $raw_user_agent and $host straight off event.properties and drops the event with a cookieless_missing_user_agent/cookieless_missing_host ingestion warning if either is absent. createCommonProperties rebuilt an allowlisted properties object that dropped both, so every event was silently discarded at ingestion. Forward them through; never add $ip, which PostHog's capture service fills in server-side from the connection. Co-authored-by: biggest-littlest * chore(config): drop the stale Cursor rules folder * docs(config): drop dangling .cursorrules references --------- Co-authored-by: biggest-littlest --- .coderabbit.yaml | 1 - .cursor/rules/codeswhat-project.mdc | 71 ----------------------------- docs/README.md | 1 - 3 files changed, 73 deletions(-) delete mode 100644 .cursor/rules/codeswhat-project.mdc diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 126537c..7f7397f 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -67,7 +67,6 @@ knowledge_base: enabled: true file_patterns: - "**/CLAUDE.md" - - "**/.cursorrules" # Web search for additional context (library docs, etc.) web_search: diff --git a/.cursor/rules/codeswhat-project.mdc b/.cursor/rules/codeswhat-project.mdc deleted file mode 100644 index 5f069e2..0000000 --- a/.cursor/rules/codeswhat-project.mdc +++ /dev/null @@ -1,71 +0,0 @@ ---- -description: CodesWhat - Software consultancy website development rules -alwaysApply: true ---- - -# CodesWhat Project Rules - -## Project Context -Building CodesWhat (codeswhat-website) - a modern software consultancy landing page. -- Next.js 15.4.0 App Router, TypeScript strict, Tailwind CSS v4 -- Vercel deployment with `frontend/` as root directory -- EmailOctopus for subscriptions, Sonner for toasts - -## Development Workflow -- Use `./start.sh` for dev server (auto-restarts, port management) -- Run `npm run check:all` before committing -- Frontend directory is the Vercel root - -## Git Commit Standards -**MANDATORY**: All commits must include emojis. - -Format: ` : ` - -Common types: -- ๐Ÿ› fix - ๐ŸŽจ style - โœจ feat - ๐Ÿ”ง config - ๐Ÿ“ docs -- ๐Ÿ”„ refactor - ๐Ÿ“ฆ deps - ๐Ÿงช test - ๐Ÿš€ deploy - ๐Ÿ—‘๏ธ remove - -Multi-change commits use emoji bullets: -``` -๐ŸŽจ style: Improve dark mode visibility - -- ๐Ÿ› fix: Toast notification positioning -- ๐ŸŽจ style: Updated color contrast -- ๐Ÿงน cleanup: Removed unused styles -``` - -## Component Patterns -- Server Components by default -- Client Components only for interactivity -- Use shadcn/ui components: `npx shadcn@canary add [component]` - -## Styling Rules -- Tailwind classes only, no inline styles -- Dark mode: use `dark:` prefix -- Toast backgrounds must be solid (no transparency) -- Background pattern uses `fixed` positioning - -## API Implementation -```typescript -// Rate limiting example -const limiter = rateLimit({ max: 5, windowMs: 60000 }) - -// Always validate and sanitize -const email = formData.get('email')?.toString().toLowerCase().trim() -``` - -## Environment Variables -Public: `NEXT_PUBLIC_SITE_URL`, `NEXT_PUBLIC_SITE_NAME` -Private: `EMAILOCTOPUS_API_KEY`, `EMAILOCTOPUS_LIST_ID` - -## Key Features -- Email subscription: 5 req/min rate limit -- Toast notifications: bottom-right, 5s duration, hover to pause -- Dark mode: logo inverts colors -- SEO: robots.ts, sitemap.ts, JSON-LD on homepage - -## Pre-deployment -1. `npm run check:all` -2. Test email subscription -3. Verify dark mode -4. Check mobile responsive diff --git a/docs/README.md b/docs/README.md index 316fa13..f32f375 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,5 +16,4 @@ npm run dev # Start development server ``` ### Other Documentation -- **Cursor Rules**: See `.cursorrules` in the project root - **Future Plans**: See `FUTURE_SECTIONS.md` in the project root \ No newline at end of file From 30cdd8ef8b7bd6beff643eec8ecff4a513a533ad Mon Sep 17 00:00:00 2001 From: Scott Benson <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 16 Aug 2026 17:52:29 -0400 Subject: [PATCH 03/12] docs(readme): describe what this repo is and how it deploys (#55) --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..73ff981 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# CodesWhat website + +Private source for the public CodesWhat org site at codeswhat.com. The +Next.js app lives in `frontend/`; everything deployable is under that +directory. + +## What this repo is (and isn't) + +This is permanently private infrastructure, not a community project. That's +why there's no LICENSE, CONTRIBUTING, SECURITY policy, or CODEOWNERS here: +the site's source isn't published, only the rendered site is. The tier +decision and rationale live in the private ops repo +(`decisions/repository-tiers.md`). + +## Working here + +- Flow: feature branches PR into `dev`, then `dev` promotes to `main`. +- CI: `website.yml` runs the website contract checks. +- Deploys: Vercel, but git-integration deploys are broken for this repo (a + private repo in a GitHub org can't deploy on the Hobby plan). Production + deploys happen from a local checkout with + `npx vercel deploy --prod --scope codeswhat`. +- Analytics: PostHog via the shared "CodesWhat Public Websites" project, + proxied through `e.codeswhat.com`, cookieless. + +## More docs + +- `docs/README.md` - development guide +- `frontend/README-VERCEL.md` - deploy specifics +- `ROADMAP.md` - planned sections and site direction From d8aec7076d62d7476c5d8c51cd2860d8acdf8c48 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:04:53 -0400 Subject: [PATCH 04/12] chore(gitignore): ignore the root .vercel link and history-backup bundles --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 430cb52..e5df7b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ .DS_Store +# vercel project link (root-level; frontend/ has its own) +.vercel/ + +# git history backups (e.g. pre-rewrite-backup-*.bundle) +*.bundle + # brand assets (scripts/capture-assets.mjs): commit the WebP + still PNGs as the # source-of-truth; the heavy GIF/APNG are regenerable, and frames are scratch. exports/**/*.gif From f50fadbc221dcb53098a7a98b15ef4571f3ddea0 Mon Sep 17 00:00:00 2001 From: Scott Benson <80784472+scttbnsn@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:00:25 -0400 Subject: [PATCH 05/12] feat(analytics): capture $pageleave and send $pathname (#60) Measured over the shared PostHog project, 208 of 432 sessions across the five instrumented sites record zero duration, and PostHog's built-in Web analytics Page/Entry page/Exit page tables return zero rows. capture_pageleave was false, so a session's last recorded timestamp is its last pageview, and a five-minute read of one page scores as zero seconds. Flipping the option alone fixes nothing: sanitizeEvent allowlisted only $pageview, cta activated, and $web_vitals, so every $pageleave posthog-js emitted would have been dropped silently with no error and no ingestion warning. This adds a $pageleave branch that rebuilds the event the same way $pageview does. capture_pageview is false here (pageviews are captured by hand), so posthog-js's _shouldCapturePageleave gate needs an explicit true rather than the default. $pathname is the property PostHog's page tables actually key off, and it was never sent. It's bound to the already-sanitized `path` value, never the raw pathname, so it can't carry a route outside ALLOWED_ROUTES and adds no information the event wasn't already sending. A regression test asserts the two never diverge. No privacy option changes: cookieless_mode, person_profiles, persistence, disable_persistence, respect_dnt, save_referrer, and save_campaign_params are untouched. Part of X16 in the ops execution plan. --- frontend/instrumentation-client.ts | 10 ++- frontend/lib/posthog-privacy.ts | 14 +++- frontend/test/posthog-source.test.mjs | 1 + frontend/test/posthog.test.ts | 96 +++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/frontend/instrumentation-client.ts b/frontend/instrumentation-client.ts index 2b5cb09..ee29658 100644 --- a/frontend/instrumentation-client.ts +++ b/frontend/instrumentation-client.ts @@ -16,7 +16,15 @@ if (typeof window !== "undefined" && posthogConfig) { api_host: posthogConfig.apiHost, ui_host: posthogConfig.uiHost, capture_pageview: false, - capture_pageleave: false, + // posthog-js only emits $pageleave when capture_pageleave === true, or + // when it's "if_capture_pageview" AND capture_pageview is enabled + // (posthog-js posthog-core.ts, _shouldCapturePageleave). capture_pageview + // is false here because pageviews are captured by hand above, so this has + // to be an explicit true rather than the default. sanitizeEvent in + // lib/posthog-privacy.ts rebuilds $pageleave the same way it rebuilds + // $pageview; without that branch flipping this alone would drop every + // $pageleave silently. + capture_pageleave: true, autocapture: false, rageclick: false, disable_session_recording: true, diff --git a/frontend/lib/posthog-privacy.ts b/frontend/lib/posthog-privacy.ts index 4809f28..c241757 100644 --- a/frontend/lib/posthog-privacy.ts +++ b/frontend/lib/posthog-privacy.ts @@ -51,7 +51,7 @@ type EventInput = { }; type SanitizedEvent = { - event: "$pageview" | "cta activated" | "$web_vitals"; + event: "$pageview" | "$pageleave" | "cta activated" | "$web_vitals"; properties: Record; timestamp?: Date; uuid?: string; @@ -138,10 +138,20 @@ export function sanitizeEvent(input: unknown): SanitizedEvent | null { const values = properties as Record; const common = createCommonProperties(values); if (common === null) return null; - if (event === "$pageview") { + // posthog-js emits $pageleave itself once capture_pageleave is true; + // nothing in this codebase calls it directly. It has to be rebuilt here + // like every other envelope โ€” before this branch existed, $pageleave fell + // through to the `return null` below and was dropped silently, which is + // why flipping capture_pageleave on the init options alone fixes nothing. + // $pathname is set to the already-sanitized `path` rather than the raw + // pathname so PostHog's Web analytics Page / Entry page / Exit page + // tables โ€” which key off $pathname โ€” resolve without leaking any route + // outside ALLOWED_ROUTES. + if (event === "$pageview" || event === "$pageleave") { return createSanitizedEvent(eventInput, event, { ...common, $current_url: `${PRODUCTION_ORIGIN}${common.path}`, + $pathname: common.path, }); } diff --git a/frontend/test/posthog-source.test.mjs b/frontend/test/posthog-source.test.mjs index 7660607..ea1f6ce 100644 --- a/frontend/test/posthog-source.test.mjs +++ b/frontend/test/posthog-source.test.mjs @@ -27,6 +27,7 @@ test("privacy posture disables persistence, recording, autocapture, and automati "NEXT_PUBLIC_POSTHOG_HOST", "NEXT_PUBLIC_POSTHOG_UI_HOST", "capture_pageview: false", + "capture_pageleave: true", "autocapture: false", "disable_session_recording: true", 'persistence: "memory"', diff --git a/frontend/test/posthog.test.ts b/frontend/test/posthog.test.ts index c2c064d..de2c021 100644 --- a/frontend/test/posthog.test.ts +++ b/frontend/test/posthog.test.ts @@ -73,12 +73,107 @@ test("pageview events keep only the sanitized pathname", () => { surface: "marketing", path: "/", $current_url: "https://codeswhat.com/", + $pathname: "/", ...COOKIELESS_HASH_PROPERTIES, }, }, ); }); +test("pageleave events mirror the pageview contract", () => { + // posthog-js emits $pageleave itself once capture_pageleave is true, so it + // reaches sanitizeEvent carrying PostHog's own automatic properties + // ($pathname among them) rather than the hand-built "path" property that + // capturePageview() passes for $pageview. sanitizeEvent has to rebuild it + // from the same allowlist; before that branch existed it fell through to + // the closing `return null` and every $pageleave was dropped silently, + // which is why flipping capture_pageleave on its own fixes nothing. + assert.deepEqual( + sanitizeEvent({ + event: "$pageleave", + properties: { + $pathname: "/?secret=1#fragment", + $current_url: "https://codeswhat.com/?secret=1#fragment", + token: "phc_public-token_123", + distinct_id: "$posthog_cookieless", + $cookieless_mode: true, + $process_person_profile: false, + ...COOKIELESS_HASH_PROPERTIES, + }, + }), + { + event: "$pageleave", + properties: { + token: "phc_public-token_123", + distinct_id: "$posthog_cookieless", + $cookieless_mode: true, + $process_person_profile: false, + schema_version: 1, + site: "codeswhat", + surface: "marketing", + path: "/", + $current_url: "https://codeswhat.com/", + $pathname: "/", + ...COOKIELESS_HASH_PROPERTIES, + }, + }, + ); +}); + +test("$pageleave requires and forwards the cookieless server-hash fields, same as $pageview", () => { + const validProperties = { + $pathname: "/", + token: "phc_public-token_123", + $cookieless_mode: true, + $process_person_profile: false, + ...COOKIELESS_HASH_PROPERTIES, + }; + + const result = sanitizeEvent({ event: "$pageleave", properties: validProperties }); + assert.ok(result); + assert.equal(result.properties.$raw_user_agent, COOKIELESS_HASH_PROPERTIES.$raw_user_agent); + assert.equal(result.properties.$host, COOKIELESS_HASH_PROPERTIES.$host); + + for (const missingKey of Object.keys(COOKIELESS_HASH_PROPERTIES)) { + const withoutField = { ...validProperties }; + delete withoutField[missingKey as keyof typeof withoutField]; + assert.equal( + sanitizeEvent({ event: "$pageleave", properties: withoutField }), + null, + `sanitizeEvent must drop $pageleave events missing ${missingKey}`, + ); + } +}); + +test("$pathname never diverges from the allowlisted path", () => { + // $pathname exists so PostHog's Web analytics Page / Entry page / Exit + // page tables resolve at all; those tables read $pathname and nothing + // else. It must stay bound to the sanitized `path`: if it ever carried the + // raw pathname instead, every unlisted route would leak into the + // analytics project past ALLOWED_ROUTES. + for (const rawPath of ["/", "/about", "/pricing?secret=1#fragment", "//evil.example/"]) { + for (const event of ["$pageview", "$pageleave"] as const) { + const result = sanitizeEvent({ + event, + properties: { + path: rawPath, + token: "phc_public-token_123", + $cookieless_mode: true, + $process_person_profile: false, + ...COOKIELESS_HASH_PROPERTIES, + }, + }); + assert.ok(result, `sanitizeEvent should accept ${event} for ${rawPath}`); + assert.equal(result.properties.$pathname, result.properties.path); + assert.equal( + String(result.properties.$pathname).includes("secret"), + false, + `unlisted route leaked into $pathname for ${rawPath}`, + ); + } + } +}); + test("CTA events are limited to the initial GitHub placements", () => { assert.deepEqual(ALLOWED_CTA_IDS, ["github_org"]); assert.deepEqual( @@ -208,6 +303,7 @@ test("the pinned PostHog before_send pipeline keeps the required cookieless enve surface: "marketing", path: "/", $current_url: "https://codeswhat.com/", + $pathname: "/", ...COOKIELESS_HASH_PROPERTIES, }, }); From 1a40882165ddb094ed185bb3c2d433ce311dc703 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:01:18 -0400 Subject: [PATCH 06/12] fix(seo): repair JSON-LD logo 404 and double-slash base URLs - fix(seo): point Organization.logo at /icon-512x512.png; the referenced /logos/codeswhat-logo-green.png never existed, so crawlers got a 404 - fix(seo): strip trailing slashes from BASE_URL and reuse it in robots.ts and sitemap.ts, so a NEXT_PUBLIC_SITE_URL set with a trailing slash can't emit //sitemap.xml-style URLs - chore(seo): disallow /studio/ in robots.txt; the capture pages already 404 in production but the exclusion shouldn't depend on that guard - chore(seo): 308 the stable *.vercel.app production aliases to codeswhat.com instead of serving duplicate content - fix(api): stop forwarding EmailOctopus error detail to subscribe clients; log it server-side and return a fixed message --- frontend/app/api/subscribe/route.ts | 5 +++-- frontend/app/robots.ts | 7 +++---- frontend/app/sitemap.ts | 19 +++---------------- frontend/lib/site-config.ts | 9 ++++++--- frontend/next.config.ts | 18 ++++++++++++++++++ 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/frontend/app/api/subscribe/route.ts b/frontend/app/api/subscribe/route.ts index 6dd1bd9..0f57d09 100644 --- a/frontend/app/api/subscribe/route.ts +++ b/frontend/app/api/subscribe/route.ts @@ -213,10 +213,11 @@ export async function POST(request: Request) { ); } - // Handle other errors + // Handle other errors. Log the upstream detail server-side only; never + // pass a third-party response field through to the client. console.error("EmailOctopus error:", data); return NextResponse.json( - { error: data.detail || "Failed to subscribe. Please try again." }, + { error: "Failed to subscribe. Please try again." }, { status: response.status }, ); } diff --git a/frontend/app/robots.ts b/frontend/app/robots.ts index 530837d..8028f5c 100644 --- a/frontend/app/robots.ts +++ b/frontend/app/robots.ts @@ -1,14 +1,13 @@ import type { MetadataRoute } from "next"; +import { BASE_URL } from "@/lib/site-config"; export default function robots(): MetadataRoute.Robots { - const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://codeswhat.com"; - return { rules: { userAgent: "*", allow: "/", - disallow: ["/api/", "/admin/", "/_next/"], + disallow: ["/api/", "/admin/", "/_next/", "/studio/"], }, - sitemap: `${baseUrl}/sitemap.xml`, + sitemap: `${BASE_URL}/sitemap.xml`, }; } diff --git a/frontend/app/sitemap.ts b/frontend/app/sitemap.ts index 99559bc..36dfa68 100644 --- a/frontend/app/sitemap.ts +++ b/frontend/app/sitemap.ts @@ -1,35 +1,22 @@ import type { MetadataRoute } from "next"; +import { BASE_URL } from "@/lib/site-config"; export default function sitemap(): MetadataRoute.Sitemap { - const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://codeswhat.com"; - // Add your routes here as you create new pages const routes = [ { - url: baseUrl, + url: BASE_URL, lastModified: new Date(), changeFrequency: "weekly" as const, priority: 1, }, // Example of additional pages (uncomment and modify as needed): // { - // url: `${baseUrl}/about`, + // url: `${BASE_URL}/about`, // lastModified: new Date(), // changeFrequency: 'monthly' as const, // priority: 0.8, // }, - // { - // url: `${baseUrl}/services`, - // lastModified: new Date(), - // changeFrequency: 'monthly' as const, - // priority: 0.8, - // }, - // { - // url: `${baseUrl}/contact`, - // lastModified: new Date(), - // changeFrequency: 'yearly' as const, - // priority: 0.5, - // }, ]; return routes; diff --git a/frontend/lib/site-config.ts b/frontend/lib/site-config.ts index 9b2d337..0d2a652 100644 --- a/frontend/lib/site-config.ts +++ b/frontend/lib/site-config.ts @@ -24,7 +24,7 @@ export const SITE_CONFIG = { /** Twitter/X profile URL (used in JSON-LD sameAs). */ twitterUrl: "https://x.com/codeswhat", /** Brand logo in /public. */ - logo: "/logos/codeswhat-logo-green.png", + logo: "/icon-512x512.png", /** Maker credit. */ author: { name: "Scott Benson", url: "https://scottbenson.dev" }, } as const; @@ -34,9 +34,12 @@ export const GITHUB_URL = `https://github.com/${githubOrg}`; /** * Site base URL. Prefers NEXT_PUBLIC_SITE_URL (Vercel/preview deploys), falls * back to the production domain. `||` (not `??`) so a set-but-empty env var - * falls back too. + * falls back too. Trailing slashes are stripped so path concatenation can't + * produce `//` URLs when the env var is set with one. */ -export const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || `https://${SITE_CONFIG.domain}`; +export const BASE_URL = ( + process.env.NEXT_PUBLIC_SITE_URL || `https://${SITE_CONFIG.domain}` +).replace(/\/+$/, ""); /** Social/OG card โ€” generated by scripts/generate-og.mjs into public/og.png. */ export const OG_IMAGE = { diff --git a/frontend/next.config.ts b/frontend/next.config.ts index a0be994..f70aeed 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -6,6 +6,24 @@ const posthogProxy = "https://e.codeswhat.com"; const themeScriptHash = `sha256-${createHash("sha256").update(THEME_INIT_SCRIPT).digest("base64")}`; const nextConfig: NextConfig = { + async redirects() { + return [ + // The stable production *.vercel.app aliases serve the full site; send + // them to the canonical domain. Hash-suffixed preview URLs don't match. + { + source: "/:path*", + has: [ + { + type: "host", + value: + "(codeswhat-website|codeswhat-website-codeswhat|codeswhat-website-git-main-codeswhat)\\.vercel\\.app", + }, + ], + destination: "https://codeswhat.com/:path*", + permanent: true, + }, + ]; + }, async headers() { return [ { From e1f7a854706eb2b98075d085268f2bdbdd6c55dc Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:15:15 -0400 Subject: [PATCH 07/12] build(deps): bump next to ^16.2.11 to clear all 35 Dependabot alerts One-line range bump; npm resolves next 16.3.3, which also pulls the patched transitive versions: postcss 8.5.23, nanoid 3.3.18, sharp 0.35.4. npm audit now reports zero vulnerabilities. No code changes needed: the app has no middleware, rewrites, server actions, CSP nonces, or next/image usage, so none of the fixed CVEs required app-side work. --- frontend/package-lock.json | 513 +++++++++++++++++++++---------------- frontend/package.json | 2 +- 2 files changed, 293 insertions(+), 222 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 966f6f8..aac4652 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,7 +15,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", - "next": "^16.1.6", + "next": "^16.2.11", "posthog-js": "1.417.0", "react": "19.2.4", "react-dom": "19.2.4", @@ -243,9 +243,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "optional": true, "dependencies": { @@ -264,9 +264,9 @@ } }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, "engines": { @@ -274,9 +274,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", "cpu": [ "arm64" ], @@ -286,19 +286,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ "x64" ], @@ -308,19 +308,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ "arm64" ], @@ -334,9 +353,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], @@ -350,12 +369,15 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -366,12 +388,15 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -382,12 +407,15 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -398,12 +426,15 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -414,12 +445,15 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -430,12 +464,15 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -446,12 +483,15 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -462,12 +502,15 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -478,204 +521,244 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.3" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.4" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", "cpu": [ "arm64" ], @@ -685,16 +768,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ "ia32" ], @@ -704,16 +787,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], @@ -723,7 +806,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -811,15 +894,15 @@ } }, "node_modules/@next/env": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", - "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.3.tgz", + "integrity": "sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", - "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.3.tgz", + "integrity": "sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==", "cpu": [ "arm64" ], @@ -833,9 +916,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.3.tgz", + "integrity": "sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==", "cpu": [ "x64" ], @@ -849,12 +932,15 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.3.tgz", + "integrity": "sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -865,12 +951,15 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.3.tgz", + "integrity": "sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -881,12 +970,15 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.3.tgz", + "integrity": "sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -897,12 +989,15 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.3.tgz", + "integrity": "sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -913,9 +1008,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.3.tgz", + "integrity": "sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==", "cpu": [ "arm64" ], @@ -929,9 +1024,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.3.tgz", + "integrity": "sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==", "cpu": [ "x64" ], @@ -1112,9 +1207,9 @@ "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -2294,9 +2389,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -2312,16 +2407,16 @@ } }, "node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.3.tgz", + "integrity": "sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==", "license": "MIT", "dependencies": { - "@next/env": "16.1.6", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "@next/env": "16.3.3", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", + "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "bin": { @@ -2331,15 +2426,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.3.3", + "@next/swc-darwin-x64": "16.3.3", + "@next/swc-linux-arm64-gnu": "16.3.3", + "@next/swc-linux-arm64-musl": "16.3.3", + "@next/swc-linux-x64-gnu": "16.3.3", + "@next/swc-linux-x64-musl": "16.3.3", + "@next/swc-win32-arm64-msvc": "16.3.3", + "@next/swc-win32-x64-msvc": "16.3.3", + "sharp": "^0.35.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -2364,34 +2459,6 @@ } } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2421,10 +2488,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -2441,7 +2507,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2571,9 +2637,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "optional": true, "bin": { @@ -2584,48 +2650,53 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shebang-command": { diff --git a/frontend/package.json b/frontend/package.json index 0edcf48..421ae9b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,7 +33,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", - "next": "^16.1.6", + "next": "^16.2.11", "posthog-js": "1.417.0", "react": "19.2.4", "react-dom": "19.2.4", From ac19fbb9e765330c77ed73f8e19e9ff605e7a245 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:16:00 -0400 Subject: [PATCH 08/12] build(deps): regenerate next-env.d.ts for next 16.3 - build(deps): pick up the root-params.d.ts reference next 16.3 adds - ci(hooks): pass --no-errors-on-unmatched to the biome pre-commit job so committing only biome-ignored files (like next-env.d.ts) doesn't fail --- frontend/next-env.d.ts | 1 + lefthook.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 9edff1c..ce4e94a 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,6 +1,7 @@ /// /// import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/lefthook.yml b/lefthook.yml index 4f383f6..59ceba1 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -4,7 +4,7 @@ pre-commit: - name: biome root: frontend/ glob: "*.{js,ts,jsx,tsx,json,css}" - run: npx @biomejs/biome check --write {staged_files} + run: npx @biomejs/biome check --write --no-errors-on-unmatched {staged_files} stage_fixed: true - name: typecheck From faea446b4e878876f8bb701a04a601c5b6a0511a Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:30:58 -0400 Subject: [PATCH 09/12] docs(roadmap): track web-analytics table coverage follow-ups (ops X37) --- ROADMAP.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 03b65bf..b3d61e6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -38,6 +38,27 @@ icon/image story coherent off the back of it: - [ ] Consider using the minted assets as the real project icons / OG images / repo avatars - [ ] Light + dark (inverted) variants where it makes sense +## Web-analytics table coverage (2026-08-28, ops X37) + +Production events carry no `$pathname` and no `$pageleave`, so PostHog's Page / +Entry / Exit tables render empty and every session counts as a zero-duration +bounce. The wiring for both landed on dev as PR #60 (sanitizer allowlists +`$pageleave` and stamps `$pathname` from the already-sanitized `path`) and +ships with the next promotion โ€” the items below are what's left after that. + +- [ ] Verify post-deploy that `$pathname` shows on fresh pageviews and the + Page/Entry/Exit tables populate (board: CodesWhat Sites Health, + project 558033, dashboard 2044260) +- [ ] `$pageleave` delivery rate: drydock lands only ~17.6% of pageview volume + (sendBeacon unload envelopes can miss `$raw_user_agent`/`$host`; relaxing + the sanitizer guard just moves the drop to cookieless ingestion's silent + `cookieless_missing_user_agent` discard). Their lane is measuring the + real fix โ€” copy their answer, don't relax the guard here. +- [ ] Acquisition data (UTM params, referrer, geo) is an open policy question + for Scott to answer directly in this lane; a relayed approval was + retracted 2026-08-28. No `save_campaign_params`, no referrer work until + then. + ## Telemetry and badge audit (2026-08-14) - The main website has no visible external provider badge surface to migrate. From 25e49cdf0ca81b2f8ac4bd55420aaa612f6dccaa Mon Sep 17 00:00:00 2001 From: Scott Benson <80784472+scttbnsn@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:39:33 -0400 Subject: [PATCH 10/12] chore: ignore .claude/ with a tracked line (#59) It was covered only by .git/info/exclude, which protects one clone and nobody else's. Without a tracked line, `git add -A` in the parent stages a nested worktree as an embedded gitlink and `git clean -ffd` deletes it. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e5df7b9..1344317 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ exports/**/*.gif exports/**/*.apng.png frontend/.capture-frames/ + +# Agent worktrees and local agent state +.claude/ From 9025b2fce507f272fd5e96dda61256247cdadfbf Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:39:52 -0400 Subject: [PATCH 11/12] docs(roadmap): point acquisition-data item at the ops analytics standard --- ROADMAP.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b3d61e6..864f0c6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -54,10 +54,15 @@ ships with the next promotion โ€” the items below are what's left after that. the sanitizer guard just moves the drop to cookieless ingestion's silent `cookieless_missing_user_agent` discard). Their lane is measuring the real fix โ€” copy their answer, don't relax the guard here. -- [ ] Acquisition data (UTM params, referrer, geo) is an open policy question - for Scott to answer directly in this lane; a relayed approval was - retracted 2026-08-28. No `save_campaign_params`, no referrer work until - then. +- [ ] Acquisition data: decided org-wide in `CodesWhat/ops` + `standards/analytics.md` ("Acquisition data and consent", as of + `c161ebc`) โ€” that file is the authority. Shape: `save_campaign_params: + true` with `gclid`/`fbclid`/`msclkid` excluded; `save_referrer: true` + with the sanitizer forwarding `$referring_domain` only (drop it unless + it's a bare hostname, never copy `$referrer`); geo stays off (cookieless + strips the IP upstream, PostHog #48660). No banner needed. Implement + AFTER the `$pathname`/`$pageleave` promotion is verified in production, + so the two changes are separately attributable. ## Telemetry and badge audit (2026-08-14) From 5aade3b7ad7a41612d7fad8da9d60d1b04a2deab Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:41:05 -0400 Subject: [PATCH 12/12] docs(roadmap): pageleave ratio is structural; note the bot-detection canary caveat --- ROADMAP.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 864f0c6..a315e29 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -49,11 +49,17 @@ ships with the next promotion โ€” the items below are what's left after that. - [ ] Verify post-deploy that `$pathname` shows on fresh pageviews and the Page/Entry/Exit tables populate (board: CodesWhat Sites Health, project 558033, dashboard 2044260) -- [ ] `$pageleave` delivery rate: drydock lands only ~17.6% of pageview volume - (sendBeacon unload envelopes can miss `$raw_user_agent`/`$host`; relaxing - the sanitizer guard just moves the drop to cookieless ingestion's silent - `cookieless_missing_user_agent` discard). Their lane is measuring the - real fix โ€” copy their answer, don't relax the guard here. +- [ ] `$pageleave` volume note (resolved 2026-08-28, standard `19a0af1`): a + pageleave-to-pageview ratio well under 100% is structural, not a bug โ€” + `$pageview` fires per client-side route change, `$pageleave` once per + document lifetime. drydock measured the envelope: the sanitizer guard + eats nothing. Don't "fix" the ratio here. +- [ ] Post-deploy canary caveat: posthog-js bot detection (`isLikelyBot` + checks `navigator.webdriver` and `userAgentData.brands`) may silently + drop Playwright-driven visits before `before_send` runs; contested + (portwing's landed), so a Playwright canary's silence proves nothing + either way. Verify with a real browser or by reading the deployed chunk + for the property names. - [ ] Acquisition data: decided org-wide in `CodesWhat/ops` `standards/analytics.md` ("Acquisition data and consent", as of `c161ebc`) โ€” that file is the authority. Shape: `save_campaign_params: