-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnext.config.ts
More file actions
176 lines (170 loc) · 7.85 KB
/
Copy pathnext.config.ts
File metadata and controls
176 lines (170 loc) · 7.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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import type { NextConfig } from "next";
import { version } from "./package.json";
/**
* Content Security Policy, sent as a header on every response.
*
* Pages are prerendered and cached (s-maxage), so a per-request nonce is not
* possible: it would force dynamic rendering. Next's inline scripts (the
* `self.__next_f` payload, bootstrap) differ on every page and every build, so
* hashes cannot be listed in a static header either. Scripts therefore keep
* `'unsafe-inline'`; the rest is locked down — no foreign script host but the
* Matomo instance, no plugin, no `<base>` hijack, no framing, forms posting
* only to the site. The site renders no user-provided HTML unsanitised (wiki
* content goes through `cleanHtml`), which is what inline script injection
* would need in the first place.
*
* The Matomo origin is read at build time (a Docker build argument), like the
* tracker itself.
*/
function contentSecurityPolicy(): string {
const development = process.env.NODE_ENV === "development";
let matomo = "";
try {
if (process.env.NEXT_PUBLIC_MATOMO_URL) matomo = new URL(process.env.NEXT_PUBLIC_MATOMO_URL).origin;
} catch {
// Malformed URL: the tracker would not load either.
}
const directives: Record<string, string[]> = {
"default-src": ["'self'"],
// React's development build evaluates code for its call stacks.
"script-src": ["'self'", "'unsafe-inline'", ...(development ? ["'unsafe-eval'"] : []), matomo],
// Server-rendered `style` attributes (bars, rarity colours) and the styles
// Next injects.
"style-src": ["'self'", "'unsafe-inline'"],
// data: placeholders and inline SVG, blob: exported cards (tier list,
// retribution), remote hosts matching `images.remotePatterns`, and the
// Matomo image fallback.
"img-src": [
"'self'",
"data:",
"blob:",
"https://static.wikia.nocookie.net",
"https://akmpicture.youngjoygame.com",
"https://akmweb.youngjoygame.com",
matomo,
],
"font-src": ["'self'", "data:"],
// Hot reload uses a WebSocket in development.
"connect-src": ["'self'", matomo, ...(development ? ["ws:", "wss:"] : [])],
// Video export preview of the retribution tool.
"media-src": ["'self'", "blob:"],
"worker-src": ["'self'"],
"manifest-src": ["'self'"],
"frame-src": ["'none'"],
"object-src": ["'none'"],
"base-uri": ["'self'"],
"form-action": ["'self'"],
"frame-ancestors": ["'none'"],
};
const policy = Object.entries(directives).map(([name, sources]) =>
[name, ...sources.filter(Boolean)].join(" "),
);
if (!development) policy.push("upgrade-insecure-requests");
return policy.join("; ");
}
/**
* The site has a server side — in-game login sessions, calls to the community
* API — so no static export. `standalone` produces a self-contained folder
* holding only the files actually used, which gives a production image much
* lighter than the full tree.
*/
const nextConfig: NextConfig = {
output: "standalone",
// Otherwise Next generates agent instruction files at the repository root.
// This project is published under its author's name: nothing of the kind
// belongs there.
agentRules: false,
reactStrictMode: true,
poweredByHeader: false,
// The root does not render <html> ([locale]/layout does): an unknown address
// outside any locale, such as /llms.txt before it existed, ended in a 500
// error in production. This global 404 renders its own document.
experimental: { globalNotFound: true },
compress: true,
// Site version, read from package.json at build time: the footer shows it
// without having to copy it by hand.
env: { VERSION_SITE: version },
images: {
// Game visuals are copied locally (public/visuels) by the sync. The wiki
// stays allowed only for images embedded in the patch notes, which are
// reused as they are.
remotePatterns: [
{ protocol: "https", hostname: "static.wikia.nocookie.net" },
// Profile avatars, served by Moonton's CDN.
{ protocol: "https", hostname: "akmpicture.youngjoygame.com" },
// Fallback skill icons, when the wiki does not provide them.
{ protocol: "https", hostname: "akmweb.youngjoygame.com" },
],
formats: ["image/avif", "image/webp"],
// 50: darkened backgrounds (detail page banner); 75: everything else.
qualities: [50, 75],
},
// Routes moved to English. The old French addresses, already indexed and
// shared, redirect permanently to the new ones.
async redirects() {
return [
{ source: "/heros/:path*", destination: "/heroes/:path*", permanent: true },
{ source: "/objets/:path*", destination: "/items/:path*", permanent: true },
{ source: "/emblemes", destination: "/emblems", permanent: true },
{ source: "/actualites/:path*", destination: "/news/:path*", permanent: true },
{ source: "/veille", destination: "/watch", permanent: true },
{ source: "/comparateur", destination: "/compare", permanent: true },
{ source: "/modes-de-jeu/:path*", destination: "/game-modes/:path*", permanent: true },
{ source: "/a-propos", destination: "/about", permanent: true },
{ source: "/mentions-legales", destination: "/legal", permanent: true },
{ source: "/confidentialite", destination: "/privacy", permanent: true },
{ source: "/compte", destination: "/account", permanent: true },
{ source: "/connexion", destination: "/login", permanent: true },
// Game visuals used to be stored under a `.png` name even though the
// wiki serves WebP bytes: the extension lied and the files were sent
// with the wrong `Content-Type`. They are now named `.webp`. The old
// addresses are public — the image sitemap handed them to Google Images,
// and service workers keep them — so they redirect for good.
{ source: "/visuels/:path(.*)\\.png", destination: "/visuels/:path.webp", permanent: true },
];
},
async headers() {
return [
// The service worker must be fetched again on every visit: a version in
// the HTTP cache would delay the offline cache update.
{
source: "/sw.js",
headers: [
{ key: "Cache-Control", value: "no-cache, no-store, must-revalidate" },
{ key: "Content-Type", value: "application/javascript; charset=utf-8" },
],
},
// Game visuals served as they are (image sitemap, offline cache, the
// canvas of the retribution trainer) used to come with `max-age=0`, the
// default for `public/`: every visit revalidated every file. A given
// path is written once and never rewritten — the sync only downloads a
// visual that is missing, and a renamed skin produces a new file name —
// so a month of browser cache costs nothing and saves a round trip per
// image. Everything the pages display goes through `/_next/image`, which
// keeps its own `minimumCacheTTL`.
{
source: "/visuels/:path*",
headers: [{ key: "Cache-Control", value: "public, max-age=2592000" }],
},
{
source: "/:chemin*",
headers: [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "X-Frame-Options", value: "DENY" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(), interest-cohort=()",
},
{ key: "Content-Security-Policy", value: contentSecurityPolicy() },
// Two years, every subdomain. The domain has no subdomain today (no
// www, no mail): any future one must be served over HTTPS. `preload`
// is left out on purpose: leaving the browser preload list takes
// months.
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
],
},
];
},
};
export default nextConfig;