fix(web): 404 for a first segment that is not a locale

Middleware leaves dotted paths alone, so /wp-login.php reached the locale
layout and rendered the home page with HTTP 200 and an invalid html lang.

Test: locale-layout.test.ts 2/2 (not-found case failed without the fix).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
This commit is contained in:
Hunter BandClaude Opus 5.5 committed 2026-09-29 04:32:51 -07:00
1 parent c37873eb00
commit 49f11f711a
2 files changed
+31 -3

No files matched your search

+8 -3
View File
@@ -1,10 +1,11 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import { notFound } from "next/navigation";
import { Nav } from "@/components/nav";
import { Footer } from "@/components/footer";
import { UsageCounting } from "@/components/usage-counting";
import { BUILD_FACTS } from "@/lib/facts";
import { localeDirection, locales, type Locale } from "@/lib/i18n/config";
import { isValidLocale, localeDirection, locales } from "@/lib/i18n/config";
import { getChrome, getHome } from "@/lib/i18n/dictionaries";
import { serializeJsonLd } from "@/lib/json-ld";
import { buildPageMetadata } from "@/lib/page-meta";
@@ -74,6 +75,10 @@ export default async function LocaleLayout({
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Middleware leaves dotted paths alone, so `/wp-login.php` reaches this
// segment with that "locale". Without this it rendered the home page with
// HTTP 200 and `<html lang="wp-login.php">`.
if (!isValidLocale(locale)) notFound();
const chrome = getChrome(locale);
// RTL locales (e.g. ar) set the document direction from the canonical
// registry so the browser handles bidirectional layout from the root.
@@ -106,9 +111,9 @@ export default async function LocaleLayout({
<a href="#main-content" className="skip-link">
{chrome.skipToContent}
</a>
<Nav locale={locale as Locale} />
<Nav locale={locale} />
<main id="main-content">{children}</main>
<Footer locale={locale as Locale} />
<Footer locale={locale} />
{/* Aggregate usage counting, on by default — see lib/telemetry. The
choice lives on the privacy page; every opt-out stays off. */}
<UsageCounting appVersion={BUILD_FACTS.version ?? "0.0.0"} />
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("next/font/local", () => ({ default: () => ({ variable: "font" }) }));
const { default: LocaleLayout } = await import("@/app/[locale]/layout");
function render(locale: string) {
return LocaleLayout({ children: null, params: Promise.resolve({ locale }) });
}
describe("locale layout", () => {
// Middleware skips dotted paths, so `/wp-login.php` reaches `[locale]`.
it("answers not-found for a segment that is not a locale", async () => {
await expect(render("wp-login.php")).rejects.toMatchObject({
digest: expect.stringContaining("404"),
});
});
it("renders a real locale", async () => {
const element = await render("en");
expect(element.props.lang).toBe("en");
});
});