Repository navigation
Expand file tree
/
Copy pathnext.config.mjs
More file actions
360 lines (340 loc) · 10 KB
/
Copy pathnext.config.mjs
File metadata and controls
360 lines (340 loc) · 10 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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
import withPWA from "next-pwa";
// ============================================================================
// Cloudflare Pages 环境检测
// CF_PAGES: Cloudflare Pages 构建时自动注入
// CF_PAGES_BRANCH: 当前部署分支
// CF_PAGES_COMMIT_SHA: Git commit hash
// CF_PAGES_URL: 部署 URL (如 https://xxx.pages.dev)
// ============================================================================
const isCloudflare = process.env.CF_PAGES === "1";
const isVercel = process.env.VERCEL === "1";
const isProduction = process.env.NODE_ENV === "production";
// 后端 API 地址
const API_BACKEND_URL =
process.env.API_BACKEND_URL ||
(isProduction
? "https://halolight-api-nestjs.h7ml.cn"
: "http://localhost:3000");
// 是否使用 standalone 输出模式(用于 Docker/Node.js 部署)
// 可通过 STANDALONE=true 环境变量显式启用
const useStandalone =
process.env.STANDALONE === "true" || (!isCloudflare && !isVercel);
// Cloudflare Images 配置(可选,需要开通 Cloudflare Images 服务)
const enableCloudflareImages = !!process.env.NEXT_PUBLIC_CF_IMAGES_ACCOUNT_HASH;
const pwaConfig = withPWA({
dest: "public",
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === "development",
runtimeCaching: [
// 本地字体缓存(已改用自托管字体,无需 Google Fonts 规则)
{
urlPattern: /\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,
handler: "CacheFirst",
options: {
cacheName: "static-font-assets",
expiration: {
maxEntries: 10,
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
},
},
},
{
urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
handler: "StaleWhileRevalidate",
options: {
cacheName: "static-image-assets",
expiration: {
maxEntries: 64,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: /\/_next\/image\?url=.+$/i,
handler: "StaleWhileRevalidate",
options: {
cacheName: "next-image",
expiration: {
maxEntries: 64,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: /\.(?:mp3|wav|ogg)$/i,
handler: "CacheFirst",
options: {
rangeRequests: true,
cacheName: "static-audio-assets",
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: /\.(?:mp4)$/i,
handler: "CacheFirst",
options: {
rangeRequests: true,
cacheName: "static-video-assets",
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
// Next.js 带哈希的静态资源(文件名包含哈希,内容不变)
// 使用 CacheFirst 因为文件名变化意味着新资源
{
urlPattern: /\/_next\/static\/.+\.(js|css)$/i,
handler: "CacheFirst",
options: {
cacheName: "next-static-assets",
expiration: {
maxEntries: 64,
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
},
},
},
// 非 _next/static 的 JS/CSS(如第三方脚本)使用 NetworkFirst
// 确保部署后用户能获取最新版本
{
urlPattern: ({ url }) => {
const pathname = url.pathname;
// 排除已处理的 _next/static
if (pathname.startsWith("/_next/static/")) return false;
return /\.(js|css)$/i.test(pathname);
},
handler: "NetworkFirst",
options: {
cacheName: "dynamic-assets",
expiration: {
maxEntries: 32,
maxAgeSeconds: 60 * 60, // 1 hour
},
networkTimeoutSeconds: 3,
},
},
// Next.js 页面数据(路由预取数据)
// 使用 NetworkFirst 确保用户获取最新页面数据
{
urlPattern: /\/_next\/data\/.+\/.+\.json$/i,
handler: "NetworkFirst",
options: {
cacheName: "next-data",
expiration: {
maxEntries: 32,
maxAgeSeconds: 60 * 60, // 1 hour
},
networkTimeoutSeconds: 3,
},
},
{
urlPattern: /\.(?:json|xml|csv)$/i,
handler: "NetworkFirst",
options: {
cacheName: "static-data-assets",
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: ({ url }) => {
const isSameOrigin = self.origin === url.origin;
if (!isSameOrigin) return false;
const pathname = url.pathname;
// Exclude /api routes
if (pathname.startsWith("/api/")) return false;
return true;
},
handler: "NetworkFirst",
options: {
cacheName: "others",
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
networkTimeoutSeconds: 10,
},
},
{
urlPattern: ({ url }) => {
const isSameOrigin = self.origin === url.origin;
return !isSameOrigin;
},
handler: "NetworkOnly", // 跨域资源不缓存,避免 OpaqueResponseBlocking
},
],
});
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
// 安全:移除 X-Powered-By 响应头
poweredByHeader: false,
// 允许在导出过程中出现特定错误
onDemandEntries: {
maxInactiveAge: 60 * 1000,
pagesBufferLength: 5,
},
// 打包优化
compiler: {
// 移除 console.log(生产环境)
removeConsole: process.env.NODE_ENV === "production" ? {
exclude: ["error", "warn"],
} : false,
},
// 实验性功能
experimental: {
// 优化包导入 - 减少打包体积
optimizePackageImports: [
"@radix-ui/react-icons",
"@radix-ui/react-alert-dialog",
"@radix-ui/react-avatar",
"@radix-ui/react-context-menu",
"@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu",
"@radix-ui/react-label",
"@radix-ui/react-popover",
"@radix-ui/react-scroll-area",
"@radix-ui/react-select",
"@radix-ui/react-separator",
"@radix-ui/react-slot",
"@radix-ui/react-switch",
"@radix-ui/react-tabs",
"@radix-ui/react-tooltip",
"lucide-react",
"framer-motion",
"@tanstack/react-query",
"recharts",
"date-fns",
"zustand",
],
// 启用服务端 Actions
serverActions: {
bodySizeLimit: "2mb",
},
},
// 图片优化
images: {
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Cloudflare Pages 不支持 Next.js 图片优化,仅在 CF 环境禁用
// 本地开发和其他部署平台可继续使用图片优化
unoptimized: isCloudflare,
// Cloudflare Images loader(可选,配置 NEXT_PUBLIC_CF_IMAGES_ACCOUNT_HASH 后启用)
// 支持本地开发测试,不限于 CF 环境
...(enableCloudflareImages
? {
loader: "custom",
loaderFile: "./src/lib/cloudflare-image-loader.ts",
}
: {}),
// 远程图片域名白名单
remotePatterns: [
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
},
{
protocol: "https",
hostname: "*.cloudinary.com",
},
],
},
// 压缩优化
compress: true,
// 生产环境 source map 关闭以减小体积和首包下载
productionBrowserSourceMaps: false,
// 输出配置
// - Docker/Node.js 部署: 使用 standalone 模式(STANDALONE=true 或非云平台环境)
// - Cloudflare Pages: 使用默认输出(由 @cloudflare/next-on-pages 处理)
// - Vercel: 使用默认输出
...(useStandalone ? { output: "standalone" } : {}),
// 静态资源缓存
headers: async () => [
{
source: "/:all*(svg|jpg|jpeg|png|gif|ico|webp|avif)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
{
source: "/_next/static/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
{
source: "/fonts/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
],
// 重定向和重写规则
async redirects() {
return [
// 旧路由重定向示例
// {
// source: '/old-path',
// destination: '/new-path',
// permanent: true,
// },
];
},
// API 代理 - 将 /api/* 请求转发到后端(Mock 模式下不启用)
async rewrites() {
// Mock 模式使用本地 API 路由
if (process.env.NEXT_PUBLIC_MOCK === "true") {
return [];
}
return [
{
source: "/api/:path*",
destination: `${API_BACKEND_URL}/api/:path*`,
},
];
},
// 忽略构建时的 ESLint 错误(CI 中单独检查)
eslint: {
// 仅在 CI 环境忽略,本地开发保持检查
ignoreDuringBuilds: process.env.CI === "true",
},
// 忽略 TypeScript 构建错误(CI 中单独检查)
typescript: {
// 仅在 CI 环境忽略,本地开发保持检查
ignoreBuildErrors: process.env.CI === "true",
},
// 暴露部署环境信息到客户端(用于调试和环境判断)
env: {
// 部署平台标识
NEXT_PUBLIC_DEPLOY_PLATFORM: isCloudflare
? "cloudflare"
: isVercel
? "vercel"
: "other",
// Git 信息(仅在非生产环境暴露,避免泄露私有仓库信息)
// 如需在生产环境显示,可设置 NEXT_PUBLIC_SHOW_GIT_INFO=true
...((!isProduction || process.env.NEXT_PUBLIC_SHOW_GIT_INFO === "true") && {
NEXT_PUBLIC_GIT_BRANCH: process.env.CF_PAGES_BRANCH || "",
NEXT_PUBLIC_GIT_COMMIT_SHA: process.env.CF_PAGES_COMMIT_SHA
? process.env.CF_PAGES_COMMIT_SHA.slice(0, 7)
: "",
}),
},
};
export default pwaConfig(nextConfig);