Files
WeHub Mirror 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
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

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