-
Notifications
You must be signed in to change notification settings - Fork 886
Expand file tree
/
Copy pathnext.config.js
More file actions
174 lines (155 loc) · 6.21 KB
/
Copy pathnext.config.js
File metadata and controls
174 lines (155 loc) · 6.21 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
/** @type {import('next').NextConfig} */
import createNextIntlPlugin from "next-intl/plugin";
import { paperCacheLifeProfiles } from "./src/lib/cache-life-profiles.data.mjs";
import {
PUBLIC_ASSET_CACHE_CONTROL,
publicAssetCacheHeaderSource,
} from "./src/lib/public-asset-cache.data.mjs";
/** Hostnames for mobile/tunnel dev (ngrok, LAN). See ALLOWED_DEV_ORIGINS in .env.example */
const allowedDevOrigins = process.env.ALLOWED_DEV_ORIGINS?.split(",")
.map((origin) => origin.trim())
.filter(Boolean);
const isDevelopment = process.env.NODE_ENV === "development";
/** Host of the configured Saleor instance — self-hosted deploys serve media from it. */
function saleorApiHostname() {
try {
return new URL(process.env.NEXT_PUBLIC_SALEOR_API_URL ?? "").hostname || null;
} catch {
return null;
}
}
/**
* `/_next/image` will optimize (and bill for) any host it is allowed to fetch, so an open
* `hostname: "*"` lets a third party turn the storefront into a free image CDN. Production
* gets an explicit allowlist; the wildcard stays in development only.
*
* Add non-Saleor sources (a DAM, a CMS) via IMAGE_ALLOWED_HOSTS. Not `NEXT_PUBLIC_` —
* it is read here at build time and must never be inlined into a client bundle.
*/
const imageRemotePatterns = [
{ hostname: "*.saleor.cloud" },
{ hostname: "*.media.saleor.cloud" },
...[saleorApiHostname()].filter(Boolean).map((hostname) => ({ hostname })),
...(process.env.IMAGE_ALLOWED_HOSTS?.split(",")
.map((host) => host.trim())
.filter(Boolean)
.map((hostname) => ({ hostname })) ?? []),
...(isDevelopment ? [{ hostname: "*" }] : []),
];
const THIRTY_ONE_DAYS_IN_SECONDS = 2678400;
/** A typo here would otherwise become `NaN` and silently disable image caching. */
function imageMinimumCacheTTLFromEnv() {
const raw = process.env.NEXT_IMAGE_MIN_CACHE_TTL;
if (!raw) return THIRTY_ONE_DAYS_IN_SECONDS;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error(`NEXT_IMAGE_MIN_CACHE_TTL must be a non-negative number of seconds, got "${raw}"`);
}
return parsed;
}
const imageMinimumCacheTTL = imageMinimumCacheTTLFromEnv();
const config = {
...(allowedDevOrigins?.length ? { allowedDevOrigins } : {}),
// Cache Components (Partial Prerendering)
// Enables mixing static, cached, and dynamic content in a single route.
// See: https://nextjs.org/docs/app/getting-started/cache-components
cacheComponents: true,
// Next.js 16.3 — prefetch one reusable loading shell per route (not per link).
// See: https://nextjs.org/blog/next-16-3-instant-navigations
partialPrefetching: true,
// Named cacheLife tiers for `"use cache"` — see src/lib/cache-life-profiles.ts
cacheLife: paperCacheLifeProfiles,
// Optimize barrel file imports for better bundle size and cold start performance
// See: https://vercel.com/blog/how-we-optimized-package-imports-in-next-js
experimental: {
optimizePackageImports: ["lucide-react", "lodash-es"],
// Note: API rate limiting is handled by RequestQueue in src/lib/graphql.ts
// (max 3 concurrent requests; the inter-request delay applies at build time only)
},
images: {
// WebP only: AVIF cold-encodes add ~500ms+ to first /_next/image hit on Vercel (hurts LCP).
formats: ["image/webp"],
remotePatterns: imageRemotePatterns,
// Vercel bills image *transformations* and image *cache writes*. The default 4h TTL
// re-optimizes the same catalog image ~180x/month; Saleor thumbnail URLs are stable
// per (media id, size, format), so a long TTL is safe for the common case.
//
// Caveat: replacing an image *in place* in Saleor reuses the URL, so the old bytes
// are served until this expires. Brands that re-shoot frequently should lower
// NEXT_IMAGE_MIN_CACHE_TTL; uploading as new media always dodges it.
minimumCacheTTL: imageMinimumCacheTTL,
// Each width in these ladders is a separately billed transformation per source image.
// Trimmed from the Next defaults: 2048/3840 only serve 4K/retina-desktop (rare, and
// the widest variants are the most expensive to generate and store), and the tiny
// 16/32/48 steps are below anything Paper actually renders.
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [64, 96, 128, 256, 384],
},
typedRoutes: false,
// Used in the Dockerfile
output:
process.env.NEXT_OUTPUT === "standalone"
? "standalone"
: process.env.NEXT_OUTPUT === "export"
? "export"
: undefined,
// Cache headers for static assets and API routes
async headers() {
const isDev = isDevelopment;
return [
// In development, prevent aggressive caching of dynamic chunks
...(isDev
? [
{
source: "/_next/static/chunks/:path*",
headers: [{ key: "Cache-Control", value: "no-store, must-revalidate" }],
},
]
: []),
// Production only — immutable breaks Turbopack HMR when applied in dev
// (stale action/chunk stubs → "module factory is not available").
...(!isDev
? [
{
source: "/_next/static/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
]
: []),
{
// /public leftovers (logos, fonts, same-origin video). Allowlist lives in
// public-asset-cache.data.mjs — a missed extension inherits document
// caching (max-age=0) and re-downloads on every visit (Fast Data Transfer).
source: publicAssetCacheHeaderSource(),
headers: [{ key: "Cache-Control", value: PUBLIC_ASSET_CACHE_CONTROL }],
},
{
// OG Image API — output is a pure function of the query string, and each render
// is expensive, so cache hard. Social crawlers refetch aggressively.
source: "/api/og",
headers: [
{
key: "Cache-Control",
value: "public, max-age=2592000, stale-while-revalidate=604800",
},
],
},
];
},
// Logging configuration
logging: {
fetches: {
fullUrl: process.env.NODE_ENV === "development",
},
},
};
// next-intl powers code-owned UI/functional strings (messages/*.json). It does NOT own
// routing — our locale lives in the `[locale]` URL segment (ADR 0001). The request config
// resolves the locale we pass explicitly; see src/i18n/request.ts.
const withNextIntl = createNextIntlPlugin();
export default withNextIntl(config);