Files
civitai__civitai/next.config.mjs
T
Justin Maier 4d9ec59205 fix(content): drop duplicate outputFileTracingIncludes
Briant landed the same standalone static-content fix in bb605de70 minutes
before d48a46b0a, so the experimental block ended up with two
outputFileTracingIncludes keys (a no-dupe-keys lint error; the later key
silently wins). Remove the redundant earlier block added by d48a46b0a and keep
bb605de70's, which is the superset (also covers /region-blocked and
/api/v1/content/[[...slug]]).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:21:23 -06:00

352 lines
13 KiB
JavaScript

// @ts-check
import { withAxiom } from '@civitai/next-axiom';
import bundlAnalyzer from '@next/bundle-analyzer';
import CircularDependencyPlugin from 'circular-dependency-plugin';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const packageJson = require('./package.json');
const isProd = process.env.NODE_ENV === 'production';
const isDev = process.env.NODE_ENV === 'development';
const analyze = process.env.ANALYZE === 'true';
const includeCircularDependencyPlugin = process.env.CIRCULAR_DEPENDENCY_PLUGIN === 'true';
const shouldOptimizeImports = (isDev && analyze) || isProd;
const withBundleAnalyzer = bundlAnalyzer({
enabled: analyze,
});
/**
* Don't be scared of the generics here.
* All they do is to give us autocompletion when using this.
*
* @template {import('next').NextConfig} T
* @param {T} config - A generic parameter that flows through to the return type
* @constraint {{import('next').NextConfig}}
*/
function defineNextConfig(config) {
return withBundleAnalyzer(config);
}
export default defineNextConfig(
withAxiom({
env: {
version: packageJson.version,
},
// webpack: (config, options) => {
// if (isDev && !options.isServer) {
// config.plugins.push(
// new CircularDependencyPlugin({
// exclude: /node_modules|\.d\.ts/, // Ignore types and external modules
// failOnError: true, // Fail build on cycle
// allowAsyncCycles: false, // Disallow lazy cycles (recommended)
// cwd: process.cwd(), // Base path for clearer output
// // `onStart` is called before the cycle detection starts
// // onStart({ compilation }) {
// // console.log('start detecting webpack modules cycles');
// // },
// // `onDetected` is called for each module that is cyclical
// onDetected({ module: webpackModuleRecord, paths, compilation }) {
// // `paths` will be an Array of the relative module paths that make up the cycle
// // `module` will be the module record generated by webpack that caused the cycle
// compilation.errors.push(new Error(paths.join(' -> ')));
// },
// // `onEnd` is called before the cycle detection ends
// // onEnd({ compilation }) {
// // console.log('end detecting webpack modules cycles');
// // },
// })
// );
// }
// return config;
// },
webpack: (config) => {
config.ignoreWarnings = [
{ module: /require-in-the-middle/ },
{ module: /@opentelemetry\/instrumentation/ },
];
return config;
},
reactStrictMode: true,
productionBrowserSourceMaps: true,
// Next.js i18n docs: https://nextjs.org/docs/advanced-features/i18n-routing
i18n: {
locales: ['en'],
defaultLocale: 'en',
},
eslint: {
ignoreDuringBuilds: true,
},
generateEtags: false,
compress: false,
images: {
remotePatterns: [
{ hostname: 's3.us-west-1.wasabisys.com' },
{ hostname: 'model-share.s3.us-west-1.wasabisys.com' },
{ hostname: 'civitai-prod.s3.us-west-1.wasabisys.com' },
{ hostname: 'civitai-dev.s3.us-west-1.wasabisys.com' },
{ hostname: 'image.civitai.com' },
],
// domains: [
// 's3.us-west-1.wasabisys.com',
// 'model-share.s3.us-west-1.wasabisys.com',
// 'civitai-prod.s3.us-west-1.wasabisys.com',
// 'civitai-dev.s3.us-west-1.wasabisys.com',
// 'image.civitai.com',
// ],
},
compiler:
process.env.NODE_ENV === 'production'
? {
reactRemoveProperties: { properties: ['^data-testid$'] },
// removeConsole: true,
}
: {},
transpilePackages: [],
experimental: {
// scrollRestoration: true,
cpus: 8,
serverSourceMaps: true,
instrumentationHook: true, // Enable instrumentation.ts for OTEL
largePageDataBytes: 512 * 100000,
serverComponentsExternalPackages: [
'redis', '@redis/client', '@redis/bloom', '@redis/json', '@redis/search', '@redis/time-series',
'@opentelemetry/sdk-node', '@opentelemetry/instrumentation', '@opentelemetry/instrumentation-http',
'@opentelemetry/instrumentation-redis', '@prisma/instrumentation',
],
optimizePackageImports: [
'@civitai/client',
'./src/libs/form',
'lodash-es',
'@tabler/icons-react',
'@headlessui/react',
],
// Several entry points read markdown from src/static-content at runtime via
// fs (dynamic string paths that @vercel/nft can't trace). With
// output: 'standalone' the build only ships traced files, so without these
// explicit includes the markdown is missing in the deployed image and every
// read hits ENOENT -> 500/404 (works locally because the full source tree is
// present). Keyed by every entry point that performs the read:
// /safety, /region-blocked - direct fs read in getServerSideProps
// /content/[[...slug]] - content.get via SSG prefetch (SSR)
// /api/trpc/[trpc] - content.get + checkTosUpdate (the
// TOS check runs for every logged-in
// user on every page)
// /api/v1/content/[[...slug]] - content.get via REST apiCaller
// In standalone mode the includes are unioned into the shared .next/standalone
// root, so any one key would suffice, but we list each read site explicitly so
// the fix survives a future move off standalone (per-function tracing).
// NOTE: on Next 14 this key lives under `experimental` (moved to top-level in
// Next 15) - placing it at the top level here is a silent no-op.
outputFileTracingIncludes: {
'/safety': ['./src/static-content/**/*'],
'/region-blocked': ['./src/static-content/**/*'],
'/content/[[...slug]]': ['./src/static-content/**/*'],
'/api/trpc/[trpc]': ['./src/static-content/**/*'],
'/api/v1/content/[[...slug]]': ['./src/static-content/**/*'],
},
},
headers: async () => {
// Add X-Robots-Tag header to all pages matching /sitemap.xml and /sitemap-models.xml /sitemap-articles.xml, etc
const headers = [
{
source: '/sitemap(-\\w+)?.xml',
headers: [
{ key: 'X-Robots-Tag', value: 'noindex' },
{ key: 'Content-Type', value: 'application/xml' },
{ key: 'Cache-Control', value: 'public, max-age=86400, must-revalidate' },
],
},
];
if (process.env.NODE_ENV !== 'production') {
headers.push({
source: '/:path*',
headers: [
{
key: 'X-Robots-Tag',
value: 'noindex',
},
],
});
}
// Allow Kinguin checkout iframe on gift cards page - NO X-Frame-Options header
// Minimal CSP that only restricts frame-src to allow Kinguin
headers.push({
source: '/gift-cards',
headers: [
{
key: 'Content-Security-Policy',
value: "frame-src 'self' https://www.kinguin.net https://sandbox.kinguin.net https://gateway.kinguin.net https://*.kinguin.net;"
}
// NOTE: Intentionally NO X-Frame-Options header as per Kinguin's documentation
// NOTE: Only setting frame-src, letting other resources use browser defaults
],
});
// Apply X-Frame-Options to all pages EXCEPT gift-cards
headers.push({
source: '/((?!gift-cards).*)',
headers: [{ key: 'X-Frame-Options', value: 'DENY' }],
});
return headers;
},
poweredByHeader: false,
redirects: async () => {
// Note: the .red-host support-portal bounce is implemented as a
// Cloudflare Redirect Rule on the civitai.red zone. Config lives at
// ops/cloudflare/civitai-red-redirects.json. A host-conditional rule
// here would be pruned to nothing at build time anyway since
// SERVER_DOMAIN_* env vars aren't exposed as Docker build ARGs.
return [
{
source: '/api/download/training-data/:modelVersionId',
destination: '/api/download/models/:modelVersionId?type=Training%20Data',
permanent: true,
},
{
source: '/github/:path*',
destination: 'https://github.com/civitai/civitai/:path*',
permanent: true,
},
{
source: '/discord',
destination: 'https://discord.gg/civitai',
permanent: true,
},
{
source: '/twitter',
destination: 'https://twitter.com/HelloCivitai',
permanent: true,
},
{
source: '/reddit',
destination: 'https://reddit.com/r/civitai',
permanent: true,
},
{
source: '/instagram',
destination: 'https://www.instagram.com/hellocivitai/',
permanent: true,
},
{
source: '/tiktok',
destination: 'https://www.tiktok.com/@hellocivitai',
permanent: true,
},
{
source: '/youtube',
destination: 'https://www.youtube.com/@civitai',
permanent: true,
},
{
source: '/twitch',
destination: 'https://www.twitch.tv/civitai',
permanent: true,
},
{
source: '/ideas',
destination: 'https://github.com/civitai/civitai/discussions/categories/ideas',
permanent: true,
},
{
source: '/v/civitai-link-intro',
destination: 'https://youtu.be/EHUjiDgh-MI',
permanent: false,
},
{
source: '/v/civitai-link-installation',
destination: 'https://youtu.be/fs-Zs-fvxb0',
permanent: false,
},
{
source: '/v/ally-parting-message',
destination: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
permanent: false,
},
{
source: '/gallery/:path*',
destination: '/images/:path*',
permanent: true,
},
{
source: '/canny/bugs',
destination:
'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
permanent: true,
},
{
source: '/bugs',
destination:
'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
permanent: true,
},
{
source: '/support-portal',
destination:
'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
permanent: true,
},
{
source: '/leaderboard',
destination: '/leaderboard/overall',
permanent: true,
},
{
source: '/forms/bounty-refund',
destination: 'https://forms.clickup.com/8459928/f/825mr-8331/R30FGV9JFHLF527GGN',
permanent: true,
},
{
source: '/air/confirm',
destination: '/studio/confirm',
permanent: true,
},
{
source: '/education',
destination: 'https://education.civitai.com',
permanent: true,
},
{
source: '/cosmetic-shop',
destination: '/shop',
permanent: true,
},
{
source: '/shop/cosmetic-shop',
destination: '/shop',
permanent: true,
},
{
source: '/projectodyssey_season2',
destination: '/collections/6503138',
permanent: true,
},
{
source: '/creators-program',
destination: '/creator-program',
permanent: true,
},
{
source: '/research/rater',
destination: '/games/knights-of-new-order',
permanent: true,
},
{
// Reserved-name redirect: the legacy 'civitai' user account moved to
// 'CivitaiOfficial'. Handled here (not in middleware) so it runs at
// framework/edge level before any middleware, with no JS execution
// per request.
source: '/user/civitai',
destination: '/user/CivitaiOfficial',
permanent: true,
},
];
},
output: 'standalone',
})
);