Skip to content

feat: forward known utm params from homepage to get-started CTA links - #549

Merged
Ehesp merged 1 commit into
mainfrom
feat/forward-utm-params-to-cta-links
Sep 11, 2026
Merged

Ehesp merged 1 commit into
mainfrom
feat/forward-utm-params-to-cta-links

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Requested by Elliot Hesp · Slack thread

Summary

Before: a visitor arrives at docs.page/?utm_source=twitter&utm_campaign=launch. The page view is captured with those params, but the two "Get started" buttons render as bare /get-started links. When the visitor clicks, /get-started sees no utm params on its own request URL, so homepage:cta_click lands with no attribution — the campaign gets credit for the visit but not for the click.

After: the same visit renders both buttons as /get-started?utm_source=twitter&utm_campaign=launch, so the click event carries the same attribution as the page view and a flight's page-view → CTA-click funnel reconciles in PostHog.

Why: without it, paid and referral traffic cannot be attributed past the first hop, which makes the homepage CTA conversion rate unmeasurable per campaign.

How: app/src/lib/utm.ts gains utmQueryString(url), which reuses the existing utmProperties() helper so the five recognised param names stay a single source of truth, and re-encodes them via URLSearchParams (empty string when none are present). The homepage branch of getServerSideProps in app/src/pages/[[...path]].tsx computes it once and passes it as a utmQuery prop down to the hero and footer CTAs. Only the five known keys are forwarded — the inbound query string is never passed through wholesale, so no arbitrary param can be reflected into an on-page link, and the CTA href prefix stays a hardcoded relative path. /get-started itself is unchanged; it already reads utm params off its own request URL. No new dependencies, no client-side PostHog.

Scope

  • app/ (hosted site, MCP, Ask AI)
  • packages/cli/
  • packages/mdx-bundler/
  • docs/ (product documentation)
  • Repo / CI / other

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactor / chore

Test plan

  • bun run check passes locally (and biome ci .)
  • bun test — 147 pass, 0 fail
  • bunx tsc --noEmit -p app/tsconfig.json — clean
  • cd app && bun run build — production build succeeds
  • Updated docs/ (if user-facing) — n/a, internal analytics plumbing
  • Verified on a docs.page URL or local preview (if rendering/routing changed) — pending Railway preview

New unit tests in app/src/lib/utm.test.ts cover no params, a subset, all five, unknown params dropped, and values needing encoding (the utm_campaign field has historically carried injection-probe strings, so the encoding case asserts the value cannot break out of the link).

Notes for reviewers

Prop drilling is two levels deep (HomepageHero / Footer); a context felt like more machinery than the one string warrants, but happy to change it.

Worth a check on the Railway preview with ?utm_source=x&utm_campaign=y appended, confirming both button hrefs carry the params and that a param that is not one of the five is dropped.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JdRBhYmUnmC9vbc7fcgXdC


Generated by Claude Code

A visitor landing on the homepage with utm params lost that attribution at
the CTA step: the in-page hrefs were bare, so `/get-started` had nothing to
read off its own request URL when capturing `homepage:cta_click`.

`getServerSideProps` now re-encodes the five recognised utm params into a
query string and passes it down to both CTA links. Only the known keys are
forwarded — unknown query params are never reflected into on-page links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JdRBhYmUnmC9vbc7fcgXdC
@railway-app
railway-app Bot temporarily deployed to docs.page / docs.page-pr-549 September 8, 2026 08:40 Destroyed
@railway-app

railway-app Bot commented Sep 8, 2026

Copy link
Copy Markdown

🚅 Deployed to the docs.page-pr-549 environment in docs.page

Service Status Web Updated
docs.page ✅ Success (View Logs) Web Sep 8, 2026 at 8:42 am UTC

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@Ehesp
Ehesp marked this pull request as ready for review September 11, 2026 14:19
@Ehesp
Ehesp merged commit 9a37953 into main Sep 11, 2026
3 checks passed
@Ehesp
Ehesp deleted the feat/forward-utm-params-to-cta-links branch September 11, 2026 14:19
claude Bot pushed a commit that referenced this pull request Sep 11, 2026
Resolves the conflict with the homepage work that landed on main since this
branch was last updated, in particular #549 (utm forwarding to the get-started
CTA), which touched both files this PR also changes.

Conflicts resolved:

- app/src/lib/utm.ts — both sides hoisted the same five-key array into a
  UTM_KEYS const. Kept it exported, because this PR's hero imports it to
  forward utm params onto the copy beacon, and merged both comments so the
  const names all three consumers. main's utmQueryString() is kept as-is.

- app/src/components/homepage/hero.tsx — auto-merged, verified by hand. main
  added the utmQuery prop and interpolated it into the get-started href; this
  PR restacked the CTA area into one centred column. Both survive: Hero takes
  utmQuery, the Link uses it, and Terminal still sits above the button.

- app/src/components/homepage/source-files.json — regenerated with
  `bun run generate:source-files` rather than resolved by hand; picks up the
  five source files main added.

bun.lock is deliberately left at the merge result. Running `bun install`
re-resolved `@types/bun` (a "latest" spec) and corrected a packages/cli
version that is already stale on main — both unrelated to this merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BgG4JVuadUeZGtry1yFFd5
claude Bot pushed a commit that referenced this pull request Sep 11, 2026
Merge main (9a37953) into cursor/404-shader-dissolve-7370.

The only conflict was app/src/components/homepage/footer.tsx, where two
orthogonal changes to the same region collided:

- This PR (#542) wraps the closer block in `{showCloser ? ... : null}` and
  adds a `showCloser` prop, so the site 404 can reuse Footer without the
  "Bring your docs into the agentic age" CTA.
- #549 on main adds a `utmQuery` prop and appends it to the /get-started
  href so UTM params are forwarded from the homepage.

Both are kept: the closer stays wrapped in `showCloser`, and the CTA href
keeps the `${utmQuery}` suffix.

`utmQuery` is typed optional with a `""` default rather than required.
Footer has two callers that pass no UTM — `docs.tsx` (pre-existing on main)
and the new `not-found.tsx` — and the closer is not rendered at all when
`showCloser` is false, so there is no UTM to forward in those cases.

app/src/lib/types.ts and app/src/pages/[[...path]].tsx merged cleanly; both
sides' changes there are in disjoint regions and are preserved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EF9amQgkVoieaefXVoHRXz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants