routing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRouting & Proxy (Next.js 16)
路由与Proxy(Next.js 16)
1. High-Level Concept
1. 核心概念
This project MUST rely on the Next.js 16 Proxy (the successor to ) to intercept requests at the edge. The Proxy's primary job is to enforce Authentication boundaries, perform Localization (i18n) redirects, and append security cookies/headers before a Page or Layout is ever rendered.
middleware.ts- Edge Execution: The Proxy runs at the edge. It MUST NOT load Heavy Node modules or the Drizzle ORM directly.
- Header Injection: You MUST append the or
x-current-pathto the requested headers so Server Components can read them later.x-locale - Cookies: You MUST use strictly for high-level state like session identifiers and localization preferences.
NextResponse.cookies
本项目必须依赖Next.js 16 Proxy(的替代方案)在边缘节点拦截请求。Proxy的主要职责是在页面或布局渲染前,强制执行认证边界、执行国际化(i18n)重定向,并添加安全Cookie/请求头。
middleware.ts- 边缘执行:Proxy运行在边缘节点,禁止直接加载重型Node模块或Drizzle ORM。
- 请求头注入:必须在请求头中添加或
x-current-path,以便后续Server Components读取。x-locale - Cookie管理:必须严格使用存储会话标识、本地化偏好等高层级状态。
NextResponse.cookies
2. Proxy Structure (Localization and Redirection)
2. Proxy结构(本地化与重定向)
You MUST export a function instead of the legacy function.
proxymiddlewareThe most common responsibility is mapping the user's to the correct URL segment or redirecting them out of if they lack a session.
localesdashboardtypescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { LOCALE, LOCALE_COOKIE_NAME } from "./shared/infrastructure/i18n/config";
import { isValidLocale, determineLocaleFromAcceptLangOrDefault } from "./shared/infrastructure/i18n/utils";
// Exclude static assets and api routes in config
export const config = {
matcher: [`/((?!api|trpc|_next|_next/image|favicon.ico|_vercel|.*\\..*).*)`],
};
const pathWithoutLocale = ["dashboard"];
// 1. Helper to append locale headers
function createLocaleHeaders({ request, locale }: { request: NextRequest; locale: LOCALE; }) {
const newHeaders = new Headers(request.headers);
newHeaders.set(LOCALE_COOKIE_NAME, locale);
return newHeaders;
}
// 2. Helper to forward request
function createNextResponse({ request, locale }: { request: NextRequest; locale: LOCALE; }) {
const requestHeaders = createLocaleHeaders({ request, locale });
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.cookies.set(LOCALE_COOKIE_NAME, locale, { httpOnly: true, path: "/" });
return response;
}
// 3. Helper to redirect request
function createLocaleRedirect({ newPath, request, locale }: { newPath: string; request: NextRequest; locale: string; }) {
const redirectUrl = new URL(newPath, request.url);
const response = NextResponse.redirect(redirectUrl);
response.cookies.set(LOCALE_COOKIE_NAME, locale, { httpOnly: true, path: "/" });
return response;
}
export function proxy(request: NextRequest) {
const { pathname, search } = request.nextUrl;
const localeCookie = request.cookies.get(LOCALE_COOKIE_NAME)?.value;
const segments = pathname.split("/");
const firstSegment = segments[1];
const preferredLocale = isValidLocale(localeCookie)
? localeCookie
: determineLocaleFromAcceptLangOrDefault(request.headers.get("accept-language"));
// Dashboards don't use i18n segments in URL
if (pathWithoutLocale.includes(firstSegment)) {
// Add authentication checks here
return createNextResponse({ request, locale: preferredLocale });
}
// Segment matches exactly
if (isValidLocale(firstSegment)) {
if (firstSegment === preferredLocale) {
return createNextResponse({ request, locale: preferredLocale });
}
// Redirect to preferred locale if wrong
const newPath = pathname.replace(`/${firstSegment}`, `/${preferredLocale}`);
return createLocaleRedirect({ newPath, request, locale: preferredLocale });
}
// Missing segment entirely
const newPath = `/${preferredLocale}${pathname}${search}`;
return createLocaleRedirect({ newPath, request, locale: preferredLocale });
}必须导出函数而非旧版的函数。
proxymiddleware最常见的职责是将用户的映射到正确的URL分段,或在用户未登录时将其从路由重定向出去。
localesdashboardtypescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { LOCALE, LOCALE_COOKIE_NAME } from "./shared/infrastructure/i18n/config";
import { isValidLocale, determineLocaleFromAcceptLangOrDefault } from "./shared/infrastructure/i18n/utils";
// Exclude static assets and api routes in config
export const config = {
matcher: [`/((?!api|trpc|_next|_next/image|favicon.ico|_vercel|.*\\..*).*)`],
};
const pathWithoutLocale = ["dashboard"];
// 1. Helper to append locale headers
function createLocaleHeaders({ request, locale }: { request: NextRequest; locale: LOCALE; }) {
const newHeaders = new Headers(request.headers);
newHeaders.set(LOCALE_COOKIE_NAME, locale);
return newHeaders;
}
// 2. Helper to forward request
function createNextResponse({ request, locale }: { request: NextRequest; locale: LOCALE; }) {
const requestHeaders = createLocaleHeaders({ request, locale });
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.cookies.set(LOCALE_COOKIE_NAME, locale, { httpOnly: true, path: "/" });
return response;
}
// 3. Helper to redirect request
function createLocaleRedirect({ newPath, request, locale }: { newPath: string; request: NextRequest; locale: string; }) {
const redirectUrl = new URL(newPath, request.url);
const response = NextResponse.redirect(redirectUrl);
response.cookies.set(LOCALE_COOKIE_NAME, locale, { httpOnly: true, path: "/" });
return response;
}
export function proxy(request: NextRequest) {
const { pathname, search } = request.nextUrl;
const localeCookie = request.cookies.get(LOCALE_COOKIE_NAME)?.value;
const segments = pathname.split("/");
const firstSegment = segments[1];
const preferredLocale = isValidLocale(localeCookie)
? localeCookie
: determineLocaleFromAcceptLangOrDefault(request.headers.get("accept-language"));
// Dashboards don't use i18n segments in URL
if (pathWithoutLocale.includes(firstSegment)) {
// Add authentication checks here
return createNextResponse({ request, locale: preferredLocale });
}
// Segment matches exactly
if (isValidLocale(firstSegment)) {
if (firstSegment === preferredLocale) {
return createNextResponse({ request, locale: preferredLocale });
}
// Redirect to preferred locale if wrong
const newPath = pathname.replace(`/${firstSegment}`, `/${preferredLocale}`);
return createLocaleRedirect({ newPath, request, locale: preferredLocale });
}
// Missing segment entirely
const newPath = `/${preferredLocale}${pathname}${search}`;
return createLocaleRedirect({ newPath, request, locale: preferredLocale });
}3. Route Layouts (Public vs Private)
3. 路由布局(公开 vs 私有)
If the project is multilingual (i18n), you MUST cleanly separate pages into routes that contain the segment in the URL (public) and those that do not (private/authenticated). A proxy handles the boundary between them.
[locale]-
(Public Routes):
app/[locale]/- You MUST place public paths like the root landing page, ,
/login, or/registerhere./pricing - The UI automatically wraps inside the localized context derived from the URL parameters instead of headers.
- You MUST NOT fetch the session aggressively from use-cases unless your explicit goal is to redirect an already-logged-in user to an authenticated route.
- You MUST place public paths like the root landing page,
-
(Private / Authenticated Routes):
app/dashboard/- You MUST NOT include in the URL segment for private, authenticated functionality.
[locale] - It MUST rely entirely on the cookie injected by the Proxy to determine language translation for components.
x-locale - Auth validation MUST be the primary responsibility of use-cases called in this layout, not the Proxy, to preserve Domain rules.
serviceContainer
- You MUST NOT include
如果项目支持多语言(i18n),必须将页面清晰划分为URL中包含分段的路由(公开)和不包含该分段的路由(私有/已认证)。Proxy负责处理两者之间的边界。
[locale]-
(公开路由):
app/[locale]/- 必须将根着陆页、、
/login或/register等公开路径放置在此处。/pricing - UI会自动包裹在从URL参数获取的本地化上下文中,而非依赖请求头。
- 除非明确要将已登录用户重定向到认证路由,否则不要在用例中主动获取会话信息。
- 必须将根着陆页、
-
(私有/已认证路由):
app/dashboard/- 私有、已认证功能的URL分段中禁止包含。
[locale] - 必须完全依赖Proxy注入的Cookie来确定组件的语言翻译。
x-locale - 认证验证必须是此布局中调用的用例的主要职责,而非Proxy,以保留领域规则。
serviceContainer
- 私有、已认证功能的URL分段中禁止包含
4. Resource Routing (IDs vs Slugs)
4. 资源路由(ID vs 别名)
When constructing dynamic URL structures, particularly in a REST API-like hierarchy, you MUST use stable entity Identifiers (IDs), NOT slugs or entity names. Names can change over time via user edits, which causes slugs to break bookmarks and historical references.
- Correct:
/dashboard/[workspaceId]/folders/[folderId] - Incorrect:
/dashboard/[workspaceSlug]/folders/[folderSlug]
构建动态URL结构时,尤其是在类REST API的层级中,必须使用稳定的实体标识符(ID),而非别名(slugs)或实体名称。名称可能会随用户编辑而更改,导致别名破坏书签和历史引用。
- 正确示例:
/dashboard/[workspaceId]/folders/[folderId] - 错误示例:
/dashboard/[workspaceSlug]/folders/[folderSlug]
5. Contextless Route Redirection
5. 无上下文路由重定向
Any route that cannot render intelligently without context MUST NOT attempt to load a generic UI. It MUST act exclusively as a redirection layer.
For example, in a multi-tenant or workspace-based application, navigating to directly lacks the necessary routing parameter to display relevant details. Therefore, MUST NOT exist as a rendered page. Instead, it MUST execute a Use Case to identify the user's primary or last-used entity and immediately to the proper context path, such as .
/dashboard{ workspaceId }/dashboard/page.tsxredirect()/dashboard/[workspaceId]任何缺乏上下文就无法智能渲染的路由,都不得尝试加载通用UI,必须仅作为重定向层。
例如,在多租户或基于工作区的应用中,直接访问缺少必要的路由参数来显示相关详情。因此,不得作为可渲染页面存在。相反,它必须执行一个用例来识别用户的主要或最近使用的实体,并立即到正确的上下文路径,如。
/dashboard{ workspaceId }/dashboard/page.tsxredirect()/dashboard/[workspaceId]6. Protected Paths and Server Components
6. 受保护路径与Server Components
You MUST NOT throw or natively redirect inside a Server Component layout unless exhausting domain errors. While the proxy CAN intercept unauthorized navigation statically, complex auth rules (e.g., "Does this user own Workspace X?") MUST be resolved defensively in the Use Case executed by a Server Component.
ForbiddenassertNever除非遇到领域错误,否则不得在Server Components布局中抛出错误或原生重定向。虽然Proxy可以静态拦截未授权导航,但复杂的认证规则(如“该用户是否拥有工作区X?”)必须由Server Components执行的用例来防御性地解决。
assertNeverForbidden7. References
7. 参考资料
For practical examples of routing and proxy patterns:
- Server Component Domain Redirect - How to properly handle complex domain-based auth and redirection inside a Next.js Layout/Page without depending on the Proxy.
- Contextless Redirect - How to handle root authenticated paths (e.g., ) that lack context parameters and MUST redirect to specific resource URLs.
/dashboard
有关路由和Proxy模式的实用示例:
- Server Component Domain Redirect - 如何在Next.js布局/页面中正确处理基于域名的复杂认证和重定向,而不依赖Proxy。
- Contextless Redirect - 如何处理缺少上下文参数的根认证路径(如),并将其重定向到特定资源URL。
/dashboard