diff --git a/web/app/[locale]/(legal)/eula/page.tsx b/web/app/[locale]/(legal)/eula/page.tsx index ddc9ae0a..9e0b108f 100644 --- a/web/app/[locale]/(legal)/eula/page.tsx +++ b/web/app/[locale]/(legal)/eula/page.tsx @@ -3,7 +3,7 @@ import type { Metadata } from "next"; export const metadata: Metadata = { title: "EULA — cmux", description: "End-User License Agreement for cmux", - alternates: { canonical: "./" }, + alternates: { canonical: "https://cmux.com/eula" }, }; export default function EulaPage() { diff --git a/web/app/[locale]/(legal)/privacy-policy/page.tsx b/web/app/[locale]/(legal)/privacy-policy/page.tsx index f0940728..b307d7ab 100644 --- a/web/app/[locale]/(legal)/privacy-policy/page.tsx +++ b/web/app/[locale]/(legal)/privacy-policy/page.tsx @@ -4,7 +4,7 @@ import { Link } from "../../../../i18n/navigation"; export const metadata: Metadata = { title: "Privacy Policy — cmux", description: "Privacy policy for cmux", - alternates: { canonical: "./" }, + alternates: { canonical: "https://cmux.com/privacy-policy" }, }; export default function PrivacyPolicyPage() { diff --git a/web/app/[locale]/(legal)/terms-of-service/page.tsx b/web/app/[locale]/(legal)/terms-of-service/page.tsx index 56b4b98e..3b048eec 100644 --- a/web/app/[locale]/(legal)/terms-of-service/page.tsx +++ b/web/app/[locale]/(legal)/terms-of-service/page.tsx @@ -3,7 +3,7 @@ import type { Metadata } from "next"; export const metadata: Metadata = { title: "Terms of Service — cmux", description: "Terms of service for cmux", - alternates: { canonical: "./" }, + alternates: { canonical: "https://cmux.com/terms-of-service" }, }; export default function TermsOfServicePage() { diff --git a/web/app/[locale]/blog/cmd-shift-u/page.tsx b/web/app/[locale]/blog/cmd-shift-u/page.tsx index e87e3fab..5cdb1ecf 100644 --- a/web/app/[locale]/blog/cmd-shift-u/page.tsx +++ b/web/app/[locale]/blog/cmd-shift-u/page.tsx @@ -1,11 +1,11 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { Link } from "../../../../i18n/navigation"; export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; const t = await getTranslations({ locale, namespace: "blog.cmdShiftU" }); - const url = locale === "en" ? "/blog/cmd-shift-u" : `/${locale}/blog/cmd-shift-u`; return { title: t("metaTitle"), description: t("metaDescription"), @@ -18,14 +18,13 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s description: t("metaDescription"), type: "article", publishedTime: "2026-03-04T00:00:00Z", - url, }, twitter: { card: "summary_large_image", title: t("metaTitle"), description: t("metaDescription"), }, - alternates: { canonical: url }, + alternates: buildAlternates(locale, "/blog/cmd-shift-u"), }; } diff --git a/web/app/[locale]/blog/introducing-cmux/page.tsx b/web/app/[locale]/blog/introducing-cmux/page.tsx index 82dadc10..811c07eb 100644 --- a/web/app/[locale]/blog/introducing-cmux/page.tsx +++ b/web/app/[locale]/blog/introducing-cmux/page.tsx @@ -1,11 +1,11 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { Link } from "../../../../i18n/navigation"; export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; const t = await getTranslations({ locale, namespace: "blog.introducingCmux" }); - const url = locale === "en" ? "/blog/introducing-cmux" : `/${locale}/blog/introducing-cmux`; return { title: t("metaTitle"), description: t("metaDescription"), @@ -18,14 +18,13 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s description: t("metaDescription"), type: "article", publishedTime: "2026-02-12T00:00:00Z", - url, }, twitter: { card: "summary_large_image", title: t("metaTitle"), description: t("metaDescription"), }, - alternates: { canonical: url }, + alternates: buildAlternates(locale, "/blog/introducing-cmux"), }; } diff --git a/web/app/[locale]/blog/layout.tsx b/web/app/[locale]/blog/layout.tsx index 30caa6cc..9a3a18aa 100644 --- a/web/app/[locale]/blog/layout.tsx +++ b/web/app/[locale]/blog/layout.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../i18n/seo"; import { SiteHeader } from "../components/site-header"; import { BlogPager } from "../components/blog-pager"; import { BlogCTA } from "../components/blog-cta"; @@ -19,9 +20,7 @@ export async function generateMetadata({ siteName: "cmux", type: "article" as const, }, - alternates: { - canonical: "./", - }, + alternates: buildAlternates(locale, "/blog"), }; } diff --git a/web/app/[locale]/blog/page.tsx b/web/app/[locale]/blog/page.tsx index 1fb32d4f..f9cf1ac3 100644 --- a/web/app/[locale]/blog/page.tsx +++ b/web/app/[locale]/blog/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../i18n/seo"; import { Link } from "../../../i18n/navigation"; export async function generateMetadata({ @@ -12,6 +13,7 @@ export async function generateMetadata({ return { title: t("metaTitle"), description: t("metaDescription"), + alternates: buildAlternates(locale, "/blog"), }; } diff --git a/web/app/[locale]/blog/show-hn-launch/page.tsx b/web/app/[locale]/blog/show-hn-launch/page.tsx index 71eb3bd9..3bf1041a 100644 --- a/web/app/[locale]/blog/show-hn-launch/page.tsx +++ b/web/app/[locale]/blog/show-hn-launch/page.tsx @@ -1,6 +1,7 @@ import Image from "next/image"; import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { Link } from "../../../../i18n/navigation"; import { Tweet } from "react-tweet"; import starHistory from "./star-history.png"; @@ -8,7 +9,6 @@ import starHistory from "./star-history.png"; export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; const t = await getTranslations({ locale, namespace: "blog.showHnLaunch" }); - const url = locale === "en" ? "/blog/show-hn-launch" : `/${locale}/blog/show-hn-launch`; return { title: t("metaTitle"), description: t("metaDescription"), @@ -22,14 +22,13 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s description: t("metaDescription"), type: "article", publishedTime: "2026-02-21T00:00:00Z", - url, }, twitter: { card: "summary_large_image", title: t("metaTitle"), description: t("metaDescription"), }, - alternates: { canonical: url }, + alternates: buildAlternates(locale, "/blog/show-hn-launch"), }; } diff --git a/web/app/[locale]/blog/zen-of-cmux/page.tsx b/web/app/[locale]/blog/zen-of-cmux/page.tsx index cff31a9c..74248fdc 100644 --- a/web/app/[locale]/blog/zen-of-cmux/page.tsx +++ b/web/app/[locale]/blog/zen-of-cmux/page.tsx @@ -1,11 +1,11 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { Link } from "../../../../i18n/navigation"; export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; const t = await getTranslations({ locale, namespace: "blog.zenOfCmux" }); - const url = locale === "en" ? "/blog/zen-of-cmux" : `/${locale}/blog/zen-of-cmux`; return { title: t("metaTitle"), description: t("metaDescription"), @@ -18,14 +18,13 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s description: t("metaDescription"), type: "article", publishedTime: "2026-02-27T00:00:00Z", - url, }, twitter: { card: "summary_large_image", title: t("metaTitle"), description: t("metaDescription"), }, - alternates: { canonical: url }, + alternates: buildAlternates(locale, "/blog/zen-of-cmux"), }; } diff --git a/web/app/[locale]/community/page.tsx b/web/app/[locale]/community/page.tsx index 3742df3b..63244579 100644 --- a/web/app/[locale]/community/page.tsx +++ b/web/app/[locale]/community/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../i18n/seo"; import { SiteHeader } from "../components/site-header"; export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { @@ -8,7 +9,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), - alternates: { canonical: "./" }, + alternates: buildAlternates(locale, "/community"), }; } diff --git a/web/app/[locale]/docs/api/page.tsx b/web/app/[locale]/docs/api/page.tsx index 712a46ef..4152abed 100644 --- a/web/app/[locale]/docs/api/page.tsx +++ b/web/app/[locale]/docs/api/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { CodeBlock } from "../../components/code-block"; import { Callout } from "../../components/callout"; @@ -9,6 +10,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), + alternates: buildAlternates(locale, "/docs/api"), }; } diff --git a/web/app/[locale]/docs/browser-automation/page.tsx b/web/app/[locale]/docs/browser-automation/page.tsx index 32d72eaa..f44cd6b0 100644 --- a/web/app/[locale]/docs/browser-automation/page.tsx +++ b/web/app/[locale]/docs/browser-automation/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { CodeBlock } from "../../components/code-block"; export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { @@ -8,6 +9,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), + alternates: buildAlternates(locale, "/docs/browser-automation"), }; } diff --git a/web/app/[locale]/docs/changelog/page.tsx b/web/app/[locale]/docs/changelog/page.tsx index b467cbdd..0cca42bd 100644 --- a/web/app/[locale]/docs/changelog/page.tsx +++ b/web/app/[locale]/docs/changelog/page.tsx @@ -3,6 +3,7 @@ import path from "path"; import Image from "next/image"; import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { changelogMedia, type VersionMedia } from "./changelog-media"; /** Read PNG dimensions from the IHDR chunk (bytes 16-23). */ @@ -21,6 +22,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), + alternates: buildAlternates(locale, "/docs/changelog"), }; } diff --git a/web/app/[locale]/docs/concepts/page.tsx b/web/app/[locale]/docs/concepts/page.tsx index 88d10319..3cb390c1 100644 --- a/web/app/[locale]/docs/concepts/page.tsx +++ b/web/app/[locale]/docs/concepts/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { CodeBlock } from "../../components/code-block"; export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { @@ -8,6 +9,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), + alternates: buildAlternates(locale, "/docs/concepts"), }; } diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index 7186f4eb..182ed116 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { CodeBlock } from "../../components/code-block"; import { Callout } from "../../components/callout"; @@ -9,6 +10,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), + alternates: buildAlternates(locale, "/docs/configuration"), }; } diff --git a/web/app/[locale]/docs/custom-commands/page.tsx b/web/app/[locale]/docs/custom-commands/page.tsx index 048f1b04..aedb01f8 100644 --- a/web/app/[locale]/docs/custom-commands/page.tsx +++ b/web/app/[locale]/docs/custom-commands/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { CodeBlock } from "../../components/code-block"; import { Callout } from "../../components/callout"; @@ -9,6 +10,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), + alternates: buildAlternates(locale, "/docs/custom-commands"), }; } diff --git a/web/app/[locale]/docs/getting-started/page.tsx b/web/app/[locale]/docs/getting-started/page.tsx index 788fff6a..e0bf5194 100644 --- a/web/app/[locale]/docs/getting-started/page.tsx +++ b/web/app/[locale]/docs/getting-started/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { CodeBlock } from "../../components/code-block"; import { Callout } from "../../components/callout"; import { DownloadButton } from "../../components/download-button"; @@ -10,6 +11,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), + alternates: buildAlternates(locale, "/docs/getting-started"), }; } diff --git a/web/app/[locale]/docs/keyboard-shortcuts/page.tsx b/web/app/[locale]/docs/keyboard-shortcuts/page.tsx index 8d2f323c..70b25ea8 100644 --- a/web/app/[locale]/docs/keyboard-shortcuts/page.tsx +++ b/web/app/[locale]/docs/keyboard-shortcuts/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { KeyboardShortcuts } from "../../keyboard-shortcuts"; export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { @@ -8,6 +9,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), + alternates: buildAlternates(locale, "/docs/keyboard-shortcuts"), }; } diff --git a/web/app/[locale]/docs/layout.tsx b/web/app/[locale]/docs/layout.tsx index 571cef00..1681d9a5 100644 --- a/web/app/[locale]/docs/layout.tsx +++ b/web/app/[locale]/docs/layout.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../i18n/seo"; import { DocsNav } from "./docs-nav"; import { SiteHeader } from "../components/site-header"; @@ -18,9 +19,7 @@ export async function generateMetadata({ siteName: "cmux", type: "article" as const, }, - alternates: { - canonical: "./", - }, + alternates: buildAlternates(locale, "/docs"), }; } diff --git a/web/app/[locale]/docs/notifications/page.tsx b/web/app/[locale]/docs/notifications/page.tsx index b02de0e1..0a337de2 100644 --- a/web/app/[locale]/docs/notifications/page.tsx +++ b/web/app/[locale]/docs/notifications/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../../i18n/seo"; import { CodeBlock } from "../../components/code-block"; import { Callout } from "../../components/callout"; @@ -9,6 +10,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), + alternates: buildAlternates(locale, "/docs/notifications"), }; } diff --git a/web/app/[locale]/layout.tsx b/web/app/[locale]/layout.tsx index 32c2cc03..ad35d0ce 100644 --- a/web/app/[locale]/layout.tsx +++ b/web/app/[locale]/layout.tsx @@ -8,6 +8,7 @@ import { } from "next-intl/server"; import { notFound } from "next/navigation"; import { routing } from "../../i18n/routing"; +import { buildAlternates } from "../../i18n/seo"; import { Providers } from "./providers"; import { DevPanel } from "./components/spacing-control"; import { SiteFooter } from "./components/site-footer"; @@ -30,8 +31,7 @@ export async function generateMetadata({ }): Promise { const { locale } = await params; const t = await getTranslations({ locale, namespace: "meta" }); - const url = - locale === "en" ? "https://cmux.com" : `https://cmux.com/${locale}`; + const alternates = buildAlternates(locale, ""); return { title: t("title"), description: t("description"), @@ -52,7 +52,7 @@ export async function generateMetadata({ openGraph: { title: t("title"), description: t("ogDescription"), - url, + url: alternates.canonical, siteName: "cmux", type: "website", }, @@ -61,6 +61,7 @@ export async function generateMetadata({ title: t("title"), description: t("ogDescription"), }, + alternates, metadataBase: new URL("https://cmux.com"), }; } diff --git a/web/app/[locale]/nightly/page.tsx b/web/app/[locale]/nightly/page.tsx index d3fb1a31..457b2e83 100644 --- a/web/app/[locale]/nightly/page.tsx +++ b/web/app/[locale]/nightly/page.tsx @@ -1,5 +1,6 @@ import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../i18n/seo"; import { SiteHeader } from "../components/site-header"; export async function generateMetadata({ @@ -12,7 +13,7 @@ export async function generateMetadata({ return { title: t("metaTitle"), description: t("metaDescription"), - alternates: { canonical: "./" }, + alternates: buildAlternates(locale, "/nightly"), }; } diff --git a/web/app/[locale]/wall-of-love/page.tsx b/web/app/[locale]/wall-of-love/page.tsx index c1ec7d41..3b1c565f 100644 --- a/web/app/[locale]/wall-of-love/page.tsx +++ b/web/app/[locale]/wall-of-love/page.tsx @@ -1,5 +1,6 @@ import { useTranslations, useLocale } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { buildAlternates } from "../../../i18n/seo"; import { SiteHeader } from "../components/site-header"; import { testimonials, TestimonialCard, getTestimonialTranslation } from "../testimonials"; @@ -9,7 +10,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s return { title: t("metaTitle"), description: t("metaDescription"), - alternates: { canonical: "./" }, + alternates: buildAlternates(locale, "/wall-of-love"), }; } diff --git a/web/app/robots.ts b/web/app/robots.ts index 1b471bcf..0a177d8c 100644 --- a/web/app/robots.ts +++ b/web/app/robots.ts @@ -2,7 +2,7 @@ import type { MetadataRoute } from "next"; export default function robots(): MetadataRoute.Robots { return { - rules: { userAgent: "*", allow: "/" }, + rules: { userAgent: "*", allow: "/", disallow: "/_next/" }, sitemap: "https://cmux.com/sitemap.xml", }; } diff --git a/web/app/sitemap.ts b/web/app/sitemap.ts index ecc6e0bb..4749347e 100644 --- a/web/app/sitemap.ts +++ b/web/app/sitemap.ts @@ -14,6 +14,7 @@ export default function sitemap(): MetadataRoute.Sitemap { { path: "/docs/getting-started", lastModified: "2026-03-18", changeFrequency: "monthly" as const, priority: 0.9 }, { path: "/docs/concepts", lastModified: "2026-03-18", changeFrequency: "monthly" as const, priority: 0.8 }, { path: "/docs/configuration", lastModified: "2026-03-18", changeFrequency: "monthly" as const, priority: 0.8 }, + { path: "/docs/custom-commands", lastModified: "2026-03-18", changeFrequency: "monthly" as const, priority: 0.7 }, { path: "/docs/keyboard-shortcuts", lastModified: "2026-03-18", changeFrequency: "monthly" as const, priority: 0.7 }, { path: "/docs/api", lastModified: "2026-03-18", changeFrequency: "monthly" as const, priority: 0.8 }, { path: "/docs/notifications", lastModified: "2026-03-18", changeFrequency: "monthly" as const, priority: 0.8 }, @@ -27,9 +28,22 @@ export default function sitemap(): MetadataRoute.Sitemap { { path: "/eula", lastModified: "2026-03-18", changeFrequency: "yearly" as const, priority: 0.3 }, ]; + // Legal pages are English-only (not translated), so they only get one entry. + const englishOnly = new Set(["/privacy-policy", "/terms-of-service", "/eula"]); + const entries: MetadataRoute.Sitemap = []; for (const { path, lastModified, changeFrequency, priority } of paths) { + if (englishOnly.has(path)) { + entries.push({ + url: `${base}${path}`, + lastModified, + changeFrequency, + priority, + }); + continue; + } + const alternates: Record = {}; for (const locale of locales) { alternates[locale] = @@ -37,13 +51,18 @@ export default function sitemap(): MetadataRoute.Sitemap { } alternates["x-default"] = `${base}${path}`; - entries.push({ - url: `${base}${path}`, - lastModified, - changeFrequency, - priority, - alternates: { languages: alternates }, - }); + // Emit a separate entry for each locale so Google sees every URL declared + for (const locale of locales) { + const url = + locale === "en" ? `${base}${path}` : `${base}/${locale}${path}`; + entries.push({ + url, + lastModified, + changeFrequency, + priority, + alternates: { languages: alternates }, + }); + } } return entries; diff --git a/web/i18n/seo.ts b/web/i18n/seo.ts new file mode 100644 index 00000000..21683e5a --- /dev/null +++ b/web/i18n/seo.ts @@ -0,0 +1,22 @@ +import { locales } from "./routing"; + +const BASE = "https://cmux.com"; + +/** + * Build the full alternates object (canonical + hreflang languages) + * for a given locale and path. Use in every generateMetadata that + * sets alternates so child metadata doesn't wipe parent hreflang. + */ +export function buildAlternates(locale: string, path: string) { + const languages: Record = {}; + for (const loc of locales) { + languages[loc] = + loc === "en" ? `${BASE}${path}` : `${BASE}/${loc}${path}`; + } + languages["x-default"] = `${BASE}${path}`; + + const canonical = + locale === "en" ? `${BASE}${path}` : `${BASE}/${locale}${path}`; + + return { canonical, languages }; +} diff --git a/web/next.config.ts b/web/next.config.ts index 3ab0db0d..8328ed22 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -5,7 +5,6 @@ import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin("./i18n/request.ts"); const nextConfig: NextConfig = { - skipTrailingSlashRedirect: true, images: { remotePatterns: [ { diff --git a/web/proxy.ts b/web/proxy.ts index 547c423b..af503681 100644 --- a/web/proxy.ts +++ b/web/proxy.ts @@ -15,6 +15,25 @@ export default function middleware(request: NextRequest) { return NextResponse.redirect(url.toString(), 301); } + // Legal pages are English-only. Redirect //legal-page to /legal-page, + // and skip next-intl for /legal-page so locale detection can't redirect back. + const legalPages = new Set(["/privacy-policy", "/terms-of-service", "/eula"]); + const { pathname } = request.nextUrl; + if (legalPages.has(pathname)) { + const url = request.nextUrl.clone(); + url.pathname = `/en${pathname}`; + return NextResponse.rewrite(url); + } + const secondSlash = pathname.indexOf("/", 1); + if (secondSlash !== -1) { + const rest = pathname.slice(secondSlash); + if (legalPages.has(rest)) { + const url = request.nextUrl.clone(); + url.pathname = rest; + return NextResponse.redirect(url, 301); + } + } + return intlMiddleware(request); }