* Revise portfolio: warm brown theme, elegant typography, optimized analytics tracking (#55) * Initial plan * Update color theme to warm brown and off-white, add elegant fonts, fix analytics tracking Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Fix 404 page integration with warm theme, update admin console colors, fix font loading Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Address code review feedback: fix navigation, add utils, improve tracking Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Fix accessibility and memory leak issues from code review Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * chore: Code cleanup, add Sentry.io monitoring, and documentation (#56) * Initial plan * Remove unused code and clean up console statements Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Remove unused components and fix type issues Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Wrap console.warn in development check Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Integrate Sentry.io monitoring and add text editing documentation Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Initial plan * feat: Add Sentry configuration files and example pages - Add sentry.server.config.ts and sentry.edge.config.ts - Update instrumentation.ts with onRequestError export - Update instrumentation-client.ts with onRouterTransitionStart export - Update global-error.tsx to capture exceptions with Sentry - Create Sentry example page at app/sentry-example-page/page.tsx - Create Sentry example API route at app/api/sentry-example-api/route.ts Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * feat: Update middleware to allow Sentry example page and fix deprecated API - Update middleware to exclude /sentry-example-page from locale routing - Remove deprecated startTransaction API from Sentry example page - Use consistent DSN configuration with fallback values Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * refactor: Improve Sentry configuration with environment-based sampling - Add comments explaining DSN fallback values - Use environment-based tracesSampleRate (10% in production, 100% in dev) - Address code review feedback for production-safe configuration Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import type { NextRequest } from "next/server";
|
|
|
|
const SUPPORTED_LOCALES = ["en", "de"] as const;
|
|
type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
|
|
|
function pickLocaleFromHeader(acceptLanguage: string | null): SupportedLocale {
|
|
if (!acceptLanguage) return "en";
|
|
const lower = acceptLanguage.toLowerCase();
|
|
// Very small parser: prefer de, then en
|
|
if (lower.includes("de")) return "de";
|
|
if (lower.includes("en")) return "en";
|
|
return "en";
|
|
}
|
|
|
|
function hasLocalePrefix(pathname: string): boolean {
|
|
return SUPPORTED_LOCALES.some((l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`));
|
|
}
|
|
|
|
export function middleware(request: NextRequest) {
|
|
const { pathname, search } = request.nextUrl;
|
|
|
|
// If a locale-prefixed request hits a public asset path (e.g. /de/images/me.jpg),
|
|
// redirect to the non-prefixed asset path.
|
|
if (hasLocalePrefix(pathname)) {
|
|
const rest = pathname.replace(/^\/(en|de)/, "") || "/";
|
|
if (rest.includes(".")) {
|
|
const responseUrl = request.nextUrl.clone();
|
|
responseUrl.pathname = rest;
|
|
const res = NextResponse.redirect(responseUrl);
|
|
return addHeaders(request, res);
|
|
}
|
|
}
|
|
|
|
// Do not locale-route public assets (anything with a dot), robots, sitemap, etc.
|
|
if (pathname.includes(".")) {
|
|
return addHeaders(request, NextResponse.next());
|
|
}
|
|
|
|
// Keep admin + APIs unlocalized for simplicity
|
|
const isAdminOrApi =
|
|
pathname.startsWith("/api/") ||
|
|
pathname === "/api" ||
|
|
pathname.startsWith("/manage") ||
|
|
pathname.startsWith("/editor") ||
|
|
pathname === "/sentry-example-page" ||
|
|
pathname.startsWith("/sentry-example-page/");
|
|
|
|
// Locale routing for public site pages
|
|
const responseUrl = request.nextUrl.clone();
|
|
|
|
if (!isAdminOrApi) {
|
|
if (hasLocalePrefix(pathname)) {
|
|
// Persist locale preference
|
|
const locale = pathname.split("/")[1] as SupportedLocale;
|
|
const res = NextResponse.next();
|
|
res.cookies.set("NEXT_LOCALE", locale, { path: "/" });
|
|
|
|
// Continue below to add security headers
|
|
return addHeaders(request, res);
|
|
}
|
|
|
|
// Redirect bare routes to locale-prefixed ones
|
|
const preferred = pickLocaleFromHeader(request.headers.get("accept-language"));
|
|
const redirectTarget =
|
|
pathname === "/" ? `/${preferred}` : `/${preferred}${pathname}${search || ""}`;
|
|
responseUrl.pathname = redirectTarget;
|
|
const res = NextResponse.redirect(responseUrl);
|
|
res.cookies.set("NEXT_LOCALE", preferred, { path: "/" });
|
|
return addHeaders(request, res);
|
|
}
|
|
|
|
// Fix for 421 Misdirected Request with Nginx Proxy Manager
|
|
// Ensure proper host header handling for reverse proxy
|
|
const hostname = request.headers.get("host") || request.headers.get("x-forwarded-host") || "";
|
|
|
|
// Add security headers to all responses
|
|
const response = NextResponse.next();
|
|
|
|
return addHeaders(request, response, hostname);
|
|
}
|
|
|
|
function addHeaders(request: NextRequest, response: NextResponse, hostnameOverride?: string) {
|
|
const hostname =
|
|
hostnameOverride ??
|
|
request.headers.get("host") ??
|
|
request.headers.get("x-forwarded-host") ??
|
|
"";
|
|
|
|
// Set proper headers for Nginx Proxy Manager
|
|
if (hostname) {
|
|
response.headers.set("X-Forwarded-Host", hostname);
|
|
response.headers.set(
|
|
"X-Real-IP",
|
|
request.headers.get("x-real-ip") || request.headers.get("x-forwarded-for") || "",
|
|
);
|
|
}
|
|
|
|
// Security headers (complementing next.config.ts headers)
|
|
response.headers.set("X-DNS-Prefetch-Control", "on");
|
|
response.headers.set("X-Frame-Options", "DENY");
|
|
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
response.headers.set("X-XSS-Protection", "1; mode=block");
|
|
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
response.headers.set(
|
|
"Permissions-Policy",
|
|
"camera=(), microphone=(), geolocation=()",
|
|
);
|
|
|
|
// Rate limiting headers for API routes
|
|
if (request.nextUrl.pathname.startsWith("/api/")) {
|
|
response.headers.set("X-RateLimit-Limit", "100");
|
|
response.headers.set("X-RateLimit-Remaining", "99");
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
export const config = {
|
|
matcher: [
|
|
/*
|
|
* Match all request paths except for the ones starting with:
|
|
* - api (all API routes)
|
|
* - _next/static (static files)
|
|
* - _next/image (image optimization files)
|
|
* - favicon.ico (favicon file)
|
|
*/
|
|
"/((?!api|_next/static|_next/image|favicon.ico).*)",
|
|
],
|
|
};
|