* Fix SEO indexing: add hreflang, canonicals, sitemap per-locale entries Google Search Console showed 380 not-indexed vs 86 indexed pages. Root causes: missing hreflang tags on rendered pages (only in sitemap), no canonical on homepage, inconsistent canonicals wiping parent hreflang, sitemap only listing English URLs, trailing slash duplicates, and _next/static chunks being crawled as pages. Changes: - Add buildAlternates() utility for consistent canonical + hreflang - Add hreflang tags to all pages via alternates.languages in metadata - Add self-referencing canonical URLs to every page (homepage had none) - Expand sitemap to emit separate entries for each locale - Add missing /docs/custom-commands to sitemap - Remove skipTrailingSlashRedirect to normalize trailing slashes - Block /_next/ in robots.txt to stop chunk crawling * Add per-page alternates to docs sub-pages and blog index Docs sub-pages and blog index only returned title/description in generateMetadata, so they inherited the parent layout's alternates (pointing to /docs or /blog). Now each page sets its own buildAlternates() with the correct path so canonical and hreflang point to the actual page URL. * Derive openGraph.url from buildAlternates to avoid drift * Redirect non-English legal pages to English, remove from sitemap Legal pages (privacy policy, TOS, EULA) are untranslated English content. Serving them under every locale creates 54 duplicate URLs. Now: - Middleware 301-redirects /ja/privacy-policy etc. to /privacy-policy - Sitemap only includes English URLs for legal pages (no locale variants) - Legal page metadata uses static English-only canonical * Fix legal page redirect to only match /<locale>/<page> paths endsWith matched too broadly (e.g. /docs/eula). Now only redirects when the path after the first segment is an exact legal page match. * Skip next-intl for legal pages to prevent locale redirect loop Without this, a Japanese user hitting /privacy-policy could be redirected by next-intl to /ja/privacy-policy, which our middleware redirects back to /privacy-policy, creating a loop. * Rewrite legal pages to /en/ instead of NextResponse.next() Pages live under app/[locale]/, so skipping next-intl entirely would break route resolution. Rewrite to /en/privacy-policy etc. so Next.js can resolve the [locale] segment correctly. --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
42 lines
1.4 KiB
TypeScript
42 lines
1.4 KiB
TypeScript
import { type NextRequest, NextResponse } from "next/server";
|
|
import createMiddleware from "next-intl/middleware";
|
|
import { routing } from "./i18n/routing";
|
|
|
|
const intlMiddleware = createMiddleware(routing);
|
|
|
|
export default function middleware(request: NextRequest) {
|
|
const host = request.headers.get("host") ?? "";
|
|
|
|
// 301 redirect cmux.dev (and www.cmux.dev) to cmux.com, preserving path and query
|
|
if (host === "cmux.dev" || host === "www.cmux.dev") {
|
|
const url = new URL(request.url);
|
|
url.host = "cmux.com";
|
|
url.protocol = "https:";
|
|
return NextResponse.redirect(url.toString(), 301);
|
|
}
|
|
|
|
// Legal pages are English-only. Redirect /<locale>/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);
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
|
|
};
|