- next.config.ts: cssChunking 'loose' → false ('loose' not in type)
- ActivityFeed.test.tsx: remove always-truthy TS2872 literal expression
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
221 lines
6.5 KiB
TypeScript
221 lines
6.5 KiB
TypeScript
import type { NextConfig } from "next";
|
|
import dotenv from "dotenv";
|
|
import path from "path";
|
|
import bundleAnalyzer from "@next/bundle-analyzer";
|
|
import createNextIntlPlugin from "next-intl/plugin";
|
|
// Load the .env file from the working directory
|
|
dotenv.config({ path: path.resolve(process.cwd(), ".env") });
|
|
|
|
const nextConfig: NextConfig = {
|
|
// Enable standalone output for Docker
|
|
output: "standalone",
|
|
outputFileTracingRoot: path.join(process.cwd()),
|
|
|
|
// Optimize for production
|
|
compress: true,
|
|
poweredByHeader: false,
|
|
|
|
// React Strict Mode
|
|
// In dev, React StrictMode double-mount can cause visible animation flicker
|
|
// (Framer Motion "fade starts, disappears, then pops").
|
|
reactStrictMode: process.env.NODE_ENV === "production",
|
|
|
|
// Disable ESLint during build for Docker
|
|
eslint: {
|
|
ignoreDuringBuilds: process.env.NODE_ENV === "production",
|
|
},
|
|
|
|
// Environment variables
|
|
env: {
|
|
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
|
|
},
|
|
|
|
// Performance optimizations
|
|
experimental: {
|
|
// Tree-shake barrel-file packages in both dev and production
|
|
optimizePackageImports: ["lucide-react", "framer-motion", "react-icons", "@tiptap/react"],
|
|
// Merge all CSS into a single chunk to eliminate the render-blocking CSS chain
|
|
// (84dc7384.css → 3aefc04b.css sequential dependency reported by PageSpeed).
|
|
cssChunking: false,
|
|
// Note: optimizeCss (critters) is intentionally disabled — it converts the main
|
|
// <link rel="stylesheet"> to a JS-deferred preload, which PageSpeed reads as a
|
|
// sequential CSS chain and reports 410ms of render-blocking.
|
|
...(process.env.NODE_ENV !== "production" ? { webpackBuildWorker: true } : {}),
|
|
},
|
|
|
|
// Image optimization
|
|
images: {
|
|
formats: ["image/webp", "image/avif"],
|
|
minimumCacheTTL: 2592000,
|
|
remotePatterns: [
|
|
{
|
|
protocol: "https",
|
|
hostname: "i.scdn.co",
|
|
},
|
|
{
|
|
protocol: "https",
|
|
hostname: "cdn.discordapp.com",
|
|
},
|
|
{
|
|
protocol: "https",
|
|
hostname: "media.discordapp.net",
|
|
},
|
|
{
|
|
protocol: "https",
|
|
hostname: "cms.dk0.dev",
|
|
},
|
|
{
|
|
protocol: "https",
|
|
hostname: "assets.hardcover.app",
|
|
},
|
|
{
|
|
protocol: "https",
|
|
hostname: "dki.one",
|
|
},
|
|
{
|
|
protocol: "https",
|
|
hostname: "images.unsplash.com",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Webpack configuration
|
|
webpack: (config, { dev, isServer, webpack }) => {
|
|
// Fix for module resolution issues
|
|
config.resolve.fallback = {
|
|
...config.resolve.fallback,
|
|
fs: false,
|
|
net: false,
|
|
tls: false,
|
|
};
|
|
|
|
// Optimize webpack cache - fix "Serializing big strings" warnings in dev by avoiding FS cache
|
|
if (dev) {
|
|
config.cache = {
|
|
type: "memory",
|
|
maxGenerations: 5,
|
|
};
|
|
|
|
if (!isServer) {
|
|
// Suppress framer-motion source map errors in development
|
|
config.plugins.push(
|
|
new webpack.SourceMapDevToolPlugin({
|
|
filename: "[file].map",
|
|
exclude: [/framer-motion/, /LayoutGroupContext/],
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
return config;
|
|
},
|
|
|
|
// Security and cache headers
|
|
async headers() {
|
|
const csp =
|
|
process.env.NODE_ENV === "production"
|
|
? // Avoid `unsafe-eval` in production (reduces XSS impact and enables stronger CSP)
|
|
"default-src 'self'; script-src 'self' 'unsafe-inline'; script-src-elem 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: blob: https:; connect-src 'self' https://*.dk0.dev; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; worker-src 'self' blob:;"
|
|
: // Dev CSP: allow eval for tooling compatibility, and localhost for HMR/API
|
|
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; script-src-elem 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: blob: https: http://localhost:3000; connect-src 'self' http://localhost:3000 ws://localhost:3000 https://*.dk0.dev; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; worker-src 'self' blob:;";
|
|
|
|
return [
|
|
{
|
|
source: "/(.*)",
|
|
headers: [
|
|
{
|
|
key: "X-DNS-Prefetch-Control",
|
|
value: "on",
|
|
},
|
|
{
|
|
key: "Strict-Transport-Security",
|
|
value: "max-age=63072000; includeSubDomains; preload",
|
|
},
|
|
{
|
|
key: "X-Frame-Options",
|
|
value: "DENY",
|
|
},
|
|
{
|
|
key: "X-Content-Type-Options",
|
|
value: "nosniff",
|
|
},
|
|
{
|
|
key: "X-XSS-Protection",
|
|
value: "1; mode=block",
|
|
},
|
|
{
|
|
key: "Referrer-Policy",
|
|
value: "strict-origin-when-cross-origin",
|
|
},
|
|
{
|
|
key: "Permissions-Policy",
|
|
value: "camera=(), microphone=(), geolocation=()",
|
|
},
|
|
{
|
|
key: "Content-Security-Policy",
|
|
value: csp,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
// Allow bfcache for n8n routes: use no-cache (revalidate) instead of no-store
|
|
source: "/api/n8n/(.*)",
|
|
headers: [
|
|
{
|
|
key: "Cache-Control",
|
|
value: "no-cache, must-revalidate",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
source: "/api/auth/(.*)",
|
|
headers: [
|
|
{
|
|
key: "Cache-Control",
|
|
value: "no-store, no-cache, must-revalidate, proxy-revalidate",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
source: "/api/email/(.*)",
|
|
headers: [
|
|
{
|
|
key: "Cache-Control",
|
|
value: "no-store, no-cache, must-revalidate, proxy-revalidate",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
source: "/api/contacts/(.*)",
|
|
headers: [
|
|
{
|
|
key: "Cache-Control",
|
|
value: "no-store, no-cache, must-revalidate, proxy-revalidate",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
source: "/_next/static/(.*)",
|
|
headers: [
|
|
{
|
|
key: "Cache-Control",
|
|
// In dev, aggressive caching breaks HMR and can brick a tab with stale chunks.
|
|
value:
|
|
process.env.NODE_ENV === "production"
|
|
? "public, max-age=31536000, immutable"
|
|
: "no-store",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
},
|
|
};
|
|
|
|
const withBundleAnalyzer = bundleAnalyzer({
|
|
enabled: process.env.ANALYZE === "true",
|
|
});
|
|
|
|
const withNextIntl = createNextIntlPlugin("./i18n/request.ts");
|
|
|
|
export default withBundleAnalyzer(withNextIntl(nextConfig));
|