Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
32 changes: 16 additions & 16 deletions e2e/app-locale-routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
});

Expand All @@ -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}`,
);
});
});
71 changes: 71 additions & 0 deletions e2e/locale-migration-l3b.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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);
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",
);
});
});
88 changes: 0 additions & 88 deletions e2e/pilot-routes.spec.ts

This file was deleted.

36 changes: 36 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
29 changes: 0 additions & 29 deletions src/app/internal-marketing/[locale]/daily/page.tsx

This file was deleted.

30 changes: 0 additions & 30 deletions src/app/internal-marketing/[locale]/layout.tsx

This file was deleted.

32 changes: 0 additions & 32 deletions src/app/internal-marketing/[locale]/page.tsx

This file was deleted.

Loading
Loading