Skip to content

Pre-flight for the Google Ads search flight: locale on every CTA, the import cut in both languages, the no-signup capture pages, and a privacy page - #29

Merged
0xkkonrad merged 5 commits into
mainfrom
preflight/google-ads-locale-cta-privacy
Sep 1, 2026
Merged

Pre-flight for the Google Ads search flight: locale on every CTA, the import cut in both languages, the no-signup capture pages, and a privacy page#29
0xkkonrad merged 5 commits into
mainfrom
preflight/google-ads-locale-cta-privacy

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Pre-flight blockers for the $1,500 Google Ads search flight, from section 9 of the campaign spec: A6, A16 (both languages) and A5, plus A7.

This repo normally ships straight to main. This batch goes through review because one of these is a privacy page, and that page needs Konrad's sign-off before merge (details below).


A6 — the hard gate: ?locale= on every localized CTA

Every localized marketing landing pointed its primary CTA at a bare /new. /new resolves language from the ps-locale cookie, which a landing states its language in its URL and never sets — so a Spanish ad → Spanish landing → English room creator. Verified live: /es-419/splitwise-alternative and /pt-br/splitwise-alternative emitted only href="/new" and href="/new?campaign=content-splitwise-alternative".

The generated /guides/* corpus has always done this correctly (lib/locale-handoff.ts is the receiving end). The authored marketing pages — the ones an advert actually pays for — did not.

withCampaign in components/marketing/mdx/blocks.tsx becomes newRoomHref(href, slug, locale): it keeps the campaign code and adds the page's own locale. An authored campaign or locale still wins (ToolPage writes its own campaign), and an href that is not /new is untouched. locale is bound in localizedMdxComponents rather than read off ContentRenderContext, because a page with no chapter mapping has no context at all and still needs its language.

Every room-creation link a localized page can reach now goes through it: Hero, CTA, RelatedLink, the prose a, ToolPage's CTA, ContentHub's button and SiteFooter's create link.

Verified against a local server, no bare /new left anywhere:

/es-419/splitwise-alternative   /new?campaign=content-splitwise-alternative&locale=es-419 · /new?locale=es-419
/pt-br/splitwise-alternative    /new?campaign=content-splitwise-alternative&locale=pt-br  · /new?locale=pt-br
/es-419/splitwise-daily-limit   /new?campaign=content-splitwise-daily-limit&locale=es-419
/pt-br/splitwise-daily-limit    /new?campaign=content-splitwise-daily-limit&locale=pt-br
/es-419/rent-split-calculator   /new?campaign=content-rent-split-calculator&locale=es-419
/es-419/blog                    /new?locale=es-419
/splitwise-alternative          /new?campaign=content-splitwise-alternative&locale=en

English pages get locale=en for the same reason, which also closes the mirror-image bug: an English click landing in a Spanish room creator because of a stale cookie.

A16 — the English import block cut, in both languages

Every other localized switcher page already strips the import surface. The reason is written into splitwise-alternative/es-419.md: "La página de importación es por ahora solo en inglés, así que esta versión no la enlaza." The two splitwise-daily-limit translations still shipped the full migration section — export the Splitwise group, open the import page, drop the CSV — plus a CTA body and an FAQ answer built on it.

Removed from both es-419.md and pt-br.md: the migration section and its <Steps>, the migration FAQ (frontmatter and block, so the FAQ-parity gate stays satisfied), and the import promise from the description, the hero subtitle and the CTA body. The in-file editorial note now carries the same rule its sibling pages carry, replacing the stale "keep the migration steps aligned with the importer".

claims and competitorClaims are untouched in both — they must match en.md (content.test.ts), and none of them was the migration. The HowTo JSON-LD goes with the <Steps>; FAQPage drops to three questions on each page. Confirmed live on both.

The spec scoped A16 to LATAM-ES, and that was a scoping oversight rather than a decision. Ad group BR3 (v2/data_pt.py:125-126, "Limite diário e plano pago") has finalUrl="https://peanutsplit.com/pt-br/splitwise-daily-limit", with a sitelink pointing at the same page — so paid Brazilian clicks were landing on the same desktop-only, English-only path the Spanish ruling exists to prevent. Both pages now argue only what they can deliver in their own language.

/es-419/import and /pt-br/import stay 404 by design.

A7 — the localized no-signup capture pages

/es-419/split-bill-no-signup and /pt-br/split-bill-no-signup are 404 in prod because both files carried draft: true, while ES2 and BR2 need somewhere to land. The drafts were also written around head terms with zero autocomplete in either language; the published versions are built around the terms that return volume — dividir gastos online and app para dividir despesas — which is what the keyword tables in §3.2 and §3.3 were already written for. The slug does not change, so the spec's URLs are already correct.

Copy by another agent, read in full here before landing. Both keep the same claims as en.md, the same three FAQs in frontmatter and block, and their head term in both the title and the h1.

Their <CTA> blocks are deliberately bare, so the A6 handoff supplies the query on its own. Verified live on the new routes:

/es-419/split-bill-no-signup (200)   /new?campaign=content-split-bill-no-signup&locale=es-419
/pt-br/split-bill-no-signup  (200)   /new?campaign=content-split-bill-no-signup&locale=pt-br

Related lists point at /{locale}/blog/split-expenses-across-currencies; the previous target is still a draft, and a published page may not link one.

A5 — a /privacy page of Split's own ⚠️ needs Konrad's sign-off

https://peanutsplit.com/privacy is a 404 today, and the footer points at peanut.me's policy. That policy describes a wallet — accounts, passkeys, identity documents, payment rails — none of which is this product, and it does not mention an advertising cookie because peanut.me does not set one. Split has run its own Google Ads tag (AW-17182428820) since 24 August.

The page is written from what the code does, not from a template. Owner, registered office and contact are Squirrel Labs Ltd's, taken from peanut.me's own policy. Everything else is read off this repo: the tag in lib/google-ads.ts, the analytics boundary in lib/analytics.ts, error reporting in instrumentation-client.ts, the receipt path in server/model.ts, the ps-locale / device-id cookies, and the stored shapes in prisma/schema.prisma. No legal language was invented.

It sits directly under (product-shell) rather than (marketing), so the one page that describes the advertising tag is the one page that does not load it. Confirmed: /privacy ships no googletagmanager script; a marketing landing still does.

The footer's Privacy notice now points at /privacy; Terms still goes to peanut.me.

Two things are TODOs in the file rather than guesses, and both are yours to rule on:

  1. Consent. The Ads tag loads on first paint with no cookie banner. Whether an advertising cookie needs a PECR/EU consent gate before this flight runs is a legal call. The page currently states the behaviour plainly; if the answer is "gate it", that is a separate change before launch.
  2. Retention for room data. The page says what the code does — a room stays until someone deletes it, nothing expires it on a timer. Whether that is also what we want to promise, and whether dormant rooms should be swept, is a product decision.

Everything else on the page is a statement of observable behaviour. Please read it end to end before merging; a wrong privacy policy is a promise we no longer keep.

English only, like the rest of the untranslated surface.


Test gate

pnpm verifyexit 0, run after every change above. Against an isolated test database, because another session is working in the same checkout and its concurrent runs were truncating the shared one.

Marketing copy audit clean
 Test Files  226 passed (226)
      Tests  3671 passed (3671)
Test Suites: 1 passed, 1 total     (api integration)
Tests:       18 passed, 18 total
passed=41 failed=0                 (settle loop)

Targeted e2e for the touched surface, content-skin.spec.ts, both device projects, each exit 0:

[mobile]   23 passed (1.1m)
[desktop]  23 passed (1.2m)
  ✓ the fronting hero CTA points at a campaign-coded, locale-pinned /new
  ✓ the es-419 switcher landing hands its language to /new
  ✓ the pt-br switcher landing hands its language to /new

The two projects are run separately: earlyoom kills the shared next dev mid-run when both run together, which is a known hazard on this box and not a product failure.

Updated tests: blocks.test.tsx (locale on every block, and an authored param still wins), SiteFooter.test.tsx (one peanut.me link left, privacy is internal), reference-budget.test.ts (the Peanut-domain literal moved from the footer to the privacy page), content-skin.spec.ts (a new per-locale assertion that no /new link on a localized landing is missing its language).

A localized landing states its language in its own URL and never sets the
ps-locale cookie, so a Spanish ad -> Spanish landing -> English room creator:
the CTA pointed at a bare /new, and /new resolves language from that cookie.
The generated guides have carried ?locale= since they shipped; the authored
marketing pages, which are the ones an advert actually pays for, did not.

withCampaign becomes newRoomHref and adds the page's own locale beside the
campaign code, so both facts ride in one link attribute. An authored campaign
or locale still wins - ToolPage writes its own campaign - and an href that is
not /new is untouched. Every block a localized page can reach goes through it:
Hero, CTA, RelatedLink, the prose link on the two calculator capture pages, the
blog hub's button and the footer's create link.

Reported against /es-419/splitwise-alternative and /pt-br/splitwise-alternative,
which emitted only href="/new" and href="/new?campaign=content-...".
Every other localized switcher page already strips the import surface, for the
reason written into splitwise-alternative/es-419.md: the import page is English
only, so a Spanish page does not link it or describe it. This one page still
shipped the full migration section - export the Splitwise group, open the
import page, drop the CSV - plus a CTA and an FAQ answer built on it.

It is about to be a paid landing for Spanish clicks, so the page now argues
only what it can deliver in Spanish. The claims and competitorClaims stay as
they are: they must match en.md, and none of them was the migration.

/es-419/import and /pt-br/import are 404 by design and stay that way. The
Portuguese daily-limit page carries the same block and is deliberately left
alone - that is a separate editorial call, not this ask.
peanutsplit.com has carried its own Google Ads tag since 2026-08-24 while the
footer pointed at peanut.me's policy. That policy describes a wallet with
accounts, passkeys, identity documents and payment rails - none of which is
this product - and it does not mention an advertising cookie, because peanut.me
does not set one. /privacy was a 404.

The page is written from what the code does rather than from a template: the
tag in lib/google-ads.ts, the analytics boundary in lib/analytics.ts, error
reporting in instrumentation-client.ts, the receipt path in server/model.ts,
and the stored shapes in prisma/schema.prisma. Owner, registered office and
contact are Squirrel Labs Ltd's, as on peanut.me's own policy.

It sits directly under (product-shell) rather than (marketing), so the one page
that describes the advertising tag is the one page that does not load it.

Two things are left as TODOs in the file rather than guessed, because both are
Konrad's call and not an engineering one: whether the tag needs a consent gate
before it loads, and what the retention period for room data should be. The
page states today's behaviour for both.

English only for now, like the rest of the untranslated surface.
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 6a16c0e5-30cc-4b75-8917-0e88b950fd42


Comment @coderabbitai help to get the list of available commands.

The A16 ruling was scoped LATAM-ES, but the Portuguese page carries the same
block and BR3 points a paid ad group straight at it, with a sitelink beside it.
So Brazilian clicks land on the migration path A16 exists to keep Spanish
clicks off: desktop-only, English-only, /pt-br/import a 404 by design.

Same edit as the Spanish page. Gone: the "Como mudar o grupo no meio da
viagem" section and its <Steps>, the migration FAQ from the frontmatter and the
block both, and the import promise from the description, the hero subtitle and
the CTA body. The editorial note now carries the rule its sibling pages carry
instead of the stale "keep the migration steps aligned with the importer".

claims and competitorClaims are untouched — they must match en.md.

The HowTo JSON-LD goes with the <Steps> and the FAQPage node drops to three
questions. What survives is what the page can deliver in Portuguese: what the
limit is, the three ways out Splitwise itself offers, and where a room differs.
A7. Both pages existed as drafts, so /es-419/split-bill-no-signup and
/pt-br/split-bill-no-signup were 404 while ES2 and BR2 needed somewhere to
land. The drafts were also written around head terms that return zero
autocomplete in both languages; these are rewritten around the terms that
return volume — "dividir gastos online" and "app para dividir despesas" —
which is what the keyword tables in the spec were already built for.

Written by another agent, read here before landing. Both keep the same claims
as en.md, the same three FAQs in frontmatter and block, and the head term in
the title and the h1. Their CTAs are deliberately bare, so the locale handoff
added earlier in this branch supplies campaign and locale on its own: verified
live, /new?campaign=content-split-bill-no-signup&locale=es-419 and the pt-br
equivalent, with no href authored anywhere in either file.

The related lists point at /{locale}/blog/split-expenses-across-currencies
rather than the previous target, which is still a draft — a published page may
not link one.
@0xkkonrad 0xkkonrad changed the title Pre-flight for the Google Ads search flight: locale on every CTA, the Spanish import cut, and a privacy page Pre-flight for the Google Ads search flight: locale on every CTA, the import cut in both languages, the no-signup capture pages, and a privacy page Sep 1, 2026
@0xkkonrad
0xkkonrad merged commit 8ebc589 into main Sep 1, 2026
8 checks passed
@0xkkonrad
0xkkonrad deleted the preflight/google-ads-locale-cta-privacy branch September 1, 2026 13:26
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.

1 participant