6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
686 lines
23 KiB
TypeScript
686 lines
23 KiB
TypeScript
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/**/*'],
|
|
'/*': [
|
|
'./node_modules/sharp/**/*',
|
|
'./node_modules/@img/**/*',
|
|
'./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).
|
|
*
|
|
* Keep the explicit pin even while we sit on 16.2.12: 16.3.0 flips this
|
|
* default to true for stable (vercel/next.js#94616), so dropping it would
|
|
* silently re-enable the slower cache the next time we take that bump.
|
|
*/
|
|
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:
|
|
* without it 16.2.12 skips 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(),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
// 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
|