Telemetry for traction: Umami + Fathom across the splash fleet
Eight sites can finally tell us if anyone's reading. Two analytics platforms, one tiny component pattern, and a production-only gate that keeps localhost out of the dashboard.
Changelog - 2026-05-06 (#1)
Telemetry for traction: Umami + Fathom across the splash fleet
Why Care?
We're shipping splash pages and project sites at a faster clip than we've ever shipped marketing surfaces before — Astro Knots itself, content-farm, memopop, the FullStack VC dojo, the personal sites. Until today, none of them could tell us a thing. People could be landing, bouncing, or not landing at all, and we'd have the same evidence either way.
The point of analytics for us isn't growth-hacking. It's showing traction to the people who care — clients deciding whether to invest, audiences deciding whether to follow, and our own future selves deciding whether a thing is worth doubling down on. A splash page with no telemetry is a story without a witness.
Two-pass approach by design. Pass 1 (today): every site emits passive pageview data, gated to production so dev traffic doesn't pollute the dashboards. Pass 2 (later, when we have a baseline): clicks, CTAs, form submissions, the "what do people actually do here" instrumentation. Premature instrumentation is noise; you need a week of "what does normal look like" first.
What's New?
Eight sites instrumented, three of them with both Umami (top-of-funnel detail) and Fathom (lightweight + unlimited).
A reusable per-site
<Analytics />component pattern — six lines, IDs hardcoded, gated onimport.meta.env.PROD.Two new prompts in
context-v/prompts/that capture the convergence: the Pass 1 setup (this work) and the Pass 2 deeper-tracking spec (next pass — clicks, flows, goals).Production-only by construction. Vite statically replaces
import.meta.env.PRODat build time, so dev servers literally never emit the script tags. Zero runtime cost, zero localhost noise.
The cool table — fleet status as of today
| Site | Repo path | Platforms | Wired into |
lossless.group | site/ | Umami + Fathom | Layout.astro + 3 slide-embed pages |
the-water-foundation.com | astro-knots/sites/twf_site | Umami + Fathom | BoilerPlateHTML.astro + OneSlideDeck.astro + MarkdownSlideDeck.astro (3 layouts) |
mpstaton.com | astro-knots/sites/mpstaton-site | Umami + Fathom | BaseLayout.astro + PromotionDeckLayout.astro |
fullstack-vc.com | astro-knots/sites/fullstack-vc | Fathom | BoilerPlateHTML.astro |
hypernova-site | astro-knots/sites/hypernova-site | Fathom | BoilerPlateHTML.astro |
lossless-group.github.io/astro-knots | astro-knots/splash | Fathom | BaseLayout.astro |
lossless-group.github.io/content-farm | content-farm/splash | Fathom | BaseLayout.astro |
lossless-group.github.io/memopop-ai | ai-labs/memopop-ai | Fathom | BaseLayout.astro |
Allocation rationale: Umami's free tier caps at three sites, so the three highest-priority surfaces (Lossless, the Water Foundation, mpstaton) get both platforms. Fathom is unlimited on traffic-based pricing, so it covers everything. Where both are present, parallel event names will let us cross-check the dashboards in Pass 2.
The component pattern
Six lines per site. Same shape everywhere; only the IDs differ.
---
// Analytics — Pass 1: passive scripts, production-only.
const isProd = import.meta.env.PROD;
---
{isProd && (
<>
<script is:inline defer src="https://cloud.umami.is/script.js" data-website-id="…"></script>
<script is:inline src="https://cdn.usefathom.com/script.js" data-site="…" defer></script>
</>
)}Two small but load-bearing decisions in those six lines:
is:inlineon the script tags. Astro's default behavior is to bundle<script>tags through Vite. For external CDN URLs we just want a literal<script src="https://…">in the rendered HTML, untouched.is:inlineis the opt-out. Without it, Astro might attempt to process the external URL and surprise us.{import.meta.env.PROD && (…)}instead of a runtime check. Vite does static replacement at build time. Inastro build, the literal becomes{true && (…)}(the script ships); inastro dev, it becomes{false && (…)}(the script is gone — not just unloaded, not in the HTML at all). No runtime overhead, no tag that gets cancelled mid-load, no localhost analytics pollution.
The two-prompt convergence
Two files now live in context-v/prompts/:
Setup-Analytics-Across-Sites.md— the Pass 1 prompt. The original "we have eight sites and two platforms, discuss" framing is preserved verbatim; below it sits a new Convergence section that locks in the platform allocation, the production gate, the per-site placement, and the not-in-this-pass deferrals. As of today the convergence section also reflects the as-shipped state with concrete file paths.Implement-Deeper-Analytics-Tracking.md— the Pass 2 prompt. Doesn't run yet. Captures the proposed event taxonomy (cta-primary,cta-secondary,outbound-click,internal-nav,form-submit,download,read-complete,tool-open), per-site instrumentation targets, a sketch of the cross-platformanalytics.tshelper, and the open design questions (PII guardrails, scroll-completion threshold, outbound-click signal-to-noise on lossless.group's article pages). Hard gate at the top: don't start until Pass 1 has shipped + a week of baseline traffic.
The deliberate split serves two purposes: it forces us to actually look at baseline numbers before instrumenting, and it lets a future agent (human or AI) pick up Pass 2 cold without re-deriving the platform decisions.
Architectural decisions worth preserving
Per-site
<Analytics />component, not a shared package. Each site is its own Astro app and (for the astro-knots sites) its own deployable submodule with its own analytics IDs. A shared package would either need IDs as props (which means every layout passes them — noise) or environment variables (which means more.envplumbing). Six lines of copied code per site is not a debt; it's a fact about the data shape.Cover every head-bearing layout, even when it means duplication.
twf_sitehas three layouts that each emit their own<html>(BoilerPlateHTML,OneSlideDeck,MarkdownSlideDeck);mpstaton-sitehas two (BaseLayout,PromotionDeckLayout). Rather than refactor those into a single<BaseHead>partial mid-flight, we instrumented all of them. Both Umami and Fathom dedupe pageviews at the session layer, so the worst case from any accidental double-rendering is a non-issue. The duplication is logged as Pass 2 cleanup, not a Pass 1 blocker.Skip debug pages on lossless.group, instrument the slide-embed pages. The
site/repo has a few pages that emit their own<head>(debug-mocs,test-tag-mocs,debug/sequential-section,backlink.astro) — these are utility/debug surfaces, no traffic worth tracking. The three slide-embed pages (pages/slides/[collection]/[...slug].astro,pages/slides/embed/[...slug].astro,pages/slides/embed/astro/[...slug].astro) are real content surfaces and got instrumented.No event instrumentation in Pass 1, even though it's tempting. The Pass 2 prompt explicitly waits for baseline data. Wiring CTAs without first knowing what "normal" looks like means we can't tell whether 20 clicks on a button is a lot, a little, or noise.
What's intentionally NOT in this work
Click tracking on any CTA, internal nav link, or outbound link. That's all Pass 2.
Custom event taxonomy or
fathom.trackEvent/umami.trackcalls. Pass 2.Goals or conversions in the Fathom dashboard. Pass 2 — they get configured against named events that don't exist yet.
A shared
analytics.tshelper. Sketched in the Pass 2 prompt, not built. Per-site helpers will land alongside per-site instrumentation.Refactoring
twf_siteandmpstaton-siteto single-<head>layouts. Logged as Pass 2 cleanup. Not blocking.Verifying the
memopop-aideploy URL. The snippet is in place; the Fathom dashboard's site config has to list whichever domain it actually deploys to (github.io subpath, Vercel, subdomain). If the dashboard never sees traffic, that's the first thing to check.
Files (astro-knots-resident — this changelog's scope)
New
sites/twf_site/src/components/Analytics.astrosites/mpstaton-site/src/components/Analytics.astrosites/fullstack-vc/src/components/Analytics.astrosites/hypernova-site/src/components/Analytics.astrosplash/src/components/Analytics.astro
Modified
sites/twf_site/src/layouts/BoilerPlateHTML.astro— import +<Analytics />before</head>sites/twf_site/src/layouts/OneSlideDeck.astro— samesites/twf_site/src/layouts/MarkdownSlideDeck.astro— same (broken-nesting layout, instrumented anyway)sites/mpstaton-site/src/layouts/BaseLayout.astro— samesites/mpstaton-site/src/layouts/PromotionDeckLayout.astro— same (private/noindex deck pages still want traction signal)sites/fullstack-vc/src/layouts/BoilerPlateHTML.astro— samesites/hypernova-site/src/layouts/BoilerPlateHTML.astro— samesplash/src/layouts/BaseLayout.astro— same
Cross-repo (logged here for completeness; the other halves live in the relevant submodules)
The same pattern landed in three sites outside astro-knots: site/ (lossless.group), content-farm/splash, and ai-labs/memopop-ai/apps/memopop-site. Each of those repos will get its own changelog entry covering the local edits.
Next session
Natural openings:
Confirm the dashboards see traffic for at least 2-3 sites within 24 hours of the next deploys. If a dashboard stays empty, the Fathom site config likely doesn't list the actual deploy domain.
Refactor
twf_siteslide layouts to share a single<BaseHead>partial. Removes the legitimately broken HTML nesting inMarkdownSlideDeckand dedupes the<head>content. Analytics is a forcing function but not the only reason.Watch baseline traffic for a week. Then open
context-v/prompts/Implement-Deeper-Analytics-Tracking.mdand start Pass 2. Begin withcta-primaryon the splashes — that's the one event whose data we'll start using right away.
