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
Conversation
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.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: Comment |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 CTAEvery localized marketing landing pointed its primary CTA at a bare
/new./newresolves language from theps-localecookie, 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-alternativeand/pt-br/splitwise-alternativeemitted onlyhref="/new"andhref="/new?campaign=content-splitwise-alternative".The generated
/guides/*corpus has always done this correctly (lib/locale-handoff.tsis the receiving end). The authored marketing pages — the ones an advert actually pays for — did not.withCampaignincomponents/marketing/mdx/blocks.tsxbecomesnewRoomHref(href, slug, locale): it keeps the campaign code and adds the page's own locale. An authoredcampaignorlocalestill wins (ToolPagewrites its own campaign), and an href that is not/newis untouched.localeis bound inlocalizedMdxComponentsrather than read offContentRenderContext, 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 prosea,ToolPage's CTA,ContentHub's button andSiteFooter's create link.Verified against a local server, no bare
/newleft anywhere:English pages get
locale=enfor 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 twosplitwise-daily-limittranslations 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.mdandpt-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".claimsandcompetitorClaimsare untouched in both — they must matchen.md(content.test.ts), and none of them was the migration. TheHowToJSON-LD goes with the<Steps>;FAQPagedrops 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") hasfinalUrl="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/importand/pt-br/importstay 404 by design.A7 — the localized no-signup capture pages
/es-419/split-bill-no-signupand/pt-br/split-bill-no-signupare 404 in prod because both files carrieddraft: 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 onlineandapp 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
claimsasen.md, the same three FAQs in frontmatter and block, and their head term in both the title and theh1.Their
<CTA>blocks are deliberately bare, so the A6 handoff supplies the query on its own. Verified live on the new routes: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⚠️ needs Konrad's sign-off
/privacypage of Split's ownhttps://peanutsplit.com/privacyis 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 inlib/analytics.ts, error reporting ininstrumentation-client.ts, the receipt path inserver/model.ts, theps-locale/device-idcookies, and the stored shapes inprisma/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:/privacyships nogoogletagmanagerscript; 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:
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 verify— exit 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.Targeted e2e for the touched surface,
content-skin.spec.ts, both device projects, each exit 0:The two projects are run separately:
earlyoomkills the sharednext devmid-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/newlink on a localized landing is missing its language).