From 616eb145c1a4b0fb75bcc3e2ab7c6169518a3e64 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 21:16:05 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(app):=20locale=20migration=20L3b=20?= =?UTF-8?q?=E2=80=94=20retire=20internal-marketing=20pilot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 308 redirects from /internal-marketing/* and /en/* to public App routes, delete the noindex pilot tree, and update proxy, playgroundRoute, e2e, and preview-smoke accordingly. --- AGENTS.md | 2 +- e2e/locale-migration-l3b.spec.ts | 61 +++++++++++++ e2e/pilot-routes.spec.ts | 88 ------------------- next.config.mjs | 36 ++++++++ .../[locale]/daily/page.tsx | 29 ------ .../internal-marketing/[locale]/layout.tsx | 30 ------- src/app/internal-marketing/[locale]/page.tsx | 32 ------- .../[locale]/playground/[[...slug]]/page.tsx | 46 ---------- .../[locale]/privacy/page.tsx | 33 ------- .../[locale]/profile/[userId]/page.tsx | 40 --------- .../internalMarketingPilotMetadata.test.ts | 29 ------ .../internalMarketingPilotMetadata.ts | 68 -------------- .../internal-marketing/pilotPageMetadata.ts | 28 ------ src/app/layout.tsx | 2 +- src/app/locale-app/LocaleAppLayout.tsx | 2 +- .../homePage/ui/MarketingHomeView.tsx | 2 +- src/proxy.ts | 17 +--- src/scripts/previewSmoke.ts | 11 +-- src/shared/hooks/usePlaygroundRoute.ts | 4 +- .../lib/__tests__/playgroundPath.test.ts | 6 +- .../lib/__tests__/playgroundRoute.test.ts | 28 +++--- src/shared/lib/playgroundRoute.ts | 33 +++---- vibe-docs/Instant-Navigations-TODO.md | 17 ++-- vibe-docs/Locale-Migration-Design.md | 39 ++++---- 24 files changed, 175 insertions(+), 508 deletions(-) create mode 100644 e2e/locale-migration-l3b.spec.ts delete mode 100644 e2e/pilot-routes.spec.ts delete mode 100644 src/app/internal-marketing/[locale]/daily/page.tsx delete mode 100644 src/app/internal-marketing/[locale]/layout.tsx delete mode 100644 src/app/internal-marketing/[locale]/page.tsx delete mode 100644 src/app/internal-marketing/[locale]/playground/[[...slug]]/page.tsx delete mode 100644 src/app/internal-marketing/[locale]/privacy/page.tsx delete mode 100644 src/app/internal-marketing/[locale]/profile/[userId]/page.tsx delete mode 100644 src/app/internal-marketing/__tests__/internalMarketingPilotMetadata.test.ts delete mode 100644 src/app/internal-marketing/internalMarketingPilotMetadata.ts delete mode 100644 src/app/internal-marketing/pilotPageMetadata.ts diff --git a/AGENTS.md b/AGENTS.md index c6b19628..97d8307e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ Cursor rules to apply (see each file for full wording): ### Instant Navigations (Next.js 16.3) — not on Pages Router yet -dStruct is **mostly Pages Router** (`src/pages/`). An **App Router pilot** lives under `src/app/internal-marketing/[locale]/` (home, privacy, daily; noindex). **Instant Navigations** (`cacheComponents`, `partialPrefetching`, `'use cache'`, `unstable_instant`, Instant Insights) requires broader App Router migration. Do not enable `cacheComponents` or add `'use cache'` under `src/pages/`. +dStruct public marketing and app routes live on **App Router** (`src/app/(default-locale)/`, `src/app/[lang]/`). Legacy `/internal-marketing/*` and `/en/*` URLs **308 redirect** to public routes (L3b). **Instant Navigations** (`cacheComponents`, `partialPrefetching`, `'use cache'`, `unstable_instant`, Instant Insights) requires resolving root `headers()` blockers. Do not enable `cacheComponents` or add `'use cache'` under `src/pages/`. Before implementing Instant Navigations, read: diff --git a/e2e/locale-migration-l3b.spec.ts b/e2e/locale-migration-l3b.spec.ts new file mode 100644 index 00000000..2618bfb7 --- /dev/null +++ b/e2e/locale-migration-l3b.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from "@playwright/test"; + +/** + * L3b: legacy `/internal-marketing/*` and duplicate `/en/*` URLs 308 to public App routes. + */ +test.describe("locale migration L3b legacy redirects", () => { + test("internal-marketing en home redirects to /", async ({ page }) => { + const response = await page.goto("/internal-marketing/en"); + expect(response?.status()).toBeLessThan(400); + await expect(page).toHaveURL(/\/$/); + await expect(page).toHaveTitle(/dStruct/); + }); + + test("internal-marketing en privacy redirects to /privacy", async ({ + page, + }) => { + await page.goto("/internal-marketing/en/privacy"); + await expect(page).toHaveURL(/\/privacy$/); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( + "href", + "https://dstruct.pro/privacy", + ); + }); + + test("internal-marketing de daily redirects to /de/daily", async ({ + page, + }) => { + await page.goto("/internal-marketing/de/daily"); + await expect(page).toHaveURL(/\/de\/daily$/); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( + "href", + "https://dstruct.pro/de/daily", + ); + }); + + test("internal-marketing en playground redirects to /playground", async ({ + page, + }) => { + await page.goto("/internal-marketing/en/playground"); + await expect(page).toHaveURL(/\/playground$/); + await expect(page).toHaveTitle(/Playground/i); + }); + + test("internal-marketing en profile redirects to /profile/:userId", async ({ + page, + }) => { + const userId = "e2e-test-user"; + await page.goto(`/internal-marketing/en/profile/${userId}`); + await expect(page).toHaveURL(new RegExp(`/profile/${userId}$`)); + await expect(page).toHaveTitle(/Profile/i); + }); + + test("/en/privacy redirects to unprefixed /privacy", async ({ page }) => { + await page.goto("/en/privacy"); + await expect(page).toHaveURL(/\/privacy$/); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( + "href", + "https://dstruct.pro/privacy", + ); + }); +}); diff --git a/e2e/pilot-routes.spec.ts b/e2e/pilot-routes.spec.ts deleted file mode 100644 index a01c7d10..00000000 --- a/e2e/pilot-routes.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { expect, test } from "@playwright/test"; - -import { dismissCookieBannerIfVisible } from "./helpers/dismissCookieBanner"; - -/** - * Smoke tests for `/internal-marketing/[locale]/*` App Router pilots. - * - * `@next/playwright` `instant()` tests require `cacheComponents` + `unstable_instant` - * (blocked until Next 16.3.x + locale migration). These assert pilot routes render and - * stay noindex with public canonicals. - */ -test.describe("internal-marketing pilot routes", () => { - test("home is noindex with public canonical", async ({ page }) => { - await page.goto("/internal-marketing/en"); - await expect(page).toHaveTitle(/dStruct/); - await expect(page.locator('meta[name="robots"]')).toHaveAttribute( - "content", - /noindex/i, - ); - await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( - "href", - "https://dstruct.pro/", - ); - }); - - test("privacy pilot is noindex with public canonical", async ({ page }) => { - await page.goto("/internal-marketing/en/privacy"); - await expect(page.locator('meta[name="robots"]')).toHaveAttribute( - "content", - /noindex/i, - ); - await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( - "href", - "https://dstruct.pro/privacy", - ); - }); - - test("daily pilot is noindex with public canonical", async ({ page }) => { - await page.goto("/internal-marketing/de/daily"); - await expect(page.locator('meta[name="robots"]')).toHaveAttribute( - "content", - /noindex/i, - ); - await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( - "href", - "https://dstruct.pro/de/daily", - ); - }); - - test("playground landing pilot is noindex with public canonical", async ({ - page, - }) => { - await page.goto("/internal-marketing/en/playground"); - await expect(page).toHaveTitle(/Playground/i); - await expect(page.locator('meta[name="robots"]')).toHaveAttribute( - "content", - /noindex/i, - ); - await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( - "href", - "https://dstruct.pro/playground", - ); - }); - - test("profile pilot is noindex with public canonical", async ({ page }) => { - const userId = "e2e-test-user"; - await page.goto(`/internal-marketing/en/profile/${userId}`); - await expect(page).toHaveTitle(/Profile/i); - await expect(page.locator('meta[name="robots"]')).toHaveAttribute( - "content", - /noindex/i, - ); - await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( - "href", - `https://dstruct.pro/profile/${userId}`, - ); - }); - - test("pilot home footer links to public privacy page", async ({ page }) => { - await page.goto("/internal-marketing/en"); - await dismissCookieBannerIfVisible(page); - const privacyLink = page - .getByRole("contentinfo") - .getByRole("link", { name: /privacy policy/i }); - await privacyLink.scrollIntoViewIfNeeded(); - await Promise.all([page.waitForURL(/\/privacy$/), privacyLink.click()]); - }); -}); diff --git a/next.config.mjs b/next.config.mjs index 5385b73f..4e23aa5f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -85,6 +85,42 @@ const config = { sassOptions: { silenceDeprecations: ["legacy-js-api"], }, + async redirects() { + return [ + // L3b: retire `/internal-marketing/*` App pilot → public App routes (308). + { + source: "/internal-marketing/en", + destination: "/", + permanent: true, + }, + { + source: "/internal-marketing/en/:path*", + destination: "/:path*", + permanent: true, + }, + { + source: "/internal-marketing/:locale", + destination: "/:locale", + permanent: true, + }, + { + source: "/internal-marketing/:locale/:path*", + destination: "/:locale/:path*", + permanent: true, + }, + // SEO: dedupe default-locale `/en/*` vs unprefixed URLs. + { + source: "/en", + destination: "/", + permanent: true, + }, + { + source: "/en/:path*", + destination: "/:path*", + permanent: true, + }, + ]; + }, turbopack: { rules: { "*.txt": { diff --git a/src/app/internal-marketing/[locale]/daily/page.tsx b/src/app/internal-marketing/[locale]/daily/page.tsx deleted file mode 100644 index 5e9eea9c..00000000 --- a/src/app/internal-marketing/[locale]/daily/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { Metadata } from "next"; - -import { DailyPageView } from "#/features/homePage/ui/DailyPageView"; -import type { Locales } from "#/i18n/i18n-types"; -import { locales } from "#/i18n/i18n-util"; - -import { pilotPageMetadataFromTranslation } from "#/app/internal-marketing/pilotPageMetadata"; - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale: localeParam } = await params; - if (!locales.includes(localeParam as Locales)) { - return { robots: { index: false, follow: false } }; - } - const locale = localeParam as Locales; - - return pilotPageMetadataFromTranslation(locale, "/daily", (translation) => ({ - title: `${translation.HOME_DAILY_SECTION_TITLE} — dStruct`, - description: `${translation.HOME_DAILY_SECTION_TITLE}. ${translation.HOME_DAILY_SECTION_LEAD}`, - })); -} - -/** Instant Nav pilot: daily problem (noindex; public `/daily` remains canonical). */ -export default function InternalMarketingDailyPage() { - return ; -} diff --git a/src/app/internal-marketing/[locale]/layout.tsx b/src/app/internal-marketing/[locale]/layout.tsx deleted file mode 100644 index 20b863a8..00000000 --- a/src/app/internal-marketing/[locale]/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { Metadata } from "next"; - -import { locales } from "#/i18n/i18n-util"; - -import { LocaleAppLayout } from "#/app/locale-app/LocaleAppLayout"; - -/** All pilot routes stay noindex even when a child page omits metadata. */ -export const metadata: Metadata = { - robots: { index: false, follow: false }, -}; - -/** Pilot routes are dynamic (daily data hooks + session); skip build-time SSG. */ -export const dynamic = "force-dynamic"; - -export default async function InternalMarketingLocaleLayout({ - children, - params, -}: { - children: React.ReactNode; - params: Promise<{ locale: string }>; -}) { - const { locale: localeParam } = await params; - return ( - {children} - ); -} - -export function generateStaticParams(): Array<{ locale: string }> { - return locales.map((locale) => ({ locale })); -} diff --git a/src/app/internal-marketing/[locale]/page.tsx b/src/app/internal-marketing/[locale]/page.tsx deleted file mode 100644 index 6125ba89..00000000 --- a/src/app/internal-marketing/[locale]/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { Metadata } from "next"; - -import { MarketingHomeView } from "#/features/homePage/ui/MarketingHomeView"; -import type { Locales } from "#/i18n/i18n-types"; -import { locales } from "#/i18n/i18n-util"; - -import { pilotPageMetadataFromTranslation } from "#/app/internal-marketing/pilotPageMetadata"; - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale: localeParam } = await params; - if (!locales.includes(localeParam as Locales)) { - return pilotPageMetadataFromTranslation("en", "/", (translation) => ({ - title: translation.SITE_SEO_TITLE, - description: translation.SITE_SEO_DESCRIPTION, - })); - } - const locale = localeParam as Locales; - - return pilotPageMetadataFromTranslation(locale, "/", (translation) => ({ - title: translation.SITE_SEO_TITLE, - description: translation.SITE_SEO_DESCRIPTION, - })); -} - -/** Instant Nav pilot (App Router). Public home is `app/(default-locale)` / `app/[lang]`. */ -export default function InternalMarketingHomePage() { - return ; -} diff --git a/src/app/internal-marketing/[locale]/playground/[[...slug]]/page.tsx b/src/app/internal-marketing/[locale]/playground/[[...slug]]/page.tsx deleted file mode 100644 index d70b34f2..00000000 --- a/src/app/internal-marketing/[locale]/playground/[[...slug]]/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type { Metadata } from "next"; -import React, { Suspense } from "react"; - -import { resolvePlaygroundPageSeo } from "#/features/playground/lib/resolvePlaygroundPageSeo"; -import { PlaygroundPageView } from "#/features/playground/ui/PlaygroundPageView"; -import type { Locales } from "#/i18n/i18n-types"; -import { locales } from "#/i18n/i18n-util"; -import { SplitPanelsLayoutSkeleton } from "#/shared/ui/templates/SplitPanelsLayout/SplitPanelsLayoutSkeleton"; - -import { internalMarketingPilotMetadata } from "#/app/internal-marketing/internalMarketingPilotMetadata"; - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string; slug?: string[] }>; -}): Promise { - const { locale: localeParam, slug } = await params; - if (!locales.includes(localeParam as Locales)) { - return { robots: { index: false, follow: false } }; - } - const locale = localeParam as Locales; - const slugStr = slug?.[0]; - const pagePath = slugStr ? `/playground/${slugStr}` : "/playground"; - const { pageTitle, pageDescription } = await resolvePlaygroundPageSeo( - locale, - slugStr, - ); - - return internalMarketingPilotMetadata({ - locale, - pagePath, - title: pageTitle, - description: pageDescription, - }); -} - -const PlaygroundPilotFallback: React.FC = () => ; - -/** Instant Nav pilot: playground shell (noindex; public `/playground` remains canonical). */ -export default function InternalMarketingPlaygroundPage() { - return ( - }> - - - ); -} diff --git a/src/app/internal-marketing/[locale]/privacy/page.tsx b/src/app/internal-marketing/[locale]/privacy/page.tsx deleted file mode 100644 index ffa565b3..00000000 --- a/src/app/internal-marketing/[locale]/privacy/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { Metadata } from "next"; - -import { PrivacyPageView } from "#/features/privacy/ui/PrivacyPageView"; -import type { Locales } from "#/i18n/i18n-types"; -import { locales } from "#/i18n/i18n-util"; - -import { pilotPageMetadataFromTranslation } from "#/app/internal-marketing/pilotPageMetadata"; - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale: localeParam } = await params; - if (!locales.includes(localeParam as Locales)) { - return { robots: { index: false, follow: false } }; - } - const locale = localeParam as Locales; - - return pilotPageMetadataFromTranslation( - locale, - "/privacy", - (translation) => ({ - title: `${translation.PRIVACY_PAGE_TITLE} — dStruct`, - description: translation.PRIVACY_INTRO, - }), - ); -} - -/** Instant Nav pilot: privacy policy (noindex; public `/privacy` remains canonical). */ -export default function InternalMarketingPrivacyPage() { - return ; -} diff --git a/src/app/internal-marketing/[locale]/profile/[userId]/page.tsx b/src/app/internal-marketing/[locale]/profile/[userId]/page.tsx deleted file mode 100644 index 8d140649..00000000 --- a/src/app/internal-marketing/[locale]/profile/[userId]/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { Metadata } from "next"; -import { notFound } from "next/navigation"; - -import { ProfilePageView } from "#/features/profile/ui/ProfilePageView"; -import type { Locales } from "#/i18n/i18n-types"; -import { locales } from "#/i18n/i18n-util"; - -import { pilotPageMetadataFromTranslation } from "#/app/internal-marketing/pilotPageMetadata"; - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string; userId: string }>; -}): Promise { - const { locale: localeParam, userId } = await params; - if (!locales.includes(localeParam as Locales) || !userId.trim()) { - return { robots: { index: false, follow: false } }; - } - const locale = localeParam as Locales; - const pagePath = `/profile/${userId}`; - - return pilotPageMetadataFromTranslation(locale, pagePath, (translation) => ({ - title: `${translation.PROFILE} — dStruct`, - description: translation.SITE_SEO_DESCRIPTION, - })); -} - -/** Instant Nav pilot: profile (noindex; public `/profile/[userId]` remains canonical). */ -export default async function InternalMarketingProfilePage({ - params, -}: { - params: Promise<{ locale: string; userId: string }>; -}) { - const { userId } = await params; - if (!userId.trim()) { - notFound(); - } - - return ; -} diff --git a/src/app/internal-marketing/__tests__/internalMarketingPilotMetadata.test.ts b/src/app/internal-marketing/__tests__/internalMarketingPilotMetadata.test.ts deleted file mode 100644 index a37aa4d3..00000000 --- a/src/app/internal-marketing/__tests__/internalMarketingPilotMetadata.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { internalMarketingPilotMetadata } from "#/app/internal-marketing/internalMarketingPilotMetadata"; - -describe("internalMarketingPilotMetadata", () => { - it("sets noindex and public canonical for pilot home", () => { - const metadata = internalMarketingPilotMetadata({ - locale: "de", - pagePath: "/", - title: "dStruct", - description: "Test description", - }); - - expect(metadata.robots).toEqual({ index: false, follow: false }); - expect(metadata.alternates?.canonical).toBe("https://dstruct.pro/de"); - expect(metadata.title).toBe("dStruct"); - }); - - it("uses locale-prefixed canonical for non-home pages", () => { - const metadata = internalMarketingPilotMetadata({ - locale: "en", - pagePath: "/privacy", - title: "Privacy", - description: "Intro", - }); - - expect(metadata.alternates?.canonical).toBe("https://dstruct.pro/privacy"); - }); -}); diff --git a/src/app/internal-marketing/internalMarketingPilotMetadata.ts b/src/app/internal-marketing/internalMarketingPilotMetadata.ts deleted file mode 100644 index 4c876813..00000000 --- a/src/app/internal-marketing/internalMarketingPilotMetadata.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { Metadata } from "next"; - -import type { Locales } from "#/i18n/i18n-types"; -import { localePathForPage } from "#/i18n/localePathForPage"; -import { - absoluteUrlFromPathname, - DEFAULT_OG_IMAGE_URL, - SITE_HOSTNAME, - truncateMetaDescription, -} from "#/shared/lib/seo"; - -const pilotRobots: NonNullable = { - index: false, - follow: false, -}; - -type InternalMarketingPilotMetadataInput = { - locale: Locales; - /** Public page path without locale prefix, e.g. `"/"` or `"/privacy"`. */ - pagePath: string; - title: string; - description: string; -}; - -/** SEO for `/internal-marketing/[locale]/*` pilot routes (noindex; canonical = public URL). */ -export function internalMarketingPilotMetadata({ - locale, - pagePath, - title, - description, -}: InternalMarketingPilotMetadataInput): Metadata { - const canonicalUrl = absoluteUrlFromPathname( - localePathForPage(locale, pagePath), - ); - const metaDescription = truncateMetaDescription(description); - - return { - title, - description: metaDescription, - robots: pilotRobots, - alternates: { canonical: canonicalUrl }, - openGraph: { - siteName: "dStruct", - title, - type: "website", - url: canonicalUrl, - description: metaDescription, - images: [ - { - url: DEFAULT_OG_IMAGE_URL, - width: 1200, - height: 630, - alt: "dStruct — LeetCode solution visualizer", - }, - ], - }, - twitter: { - card: "summary_large_image", - title, - description: metaDescription, - images: [DEFAULT_OG_IMAGE_URL], - }, - other: { - "twitter:domain": SITE_HOSTNAME, - "twitter:url": canonicalUrl, - }, - }; -} diff --git a/src/app/internal-marketing/pilotPageMetadata.ts b/src/app/internal-marketing/pilotPageMetadata.ts deleted file mode 100644 index de2c9013..00000000 --- a/src/app/internal-marketing/pilotPageMetadata.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Metadata } from "next"; - -import type { Locales, Translation } from "#/i18n/i18n-types"; -import { importLocaleAsync } from "#/i18n/i18n-util.async"; - -import { internalMarketingPilotMetadata } from "#/app/internal-marketing/internalMarketingPilotMetadata"; - -type PilotPageCopy = { - title: string; - description: string; -}; - -/** Server-only metadata for pilot routes using raw locale strings (no `i18nObject`). */ -export async function pilotPageMetadataFromTranslation( - locale: Locales, - pagePath: string, - pickCopy: (translation: Translation) => PilotPageCopy, -): Promise { - const translation = await importLocaleAsync(locale); - const { title, description } = pickCopy(translation); - - return internalMarketingPilotMetadata({ - locale, - pagePath, - title, - description, - }); -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b4f50d7b..23973c6c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -19,7 +19,7 @@ export { appDocumentMetadata as metadata, appDocumentViewport as viewport }; /** * Minimal root shell for App Router only. Locale comes from {@link APP_ROUTER_LOCALE_HEADER} - * (set in proxy for direct `/internal-marketing/[locale]` visits). + * (set in proxy for App Router locale paths). */ export default async function RootLayout({ children, diff --git a/src/app/locale-app/LocaleAppLayout.tsx b/src/app/locale-app/LocaleAppLayout.tsx index c1f4da66..3e2b7dff 100644 --- a/src/app/locale-app/LocaleAppLayout.tsx +++ b/src/app/locale-app/LocaleAppLayout.tsx @@ -8,7 +8,7 @@ import { authOptions } from "#/server/auth/authOptions"; import { AppRootLayoutClient } from "#/app/AppRootLayoutClient"; -/** Shared App Router locale layout for `app/[lang]` and internal-marketing pilot. */ +/** Shared App Router locale layout for `app/[lang]`. */ export async function LocaleAppLayout({ children, localeParam, diff --git a/src/features/homePage/ui/MarketingHomeView.tsx b/src/features/homePage/ui/MarketingHomeView.tsx index 30ee42fc..19e20a81 100644 --- a/src/features/homePage/ui/MarketingHomeView.tsx +++ b/src/features/homePage/ui/MarketingHomeView.tsx @@ -1,7 +1,7 @@ "use client"; /** - * Marketing home UI. Public `/` and `app/[lang]` reuse this; internal-marketing pilot too. + * Marketing home UI. Public `/` and `app/[lang]` reuse this. */ import { useState } from "react"; diff --git a/src/proxy.ts b/src/proxy.ts index cf10a375..ffdefc6d 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -7,8 +7,6 @@ import { APP_ROUTER_LOCALE_HEADER } from "#/shared/lib/appRouterLocaleHeader"; const localeSet = new Set(locales); -const INTERNAL_MARKETING_PREFIX = "/internal-marketing"; - function withLocaleHeader(request: NextRequest, locale: string): Headers { const requestHeaders = new Headers(request.headers); requestHeaders.set(APP_ROUTER_LOCALE_HEADER, locale); @@ -28,7 +26,7 @@ function localeFromPathname(pathname: string): string | null { * * - Serves `/api/config` from Edge Config. * - Sets {@link APP_ROUTER_LOCALE_HEADER} for App Router locale paths - * (`/internal-marketing/[locale]/*`, `/[lang]/*`, and L2 unprefixed default-locale marketing). + * (`/[lang]/*` and L2 unprefixed default-locale marketing). * * Unprefixed `/`, `/privacy`, … are App `(default-locale)` routes (L2). */ @@ -40,15 +38,6 @@ export async function proxy(request: NextRequest) { return NextResponse.json(config ?? {}); } - if (pathname.startsWith(`${INTERNAL_MARKETING_PREFIX}/`)) { - const segment = pathname.split("/").filter(Boolean)[1]; - if (segment && localeSet.has(segment)) { - return NextResponse.next({ - request: { headers: withLocaleHeader(request, segment) }, - }); - } - } - if (isDefaultLocalePublicMarketingPath(pathname)) { return NextResponse.next({ request: { headers: withLocaleHeader(request, baseLocale) }, @@ -73,10 +62,6 @@ export const config = { "/daily", "/playground/:path*", "/profile/:path*", - { - source: "/internal-marketing/:path*", - locale: false, - }, { source: "/:locale", locale: false, diff --git a/src/scripts/previewSmoke.ts b/src/scripts/previewSmoke.ts index a0429c83..2bbac64f 100644 --- a/src/scripts/previewSmoke.ts +++ b/src/scripts/previewSmoke.ts @@ -1,6 +1,6 @@ #!/usr/bin/env tsx /** - * Vercel/preview merge-gate smoke checks (API routing + pilot routes). + * Vercel/preview merge-gate smoke checks (API routing + public App routes). * * Usage: * PLAYWRIGHT_BASE_URL=https://your-preview.vercel.app pnpm preview-smoke @@ -72,18 +72,15 @@ const pagePaths = [ "/daily", "/playground", "/profile/smoke-user", + "/de/playground/invert-binary-tree", + // L3b legacy URLs — should 308 then 200 after redirect follow. "/internal-marketing/en", "/internal-marketing/en/privacy", - "/internal-marketing/en/daily", - "/internal-marketing/en/playground", + "/internal-marketing/de/daily", "/internal-marketing/en/playground/invert-binary-tree", "/internal-marketing/en/profile/smoke-user", - "/en", "/en/privacy", - "/en/daily", "/en/playground", - "/de/playground/invert-binary-tree", - "/en/profile/smoke-user", ]; const apiChecks: Array<{ path: string; matchedPath: string }> = [ diff --git a/src/shared/hooks/usePlaygroundRoute.ts b/src/shared/hooks/usePlaygroundRoute.ts index d1ff6c87..ced651bb 100644 --- a/src/shared/hooks/usePlaygroundRoute.ts +++ b/src/shared/hooks/usePlaygroundRoute.ts @@ -29,8 +29,8 @@ export type PlaygroundRouteContext = { }; /** - * Unified playground route state for Pages (`/playground`) and App pilot - * (`/internal-marketing/[locale]/playground`). + * Unified playground route state for App Router public paths + * (`/playground`, `/{lang}/playground`, and legacy pilot bookmarks). */ export const usePlaygroundRoute = (): PlaygroundRouteContext | null => { const pagesRouter = usePagesRouterCompat(); diff --git a/src/shared/lib/__tests__/playgroundPath.test.ts b/src/shared/lib/__tests__/playgroundPath.test.ts index 75ce86fa..dfb15484 100644 --- a/src/shared/lib/__tests__/playgroundPath.test.ts +++ b/src/shared/lib/__tests__/playgroundPath.test.ts @@ -56,9 +56,9 @@ describe("playgroundPath", () => { it("remaps slug segments when targetBasePath is provided", () => { const path = "/playground/some-project"; - expect( - getRestorablePlaygroundPath(path, "/internal-marketing/de/playground"), - ).toBe("/internal-marketing/de/playground/some-project"); + expect(getRestorablePlaygroundPath(path, "/de/playground")).toBe( + "/de/playground/some-project", + ); }); it("returns null when path is invalid", () => { diff --git a/src/shared/lib/__tests__/playgroundRoute.test.ts b/src/shared/lib/__tests__/playgroundRoute.test.ts index c431f532..3d1da2f0 100644 --- a/src/shared/lib/__tests__/playgroundRoute.test.ts +++ b/src/shared/lib/__tests__/playgroundRoute.test.ts @@ -3,9 +3,9 @@ import { describe, expect, it } from "vitest"; import { appLocalePlaygroundBasePath, buildPlaygroundPath, - internalMarketingPlaygroundBasePath, parsePlaygroundPathname, PLAYGROUND_PUBLIC_BASE_PATH, + playgroundBasePathForLocale, remapPlaygroundPathToBase, } from "#/shared/lib/playgroundRoute"; @@ -25,12 +25,11 @@ describe("playgroundRoute", () => { }); }); - it("parses internal-marketing pilot playground paths", () => { - const basePath = internalMarketingPlaygroundBasePath("de"); + it("normalizes legacy internal-marketing pilot paths to public bases", () => { expect( - parsePlaygroundPathname("/internal-marketing/de/playground"), + parsePlaygroundPathname("/internal-marketing/en/playground"), ).toEqual({ - basePath, + basePath: PLAYGROUND_PUBLIC_BASE_PATH, slug: [], }); expect( @@ -38,7 +37,7 @@ describe("playgroundRoute", () => { "/internal-marketing/de/playground/invert-binary-tree", ), ).toEqual({ - basePath, + basePath: appLocalePlaygroundBasePath("de"), slug: ["invert-binary-tree"], }); }); @@ -82,20 +81,25 @@ describe("playgroundRoute", () => { ).toBe("/playground/foo/case"); }); - it("remaps stored paths onto a different base (pilot vs public)", () => { - const pilotBase = internalMarketingPlaygroundBasePath("de"); + it("playgroundBasePathForLocale uses unprefixed path for default locale", () => { + expect(playgroundBasePathForLocale("en")).toBe(PLAYGROUND_PUBLIC_BASE_PATH); + expect(playgroundBasePathForLocale("de")).toBe("/de/playground"); + }); + + it("remaps stored paths onto a different base (legacy pilot vs public)", () => { + const deBase = appLocalePlaygroundBasePath("de"); expect( - remapPlaygroundPathToBase("/playground/invert-binary-tree", pilotBase), - ).toBe("/internal-marketing/de/playground/invert-binary-tree"); + remapPlaygroundPathToBase("/playground/invert-binary-tree", deBase), + ).toBe("/de/playground/invert-binary-tree"); expect( remapPlaygroundPathToBase( "/internal-marketing/en/playground/foo/bar", PLAYGROUND_PUBLIC_BASE_PATH, ), ).toBe("/playground/foo/bar"); - expect(remapPlaygroundPathToBase("/playground", pilotBase)).toBeNull(); + expect(remapPlaygroundPathToBase("/playground", deBase)).toBeNull(); expect( - remapPlaygroundPathToBase("/playground/[[...slug]]", pilotBase), + remapPlaygroundPathToBase("/playground/[[...slug]]", deBase), ).toBeNull(); }); }); diff --git a/src/shared/lib/playgroundRoute.ts b/src/shared/lib/playgroundRoute.ts index c1c0b198..eb76412f 100644 --- a/src/shared/lib/playgroundRoute.ts +++ b/src/shared/lib/playgroundRoute.ts @@ -1,10 +1,8 @@ -import { locales } from "#/i18n/i18n-util"; +import { baseLocale, locales } from "#/i18n/i18n-util"; /** Public Pages Router playground prefix (canonical URLs). */ export const PLAYGROUND_PUBLIC_BASE_PATH = "/playground"; -const INTERNAL_MARKETING_PREFIX = "/internal-marketing"; - const localeCodeSet = new Set(locales); export type ParsedPlaygroundRoute = { @@ -12,19 +10,24 @@ export type ParsedPlaygroundRoute = { slug: string[]; }; -/** App Router pilot base for a locale segment. */ -export function internalMarketingPlaygroundBasePath(locale: string): string { - return `${INTERNAL_MARKETING_PREFIX}/${locale}/playground`; -} - /** App Router public base: `/{lang}/playground` (L1 locale migration). */ export function appLocalePlaygroundBasePath(lang: string): string { return `/${lang}/playground`; } +/** Canonical playground base for a locale (unprefixed for default locale). */ +export function playgroundBasePathForLocale(locale: string): string { + if (locale === baseLocale) { + return PLAYGROUND_PUBLIC_BASE_PATH; + } + return appLocalePlaygroundBasePath(locale); +} + /** - * Parses `/playground/...`, `/internal-marketing/{locale}/playground/...`, + * Parses `/playground/...`, legacy `/internal-marketing/{locale}/playground/...`, * or `/{lang}/playground/...` when `lang` is a known locale. + * + * Legacy pilot paths normalize to public bases (bookmarks / localStorage). */ export function parsePlaygroundPathname( pathname: string, @@ -43,17 +46,17 @@ export function parsePlaygroundPathname( return { basePath: PLAYGROUND_PUBLIC_BASE_PATH, slug }; } - const pilotMatch = pathOnly.match( + const legacyPilotMatch = pathOnly.match( /^\/internal-marketing\/([^/]+)\/playground(?:\/(.*))?$/, ); - if (pilotMatch) { - const locale = pilotMatch[1] ?? ""; - const slugPart = pilotMatch[2]; + if (legacyPilotMatch) { + const locale = legacyPilotMatch[1] ?? ""; + const slugPart = legacyPilotMatch[2]; const slug = slugPart ? slugPart.split("/").filter((segment) => segment.length > 0) : []; return { - basePath: internalMarketingPlaygroundBasePath(locale), + basePath: playgroundBasePathForLocale(locale), slug, }; } @@ -85,7 +88,7 @@ export function buildPlaygroundPath(basePath: string, slug: string[]): string { /** * Remaps slug segments from a stored playground path onto `targetBasePath`. - * Used when restoring last project on pilot vs public routes. + * Used when restoring last project across public vs locale-prefixed routes. */ export function remapPlaygroundPathToBase( path: string, diff --git a/vibe-docs/Instant-Navigations-TODO.md b/vibe-docs/Instant-Navigations-TODO.md index 3e530b4b..06d91f4f 100644 --- a/vibe-docs/Instant-Navigations-TODO.md +++ b/vibe-docs/Instant-Navigations-TODO.md @@ -19,21 +19,22 @@ - [x] `app/[lang]/` public routes (mirror internal-marketing pilots) — merged #172 - [x] Rewrites/redirects from Pages to App (L2) — `app/(default-locale)/` at unprefixed URLs -- [x] Remove default-locale Pages marketing (`index`, `privacy`, `daily`, `playground`, `profile`) — L3 partial -- [x] Remove `i18n` from `next.config.mjs` (required once Pages marketing deleted — L2/L4) +- [x] Remove default-locale Pages marketing — L2/L3 +- [x] Remove `i18n` from `next.config.mjs` — L2 +- [x] Retire `/internal-marketing/*` pilot (L3b redirects + delete tree) ## Phase 2 — App Router pilot - [x] `TrpcProvider` + `AppRootLayoutClient` -- [x] `src/app/layout.tsx` + `internal-marketing/[locale]/` pilot +- [x] `src/app/layout.tsx` + public App locale routes - [x] `MarketingHomeView` shared by Pages home + App pilot - [x] Dual-router shell (`next/compat/router`) so App pilot does not throw - [x] Public `/` served from App `(default-locale)` (Pages marketing removed L2/L3) -- [x] `proxy.ts`: `/api/config` + locale header for direct `/internal-marketing/*` +- [x] `proxy.ts`: `/api/config` + locale header for App Router paths - [x] App layout metadata (viewport, icons, Material Icons) - [x] `robots: noindex` on pilot routes (`internalMarketingPilotMetadata` + layout default) - [x] Extend pilot to `/internal-marketing/[locale]/privacy` and `/daily` -- [x] Public cutover of home to App (L2 `(default-locale)/`; pilot `/internal-marketing` until L3b) +- [x] Public cutover of home to App (L2 `(default-locale)/`) - [ ] `cacheComponents` / `partialPrefetching` (blocked: root `headers()` + need 16.3) - [ ] `unstable_instant` on pilot routes (blocked until `cacheComponents`) - [x] Remove unused `@trpc/next` dependency @@ -44,13 +45,13 @@ ## Phase 3+ — Playground / full migration -- [x] Playground App route shell (`/internal-marketing/[locale]/playground/[[...slug]]`) +- [x] Playground App route shell (`app/[lang]/playground`, `(default-locale)/playground`) - [x] `PlaygroundPageView` shared by Pages + App pilot - [x] `usePlaygroundRoute` bridge for slug navigation under App Router -- [x] Profile App route shell (`/internal-marketing/[locale]/profile/[userId]`) +- [x] Profile App route shell (`app/[lang]/profile`, `(default-locale)/profile`) - [x] `ProfilePageView` shared by Pages + App pilot - [x] `useProfileUserId` bridge for App vs Pages route param -- [x] Playwright pilot smoke e2e (`e2e/pilot-routes.spec.ts`, `e2e/api-smoke.spec.ts`) +- [x] Playwright locale migration e2e (`e2e/locale-migration-l*.spec.ts`, `e2e/api-smoke.spec.ts`) - [x] `pnpm preview-smoke` script for Vercel merge-gate checks - [x] GitHub Actions e2e on Vercel preview (`.github/workflows/e2e-preview.yml`, `deployment_status`) - [ ] `@next/playwright` `instant()` tests (blocked: `cacheComponents` + `unstable_instant` + 16.3.x) diff --git a/vibe-docs/Locale-Migration-Design.md b/vibe-docs/Locale-Migration-Design.md index 40f6b5aa..3233114b 100644 --- a/vibe-docs/Locale-Migration-Design.md +++ b/vibe-docs/Locale-Migration-Design.md @@ -12,15 +12,14 @@ Remove `i18n` from `next.config.mjs` and serve user-facing marketing + app route --- -## Current state (post #168–#171) +## Current state (post L1–L3b) | URL | Router | Notes | |-----|--------|--------| -| `/`, `/{locale}` | Pages `index.tsx` | `next.config` `i18n` | -| `/privacy`, `/daily` | Pages SSG | same | -| `/playground/[[...slug]]`, `/profile/[userId]` | Pages SSR | same | -| `/internal-marketing/[locale]/*` | App pilot | **noindex**, canonical → public URL | -| `/api/*` | Pages API | must stay routable under `i18n` on Vercel | +| `/`, `/privacy`, `/daily`, `/playground`, `/profile/*` | App `(default-locale)/` | default locale (en), indexable | +| `/{locale}`, `/{locale}/*` | App `[lang]/` | non-default locales, indexable | +| `/internal-marketing/*`, `/en/*` | **308 redirect** | L3b → public App routes | +| `/api/*` | Pages API | unchanged | Shared views already exist: `MarketingHomeView`, `PrivacyPageView`, `DailyPageView`, `PlaygroundPageView`, `ProfilePageView`. @@ -28,15 +27,13 @@ Dual-router bridges: `usePlaygroundRoute`, `useProfileUserId`, `usePagesRouterCo --- -## Why not flip `i18n` off yet +## Why not flip `i18n` off yet (historical) -Pages `i18n` and App `[lang]` **conflict** if both own locale prefixes. Removing `i18n` before App public routes exist breaks `/de`, `/fr`, … and SEO. - -Pilot lives under `/internal-marketing/` so public Pages routes stay canonical until cutover. +Pages `i18n` and App `[lang]` **conflicted** if both owned locale prefixes. The pilot lived under `/internal-marketing/` until L3b; **`i18n` was removed in L2/L4** once Pages marketing was deleted. --- -## Target routing +## Target routing (achieved) ``` app/[lang]/layout.tsx # Providers, lang from params / proxy header @@ -76,14 +73,20 @@ Pages `i18n` auto-redirects `/en/*` → unprefixed URLs, so **`next.config` rewr ### L3 — Remove Pages marketing (default locale done in L2) -1. ~~Delete `pages/index.tsx`, `pages/privacy.tsx`, `pages/daily.tsx`~~ (done with L2 — Next.js forbids duplicate App/Pages paths). -2. Remove `/internal-marketing/*` App pilot (or 301 → public App). +1. ~~Delete `pages/index.tsx`, `pages/privacy.tsx`, `pages/daily.tsx`~~ (done with L2). +2. ~~Remove `/internal-marketing/*` App pilot~~ — **L3b**: 308 redirects + delete pilot tree. + +### L3b — Retire internal-marketing pilot + +1. `next.config` 308 redirects: `/internal-marketing/*` → public App; `/en/*` → unprefixed. +2. Delete `src/app/internal-marketing/`. +3. Update `proxy.ts`, `playgroundRoute.ts`, e2e, `preview-smoke`. ### L4 — Playground + profile on App only -1. Move public traffic to `app/[lang]/playground` + `profile` (already prototyped). -2. Delete Pages `playground` / `profile` after parity tests. -3. **`i18n` block removed** from `next.config.mjs` (required for `app/[lang]` after Pages marketing deleted). +1. Move public traffic to `app/[lang]/playground` + `profile` (done in L1/L2). +2. ~~Delete Pages `playground` / `profile`~~ (done L2). +3. ~~**`i18n` block removed** from `next.config.mjs`~~ (done L2). ### L5 — Instant Nav flags @@ -119,5 +122,5 @@ Pages `i18n` auto-redirects `/en/*` → unprefixed URLs, so **`next.config` rewr - `vibe-docs/Instant-Navigations-Design.md` - `vibe-docs/Instant-Navigations-TODO.md` -- `src/proxy.ts` — locale header for App pilot -- `src/app/internal-marketing/` — current pilot implementation +- `src/proxy.ts` — locale header for App Router paths +- ~~`src/app/internal-marketing/`~~ — removed L3b (308 redirects to public App) From a4d81fba394f95285ea7ca93ce4cac343591e252 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 23 Aug 2026 18:24:47 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(app):=20address=20L3b=20review=20?= =?UTF-8?q?=E2=80=94=20playground=20path=20normalization=20and=20e2e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use playgroundBasePathForLocale for /en/playground parsing, scope app-locale-routes e2e to non-default locales, assert 308 redirect status, and update stale pilot TODO wording. --- e2e/app-locale-routes.spec.ts | 32 +++++++++---------- e2e/locale-migration-l3b.spec.ts | 10 ++++++ .../lib/__tests__/playgroundRoute.test.ts | 9 ++++-- src/shared/lib/playgroundRoute.ts | 4 +-- vibe-docs/Instant-Navigations-TODO.md | 6 ++-- 5 files changed, 37 insertions(+), 24 deletions(-) diff --git a/e2e/app-locale-routes.spec.ts b/e2e/app-locale-routes.spec.ts index d2b34513..eefd2665 100644 --- a/e2e/app-locale-routes.spec.ts +++ b/e2e/app-locale-routes.spec.ts @@ -3,26 +3,26 @@ import { expect, test } from "@playwright/test"; /** * Smoke tests for public `app/[lang]/*` routes (locale migration L1). * - * Explicit `/en/*` URLs redirect to unprefixed canonicals in L2; see - * `e2e/locale-migration-l2.spec.ts` for unprefixed `/` and `/privacy`. + * Default locale (`en`) is served from `(default-locale)/` at unprefixed URLs; + * see `e2e/locale-migration-l2.spec.ts` and `e2e/locale-migration-l3b.spec.ts`. */ -test.describe("app/[lang] public routes", () => { - test("en home is indexable with public canonical", async ({ page }) => { - await page.goto("/en"); +test.describe("app/[lang] public routes (non-default locales)", () => { + test("de home is indexable with locale canonical", async ({ page }) => { + await page.goto("/de"); await expect(page).toHaveTitle(/dStruct/); await expect(page.locator('meta[name="robots"]')).toHaveCount(0); await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( "href", - "https://dstruct.pro/", + "https://dstruct.pro/de", ); }); - test("en privacy is indexable with public canonical", async ({ page }) => { - await page.goto("/en/privacy"); + test("de privacy is indexable with locale canonical", async ({ page }) => { + await page.goto("/de/privacy"); await expect(page.locator('meta[name="robots"]')).toHaveCount(0); await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( "href", - "https://dstruct.pro/privacy", + "https://dstruct.pro/de/privacy", ); }); @@ -35,29 +35,29 @@ test.describe("app/[lang] public routes", () => { ); }); - test("en playground landing is indexable with public canonical", async ({ + test("de playground landing is indexable with locale canonical", async ({ page, }) => { - await page.goto("/en/playground"); + await page.goto("/de/playground"); await expect(page).toHaveTitle(/Playground/i); await expect(page.locator('meta[name="robots"]')).toHaveCount(0); await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( "href", - "https://dstruct.pro/playground", + "https://dstruct.pro/de/playground", ); }); - test("en profile is noindex with public canonical", async ({ page }) => { + test("de profile is noindex with locale canonical", async ({ page }) => { const userId = "e2e-app-lang-user"; - await page.goto(`/en/profile/${userId}`); - await expect(page).toHaveTitle(/Profile/i); + await page.goto(`/de/profile/${userId}`); + await expect(page).toHaveTitle(/Profil|Profile/i); await expect(page.locator('meta[name="robots"]')).toHaveAttribute( "content", /noindex/i, ); await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( "href", - `https://dstruct.pro/profile/${userId}`, + `https://dstruct.pro/de/profile/${userId}`, ); }); }); diff --git a/e2e/locale-migration-l3b.spec.ts b/e2e/locale-migration-l3b.spec.ts index 2618bfb7..2b015daf 100644 --- a/e2e/locale-migration-l3b.spec.ts +++ b/e2e/locale-migration-l3b.spec.ts @@ -4,6 +4,16 @@ import { expect, test } from "@playwright/test"; * L3b: legacy `/internal-marketing/*` and duplicate `/en/*` URLs 308 to public App routes. */ test.describe("locale migration L3b legacy redirects", () => { + test("legacy URLs respond with 308 permanent redirect", async ({ + request, + }) => { + const response = await request.get("/internal-marketing/en/privacy", { + maxRedirects: 0, + }); + expect(response.status()).toBe(308); + expect(response.headers().location).toBe("/privacy"); + }); + test("internal-marketing en home redirects to /", async ({ page }) => { const response = await page.goto("/internal-marketing/en"); expect(response?.status()).toBeLessThan(400); diff --git a/src/shared/lib/__tests__/playgroundRoute.test.ts b/src/shared/lib/__tests__/playgroundRoute.test.ts index 3d1da2f0..ed20ebe9 100644 --- a/src/shared/lib/__tests__/playgroundRoute.test.ts +++ b/src/shared/lib/__tests__/playgroundRoute.test.ts @@ -43,9 +43,8 @@ describe("playgroundRoute", () => { }); it("parses app/[lang] playground paths", () => { - const basePath = appLocalePlaygroundBasePath("en"); expect(parsePlaygroundPathname("/en/playground")).toEqual({ - basePath, + basePath: PLAYGROUND_PUBLIC_BASE_PATH, slug: [], }); expect( @@ -97,6 +96,12 @@ describe("playgroundRoute", () => { PLAYGROUND_PUBLIC_BASE_PATH, ), ).toBe("/playground/foo/bar"); + expect( + remapPlaygroundPathToBase( + "/en/playground/foo/bar", + PLAYGROUND_PUBLIC_BASE_PATH, + ), + ).toBe("/playground/foo/bar"); expect(remapPlaygroundPathToBase("/playground", deBase)).toBeNull(); expect( remapPlaygroundPathToBase("/playground/[[...slug]]", deBase), diff --git a/src/shared/lib/playgroundRoute.ts b/src/shared/lib/playgroundRoute.ts index eb76412f..055aa001 100644 --- a/src/shared/lib/playgroundRoute.ts +++ b/src/shared/lib/playgroundRoute.ts @@ -69,7 +69,7 @@ export function parsePlaygroundPathname( ? slugPart.split("/").filter((segment) => segment.length > 0) : []; return { - basePath: appLocalePlaygroundBasePath(lang), + basePath: playgroundBasePathForLocale(lang), slug, }; } @@ -77,7 +77,7 @@ export function parsePlaygroundPathname( return null; } -/** Builds a playground path under the given base (public or pilot). Empty segments are omitted. */ +/** Builds a playground path under the given base. Empty segments are omitted. */ export function buildPlaygroundPath(basePath: string, slug: string[]): string { const segments = slug.filter((segment) => segment.length > 0); if (segments.length === 0) { diff --git a/vibe-docs/Instant-Navigations-TODO.md b/vibe-docs/Instant-Navigations-TODO.md index 06d91f4f..0e8a83ce 100644 --- a/vibe-docs/Instant-Navigations-TODO.md +++ b/vibe-docs/Instant-Navigations-TODO.md @@ -32,11 +32,9 @@ - [x] Public `/` served from App `(default-locale)` (Pages marketing removed L2/L3) - [x] `proxy.ts`: `/api/config` + locale header for App Router paths - [x] App layout metadata (viewport, icons, Material Icons) -- [x] `robots: noindex` on pilot routes (`internalMarketingPilotMetadata` + layout default) -- [x] Extend pilot to `/internal-marketing/[locale]/privacy` and `/daily` -- [x] Public cutover of home to App (L2 `(default-locale)/`) +- [x] ~~Pilot noindex metadata~~ (removed with L3b internal-marketing pilot) - [ ] `cacheComponents` / `partialPrefetching` (blocked: root `headers()` + need 16.3) -- [ ] `unstable_instant` on pilot routes (blocked until `cacheComponents`) +- [ ] `unstable_instant` on marketing routes (blocked until `cacheComponents`) - [x] Remove unused `@trpc/next` dependency - [x] Extract `authOptions` to `src/server/auth/authOptions.ts` - [x] Extract `AppShellProviders` shared by `_app` and `AppRootLayoutClient`