Files
novalon-website/next.config.mjs
T
zhangxiang a0328a623f chore(qa): 验收台账与证据入库 + 构建/部署配置同步
- docs/acceptance/qa-tracker.md:跨周期缺陷单一真源台账(§7=第五轮)。
- 周期 1/2 + iPhone SE/axe 验收证据目录、ACCEPTANCE_REVIEW 快照入库。
- 同步 README/CONTEXT/CLAUDE/DESIGN/testing/deployment/lessons-learned 口径;
  next.config/Dockerfile/nginx/Jenkinsfile/docker-compose/sentry/prisma 对齐
  standalone 产物装配与部署形态。
2026-09-28 10:48:09 +08:00

132 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { withSentryConfig } from '@sentry/nextjs';
const cdnDomain = process.env.CDN_DOMAIN || '';
/**
* Sentry 客户端 DSN → CSP 放行域。
* 为什么必须在此处理:CSP 是本站安全响应的唯一来源(见下方 APP_SECURITY_HEADERS),
* 若不给 DSN 的 ingest 域开 connect-src,浏览器会在 SDK 发 envelope 时直接拦截,
* "接线成功但上报被 CSP 拦死"(2026-09-23 验收 final-verdict §4.3 的前置缺口)。
* 做法:仅当 NEXT_PUBLIC_SENTRY_DSN 存在时,从其 URL 中取 protocol//host 追加进 connect-src;
* DSN 缺失时 CSP 与接线前逐字节一致(与 GA/监控一致的"无配置即惰性"约定)。
* worker-src 'self' blob: 亦仅在 Sentry 启用时追加:replayIntegration 以
* URL.createObjectURL 生成的 blob worker 运行(证据:
* node_modules/@sentry/replay/build/npm/esm/index.js:5221、5637 `new Worker(workerUrl)`),
* 默认 worker-src 回落到 default-src 'self' 会拦截 blob:。
*/
const sentryPublicDsn = process.env.NEXT_PUBLIC_SENTRY_DSN || '';
let sentryConnectSrcExtra = '';
let sentryWorkerSrcExtra = '';
if (sentryPublicDsn) {
try {
const dsnUrl = new URL(sentryPublicDsn);
sentryConnectSrcExtra = ` ${dsnUrl.protocol}//${dsnUrl.host}`;
sentryWorkerSrcExtra = " worker-src 'self' blob:;";
} catch {
console.warn('[next.config] NEXT_PUBLIC_SENTRY_DSN 不是合法 URL,CSP 未追加 Sentry 放行域');
}
}
/**
* 应用层安全响应头的唯一来源(single source of truth)。
*
* 为什么放在这里而不是 nginx:standalone 产物由 `node dist/standalone/server.js` 提供服务,
* 同一份构建会在 nginx 之后、纯 Node、本地 `next start` 等环境下给出完全一致的头部;
* 也避免了两层各自 add_header 造成的重复值(历史缺陷:浏览器收到
* `X-Frame-Options: DENY, SAMEORIGIN` 这类非法的重复/互斥值)。
* 覆盖面(官方文档 headers.md):'Headers are checked before the filesystem which includes
* pages and /public files.' ⇒ 连 public/uploads/* 也会拿到本数组,无需边缘重复声明。
*
* 分工约定(改头部前先对齐):
* - next.config.mjs(本数组):随应用版本演进的文档级头部 —— CSP / XFO / nosniff /
* Referrer-Policy / Permissions-Policy / X-XSS-Protection / X-DNS-Prefetch-Control。
* - nginx(nginx-static*.conf):只保留 TLS 与 301、HSTS(Next 不设,且只有终结 TLS 的
* 边缘知道协议与证书状态)、gzip、limit_req,以及 /uploads/ 的落盘加固(见下)。
* - 例外:/uploads/ 在 nginx-static-production.conf 中由磁盘直供、Next 不参与应答,
* 故该 location 块必须自行声明 nosniff 与 sandbox CSP;这不属于跨层重复。
*/
const APP_SECURITY_HEADERS = [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
// DENY(而非 nginx 曾设置的 SAMEORIGIN):全仓无以 iframe 嵌入本站页面的用例
// ——src 内所有 iframe 命中都在 src/lib/sanitize.ts 的剔除名单及其测试里(语义为“禁止 iframe”),
// e2e 目录零 iframe 引用,管理后台也不以同源 iframe 嵌入任何页面;取更严格值不破坏现有功能。
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: 'Content-Security-Policy',
// Sentry 扩展位(sentryConnectSrcExtra / sentryWorkerSrcExtra)仅在有 NEXT_PUBLIC_SENTRY_DSN 时非空,
// 其余内容与收紧前逐字节一致(见文件头 APP_SECURITY_HEADERS 注释与 Sentry DSN 段落)。
value: `default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://ssl.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.google-analytics.com https://ssl.google-analytics.com https://www.googletagmanager.com; font-src 'self'; connect-src 'self' https://www.google-analytics.com https://ssl.google-analytics.com https://www.googletagmanager.com${sentryConnectSrcExtra}; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';${sentryWorkerSrcExtra}`,
},
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()' },
// 原由 nginx-static.conf 设置;移入应用层,使无 nginx 的环境也带上(Next 官方文档
// 即以 headers() 设置此头:node_modules/next/dist/docs/.../next-config-js/headers.md)。
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
];
const nextConfig = {
// 部署层必须按 standalone 契约服务:`node dist/standalone/server.js` + HOSTNAME=0.0.0.0,
// 并把 dist/static 拷到 <standalone>/<distDir>/static、public 拷到 <standalone>/public
// (Next 官方 output 文档:minimal server 默认不带 public 与 static)。
// 见 Dockerfile / Dockerfile.prod;不要把 dist/ 当作静态 HTML 根交给 nginx。
output: 'standalone',
// 默认 dist/;可用 NEXT_DIST_DIR 覆盖(CI 分环境输出,或本地需要避开既有构建产物)
distDir: process.env.NEXT_DIST_DIR || 'dist',
assetPrefix: cdnDomain || undefined,
images: {
// 关闭 Next 的图片优化(/_next/image 代理 + 服务端重编码),图片以原文件分发。
// 真实原因(与 output 模式无关,standalone 下同样成立):
// 1. 图片以 CDN 分发为主(assetPrefix 由 CDN_DOMAIN 驱动,见 docs/CDN_CONFIGURATION.md),
// 优化端点的收益被 CDN 抵消;
// 2. /_next/image 会替换上游响应,/uploads/ 与 CDN 侧的缓存/Content-Disposition 语义需另行处理。
// 与 sharp 无关:本项目在 src/lib/media/image-processor.ts 里直接用 sharp 生成缩略图/derivative,
// 无论本项取何值都需要 musl 平台二进制(见 Dockerfile.prod 的 sharp-deps 阶段)。
// 若要改为 false:必须同时确认所有环境都有 Node 运行时接管 /_next/image(否则图片 404),
// 并复核 CDN 回源规则。该改动影响图片服务链路与带宽,属部署决策,勿顺手改。
unoptimized: true,
// 注:此处曾声明 formats: ['image/avif', 'image/webp']。unoptimized 为 true 时该字段
// 不参与产物生成(死配置),已移除;恢复图片优化时再按需加回。
},
compress: true,
poweredByHeader: false,
reactStrictMode: true,
experimental: {
optimizePackageImports: ['lucide-react', 'framer-motion'],
},
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
generateEtags: true,
httpAgentOptions: {
keepAlive: true,
},
async headers() {
return [
{
source: '/(.*)',
headers: APP_SECURITY_HEADERS,
},
];
},
};
/**
* withSentryConfig 包装(@sentry/nextjs 10.67.0,导出见
* node_modules/@sentry/nextjs/build/types/index.types.d.ts:27)。它是构建期接线的唯一入口:
* - Turbopack 路径(Next 16 默认 bundler,node_modules/next/dist/docs/01-app/02-guides/upgrading/version-16.md:128):
* 向 instrumentation 与 instrumentation-client 两个入口文件注入 value-injection 规则
* (node_modules/@sentry/nextjs/build/esm/config/turbopack/generateValueInjectionRules.js:34、49)、
* compiler.runAfterProductionCompile(sourcemaps/发布管理)与 productionBrowserSourceMaps:true
* (getFinalConfigObjectBundlerUtils.js:47-120)。
* - 只在 bundler 判定为 webpack 时才追加 custom webpack 键(getFinalConfigObjectBundlerUtils.js:157-159),
* 不会触发 Next 16 "Turbopack 构建遇到 webpack 配置即失败" 的保护(version-16.md:154)。
* - 未设 SENTRY_AUTH_TOKEN 时发布/sourcemap 上传仅告警跳过,不会红构建
* (node_modules/@sentry/bundler-plugin-core/dist/cjs/index.js:5743、5992);
* 是否启用上传属部署决策,本轮不额外传 options。
* 运行时初始化入口在 src/instrumentation.ts(server/edge)与 src/instrumentation-client.ts(browser),
* 无 DSN 时全部惰性(见文件头 Sentry 段落与 sentry.*.config.ts 的 if (SENTRY_DSN) 门禁)。
*/
export default withSentryConfig(nextConfig);