From 3f1244b108f614e31ede560afb7fda2e49f8e1b5 Mon Sep 17 00:00:00 2001 From: Jan Gregor Emge-Triebel Date: Tue, 20 Oct 2026 16:31:05 +0200 Subject: [PATCH 1/5] Fathom: stop attributing navigations to the previous page With canonical: true, the script read the path from the canonical link when the router called pushState, before Nuxt had updated it. Every client-side page view, and every departure ping, went to the page the visitor had just left. Each page sets its canonical to its own path, so reading location instead loses nothing. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RE7Gq8fzzVAfBahzvcUqTP --- nuxt-app/plugins/fathom.client.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nuxt-app/plugins/fathom.client.ts b/nuxt-app/plugins/fathom.client.ts index f1ad0b0f..dec8bbc0 100644 --- a/nuxt-app/plugins/fathom.client.ts +++ b/nuxt-app/plugins/fathom.client.ts @@ -6,6 +6,9 @@ export default defineNuxtPlugin(() => { url: 'https://ziggy-stardust-six.programmier.bar/script.js', spa: 'history', honorDNT: false, - canonical: true, + // Must stay false. The script records a client-side navigation as soon as the router calls + // `pushState`, before Nuxt has updated the canonical link, so reading the canonical would + // attribute every navigation to the previous page. + canonical: false, }) }) From 14200b6ccd0f9d32a2f402ae8e320b3997f9b84c Mon Sep 17 00:00:00 2001 From: Jan Gregor Emge-Triebel Date: Tue, 20 Oct 2026 16:31:05 +0200 Subject: [PATCH 2/5] Fathom: only track on www.programmier.bar The plugin loads on previews and local runs too. With canonical: true they had reported as production, since WEBSITE_URL names production there. Without it they would report their own hostnames into the same site. includedDomains makes the script send nothing elsewhere. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RE7Gq8fzzVAfBahzvcUqTP --- nuxt-app/plugins/fathom.client.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nuxt-app/plugins/fathom.client.ts b/nuxt-app/plugins/fathom.client.ts index dec8bbc0..e5d11925 100644 --- a/nuxt-app/plugins/fathom.client.ts +++ b/nuxt-app/plugins/fathom.client.ts @@ -10,5 +10,8 @@ export default defineNuxtPlugin(() => { // `pushState`, before Nuxt has updated the canonical link, so reading the canonical would // attribute every navigation to the previous page. canonical: false, + // Previews and local runs load this plugin too. Without the canonical link they would report + // their own hostname into the production site's totals. + includedDomains: ['www.programmier.bar'], }) }) From 2d09f77b022be03437549f573144ae88ca7b890e Mon Sep 17 00:00:00 2001 From: Jan Gregor Emge-Triebel Date: Tue, 20 Oct 2026 16:31:05 +0200 Subject: [PATCH 3/5] Plan: record the Fathom attribution fix Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RE7Gq8fzzVAfBahzvcUqTP --- docs/dependency-upgrade-plan.md | 62 ++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/docs/dependency-upgrade-plan.md b/docs/dependency-upgrade-plan.md index b729ae80..e0ccdfc3 100644 --- a/docs/dependency-upgrade-plan.md +++ b/docs/dependency-upgrade-plan.md @@ -2502,14 +2502,60 @@ browser-support decision nobody made. declared at the 3.7.2 already installed, and the plugin calls `load()` with the same options. In a browser, both builds load the same script with the same attributes and attempt the same beacons (blocked in the test, so nothing reached production analytics). -- [ ] ⚠️ **Client-side navigations are counted as page views of the previous page, twice.** Found - while verifying the Fathom swap, and identical on `main`. After clicking from `/` to `/podcast`, - Fathom sends **two** beacons, both with `p=/`, although `location.pathname` and the canonical - link already read `/podcast` a moment later. The likely cause is `canonical: true`: Fathom reads - the canonical link when the route changes, before Nuxt updates the head. So in-site navigations - are probably attributed to the page just left. Confirm against the Fathom dashboard before - fixing; the fix is probably `canonical: false`, or a manual `trackPageview()` after the head - updates. +- [x] ✅ **Fixed 2026-10-20 with `canonical: false`** in `plugins/fathom.client.ts`. The diagnosis + below was half right. The cause was `canonical: true`, but nothing was counted twice. + + **What the script does.** Our Fathom script (fetched from the custom domain and read) patches + `history.pushState`. On each call it sends a departure ping for the page being left (`dp=1`, with + the time spent) and then a page view. With `canonical: true` the page view takes its path from + `link[rel=canonical]`. The router calls `pushState` before Nuxt updates the head, so the canonical + link still names the previous page. + + **Reproduced** in a production build, with every request to the Fathom domain intercepted and + answered locally, so nothing reached the real analytics: + + | step | actually on | page view sent, before | after the fix | + | --- | --- | --- | --- | + | click → `/podcast` | `/podcast` | `/` | `/podcast` | + | click → episode | episode | `/podcast` | episode | + | back | `/podcast` | episode | `/podcast` | + | click → `/meetup` | `/meetup` | `/podcast` | `/meetup` | + + The departure pings carried the same stale path, so time on page was misattributed too. Entry pages + were counted correctly. Every page reached by a click was counted only when the visitor left it, + under the previous page's name, and the last page of a session was never counted. The "two + beacons" were a departure ping and a page view, which is correct behaviour. + + **Nothing is lost by turning it off.** Every page builds its canonical as `WEBSITE_URL` plus + `route.path` or its own fixed path, so canonical and URL never differ. Production serves + `www.programmier.bar` (the apex 308-redirects there), so the reported hostname is unchanged. + + **Turning it off exposed a second problem, fixed with `includedDomains: ['www.programmier.bar']`.** + The plugin loads in every environment, and `WEBSITE_URL` names production on Vercel previews too + (a preview renders ``). So with + `canonical: true`, previews and local runs had reported their page views **as production**, + hostname included, indistinguishable from real traffic. Without the canonical, they would have + reported their own hostnames into the same Fathom site instead. The script's + `data-included-domains`, which `fathom-client` sets from `includedDomains`, makes it send nothing + on any other host. Verified in both directions in a production build: + + - With `localhost` temporarily added to the list, the script tag carries + `data-included-domains="www.programmier.bar,localhost"`, and every step of the table above sends + the correct path. + - With the final list, the same five steps send **nothing**, including the initial page view. + + Whether the Fathom site itself has allowed domains configured server-side was not checked. It + would only have caught the second problem, never the first, because the previews claimed the + production hostname. **Not verifiable locally:** that production still sends. After deploying, a + network check on `www.programmier.bar` should show one page view per navigation. + + **Historical data:** `canonical: true` dates from 2022-05-05, and from the Nuxt 3 migration + (2023-02-10) in its current form. Per-page numbers for in-site navigation are unreliable for at + least that period. + + ~~**Client-side navigations are counted as page views of the previous page, twice.**~~ Found while + verifying the Fathom swap, and identical on `main`. After clicking from `/` to `/podcast`, Fathom + sends **two** beacons, both with `p=/`. The likely cause is `canonical: true`. - [ ] 💳 **Move the Stripe API version `2026-02-25.clover` → `2026-07-29.dahlia`.** One line in `server/utils/stripe.ts`, and it removes the cast there along with the types-describe-dahlia / wire-speaks-clover mismatch the pin introduced. From edd4a21a56a2033a446ace4f5b9c4460d05f631c Mon Sep 17 00:00:00 2001 From: Jan Gregor Emge-Triebel Date: Tue, 20 Oct 2026 16:43:06 +0200 Subject: [PATCH 4/5] Move the Fathom hostname list into config.ts Explicit rather than derived from WEBSITE_URL, which is set per environment (localhost in .env.example), so a derived list would track the local runs it is meant to exclude. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RE7Gq8fzzVAfBahzvcUqTP --- nuxt-app/config.ts | 5 +++++ nuxt-app/plugins/fathom.client.ts | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/nuxt-app/config.ts b/nuxt-app/config.ts index 196ce71c..35c7311f 100644 --- a/nuxt-app/config.ts +++ b/nuxt-app/config.ts @@ -26,6 +26,11 @@ export const BUZZSPROUT_TRACKING_URL = 'https://op3.dev/e/https://www.podtrac.co // so different listeners can share it — hence a time window rather than one vote per hash forever. export const VOTE_COOLDOWN_MINUTES = 10 +// Fathom only tracks on these hostnames, so previews and local runs stay out of the production site. +// Deliberately not derived from WEBSITE_URL: that is set per environment (`.env.example` uses +// localhost), so deriving it would track exactly the runs this list exists to exclude. +export const FATHOM_INCLUDED_DOMAINS = ['www.programmier.bar'] + // programmier.bar export const WEBSITE_URL = process.env.WEBSITE_URL || 'https://www.programmier.bar' export const WEBSITE_NAME = 'programmier.bar' diff --git a/nuxt-app/plugins/fathom.client.ts b/nuxt-app/plugins/fathom.client.ts index e5d11925..ab7c9d6f 100644 --- a/nuxt-app/plugins/fathom.client.ts +++ b/nuxt-app/plugins/fathom.client.ts @@ -1,4 +1,5 @@ import { defineNuxtPlugin } from '#app' +import { FATHOM_INCLUDED_DOMAINS } from '~/config' import { load } from 'fathom-client' export default defineNuxtPlugin(() => { @@ -10,8 +11,6 @@ export default defineNuxtPlugin(() => { // `pushState`, before Nuxt has updated the canonical link, so reading the canonical would // attribute every navigation to the previous page. canonical: false, - // Previews and local runs load this plugin too. Without the canonical link they would report - // their own hostname into the production site's totals. - includedDomains: ['www.programmier.bar'], + includedDomains: FATHOM_INCLUDED_DOMAINS, }) }) From e09bf9d441d2d4b7fb24d3bb4a2e37b04bdd862b Mon Sep 17 00:00:00 2001 From: Jan Gregor Emge-Triebel Date: Tue, 20 Oct 2026 16:43:06 +0200 Subject: [PATCH 5/5] Plan: record the Fathom config review Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RE7Gq8fzzVAfBahzvcUqTP --- docs/dependency-upgrade-plan.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/dependency-upgrade-plan.md b/docs/dependency-upgrade-plan.md index e0ccdfc3..4c06f9b0 100644 --- a/docs/dependency-upgrade-plan.md +++ b/docs/dependency-upgrade-plan.md @@ -2530,7 +2530,11 @@ browser-support decision nobody made. `route.path` or its own fixed path, so canonical and URL never differ. Production serves `www.programmier.bar` (the apex 308-redirects there), so the reported hostname is unchanged. - **Turning it off exposed a second problem, fixed with `includedDomains: ['www.programmier.bar']`.** + **Turning it off exposed a second problem, fixed with `includedDomains`**, set from + `FATHOM_INCLUDED_DOMAINS` in `config.ts` (`['www.programmier.bar']`). Review (#279) asked to derive + it from `WEBSITE_URL` instead. That would be wrong: `WEBSITE_URL` is set per environment, and + `.env.example` sets it to `http://localhost:3000`, so a derived list would track exactly the local + runs it exists to exclude. The constant is explicit and says why. The plugin loads in every environment, and `WEBSITE_URL` names production on Vercel previews too (a preview renders ``). So with `canonical: true`, previews and local runs had reported their page views **as production**,