diff --git a/packages/twenty-website-redone/.dev.vars.example b/packages/twenty-website-redone/.dev.vars.example
new file mode 100644
index 0000000000000..9cd2773ff6c25
--- /dev/null
+++ b/packages/twenty-website-redone/.dev.vars.example
@@ -0,0 +1,7 @@
+NEXTJS_ENV=development
+
+# Stripe — use a test-mode key locally
+STRIPE_SECRET_KEY=
+
+# Enterprise JWT — generate a test RSA keypair for local dev
+ENTERPRISE_JWT_PRIVATE_KEY=
diff --git a/packages/twenty-website-redone/.gitignore b/packages/twenty-website-redone/.gitignore
index 048c8bda80d28..2f66d3a5c20a3 100644
--- a/packages/twenty-website-redone/.gitignore
+++ b/packages/twenty-website-redone/.gitignore
@@ -9,3 +9,9 @@ yarn-error.log*
.env*
*.tsbuildinfo
next-env.d.ts
+
+# OpenNext / Cloudflare Workers
+.open-next/
+.wrangler/
+.dev.vars
+cloudflare-env.d.ts
diff --git a/packages/twenty-website-redone/next.config.ts b/packages/twenty-website-redone/next.config.ts
index 114bdc7b6c40f..f366dddfa1cc0 100644
--- a/packages/twenty-website-redone/next.config.ts
+++ b/packages/twenty-website-redone/next.config.ts
@@ -1,4 +1,5 @@
import path from 'path';
+import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
import withLinaria, { type LinariaConfig } from 'next-with-linaria';
import { localeToUrlSegment } from './src/platform/i18n/locale-to-url-segment';
@@ -48,3 +49,7 @@ const nextConfig: LinariaConfig = {
};
export default withLinaria(nextConfig);
+
+// Binds the Cloudflare dev context (R2 incremental cache, env vars) into
+// `next dev` so local runs mirror the deployed OpenNext worker.
+initOpenNextCloudflareForDev();
diff --git a/packages/twenty-website-redone/open-next.config.ts b/packages/twenty-website-redone/open-next.config.ts
new file mode 100644
index 0000000000000..34c3cbffa7295
--- /dev/null
+++ b/packages/twenty-website-redone/open-next.config.ts
@@ -0,0 +1,34 @@
+import { defineCloudflareConfig } from '@opennextjs/cloudflare';
+import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache';
+import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
+
+// Worker custom-domain hostnames bypass the zone-level Cache Rule for
+// synthetic responses (OpenNext builds responses from R2 reads rather than
+// `fetch()`-ing an origin). Wrapping the R2 incremental cache with the
+// regional cache means cache-hit reads come from CF's per-region Cache API
+// (~10ms) instead of R2 (~100ms), recovering most of the TTFB the migration
+// lost.
+const incrementalCache = withRegionalCache(r2IncrementalCache, {
+ mode: 'long-lived',
+});
+
+const baseConfig = defineCloudflareConfig({
+ incrementalCache,
+});
+
+// `defineCloudflareConfig` only takes the `CloudflareOverrides` subset of the
+// config today; `skewProtection` lives directly under `cloudflare.*` and has to
+// be merged in. See packages/cloudflare/src/api/config.ts in opennextjs-cloudflare.
+export default {
+ ...baseConfig,
+ cloudflare: {
+ ...baseConfig.cloudflare,
+ skewProtection: {
+ enabled: true,
+ // Window large enough to keep prod-version history for skew routing AND
+ // hold one slot per open PR preview. `maxVersionAgeDays` prunes the rest.
+ maxNumberOfVersions: 50,
+ maxVersionAgeDays: 14,
+ },
+ },
+};
diff --git a/packages/twenty-website-redone/package.json b/packages/twenty-website-redone/package.json
index c632b51a0442f..9b53be79ce1c6 100644
--- a/packages/twenty-website-redone/package.json
+++ b/packages/twenty-website-redone/package.json
@@ -4,7 +4,11 @@
"scripts": {
"dev": "npx next dev --port 3004",
"build": "npx next build",
- "start": "npx next start --port 3004"
+ "start": "npx next start --port 3004",
+ "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
+ "deploy:dev": "opennextjs-cloudflare build && opennextjs-cloudflare deploy --env dev",
+ "deploy:prod": "opennextjs-cloudflare build && opennextjs-cloudflare deploy --env prod",
+ "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
},
"dependencies": {
"@babel/runtime": "^7.27.6",
@@ -34,9 +38,11 @@
"@lingui/conf": "5.1.2",
"@lingui/format-po": "5.1.2",
"@lingui/swc-plugin": "^5.11.0",
+ "@opennextjs/cloudflare": "^1.0.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
- "@types/three": "^0.184.1"
+ "@types/three": "^0.184.1",
+ "wrangler": "^4.0.0"
}
}
diff --git a/packages/twenty-website-redone/project.json b/packages/twenty-website-redone/project.json
index 4d15ae39c215d..9779c41f081a8 100644
--- a/packages/twenty-website-redone/project.json
+++ b/packages/twenty-website-redone/project.json
@@ -39,7 +39,7 @@
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
- "command": "node scripts/check-conventions.mjs && node scripts/check-translations.mjs && npx oxlint -c .oxlintrc.json . && npx oxfmt --check ."
+ "command": "node scripts/check-conventions.mjs && npx oxlint -c .oxlintrc.json . && npx oxfmt --check ."
},
"configurations": {
"fix": {
diff --git a/packages/twenty-website-redone/public/images/menu/developers.webp b/packages/twenty-website-redone/public/images/menu/developers.webp
index b521308da65a5..d5048ad9af37d 100644
Binary files a/packages/twenty-website-redone/public/images/menu/developers.webp and b/packages/twenty-website-redone/public/images/menu/developers.webp differ
diff --git a/packages/twenty-website-redone/public/images/menu/user-guide.webp b/packages/twenty-website-redone/public/images/menu/user-guide.webp
index 98e171701ce1b..35e119c8e2d91 100644
Binary files a/packages/twenty-website-redone/public/images/menu/user-guide.webp and b/packages/twenty-website-redone/public/images/menu/user-guide.webp differ
diff --git a/packages/twenty-website-redone/scripts/check-translations.mjs b/packages/twenty-website-redone/scripts/check-translations.mjs
deleted file mode 100644
index 3821dfdfd7f01..0000000000000
--- a/packages/twenty-website-redone/scripts/check-translations.mjs
+++ /dev/null
@@ -1,65 +0,0 @@
-import { execSync } from 'child_process';
-import fs from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-
-// Translations are authored by the Crowdin automation in CI, so empty
-// msgstr entries are a normal intermediate state and are NOT checked here.
-// What IS the engineer's responsibility: every msg in code must be
-// extracted into en.po, where Crowdin can see it. This re-runs extraction
-// against a copy and fails when the catalog would change (unextracted new
-// strings, or stale entries that --clean would remove).
-const packageRoot = path.join(
- path.dirname(fileURLToPath(import.meta.url)),
- '..',
-);
-const localesDirectory = path.join(packageRoot, 'src', 'locales');
-
-const readMsgIds = (file) =>
- new Set(
- [...fs.readFileSync(file, 'utf8').matchAll(/^msgid "(.+)"$/gm)].map(
- (match) => match[1],
- ),
- );
-
-const catalogFiles = fs
- .readdirSync(localesDirectory)
- .filter((name) => name.endsWith('.po'));
-const backups = new Map(
- catalogFiles.map((name) => [
- name,
- fs.readFileSync(path.join(localesDirectory, name), 'utf8'),
- ]),
-);
-const before = readMsgIds(path.join(localesDirectory, 'en.po'));
-
-try {
- execSync('npx lingui extract --overwrite --clean', {
- cwd: packageRoot,
- stdio: 'pipe',
- });
- const after = readMsgIds(path.join(localesDirectory, 'en.po'));
-
- const unextracted = [...after].filter((id) => !before.has(id));
- const stale = [...before].filter((id) => !after.has(id));
-
- if (unextracted.length > 0 || stale.length > 0) {
- console.error(
- 'check-translations: FAILED — run `lingui extract` and commit the catalogs',
- );
- for (const id of unextracted) {
- console.error(` not extracted: "${id.slice(0, 70)}"`);
- }
- for (const id of stale) {
- console.error(` stale entry: "${id.slice(0, 70)}"`);
- }
- process.exitCode = 1;
- } else {
- console.log('check-translations: OK (catalogs in sync with source)');
- }
-} finally {
- // The check must never mutate the working tree.
- for (const [name, content] of backups) {
- fs.writeFileSync(path.join(localesDirectory, name), content);
- }
-}
diff --git a/packages/twenty-website-redone/src/sections/footer/footer.data.ts b/packages/twenty-website-redone/src/sections/footer/footer.data.ts
index d48f9cbfdd289..6ee0d7129ba59 100644
--- a/packages/twenty-website-redone/src/sections/footer/footer.data.ts
+++ b/packages/twenty-website-redone/src/sections/footer/footer.data.ts
@@ -38,8 +38,8 @@ export type FooterSocialLink = {
icon: IconComponent;
};
-// Sitemap now includes Product and Customers — both pages exist in the nav
-// but were missing from the old footer.
+// Sitemap includes Customers — it exists in the nav but was missing from the
+// old footer.
export const FOOTER: {
navGroups: readonly FooterNavGroup[];
socialLinks: readonly FooterSocialLink[];
@@ -50,7 +50,6 @@ export const FOOTER: {
title: msg`Sitemap`,
links: [
{ label: msg`Home`, href: '/' },
- { label: msg`Product`, href: '/product' },
{ label: msg`Pricing`, href: '/pricing' },
{ label: msg`Customers`, href: '/customers' },
{ label: msg`Partners`, href: '/partners' },
diff --git a/packages/twenty-website-redone/src/sections/home-hero/HomeHero.tsx b/packages/twenty-website-redone/src/sections/home-hero/HomeHero.tsx
index 46bc94716ba47..ef1e8a944e087 100644
--- a/packages/twenty-website-redone/src/sections/home-hero/HomeHero.tsx
+++ b/packages/twenty-website-redone/src/sections/home-hero/HomeHero.tsx
@@ -82,6 +82,7 @@ export function HomeHero() {
}
+ fullBleedBackground
rhythm="hero"
scheme="muted"
>
diff --git a/packages/twenty-website-redone/src/sections/menu/MenuDropdown.tsx b/packages/twenty-website-redone/src/sections/menu/MenuDropdown.tsx
index 16fd4b7c07fca..4fe951da485cd 100644
--- a/packages/twenty-website-redone/src/sections/menu/MenuDropdown.tsx
+++ b/packages/twenty-website-redone/src/sections/menu/MenuDropdown.tsx
@@ -8,6 +8,7 @@ import { useState } from 'react';
import { ArrowUpRight } from '@/icons';
import { LocalizedLink } from '@/platform/i18n/LocalizedLink';
+import { useUnlocalizedPathname } from '@/platform/i18n/use-unlocalized-pathname';
import { ExternalLink } from '@/ui';
import {
EASING,
@@ -66,6 +67,10 @@ const IconWrap = styled.span`
justify-content: center;
position: relative;
width: 32px;
+
+ ${DropdownLink}[data-active] & {
+ color: ${color('blue')};
+ }
`;
const ExternalBadge = styled.span`
@@ -100,6 +105,24 @@ const ItemLabel = styled.span`
letter-spacing: 0;
line-height: 1.1;
text-transform: uppercase;
+
+ &::before {
+ background: ${color('blue')};
+ content: '';
+ display: none;
+ height: 2px;
+ margin-right: ${spacing(1.5)};
+ vertical-align: middle;
+ width: 8px;
+ }
+
+ ${DropdownLink}[data-active] & {
+ color: ${color('blue')};
+ }
+
+ ${DropdownLink}[data-active] &::before {
+ display: inline-block;
+ }
`;
const ItemDescription = styled.span`
@@ -123,7 +146,7 @@ const PreviewFrame = styled.div`
border: 1px solid ${semanticColor.line};
border-radius: ${radius(2)};
flex: 1;
- min-height: 220px;
+ min-height: 160px;
overflow: hidden;
position: relative;
width: 100%;
@@ -159,6 +182,7 @@ export type MenuDropdownProps = {
export function MenuDropdown({ items }: MenuDropdownProps) {
const { i18n } = useLingui();
+ const pathname = useUnlocalizedPathname();
const [activeHref, setActiveHref] = useState(items[0]?.href ?? '');
const activeItem =
items.find((item) => item.href === activeHref) ?? items[0] ?? null;
@@ -168,6 +192,9 @@ export function MenuDropdown({ items }: MenuDropdownProps) {
{items.map((child) => {
const IconComponent = child.icon;
+ const isCurrentPage =
+ child.external !== true &&
+ (pathname === child.href || pathname.startsWith(`${child.href}/`));
return (
setActiveHref(child.href)}
>
@@ -206,7 +234,7 @@ export function MenuDropdown({ items }: MenuDropdownProps) {
}
+ fullBleedBackground
rhythm="spacious"
scheme="light"
>
diff --git a/packages/twenty-website-redone/src/sections/testimonials/Testimonials.tsx b/packages/twenty-website-redone/src/sections/testimonials/Testimonials.tsx
index 68c0d9c896a68..740207620291d 100644
--- a/packages/twenty-website-redone/src/sections/testimonials/Testimonials.tsx
+++ b/packages/twenty-website-redone/src/sections/testimonials/Testimonials.tsx
@@ -7,6 +7,7 @@ export function Testimonials() {
return (
}
+ fullBleedBackground
rhythm="spacious"
scheme="muted"
>
diff --git a/packages/twenty-website-redone/src/ui/NotchedCardShape.tsx b/packages/twenty-website-redone/src/ui/NotchedCardShape.tsx
index 7e9c5d6256022..462fcbd72cef7 100644
--- a/packages/twenty-website-redone/src/ui/NotchedCardShape.tsx
+++ b/packages/twenty-website-redone/src/ui/NotchedCardShape.tsx
@@ -1,21 +1,26 @@
import { styled } from '@linaria/react';
-import { buildSchemeDeclarations, type Scheme, semanticColor } from '@/tokens';
-
-// A card with the sculpted notched top edge — shared by the footer stage card
-// and the testimonials card. The card presents a surface scheme: its fill is
-// that scheme's surface (light → white, dark → black). On a light section a
-// dark card reads as a dark panel whose notch reveals the white surface behind
-// it. The old site stretched one 1360px-wide path with
-// preserveAspectRatio="none", distorting the notch slopes at every other width.
-// Here the slopes keep their authored geometry (fixed-width SVG caps extracted
-// from the original path) and only the flat runs flex, in the original
-// 344 : 518 : 343 proportions.
+import {
+ buildSchemeDeclarations,
+ MAX_CONTENT_WIDTH_PX,
+ type Scheme,
+ semanticColor,
+} from '@/tokens';
+
const CAP_HEIGHT_PX = 20;
const LEFT_SLOPE_WIDTH_PX = 74;
const RIGHT_SLOPE_WIDTH_PX = 73;
+const LEFT_FLAT_GROW = 344;
+const NOTCH_GROW = 518;
+const RIGHT_FLAT_GROW = 343;
+
+const NOTCH_MAX_WIDTH_PX = Math.round(
+ (NOTCH_GROW / (LEFT_FLAT_GROW + NOTCH_GROW + RIGHT_FLAT_GROW)) *
+ (MAX_CONTENT_WIDTH_PX - LEFT_SLOPE_WIDTH_PX - RIGHT_SLOPE_WIDTH_PX),
+);
+
const LEFT_SLOPE_PATH =
'M0 0 C4.197 0 8.369 0.66 12.361 1.958 L61.861 18.042 A40 40 0 0 0 74.222 20 L0 20 Z';
const RIGHT_SLOPE_PATH =
@@ -43,17 +48,18 @@ const FlatRun = styled.div`
min-width: 0;
&[data-edge='left'] {
- flex-grow: 344;
+ flex-grow: ${LEFT_FLAT_GROW};
}
&[data-edge='right'] {
- flex-grow: 343;
+ flex-grow: ${RIGHT_FLAT_GROW};
}
`;
const Plateau = styled.div`
flex-basis: 0;
- flex-grow: 518;
+ flex-grow: ${NOTCH_GROW};
+ max-width: ${NOTCH_MAX_WIDTH_PX}px;
min-width: 0;
`;
@@ -63,14 +69,9 @@ const BodyFill = styled.div`
left: 0;
position: absolute;
right: 0;
- /* 1px overlap with the cap row prevents subpixel seams. */
top: ${CAP_HEIGHT_PX - 1}px;
`;
-// The card carries its own surface scheme so its fill is independent of the
-// host section's scheme — a white card on a muted section, a black card on a
-// light one. The shape draws no text, so it adopts only the scheme variables
-// (it reads `surface`); the ink colour belongs to its content, not the shape.
const ShapeLayer = styled.div`
inset: 0;
pointer-events: none;
diff --git a/packages/twenty-website-redone/src/ui/SectionShell.tsx b/packages/twenty-website-redone/src/ui/SectionShell.tsx
index e035237ecd59d..4389653ead105 100644
--- a/packages/twenty-website-redone/src/ui/SectionShell.tsx
+++ b/packages/twenty-website-redone/src/ui/SectionShell.tsx
@@ -13,9 +13,6 @@ import {
import { Container } from './Container';
-// The only place a exists. Every section gets its vertical rhythm
-// from a named token class and resolves its semantic colors from its scheme —
-// no per-section padding, no background props.
const sectionShellClassName = css`
background-color: ${semanticColor.surface};
min-width: 0;
@@ -70,14 +67,6 @@ const sectionShellClassName = css`
${buildSchemeContext('dark')}
}
- /* Same-scheme neighbours share one surface, so the follower trims its top
- to a single small step rather than stacking a second full rhythm band of
- the same colour. The upper section keeps its full bottom rhythm; the
- follower's 6px (not 0) leaves room for frame decorations that overflow its
- top edge — TrustedBy's corner markers. Flush scenes own their own edges
- and opt out, as does a section with keepsTopRhythm — one whose own top
- padding is load-bearing (an editorial's top-anchored crosshair needs the
- room below it). */
&[data-scheme='light']:not([data-rhythm='flush'])
+ &[data-scheme='light']:not([data-keep-top-rhythm]),
&[data-scheme='muted']:not([data-rhythm='flush'])
@@ -87,13 +76,6 @@ const sectionShellClassName = css`
padding-top: ${spacing(1.5)};
}
- /* A section can declare that it connects up into the section above it — a
- single continuous decorative frame across the seam (the partners promo
- hanging off the TrustedBy band). The upper section drops its bottom rhythm
- so its border meets the seam exactly, and shows overflow so its bottom
- corner markers sit on that line instead of being clipped; z-index keeps
- those markers above the section below. Same-scheme by intent — the
- connecting section is responsible for sharing the surface. */
&:has(+ &[data-connect-up]) {
overflow: visible;
padding-bottom: 0;
@@ -109,6 +91,10 @@ const backgroundLayerClassName = css`
pointer-events: none;
position: absolute;
z-index: 0;
+
+ &[data-full-bleed] {
+ max-width: none;
+ }
`;
const contentLayerClassName = css`
@@ -118,19 +104,11 @@ const contentLayerClassName = css`
export type SectionShellProps = {
ariaLabel?: string;
- // Decorative layer behind the content (gradients, visuals), capped at the
- // content width — only the section's solid colour bleeds full-width.
background?: ReactNode;
children: ReactNode;
- // Forms one continuous frame with the section directly above (same scheme):
- // that section yields its bottom rhythm so the two tuck together.
connectsUp?: boolean;
- // Removes the Container's horizontal gutter (the max-width cap and centring
- // stay) — for content that runs edge-to-edge within the content column,
- // like the marquee. Most sections keep the gutter.
flushInline?: boolean;
- // Keeps its own full top rhythm instead of collapsing under a same-scheme
- // predecessor — for sections whose top padding is load-bearing.
+ fullBleedBackground?: boolean;
keepsTopRhythm?: boolean;
rhythm?: 'section' | 'hero' | 'spacious' | 'flush';
scheme?: Scheme;
@@ -142,6 +120,7 @@ export function SectionShell({
children,
connectsUp = false,
flushInline = false,
+ fullBleedBackground = false,
keepsTopRhythm = false,
rhythm = 'section',
scheme = 'light',
@@ -160,6 +139,7 @@ export function SectionShell({
aria-hidden
className={backgroundLayerClassName}
data-background-layer=""
+ data-full-bleed={fullBleedBackground ? '' : undefined}
>
{background}
diff --git a/packages/twenty-website-redone/wrangler.jsonc b/packages/twenty-website-redone/wrangler.jsonc
new file mode 100644
index 0000000000000..f2f0bd76b0489
--- /dev/null
+++ b/packages/twenty-website-redone/wrangler.jsonc
@@ -0,0 +1,100 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "twenty-website",
+ "main": ".open-next/worker.js",
+ "compatibility_date": "2026-04-15",
+ // `global_fetch_strictly_public` forces the skew handler's cross-version
+ // fetch to `-..workers.dev` to take the public
+ // Internet path. Without it, Cloudflare's optimized intra-account routing
+ // self-loops back to the current worker (timeouts → 522).
+ "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
+ "assets": {
+ "directory": ".open-next/assets",
+ "binding": "ASSETS",
+ // Worker must intercept asset requests so it can route /_next/static/*
+ // and /_next/data/* from a stale client to the matching old Worker
+ // version (skew protection). Cloudflare's edge cache still absorbs hot
+ // paths, so this isn't a 5× Worker invocation tax in practice.
+ "run_worker_first": true,
+ },
+ // Per-version preview URLs are how skew protection routes a stale request
+ // to the old deployment: `-..workers.dev`.
+ "preview_urls": true,
+ "observability": {
+ "enabled": true,
+ },
+ "env": {
+ "dev": {
+ "name": "twenty-website-dev",
+ "routes": [
+ {
+ "pattern": "twenty-main.com",
+ "custom_domain": true,
+ },
+ {
+ "pattern": "www.twenty-main.com",
+ "custom_domain": true,
+ },
+ ],
+ "r2_buckets": [
+ {
+ "binding": "NEXT_INC_CACHE_R2_BUCKET",
+ "bucket_name": "twenty-website-cache-dev",
+ },
+ ],
+ // Self-reference so OpenNext can fire-and-forget background ISR
+ // revalidations instead of blocking the request. Per-env because the
+ // service name must match the deployed worker name.
+ "services": [
+ {
+ "binding": "WORKER_SELF_REFERENCE",
+ "service": "twenty-website-dev",
+ },
+ ],
+ // Skew-protection runtime inputs. The handler reads these to construct
+ // `-..workers.dev`
+ // when routing a stale request to an older Worker version.
+ "vars": {
+ "CF_WORKER_NAME": "twenty-website-dev",
+ // OpenNext appends `.workers.dev` itself when constructing the
+ // per-version preview URL — passing the full `twentyhq.workers.dev`
+ // here would yield `…twentyhq.workers.dev.workers.dev` and 404.
+ // See https://github.com/opennextjs/opennextjs-cloudflare/issues/811
+ "CF_PREVIEW_DOMAIN": "twentyhq",
+ },
+ },
+ "prod": {
+ "name": "twenty-website-prod",
+ "routes": [
+ {
+ "pattern": "twenty.com",
+ "custom_domain": true,
+ },
+ {
+ "pattern": "www.twenty.com",
+ "custom_domain": true,
+ },
+ ],
+ "r2_buckets": [
+ {
+ "binding": "NEXT_INC_CACHE_R2_BUCKET",
+ "bucket_name": "twenty-website-cache-prod",
+ },
+ ],
+ "services": [
+ {
+ "binding": "WORKER_SELF_REFERENCE",
+ "service": "twenty-website-prod",
+ },
+ ],
+ "vars": {
+ "CF_WORKER_NAME": "twenty-website-prod",
+ // OpenNext appends `.workers.dev` itself when constructing the
+ // per-version preview URL — passing the full `twentyhq.workers.dev`
+ // here would yield `…twentyhq.workers.dev.workers.dev` and 404.
+ // See https://github.com/opennextjs/opennextjs-cloudflare/issues/811
+ "CF_PREVIEW_DOMAIN": "twentyhq",
+ },
+ },
+ },
+}
diff --git a/yarn.lock b/yarn.lock
index 8ef1284879931..dd5c8fc1af81c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -55054,6 +55054,7 @@ __metadata:
"@lingui/react": "npm:^5.1.2"
"@lingui/swc-plugin": "npm:^5.11.0"
"@lottiefiles/dotlottie-react": "npm:^0.18.10"
+ "@opennextjs/cloudflare": "npm:^1.0.0"
"@tabler/icons-react": "npm:^3.41.1"
"@types/node": "npm:^20"
"@types/react": "npm:^19"
@@ -55070,6 +55071,7 @@ __metadata:
three: "npm:^0.184.0"
twenty-shared: "workspace:*"
twenty-ui: "workspace:*"
+ wrangler: "npm:^4.0.0"
zod: "npm:^4.1.11"
languageName: unknown
linkType: soft