Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/twenty-website-redone/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -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=
6 changes: 6 additions & 0 deletions packages/twenty-website-redone/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions packages/twenty-website-redone/next.config.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: initOpenNextCloudflareForDev() called unconditionally will hang next typegen and leave open handles in Jest tests. The Cloudflare dev context binds active Node process handles (R2 incremental cache, env wrangler context) that prevent clean process exit in non-dev commands.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-website-redone/next.config.ts, line 55:

<comment>`initOpenNextCloudflareForDev()` called unconditionally will hang `next typegen` and leave open handles in Jest tests. The Cloudflare dev context binds active Node process handles (R2 incremental cache, env wrangler context) that prevent clean process exit in non-dev commands.</comment>

<file context>
@@ -48,3 +49,7 @@ const nextConfig: LinariaConfig = {
+
+// Binds the Cloudflare dev context (R2 incremental cache, env vars) into
+// `next dev` so local runs mirror the deployed OpenNext worker.
+initOpenNextCloudflareForDev();
</file context>
Suggested change
initOpenNextCloudflareForDev();
if (process.env.NODE_ENV === 'development') initOpenNextCloudflareForDev();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The premise that the call is "unconditional" and therefore binds live handles in every command is incorrect. initOpenNextCloudflareForDev() is written to be called unconditionally (that's the official OpenNext pattern), but it self-guards before doing anything.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it — thanks for the correction.

34 changes: 34 additions & 0 deletions packages/twenty-website-redone/open-next.config.ts
Original file line number Diff line number Diff line change
@@ -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,
},
},
};
10 changes: 8 additions & 2 deletions packages/twenty-website-redone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
}
2 changes: 1 addition & 1 deletion packages/twenty-website-redone/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 0 additions & 65 deletions packages/twenty-website-redone/scripts/check-translations.mjs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function HomeHero() {
<HeroBackdrop />
</GradientBackdrop>
}
fullBleedBackground
rhythm="hero"
scheme="muted"
>
Expand Down
32 changes: 30 additions & 2 deletions packages/twenty-website-redone/src/sections/menu/MenuDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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`
Expand All @@ -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%;
Expand Down Expand Up @@ -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;
Expand All @@ -168,13 +192,17 @@ 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}
onFocus={() => setActiveHref(child.href)}
onMouseEnter={() => setActiveHref(child.href)}
>
<DropdownLink
data-active={isCurrentPage ? '' : undefined}
render={
child.external === true ? (
<ExternalLink href={child.href} />
Expand Down Expand Up @@ -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',
Expand Down
16 changes: 1 addition & 15 deletions packages/twenty-website-redone/src/sections/menu/menu.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import {
IconBook,
IconBulb,
IconBrandLinkedin,
IconBrandX,
IconCode,
Expand Down Expand Up @@ -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`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function PartnerTestimonials() {
return (
<SectionShell
background={<NotchedCardShape cardScheme="dark" />}
fullBleedBackground
rhythm="spacious"
scheme="light"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export function Testimonials() {
return (
<SectionShell
background={<NotchedCardShape />}
fullBleedBackground
rhythm="spacious"
scheme="muted"
>
Expand Down
Loading
Loading