Update homepage routes to include language and mode
Build & Deploy / Build & Push Docker Image (push) Successful in 2m51s
Test / Type Check (all packages) (push) Successful in 52s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 49s
Test / Homepage Unit Tests (push) Successful in 44s
Test / Storefront Unit Tests (push) Successful in 39s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 40s
Test / API Integration Tests (push) Successful in 1m0s

This commit is contained in:
root
2026-07-02 14:10:17 -04:00
parent 56c3bcf666
commit 781512399b
51 changed files with 792 additions and 344 deletions
+98 -45
View File
@@ -1,14 +1,21 @@
import {
defaultMode,
defaultLocale,
isMode,
isLocale,
localeCookie,
localeCookieMaxAgeSeconds,
localeFromAcceptLanguage,
localizedPath,
localizedModePath,
routeIdFromAnyLocaleSlug,
routeIdFromSlug,
} from '@/lib/localization/config';
import { buildContentSecurityPolicy, createNonce } from '@/lib/security/csp';
import {
isThemePreference,
themeCookie,
themeCookieMaxAgeSeconds,
} from '@/lib/theme/config';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
@@ -39,6 +46,38 @@ function resolveApiOrigin(): string | undefined {
return origins.size > 0 ? Array.from(origins).join(' ') : undefined;
}
function resolveRequestLocale(request: NextRequest) {
const cookieLocale = request.cookies.get(localeCookie)?.value;
return isLocale(cookieLocale)
? cookieLocale
: localeFromAcceptLanguage(request.headers.get('accept-language')) || defaultLocale;
}
function resolveRequestMode(request: NextRequest) {
const cookieMode = request.cookies.get(themeCookie)?.value;
return isThemePreference(cookieMode) ? cookieMode : defaultMode;
}
function setLocaleCookie(response: NextResponse, locale: string): void {
response.cookies.set(localeCookie, locale, {
path: '/',
sameSite: 'lax',
maxAge: localeCookieMaxAgeSeconds,
secure: process.env.NODE_ENV === 'production',
httpOnly: false,
});
}
function setModeCookie(response: NextResponse, mode: string): void {
response.cookies.set(themeCookie, mode, {
path: '/',
sameSite: 'lax',
maxAge: themeCookieMaxAgeSeconds,
secure: process.env.NODE_ENV === 'production',
httpOnly: false,
});
}
export function proxy(request: NextRequest): NextResponse {
const nonce = createNonce();
const isDev = process.env.NODE_ENV !== 'production';
@@ -49,20 +88,13 @@ export function proxy(request: NextRequest): NextResponse {
requestHeaders.set('Content-Security-Policy', csp);
if (request.nextUrl.pathname === '/') {
const cookieLocale = request.cookies.get(localeCookie)?.value;
const locale = isLocale(cookieLocale)
? cookieLocale
: localeFromAcceptLanguage(request.headers.get('accept-language')) || defaultLocale;
const locale = resolveRequestLocale(request);
const mode = resolveRequestMode(request);
const destination = request.nextUrl.clone();
destination.pathname = `/${locale}`;
destination.pathname = `/${locale}/${mode}`;
const response = NextResponse.redirect(destination);
response.cookies.set(localeCookie, locale, {
path: '/',
sameSite: 'lax',
maxAge: localeCookieMaxAgeSeconds,
secure: process.env.NODE_ENV === 'production',
httpOnly: false,
});
setLocaleCookie(response, locale);
setModeCookie(response, mode);
return applySecurityHeaders(response, csp);
}
@@ -72,32 +104,34 @@ export function proxy(request: NextRequest): NextResponse {
destination.pathname = `/${pathSegments.slice(1).join('/')}`;
return applySecurityHeaders(NextResponse.redirect(destination), csp);
}
// Redirect locale-less paths (e.g. /sign-in → /en/sign-in), but not proxied app paths or files.
const firstSegment = request.nextUrl.pathname.split('/')[1];
const isFile = /\.\w+$/.test(request.nextUrl.pathname);
if (firstSegment && !isFile && !isLocale(firstSegment) && !reservedProxySegments.has(firstSegment)) {
const cookieLocale = request.cookies.get(localeCookie)?.value;
const locale = isLocale(cookieLocale)
? cookieLocale
: localeFromAcceptLanguage(request.headers.get('accept-language')) || defaultLocale;
if (isLocale(pathSegments[0]) && isMode(pathSegments[1]) && pathSegments[2] === 'storefront') {
const destination = request.nextUrl.clone();
destination.pathname = `/${locale}${request.nextUrl.pathname}`;
destination.pathname = `/${pathSegments.slice(2).join('/')}`;
return applySecurityHeaders(NextResponse.redirect(destination), csp);
}
if (isLocale(firstSegment) && request.nextUrl.pathname === `/${firstSegment}/subscription`) {
// Redirect locale-less paths (e.g. /sign-in → /en/system/sign-in), but not proxied app paths or files.
const firstSegment = request.nextUrl.pathname.split('/')[1];
const isFile = /\.\w+$/.test(request.nextUrl.pathname);
if (firstSegment && !isFile && !isLocale(firstSegment) && !reservedProxySegments.has(firstSegment)) {
const locale = resolveRequestLocale(request);
const mode = resolveRequestMode(request);
const destination = request.nextUrl.clone();
destination.pathname = `/${locale}/${mode}${request.nextUrl.pathname}`;
return applySecurityHeaders(NextResponse.redirect(destination), csp);
}
if (
isLocale(pathSegments[0]) &&
isMode(pathSegments[1]) &&
request.nextUrl.pathname === `/${pathSegments[0]}/${pathSegments[1]}/subscription`
) {
const destination = request.nextUrl.clone();
destination.pathname = '/dashboard/subscription';
const response = NextResponse.redirect(destination);
response.cookies.set(localeCookie, firstSegment, {
path: '/',
sameSite: 'lax',
maxAge: localeCookieMaxAgeSeconds,
secure: process.env.NODE_ENV === 'production',
httpOnly: false,
});
response.cookies.set(dashboardLanguageCookie, firstSegment, {
setLocaleCookie(response, pathSegments[0]);
setModeCookie(response, pathSegments[1]);
response.cookies.set(dashboardLanguageCookie, pathSegments[0], {
path: '/',
sameSite: 'lax',
maxAge: localeCookieMaxAgeSeconds,
@@ -107,29 +141,48 @@ export function proxy(request: NextRequest): NextResponse {
return applySecurityHeaders(response, csp);
}
if (isLocale(firstSegment)) {
const segments = request.nextUrl.pathname.split('/').filter(Boolean);
const slug = segments.slice(1).join('/');
if (slug && !routeIdFromSlug(firstSegment, slug)) {
if (isLocale(pathSegments[0]) && !isMode(pathSegments[1])) {
const mode = resolveRequestMode(request);
const slug = pathSegments.slice(1).join('/');
if (!slug) {
const destination = request.nextUrl.clone();
destination.pathname = localizedModePath('home', pathSegments[0], mode);
return applySecurityHeaders(NextResponse.redirect(destination), csp);
}
const routeId = routeIdFromSlug(pathSegments[0], slug) ?? routeIdFromAnyLocaleSlug(slug);
if (routeId) {
const destination = request.nextUrl.clone();
destination.pathname = localizedModePath(routeId, pathSegments[0], mode);
return applySecurityHeaders(NextResponse.redirect(destination), csp);
}
if (slug === 'component-lab') {
const destination = request.nextUrl.clone();
destination.pathname = `/${pathSegments[0]}/${mode}/component-lab`;
return applySecurityHeaders(NextResponse.redirect(destination), csp);
}
}
if (isLocale(pathSegments[0]) && isMode(pathSegments[1])) {
requestHeaders.set('x-theme-preference', pathSegments[1]);
const slug = pathSegments.slice(2).join('/');
if (slug && !routeIdFromSlug(pathSegments[0], slug)) {
const routeId = routeIdFromAnyLocaleSlug(slug);
if (routeId) {
const destination = request.nextUrl.clone();
destination.pathname = localizedPath(routeId, firstSegment);
destination.pathname = localizedModePath(routeId, pathSegments[0], pathSegments[1]);
return applySecurityHeaders(NextResponse.redirect(destination), csp);
}
}
}
const response = NextResponse.next({ request: { headers: requestHeaders } });
const explicitLocale = request.nextUrl.pathname.split('/')[1];
const explicitLocale = pathSegments[0];
if (isLocale(explicitLocale)) {
response.cookies.set(localeCookie, explicitLocale, {
path: '/',
sameSite: 'lax',
maxAge: localeCookieMaxAgeSeconds,
secure: process.env.NODE_ENV === 'production',
httpOnly: false,
});
setLocaleCookie(response, explicitLocale);
}
const explicitMode = pathSegments[1];
if (isMode(explicitMode)) {
setModeCookie(response, explicitMode);
}
return applySecurityHeaders(response, csp);
}