[Website] Full-bleed hero/testimonials, nav restructure, OpenNext deploy config (#21794)
### Full-bleed backgrounds on wide screens Decorative section backgrounds were capped at the 1512px content width, leaving large empty gutters on ultra-wide viewports. - Added an opt-in `fullBleedBackground` prop to `SectionShell` that lifts the background layer off the content-width cap (default unchanged, so every other section is untouched). - Applied it to `HomeHero` (bridge halftone field) and both testimonials sections. - Capped the `NotchedCardShape` notch at its width at the content cap, so the white/dark card can span the full viewport while the notch stays fixed and centered. The footer and any other capped card are unaffected. <img width="3024" height="1718" alt="image" src="https://github.com/user-attachments/assets/a2200677-58af-4740-9257-77f6385ade28" /> <img width="3024" height="1224" alt="image" src="https://github.com/user-attachments/assets/73aa6745-b766-405e-b736-8c0c7591232e" /> ### Navigation restructure - Removed **Product** from the menu and footer nav, and promoted **Why** out of the Resources dropdown to a top-level item in Product's place. (The Product page itself is unchanged). - Resources dropdown polish: tightened the preview frame height now that the list is shorter; restored the cleaner/brighter User Guide and Developers preview assets from `twenty-website`; gave User Guide a center + 2× image scale so its halftone fills the frame like Developers, and raised `NextImage` `sizes` to keep the fine halftone crisp through that magnification. - Restored the **current-page highlight** in the Resources dropdown (active icon/label in highlight blue + marker bar), matching the old `NavDropdown`. <p> <img width="1509" height="323" alt="image" src="https://github.com/user-attachments/assets/d5702761-31f5-4b4d-9fcc-c33d5c7ae6ab" /> </p> ### OpenNext / Cloudflare deployment config Ported the Cloudflare Workers deployment setup from `twenty-website` so the same CI/deploy pipeline works against the redone package: - `open-next.config.ts` (R2 incremental cache + regional cache + skew protection), `wrangler.jsonc` (dev/prod envs — **worker names, routes, R2 buckets kept identical** for a seamless cutover), `initOpenNextCloudflareForDev()` in `next.config.ts`, the `preview`/`deploy:*`/`cf-typegen` scripts, the `@opennextjs/cloudflare` + `wrangler` devDependencies, a `.dev.vars.example` template, and the relevant `.gitignore` entries.
This commit is contained in:
@@ -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=
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 63 KiB |
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
|
||||
@@ -82,6 +82,7 @@ export function HomeHero() {
|
||||
<HeroBackdrop />
|
||||
</GradientBackdrop>
|
||||
}
|
||||
fullBleedBackground
|
||||
rhythm="hero"
|
||||
scheme="muted"
|
||||
>
|
||||
|
||||
@@ -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) {
|
||||
<DropdownList>
|
||||
{items.map((child) => {
|
||||
const IconComponent = child.icon;
|
||||
const isCurrentPage =
|
||||
child.external !== true &&
|
||||
(pathname === child.href || pathname.startsWith(`${child.href}/`));
|
||||
return (
|
||||
<li
|
||||
key={child.href}
|
||||
@@ -175,6 +202,7 @@ export function MenuDropdown({ items }: MenuDropdownProps) {
|
||||
onMouseEnter={() => setActiveHref(child.href)}
|
||||
>
|
||||
<DropdownLink
|
||||
data-active={isCurrentPage ? '' : undefined}
|
||||
render={
|
||||
child.external === true ? (
|
||||
<ExternalLink href={child.href} />
|
||||
@@ -206,7 +234,7 @@ export function MenuDropdown({ items }: MenuDropdownProps) {
|
||||
<NextImage
|
||||
alt={i18n._(activeItem.preview.imageAlt)}
|
||||
fill
|
||||
sizes="360px"
|
||||
sizes="720px"
|
||||
src={activeItem.preview.image}
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
|
||||
@@ -2,7 +2,6 @@ import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
IconBook,
|
||||
IconBulb,
|
||||
IconBrandLinkedin,
|
||||
IconBrandX,
|
||||
IconCode,
|
||||
@@ -52,23 +51,10 @@ export const MENU: {
|
||||
} = {
|
||||
appUrl: SITE_URLS.appWelcome,
|
||||
navItems: [
|
||||
{ href: '/product', label: msg`Product` },
|
||||
{ href: '/why-twenty', label: msg`Why` },
|
||||
{
|
||||
label: msg`Resources`,
|
||||
children: [
|
||||
{
|
||||
label: msg`Why`,
|
||||
description: msg`The story behind Twenty`,
|
||||
href: '/why-twenty',
|
||||
icon: IconBulb,
|
||||
preview: {
|
||||
image: '/images/menu/why.webp',
|
||||
imageAlt: msg`Why Twenty illustration`,
|
||||
imagePosition: 'center',
|
||||
title: msg`Why teams choose Twenty`,
|
||||
description: msg`The principles and product philosophy behind the open source CRM.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: msg`User Guide`,
|
||||
description: msg`Learn how to use Twenty`,
|
||||
|
||||
@@ -19,6 +19,7 @@ export function PartnerTestimonials() {
|
||||
return (
|
||||
<SectionShell
|
||||
background={<NotchedCardShape cardScheme="dark" />}
|
||||
fullBleedBackground
|
||||
rhythm="spacious"
|
||||
scheme="light"
|
||||
>
|
||||
|
||||
@@ -7,6 +7,7 @@ export function Testimonials() {
|
||||
return (
|
||||
<SectionShell
|
||||
background={<NotchedCardShape />}
|
||||
fullBleedBackground
|
||||
rhythm="spacious"
|
||||
scheme="muted"
|
||||
>
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { buildSchemeDeclarations, type Scheme, semanticColor } from '@/tokens';
|
||||
import {
|
||||
buildSchemeDeclarations,
|
||||
MAX_CONTENT_WIDTH_PX,
|
||||
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.
|
||||
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;
|
||||
|
||||
@@ -13,9 +13,6 @@ import {
|
||||
|
||||
import { Container } from './Container';
|
||||
|
||||
// The only place a <section> 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}
|
||||
</div>
|
||||
|
||||
@@ -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 `<version>-<worker>.<account>.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: `<version>-<worker>.<account>.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
|
||||
// `<old-version-id>-<CF_WORKER_NAME>.<CF_PREVIEW_DOMAIN>.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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -55055,6 +55055,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"
|
||||
@@ -55071,6 +55072,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
|
||||
|
||||
Reference in New Issue
Block a user