Lossless Changelog
fleet

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.

Michael StatonMusic Editoraugmented with Claude Code on Claude Opus 4.7

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 on import.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.PROD at 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

SiteRepo pathPlatformsWired into
lossless.groupsite/Umami + FathomLayout.astro + 3 slide-embed pages
the-water-foundation.comastro-knots/sites/twf_siteUmami + FathomBoilerPlateHTML.astro + OneSlideDeck.astro + MarkdownSlideDeck.astro (3 layouts)
mpstaton.comastro-knots/sites/mpstaton-siteUmami + FathomBaseLayout.astro + PromotionDeckLayout.astro
fullstack-vc.comastro-knots/sites/fullstack-vcFathomBoilerPlateHTML.astro
hypernova-siteastro-knots/sites/hypernova-siteFathomBoilerPlateHTML.astro
lossless-group.github.io/astro-knotsastro-knots/splashFathomBaseLayout.astro
lossless-group.github.io/content-farmcontent-farm/splashFathomBaseLayout.astro
lossless-group.github.io/memopop-aiai-labs/memopop-aiFathomBaseLayout.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.

ASTRO
---
// 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:

  1. is:inline on 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:inline is the opt-out. Without it, Astro might attempt to process the external URL and surprise us.

  2. {import.meta.env.PROD && (…)} instead of a runtime check. Vite does static replacement at build time. In astro build, the literal becomes {true && (…)} (the script ships); in astro 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-platform analytics.ts helper, 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

  1. 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 .env plumbing). Six lines of copied code per site is not a debt; it's a fact about the data shape.

  2. Cover every head-bearing layout, even when it means duplication. twf_site has three layouts that each emit their own <html> (BoilerPlateHTML, OneSlideDeck, MarkdownSlideDeck); mpstaton-site has 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.

  3. 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.

  4. 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.track calls. 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.ts helper. Sketched in the Pass 2 prompt, not built. Per-site helpers will land alongside per-site instrumentation.

  • Refactoring twf_site and mpstaton-site to single-<head> layouts. Logged as Pass 2 cleanup. Not blocking.

  • Verifying the memopop-ai deploy 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.astro

  • sites/mpstaton-site/src/components/Analytics.astro

  • sites/fullstack-vc/src/components/Analytics.astro

  • sites/hypernova-site/src/components/Analytics.astro

  • splash/src/components/Analytics.astro

Modified

  • sites/twf_site/src/layouts/BoilerPlateHTML.astro — import + <Analytics /> before </head>

  • sites/twf_site/src/layouts/OneSlideDeck.astro — same

  • sites/twf_site/src/layouts/MarkdownSlideDeck.astro — same (broken-nesting layout, instrumented anyway)

  • sites/mpstaton-site/src/layouts/BaseLayout.astro — same

  • sites/mpstaton-site/src/layouts/PromotionDeckLayout.astro — same (private/noindex deck pages still want traction signal)

  • sites/fullstack-vc/src/layouts/BoilerPlateHTML.astro — same

  • sites/hypernova-site/src/layouts/BoilerPlateHTML.astro — same

  • splash/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:

  1. 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.

  2. Refactor twf_site slide layouts to share a single <BaseHead> partial. Removes the legitimately broken HTML nesting in MarkdownSlideDeck and dedupes the <head> content. Analytics is a forcing function but not the only reason.

  3. Watch baseline traffic for a week. Then open context-v/prompts/Implement-Deeper-Analytics-Tracking.md and start Pass 2. Begin with cta-primary on the splashes — that's the one event whose data we'll start using right away.