From 5c8a914e5e0fef7092a3e84fb5c8342822599dd8 Mon Sep 17 00:00:00 2001 From: 0xkkonrad Date: Tue, 1 Sep 2026 11:34:40 +0000 Subject: [PATCH 1/5] Hand the reader's language to /new from every localized page 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-...". --- apps/web/e2e/content-skin.spec.ts | 22 +++++++- .../src/components/marketing/ContentHub.tsx | 9 ++-- .../src/components/marketing/SiteFooter.tsx | 5 +- .../components/marketing/mdx/blocks.test.tsx | 53 +++++++++++++------ .../src/components/marketing/mdx/blocks.tsx | 47 ++++++++++------ .../components/marketing/mdx/components.tsx | 10 +++- apps/web/src/components/tools/ToolPage.tsx | 6 ++- 7 files changed, 111 insertions(+), 41 deletions(-) diff --git a/apps/web/e2e/content-skin.spec.ts b/apps/web/e2e/content-skin.spec.ts index 490ff5b9..46127eee 100644 --- a/apps/web/e2e/content-skin.spec.ts +++ b/apps/web/e2e/content-skin.spec.ts @@ -113,10 +113,28 @@ test.describe('content SEO loops', () => { }) } - test('the fronting hero CTA points at a campaign-coded /new', async ({ page }) => { + test('the fronting hero CTA points at a campaign-coded, locale-pinned /new', async ({ page }) => { await page.goto('/blog/fronting-a-group-trip') - await expect(page.locator('a[href="/new?campaign=content-fronting-a-group-trip"]').first()).toBeVisible() + await expect( + page.locator('a[href="/new?campaign=content-fronting-a-group-trip&locale=en"]').first() + ).toBeVisible() }) + + // A6: a paid Spanish click landed in an English room creator, because `/new` reads a cookie + // the landing never sets. Both localized landings state their language in the link itself. + for (const [path, locale] of [ + ['/es-419/splitwise-alternative', 'es-419'], + ['/pt-br/splitwise-alternative', 'pt-br'], + ]) { + test(`the ${locale} switcher landing hands its language to /new`, async ({ page }) => { + await page.goto(path) + const links = page.locator('a[href^="/new"]') + await expect(links.first()).toBeVisible() + for (const href of await links.evaluateAll((all) => all.map((a) => a.getAttribute('href')))) { + expect(href).toContain(`locale=${locale}`) + } + }) + } }) test.describe('tool page footer pin', () => { diff --git a/apps/web/src/components/marketing/ContentHub.tsx b/apps/web/src/components/marketing/ContentHub.tsx index 042b8ca8..363c6a44 100644 --- a/apps/web/src/components/marketing/ContentHub.tsx +++ b/apps/web/src/components/marketing/ContentHub.tsx @@ -4,6 +4,7 @@ import { Breadcrumbs } from '@/components/marketing/Breadcrumbs' import { JsonLd } from '@/components/marketing/JsonLd' import { SiteFooter } from '@/components/marketing/SiteFooter' import { LanguageLinks } from '@/components/marketing/LanguageLinks' +import { newRoomHref } from '@/components/marketing/mdx/blocks' import { buttonClassName } from '@/components/ui/button-style' import { STATIC_PAGES } from '@/data/static-pages' import { listAllDocs } from '@/lib/content' @@ -133,10 +134,12 @@ export async function ContentHub({ locale }: { locale: Locale }) { )} - {/* `/new` is app shell too — one URL, cookie decides the language. `/es-419/new` and - `/pt-br/new` are not routes, so prefixing this made the hub's only CTA a 404. */} + {/* `/new` is app shell too — one URL, and the language rides in the query rather + than the path: `/es-419/new` and `/pt-br/new` are not routes, so prefixing this + made the hub's only CTA a 404, while the cookie alone sent a Spanish reader to + an English room creator (`locale-handoff.ts`). */} {t('startRoom')} diff --git a/apps/web/src/components/marketing/SiteFooter.tsx b/apps/web/src/components/marketing/SiteFooter.tsx index 58a2a011..81459621 100644 --- a/apps/web/src/components/marketing/SiteFooter.tsx +++ b/apps/web/src/components/marketing/SiteFooter.tsx @@ -1,5 +1,6 @@ import Link from 'next/link' import { useLocale, useTranslations } from 'next-intl' +import { newRoomHref } from '@/components/marketing/mdx/blocks' import { LocaleSwitcher } from '@/components/ui/LocaleSwitcher' import { INDEXED_LOCALES, asLocale, type Locale } from '@/i18n/locales' import { localizedPath } from '@/i18n/paths' @@ -58,7 +59,9 @@ export function SiteFooter({ showLocaleSwitcher = true }: { showLocaleSwitcher?:

{t('colSplit')}