Repository navigation
Expand file tree
/
Copy pathnext.config.ts
More file actions
695 lines (677 loc) · 23.4 KB
/
Copy pathnext.config.ts
File metadata and controls
695 lines (677 loc) · 23.4 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
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
import path from 'node:path'
import type { NextConfig } from 'next'
import { env, isTruthy } from './lib/core/config/env'
import { isDev } from './lib/core/config/env-flags'
import {
getChatEmbedCSPPolicy,
getMainCSPPolicy,
getWorkflowExecutionCSPPolicy,
} from './lib/core/security/csp'
/**
* Marketing routes (`app/(landing)/**`, plus the root) exempted from COEP.
*
* COEP is a *document* header and is inherited across client-side `<Link>`
* navigations, so `/demo`'s own exemption only applies on a direct load. Any
* landing page left isolated soft-navigates into `/demo` still credentialless,
* where the Cal.com booker iframe loads uncredentialed and hangs forever.
* Every route under `app/(landing)` must be listed here.
*/
const LANDING_ROUTES = [
'blog',
'careers',
'changelog',
'comparisons',
'contact',
'demo',
'enterprise',
'files',
'integrations',
'knowledge',
'library',
'logs',
'models',
'pricing',
'privacy',
'solutions',
'tables',
'terms',
'workflows',
] as const
const nextConfig: NextConfig = {
devIndicators: false,
poweredByHeader: false,
// Safe here since this repo's source is already fully public on GitHub -
// no additional exposure versus Next's default (disabled to avoid leaking
// source on the client).
productionBrowserSourceMaps: true,
turbopack: {
root: path.join(import.meta.dirname, '../..'),
},
images: {
formats: ['image/avif', 'image/webp'],
/**
* Allowed `quality` values for next/image. 75 is the app-wide default;
* 90 exists for large photographic marketing assets (the landing hero
* backdrop) where the default visibly softens texture.
*/
qualities: [75, 90],
remotePatterns: [
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
},
{
protocol: 'https',
hostname: 'api.stability.ai',
},
// Azure Blob Storage
{
protocol: 'https',
hostname: '*.blob.core.windows.net',
},
// AWS S3
{
protocol: 'https',
hostname: '*.s3.amazonaws.com',
},
{
protocol: 'https',
hostname: '*.s3.*.amazonaws.com',
},
{
protocol: 'https',
hostname: 'lh3.googleusercontent.com',
},
// Brand logo domain if configured
...(process.env.NEXT_PUBLIC_BRAND_LOGO_URL
? (() => {
try {
return [
{
protocol: 'https' as const,
hostname: new URL(process.env.NEXT_PUBLIC_BRAND_LOGO_URL!).hostname,
},
]
} catch {
return []
}
})()
: []),
// Brand favicon domain if configured
...(process.env.NEXT_PUBLIC_BRAND_FAVICON_URL
? (() => {
try {
return [
{
protocol: 'https' as const,
hostname: new URL(process.env.NEXT_PUBLIC_BRAND_FAVICON_URL!).hostname,
},
]
} catch {
return []
}
})()
: []),
],
},
typescript: {
ignoreBuildErrors: isTruthy(env.DOCKER_BUILD),
},
output: isTruthy(env.DOCKER_BUILD) ? 'standalone' : undefined,
serverExternalPackages: [
'@1password/sdk',
'unpdf',
'fluent-ffmpeg',
'ws',
'isolated-vm',
'@e2b/code-interpreter',
'e2b',
'@daytona/sdk',
'@earendil-works/pi-ai',
'@earendil-works/pi-coding-agent',
// The collab-doc seed converter lazily `require`s jsdom for a headless TipTap editor. Keep it
// external so webpack doesn't try to bundle jsdom's dynamic internal requires.
'jsdom',
// The collab-doc converter runs TipTap + Yjs headlessly server-side. Two reasons these must be
// external (native Node require), not bundled: (1) the server bundler gives bundled TipTap a
// `window` that does NOT read `globalThis`, so `elementFromString` throws "no window object" even
// after the converter installs a jsdom window; (2) bundling would load a SECOND copy of `yjs`, so
// `@tiptap/y-tiptap`'s `item instanceof Y.XmlElement` checks — against the external `yjs` — would
// fail on nodes the app created with the bundled `yjs` ("Unexpected case"). One external copy fixes
// both. Server-only — the client editor bundles its own copies for the browser.
'yjs',
'y-protocols',
'lib0',
'@tiptap/core',
'@tiptap/pm',
'@tiptap/markdown',
'@tiptap/y-tiptap',
'@tiptap/starter-kit',
'@tiptap/extension-code',
'@tiptap/extension-code-block',
'@tiptap/extension-image',
'@tiptap/extension-list',
'@tiptap/extension-paragraph',
'@tiptap/extension-table',
'@tiptap/extension-highlight',
],
outputFileTracingIncludes: {
'/api/tools/stagehand/*': ['./node_modules/ws/**/*'],
// The seed, merge, and persist endpoints all lazily `require('jsdom')` (via the collab-doc
// converter), which is invisible to the standalone file tracer, so force jsdom (and its transitive
// deps, followed from its static requires) into the trace — otherwise a Docker/standalone build
// omits it and the endpoint 500s with MODULE_NOT_FOUND. (The Yjs external stack — yjs/lib0/
// y-protocols — is copied whole in docker/app.Dockerfile: its glob would resolve against apps/sim
// but those deps hoist to the monorepo root, so a trace include can't reach them.)
'/api/internal/file-doc/seed': ['./node_modules/jsdom/**/*'],
'/api/internal/file-doc/merge': ['./node_modules/jsdom/**/*'],
'/api/internal/file-doc/persist': ['./node_modules/jsdom/**/*'],
/**
* No `sharp`/`@img` entries: these globs resolve against apps/sim while both hoist to the
* monorepo root, so they matched nothing. docker/app.Dockerfile copies them instead.
*/
'/*': ['./lib/execution/sandbox/bundles/*.cjs'],
},
experimental: {
/**
* Turbopack's dev filesystem cache stays ON (this is also the Next default
* since v16.1). It is what makes a dev-server restart cheap: without it every
* restart recompiles the route graph from scratch.
*
* Measured locally on `/workspace/[workspaceId]/w`, n=3 per cell, restarting
* the dev server between each run:
*
* cache OFF 31.4s / 30.1s / 31.9s RSS ~9.0-9.8 GB
* cache ON 5.7s / 6.1s / 5.7s RSS ~4.8-5.1 GB
*
* 5.4x faster restarts and ~1.9x less memory. Cold compile with an empty
* cache is unchanged (~32s either way) — the cache only pays back on restart.
*
* This is deliberately NOT the same decision as `turbopackFileSystemCacheForBuild`
* below. That one is measured-harmful for `next build`; this one is
* measured-beneficial for `next dev`. It was previously `false`, but that was
* incidental — it was introduced by a landing-page redesign (#5408) whose
* description never mentions Turbopack, caching, or dev performance, and it
* is not covered by the #6078 build A/B cited below.
*
* The cache is unbounded on disk (an abandoned one reached 78 GB here), so
* `scripts/prune-turbopack-cache.ts` is chained into every `dev` script to cap it.
* A *corrupted* cache can abort Turbopack outright ("Cache corruption
* detected: checksum mismatch") rather than falling back — it depends whether
* the damaged region is read. `bun run dev:clean` and restart is the fix.
*
* If you re-measure any of this: `next dev` compiles routes on demand, so
* startup time means nothing — time the first request to a route, restart the
* server between runs, and stop it with SIGINT. A `kill -9` mid-write makes
* Turbopack discard the partially-written cache and rebuild silently, which
* reads as "the cache does nothing" and is how this flag stayed wrong.
*/
turbopackFileSystemCacheForDev: true,
/**
* Turbopack's persistent build cache (beta) stays off — it is a net loss at
* this app's size. A controlled A/B on a byte-identical module graph (PR
* #6078) measured compile at 113s with it off, 162s cold with it on, and 360s
* warm: the cache made the same build 3.2x slower. It also grew 5.1 GB ->
* 12 GB across two runs of an unchanged tree, so a cache degrades the longer
* it lives. Restoring across commits is separately undocumented-as-supported
* (vercel/next.js#87283 reports stale HTML from a cache built elsewhere).
*
* The explicit pin is load-bearing: 16.3.0 flipped this default to true for
* stable (vercel/next.js#94616), so dropping it re-enables the slower cache.
*/
turbopackFileSystemCacheForBuild: false,
/**
* TypeScript 7 ships no JavaScript compiler API until 7.1, so Next's default
* checker cannot load it — this shells out to the project-local `tsc` instead.
* Pinned because the failure mode is not slower type checking but none at all:
* 16.2.12 skipped the stage silently in 138ms.
*/
useTypeScriptCli: true,
preloadEntriesOnStart: false,
/**
* Under Turbopack this is not a no-op: the list feeds
* `side_effect_free_packages` and is force-appended to `transpiledPackages`,
* which also removes each entry from the server externals set. Entries here
* must be real barrel packages that are actually imported - a stale entry
* costs transform work and overrides that package's own `sideEffects`
* declaration.
*/
optimizePackageImports: [
'framer-motion',
'reactflow',
'@radix-ui/react-dialog',
'@radix-ui/react-dropdown-menu',
'@radix-ui/react-popover',
'@radix-ui/react-select',
'@radix-ui/react-tabs',
'@radix-ui/react-checkbox',
'@radix-ui/react-switch',
'@radix-ui/react-slider',
'streamdown',
'zod',
],
},
...(isDev && {
allowedDevOrigins: [
...(env.NEXT_PUBLIC_APP_URL
? (() => {
try {
return [new URL(env.NEXT_PUBLIC_APP_URL).host]
} catch {
return []
}
})()
: []),
'localhost:3000',
'localhost:3001',
'127.0.0.1',
'127.0.0.1:3011',
'127.0.0.1:3012',
],
}),
transpilePackages: [
'@react-email/components',
'@react-email/render',
'@t3-oss/env-nextjs',
'@t3-oss/env-core',
'@sim/db',
'@sim/emcn',
'@sim/workflow-renderer',
],
async headers() {
return [
{
// `/public`-served assets keep their path across deploys (no content
// hash), so a shorter TTL + revalidation window bounds how long a
// changed asset can serve stale.
source:
'/((?!api/|_next/static/).*\\.(?:svg|jpg|jpeg|png|gif|ico|webp|avif|woff|woff2|ttf|eot))',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=86400, stale-while-revalidate=604800',
},
],
},
{
source: '/.well-known/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET, OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Accept' },
],
},
// /api/* CORS is set at runtime in proxy.ts (resolveApiCorsPolicy).
{
source: '/api/workflows/:id/execute',
headers: [
{ key: 'Cross-Origin-Embedder-Policy', value: 'unsafe-none' },
{ key: 'Cross-Origin-Opener-Policy', value: 'unsafe-none' },
{
key: 'Content-Security-Policy',
value: getWorkflowExecutionCSPPolicy(),
},
],
},
{
source: '/api/v2/workflows/:id/execute',
headers: [
{ key: 'Cross-Origin-Embedder-Policy', value: 'unsafe-none' },
{ key: 'Cross-Origin-Opener-Policy', value: 'unsafe-none' },
{
key: 'Content-Security-Policy',
value: getWorkflowExecutionCSPPolicy(),
},
],
},
{
// Exclude Vercel internal resources and static assets from strict COOP, Google Drive Picker
// and the /demo Cal.com booking embed to prevent 'refused to connect' / slow-load issues.
// The pages an OAuth popup can land on are excluded too: `same-origin` would disown the
// popup from its opener, leaving it not reliably script-closable and reporting `closed`
// for a live window.
source:
'/((?!_next|_vercel|api|favicon.ico|w/.*|workspace|api/tools/drive|demo|oauth-error|oauth/chat-complete).*)',
headers: [
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
],
},
{
// COEP stays on by default - a new route is cross-origin isolated unless
// it is named here. The exemptions are the app surfaces that embed
// credentialed third parties (Drive Picker, Vercel resources) and the
// marketing surface, which must opt out wholesale: see LANDING_ROUTES.
// The trailing `|$` exempts the root path.
source: `/((?!_next|_vercel|api|favicon.ico|w/.*|workspace/.*|api/tools/drive|${LANDING_ROUTES.join('|')}|$).*)`,
headers: [
{
key: 'Cross-Origin-Embedder-Policy',
value: 'credentialless',
},
],
},
{
// For main app routes, Google Drive Picker, the /demo Cal.com embed, the OAuth popup pages,
// and Vercel resources - use permissive policies. The popup pages match their opener's
// value so the two stay in one browsing-context group.
source:
'/(w/.*|workspace.*|api/tools/drive|demo.*|oauth-error|oauth/chat-complete|_next/.*|_vercel/.*)',
headers: [
{
key: 'Cross-Origin-Embedder-Policy',
value: 'unsafe-none',
},
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin-allow-popups',
},
],
},
// Keeps sourcemap files out of search indexes. This does NOT block
// access to them - `productionBrowserSourceMaps` is on and the `.map`
// files ship publicly in the production image; nothing here restricts
// who can fetch one. The trailing
// `$` this rule previously ended with is not a regex anchor in Next's
// `source` matcher (path-to-regexp syntax, not raw regex) - it matched
// a literal `$` character, so this rule never actually fired against
// real `.map` URLs. Next already anchors the compiled pattern at both
// ends, so no trailing anchor is needed here.
//
// Also bounds `.map` files to a short, revalidated TTL rather than
// Next's built-in 1yr immutable default for `_next/static/*` - maps
// are content-hashed like their JS, so this isn't about staleness,
// it's so a future decision to stop shipping `productionBrowserSourceMaps`
// isn't undermined by browsers/edges holding old maps for a year.
{
source: '/(.*)\\.map',
headers: [
{
key: 'x-robots-tag',
value: 'noindex',
},
{
key: 'Cache-Control',
value: 'public, max-age=86400, stale-while-revalidate=604800',
},
],
},
// Chat pages - allow iframe embedding from any origin
{
source: '/chat/:path*',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
// No X-Frame-Options to allow iframe embedding
{
key: 'Content-Security-Policy',
value: getChatEmbedCSPPolicy(),
},
// Permissive CORS for chat requests from embedded chats
{ key: 'Cross-Origin-Embedder-Policy', value: 'unsafe-none' },
{ key: 'Cross-Origin-Opener-Policy', value: 'unsafe-none' },
],
},
// Apply security headers to routes not handled by middleware runtime CSP
// Middleware handles: /, /login, /signup, /workspace/*
// Exclude chat routes which have their own permissive embed headers
{
source: '/((?!workspace|chat|login|signup|$).*)',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'Content-Security-Policy',
value: getMainCSPPolicy(),
},
],
},
]
},
async redirects() {
const redirects = []
// Social link redirects (used in emails to avoid spam filter issues)
redirects.push(
{
source: '/discord',
destination: 'https://discord.gg/Hr4UWYEcTT',
permanent: false,
},
{
source: '/slack',
destination:
'https://join.slack.com/t/sim-ott9864/shared_invite/zt-43lp8tc5v-0qrrqHGBKUsvQlpoouH~TA',
permanent: false,
},
{
source: '/x',
destination: 'https://x.com/simdotai',
permanent: false,
},
{
source: '/linkedin',
destination: 'https://www.linkedin.com/company/simstudioai/',
permanent: false,
},
{
source: '/github',
destination: 'https://github.com/simstudioai/sim',
permanent: false,
},
{
source: '/team',
destination: 'https://cal.com/team/sim/demo',
permanent: false,
}
)
// Redirect /building and /studio to /blog (legacy URL support)
redirects.push(
{
source: '/building/:path*',
destination: 'https://www.sim.ai/blog/:path*',
permanent: true,
},
{
source: '/studio/:path*',
destination: 'https://www.sim.ai/blog/:path*',
permanent: true,
}
)
// The scheduled-tasks marketing page is retired with the feature. The URL is
// indexed, so send it to the surface that still carries scheduled execution
// (the workflow Schedule trigger) instead of letting it 404.
redirects.push({
source: '/scheduled-tasks',
destination: '/workflows',
permanent: true,
})
/**
* The marketing Academy course/lesson pages were removed; content is
* consolidated into the docs site instead. Old course/lesson slugs have
* no equivalent path there, so every sub-path collapses to the new
* landing page rather than forwarding to a path that may not exist.
*/
redirects.push({
source: '/academy/:path*',
destination: 'https://docs.sim.ai/academy',
permanent: true,
})
// Move root feeds to blog namespace
redirects.push(
{
source: '/rss.xml',
destination: '/blog/rss.xml',
permanent: true,
},
{
source: '/sitemap-images.xml',
destination: '/blog/sitemap-images.xml',
permanent: true,
}
)
// Legacy chat URL support: the workspace chat route was renamed from
// `/workspace/:workspaceId/task/:chatId` to `/workspace/:workspaceId/chat/:chatId`.
// Preserve existing bookmarks and deeplinks.
redirects.push({
source: '/workspace/:workspaceId/task/:chatId',
destination: '/workspace/:workspaceId/chat/:chatId',
permanent: true,
})
// Legacy integration slug: the incident.io block's display name was fixed
// from `incidentio` to `incident.io`, which moved its catalog slug.
// Preserve the previously indexed landing URL.
redirects.push({
source: '/integrations/incidentio',
destination: '/integrations/incident-io',
permanent: true,
})
/**
* Legacy integration slug: the SAP block's display name was fixed from
* `SAP S/4HANA` to `SAP S4HANA`, which moved its catalog slug. Preserves
* the previously indexed landing URL.
*/
redirects.push({
source: '/integrations/sap-s-4hana',
destination: '/integrations/sap-s4hana',
permanent: true,
})
/**
* Legacy integration slug: the Cal.com block's display name briefly
* shipped as `CalCom` before being fixed to `Cal Com`/`Cal.com`, which
* moved its catalog slug from `calcom` to `cal-com`.
*/
redirects.push({
source: '/integrations/calcom',
destination: '/integrations/cal-com',
permanent: true,
})
/**
* The partner program page was removed; routes existing links/bookmarks
* to contact instead of leaving a dead, previously-indexed URL.
*/
redirects.push({
source: '/partners',
destination: '/contact',
permanent: true,
})
/**
* AEO/GEO-style posts (listicles, comparisons, how-tos) were split out of
* `/blog` into the dedicated `/library` section so `/blog` stays
* editorial-only. Preserve previously indexed URLs for the moved posts.
*/
for (const slug of [
'best-zapier-alternatives',
'ai-agents-vs-rpa',
'ai-agent-vs-chatbot',
'openai-vs-n8n-vs-sim',
'ai-agent-ideas',
'how-to-create-an-ai-agent',
]) {
redirects.push({
source: `/blog/${slug}`,
destination: `/library/${slug}`,
permanent: true,
})
}
/**
* The comparison route was renamed from `/comparison` to `/comparisons`
* for naming consistency with `/integrations/[slug]` (plural category,
* singular item). Preserve previously indexed URLs for the hub page and
* every competitor detail page.
*/
redirects.push(
{
source: '/comparison',
destination: '/comparisons',
permanent: true,
},
{
source: '/comparison/:path*',
destination: '/comparisons/:path*',
permanent: true,
}
)
/**
* Stray crawler/artifact URLs picked up in an external SEO audit — no
* page ever existed at these paths, but they were indexed or linked
* somewhere with junk characters/casing. Send them home instead of 404.
*/
redirects.push(
{
source: '/$',
destination: '/',
permanent: true,
},
{
source: '/&',
destination: '/',
permanent: true,
},
{
source: '/Sim',
destination: '/',
permanent: true,
},
{
source: '/homepage',
destination: '/',
permanent: true,
},
{
source: '/logo',
destination: '/',
permanent: true,
},
{
source: '/en-US',
destination: '/',
permanent: true,
}
)
/**
* Indexed 404s from an external SEO audit. The capability paths read as
* tool/feature pages and map to the integrations catalog; the rest have no
* closer successor than the homepage.
*
* `/security` is deliberately excluded: security.txt advertises it as the
* RFC 9116 `Policy` URI, so a permanent redirect to marketing would both
* mislead that link and shadow a real policy page added later.
*/
redirects.push(
...['read', 'research', 'scrape'].map((slug) => ({
source: `/${slug}`,
destination: '/integrations',
permanent: true,
})),
...['actions', 'crawl', 'fast'].map((slug) => ({
source: `/${slug}`,
destination: '/',
permanent: true,
}))
)
return redirects
},
async rewrites() {
return [
{
source: '/favicon.ico',
destination: '/icon.svg',
},
{
source: '/r/:shortCode',
destination: 'https://go.trybeluga.ai/:shortCode',
},
]
},
}
export default nextConfig