Repository navigation
Expand file tree
/
Copy pathnext.config.ts
More file actions
130 lines (127 loc) · 4.85 KB
/
Copy pathnext.config.ts
File metadata and controls
130 lines (127 loc) · 4.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import type { NextConfig } from "next";
/**
* Baseline security headers applied to every response.
*
* CSP ships as `Content-Security-Policy-Report-Only` so the browser
* surfaces violations in the console without blocking anything — once
* we have confidence nothing legit trips it (two deploys, a pass on
* every route), flip the key to `Content-Security-Policy` to enforce.
*
* The rest of the headers are straight blocks, safe to enforce today:
* - HSTS: only meaningful on HTTPS (no-op on http://localhost).
* - X-Content-Type-Options / X-Frame-Options / Referrer-Policy:
* baseline OWASP hardening, no behavioural cost.
* - Permissions-Policy: we don't use camera / microphone / etc, so
* deny them. A supply-chain compromise or a forgotten plugin
* can't silently opt back in.
*/
const SECURITY_HEADERS = [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(), payment=(), usb=()",
},
{
key: "Content-Security-Policy-Report-Only",
value: [
"default-src 'self'",
// Next.js needs 'unsafe-inline' for its inline hydration script
// and 'unsafe-eval' in dev + some production optimisations.
// Nonce-based CSP is a later project.
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
// Tailwind + inline style attributes on lots of components.
"style-src 'self' 'unsafe-inline'",
// Supabase public-bucket avatars, contact avatars (arbitrary
// https URLs paste-able from the UI), OG images, data URLs for
// tiny inline assets.
"img-src 'self' data: blob: https:",
"font-src 'self' data:",
// Supabase REST + realtime (WSS). All Meta API calls happen
// server-side, so graph.facebook.com does not belong here.
"connect-src 'self' https://*.supabase.co wss://*.supabase.co",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join("; "),
},
] as const;
const nextConfig: NextConfig = {
/**
* Cache-Control policy.
*
* Why this exists:
* Hostinger's CDN was applying `s-maxage=31536000` (1 year) to
* prerendered HTML pages by default. When a new deploy shipped
* fresh Turbopack chunk hashes, the edge kept serving year-old
* HTML referencing chunk filenames that no longer existed on
* disk — result: HTML 200, every /_next/static/*.js and .css
* came back 404, the page rendered unstyled. Private/incognito
* did nothing because the cache is server-side.
*
* Strategy:
* - /_next/static/* — immutable for a year. Filenames are
* content-hashed, so a new build produces new filenames; the
* old ones are safe to keep indefinitely in caches.
* - /api/* — no-store. API responses are per-user and
* must never be shared across requests at the edge.
* - Everything else — public, brief s-maxage + generous
* stale-while-revalidate. The edge serves instantly from cache
* for the first 5 min, then returns cached content while
* refreshing in the background for up to 24 h. A deploy's
* chunk-hash drift self-heals within ~5 min with no user-
* visible latency.
*
* Note: dynamic dashboard routes (/inbox, /contacts, /pipelines,
* /broadcasts, etc.) are server-rendered per request — Next.js
* and Supabase auth already prevent them from being served
* from a shared cache. The s-maxage here is a ceiling; Next.js
* and auth middleware still set `private` / `no-store` for
* per-user responses.
*
* Security headers are appended via a separate catch-all rule
* below — Next.js merges headers from every matching rule, so
* they apply to every response regardless of which cache rule
* matched.
*/
async headers() {
return [
{
source: "/_next/static/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
{
source: "/api/:path*",
headers: [{ key: "Cache-Control", value: "no-store" }],
},
{
source: "/:path*",
headers: [
{
key: "Cache-Control",
value:
"public, max-age=0, s-maxage=300, stale-while-revalidate=86400",
},
],
},
{
// Security headers on every response, including /_next/static
// assets (nosniff matters there) and /api/* (HSTS + referrer-
// policy don't hurt).
source: "/:path*",
headers: [...SECURITY_HEADERS],
},
];
},
};
export default nextConfig;