Skip to content
Merged
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
66 changes: 58 additions & 8 deletions docs/dependency-upgrade-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -2502,14 +2502,64 @@ 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`**, 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 `<link rel="canonical" href="https://www.programmier.bar/podcast">`). 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.
Expand Down
5 changes: 5 additions & 0 deletions nuxt-app/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
// 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'
Expand Down Expand Up @@ -108,7 +113,7 @@
export const DOWNLOAD_PODCAST_EVENT_ID = 'DOWNLOAD_PODCAST'
export const OPEN_MASTODON_EVENT_ID = 'OPEN_MASTODON'
export const OPEN_LINKEDIN_EVENT_ID = 'OPEN_LINKEDIN'
export const OPEN_TWITTER_EVENT_ID = 'OPEN_TWITTER'

Check warning on line 116 in nuxt-app/config.ts

View workflow job for this annotation

GitHub Actions / nuxt-app-test

Unused export

Exported value 'OPEN_TWITTER_EVENT_ID' is never imported by other modules. If this export is part of a public API, consider adding it to the entry configuration. Otherwise, remove the export keyword or delete the declaration.
export const OPEN_BLUESKY_EVENT_ID = 'OPEN_BLUESKY'
export const OPEN_GITHUB_EVENT_ID = 'OPEN_GITHUB'
export const OPEN_MEETUP_EVENT_ID = 'OPEN_MEETUP'
Expand Down Expand Up @@ -146,6 +151,6 @@
export const CLICK_SCROLL_DOWN_MOUSE_EVENT_ID = 'CLICK_SCROLL_DOWN_MOUSE'
export const CLICK_SCROLL_LEFT_ARROW_EVENT_ID = 'CLICK_SCROLL_LEFT_ARROW'
export const CLICK_SCROLL_RIGHT_ARROW_EVENT_ID = 'CLICK_SCROLL_RIGHT_ARROW'
export const OPEN_PICK_OF_THE_DAY_EVENT_ID = 'OPEN_PICK_OF_THE_DAY'

Check warning on line 154 in nuxt-app/config.ts

View workflow job for this annotation

GitHub Actions / nuxt-app-test

Unused export

Exported value 'OPEN_PICK_OF_THE_DAY_EVENT_ID' is never imported by other modules. If this export is part of a public API, consider adding it to the entry configuration. Otherwise, remove the export keyword or delete the declaration.
export const SEND_CONTACT_FORM_EVENT_ID = 'SEND_CONTACT_FORM'
export const OPEN_GOOGLE_MAPS_EVENT_ID = 'OPEN_GOOGLE_MAPS'
7 changes: 6 additions & 1 deletion nuxt-app/plugins/fathom.client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { defineNuxtPlugin } from '#app'
import { FATHOM_INCLUDED_DOMAINS } from '~/config'
import { load } from 'fathom-client'

export default defineNuxtPlugin(() => {
load('XSJTTACD', {
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,
includedDomains: FATHOM_INCLUDED_DOMAINS,
})
})
Loading