Partners, customers and more (#19862)

## Summary
- Refresh the Twenty website with updated homepage, product, pricing,
partner, customer, case study, and release content
- Add and replace supporting imagery, illustrations, and Lottie assets
used across the site
- Adjust layout constants, navigation/footer content, and page-level
copy for the updated marketing experience
- Update Next.js config and ignore rules to support the new assets and
build output

## Testing
- Not run (not requested)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
This commit is contained in:
Thomas des Francs 2026-04-20 09:13:56 +02:00 committed by GitHub
parent 46aedcf133
commit 0729ad27b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
200 changed files with 11661 additions and 1691 deletions

2
.gitignore vendored
View file

@ -1,7 +1,7 @@
**/**/.env **/**/.env
.DS_Store .DS_Store
/.idea /.idea
.claude/settings.json .claude/
.cursor/debug-*.log .cursor/debug-*.log
**/**/node_modules/ **/**/node_modules/
.cache .cache

BIN
logic-3500-crop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
logic-3500.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
logic-3800.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
logic-4100.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -1,3 +1,4 @@
import path from 'path';
import withLinaria, { type LinariaConfig } from 'next-with-linaria'; import withLinaria, { type LinariaConfig } from 'next-with-linaria';
const nextConfig: LinariaConfig = { const nextConfig: LinariaConfig = {
@ -15,6 +16,9 @@ const nextConfig: LinariaConfig = {
}, },
], ],
}, },
linaria: {
configFile: path.resolve(__dirname, 'wyw-in-js.config.cjs'),
},
reactCompiler: true, reactCompiler: true,
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -22,10 +22,10 @@ export const HELPED_DATA: HelpedDataType = {
icon: 'w3villa', icon: 'w3villa',
heading: { text: 'Ship a product on Twenty', fontFamily: 'sans' }, heading: { text: 'Ship a product on Twenty', fontFamily: 'sans' },
body: { body: {
text: 'W3villa built W3Grads — AI mock interviews at scale — on Twenty as the operational backbone.', text: 'W3villa built W3Grads for AI mock interviews at scale, with Twenty as the operational backbone.',
}, },
illustration: 'target', illustration: 'target',
href: '/case-studies/w3villa', href: '/customers/w3villa',
}, },
{ {
icon: 'act-education', icon: 'act-education',
@ -34,7 +34,7 @@ export const HELPED_DATA: HelpedDataType = {
text: 'AC&T replaced a shuttered vendor CRM with self-hosted Twenty and cut CRM costs by more than 90%.', text: 'AC&T replaced a shuttered vendor CRM with self-hosted Twenty and cut CRM costs by more than 90%.',
}, },
illustration: 'spaceship', illustration: 'spaceship',
href: '/case-studies/act-education', href: '/customers/act-education',
}, },
{ {
icon: 'netzero', icon: 'netzero',
@ -43,7 +43,7 @@ export const HELPED_DATA: HelpedDataType = {
text: 'NetZero runs a modular Twenty setup across carbon credits, ag products, and industrial systems.', text: 'NetZero runs a modular Twenty setup across carbon credits, ag products, and industrial systems.',
}, },
illustration: 'money', illustration: 'money',
href: '/case-studies/netzero', href: '/customers/netzero',
}, },
], ],
}; };

View file

@ -4,7 +4,9 @@ export const PROBLEM_DATA: ProblemDataType = {
eyebrow: { heading: { text: 'The Problem.', fontFamily: 'sans' } }, eyebrow: { heading: { text: 'The Problem.', fontFamily: 'sans' } },
heading: [ heading: [
{ text: 'A custom CRM gives your org an edge, ', fontFamily: 'serif' }, { text: 'A custom CRM gives your org an edge, ', fontFamily: 'serif' },
{ text: 'but building one comes with tradeoffs', fontFamily: 'sans' }, { text: 'but building one ', fontFamily: 'sans' },
{ text: 'comes with ', fontFamily: 'serif' },
{ text: 'tradeoffs', fontFamily: 'sans' },
], ],
points: [ points: [
{ {

View file

@ -15,25 +15,25 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
{ {
heading: { text: 'Production grade quality', fontFamily: 'sans' }, heading: { text: 'Production grade quality', fontFamily: 'sans' },
body: { body: {
text: "What stood out with Twenty wasn't just flexibility it was the quality. The system feels stable, polished, and ready for real teams with real stakes.", text: 'W3villa used Twenty as a production-grade framework for the data model, permissions, authentication, and workflow engine they would otherwise have rebuilt themselves.',
}, },
benefits: undefined, benefits: undefined,
attribution: { attribution: {
role: { text: 'Head of Engineering' }, role: { text: 'VP of Engineering' },
company: { text: 'Mid-Market Fintech' }, company: { text: 'W3villa Technologies' },
}, },
illustration: 'diamond', illustration: 'diamond',
caseStudySlug: '9dots', caseStudySlug: 'w3villa',
}, },
{ {
heading: { text: 'AI for rapid iterations', fontFamily: 'sans' }, heading: { text: 'AI for rapid iterations', fontFamily: 'sans' },
body: { body: {
text: 'Twenty removes friction from iteration and lets us adapt our CRM as the business changes.', text: 'Alternative Partners used agentic AI to compress what would typically be weeks of Salesforce migration work into something a single person could oversee.',
}, },
benefits: undefined, benefits: undefined,
attribution: { attribution: {
role: { text: 'Head of Engineering' }, role: { text: 'Principal and Founder' },
company: { text: 'Mid-Market Fintech' }, company: { text: 'Alternative Partners' },
}, },
illustration: 'flash', illustration: 'flash',
caseStudySlug: 'alternative-partners', caseStudySlug: 'alternative-partners',
@ -41,15 +41,15 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
{ {
heading: { text: 'Control without drag', fontFamily: 'sans' }, heading: { text: 'Control without drag', fontFamily: 'sans' },
body: { body: {
text: "It's a modern, open-source alternative to Salesforce/Hubspot that lets you manage your customer relationships in a secure + privacy-first way.", text: 'AC&T moved to a self-hosted Twenty instance with no vendor risk, no forced migration, and CRM costs reduced by more than 90%.',
}, },
benefits: undefined, benefits: undefined,
attribution: { attribution: {
role: { text: 'Head of Engineering' }, role: { text: 'CRM Engineer' },
company: { text: 'Mid-Market Fintech' }, company: { text: 'AC&T Education Migration' },
}, },
illustration: 'lock', illustration: 'lock',
caseStudySlug: 'elevate-consulting', caseStudySlug: 'act-education',
}, },
], ],
}; };

View file

@ -28,10 +28,12 @@ import { styled } from '@linaria/react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Twenty Open Source CRM', title: 'Twenty | Open Source CRM',
description: 'Modular, scalable open source CRM for modern teams.', description: 'Modular, scalable open source CRM for modern teams.',
}; };
const HOME_TOP_BACKGROUND_COLOR = '#F4F4F4';
const HeroHeadingGroup = styled.div` const HeroHeadingGroup = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
@ -93,7 +95,7 @@ export default async function HomePage() {
return ( return (
<> <>
<Menu.Root <Menu.Root
backgroundColor={theme.colors.secondary.background[5]} backgroundColor={HOME_TOP_BACKGROUND_COLOR}
scheme="primary" scheme="primary"
navItems={MENU_DATA.navItems} navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks} socialLinks={menuSocialLinks}
@ -104,10 +106,7 @@ export default async function HomePage() {
<Menu.Cta scheme="primary" /> <Menu.Cta scheme="primary" />
</Menu.Root> </Menu.Root>
<Hero.Root <Hero.Root backgroundColor={HOME_TOP_BACKGROUND_COLOR} showHomeBackground>
backgroundColor={theme.colors.secondary.background[5]}
showHomeBackground
>
<HeroIntroGroup data-halftone-exclude> <HeroIntroGroup data-halftone-exclude>
<HeroHeadingGroup> <HeroHeadingGroup>
<Hero.Heading page={Pages.Home} segments={HERO_DATA.heading} /> <Hero.Heading page={Pages.Home} segments={HERO_DATA.heading} />
@ -134,9 +133,7 @@ export default async function HomePage() {
<TrustedBy.Root> <TrustedBy.Root>
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} /> <TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
<TrustedBy.Logos logos={TRUSTED_BY_DATA.logos} /> <TrustedBy.Logos logos={TRUSTED_BY_DATA.logos} />
<TrustedBy.ClientCount <TrustedBy.ClientCount label={TRUSTED_BY_DATA.clientCountLabel.text} />
label={TRUSTED_BY_DATA.clientCountLabel.text}
/>
</TrustedBy.Root> </TrustedBy.Root>
<Problem.Root> <Problem.Root>
@ -191,13 +188,6 @@ export default async function HomePage() {
size="lg" size="lg"
weight="light" weight="light"
/> />
<LinkButton
color="secondary"
href="/product"
label="Visit product page"
type="link"
variant="contained"
/>
</ThreeCards.Intro> </ThreeCards.Intro>
<ThreeCards.FeatureCards <ThreeCards.FeatureCards
featureCards={THREE_CARDS_FEATURE_DATA.featureCards} featureCards={THREE_CARDS_FEATURE_DATA.featureCards}

View file

@ -19,13 +19,22 @@ export const FAQ_DATA: FaqDataType = {
}, },
], ],
questions: [ questions: [
{
question: {
text: 'Is Twenty really open-source?',
fontFamily: 'sans',
},
answer: {
text: "Yes. Twenty is the #1 open-source CRM on GitHub. Most teams run it on our managed cloud for zero-ops setup; self-hosting is always available if you'd rather own the infrastructure.",
},
},
{ {
question: { question: {
text: 'How long does it take to get started?', text: 'How long does it take to get started?',
fontFamily: 'sans', fontFamily: 'sans',
}, },
answer: { answer: {
text: 'You can seamlessly deploy Twenty CRM to your production within hours. For highly bespoke on-premise implementations and massive scale data structures, our guided integration typically takes less than two weeks.', text: 'Sign up for Cloud in under a minute and start your 30-day trial. For larger rollouts, our 4-hour Onboarding Packs or certified partners get you live in 12 weeks.',
}, },
}, },
{ {
@ -34,7 +43,7 @@ export const FAQ_DATA: FaqDataType = {
fontFamily: 'sans', fontFamily: 'sans',
}, },
answer: { answer: {
text: 'Yes. We provide migration tools and support to help you move your data from Salesforce, HubSpot, and other CRMs into Twenty.', text: 'Yes. Import your data via CSV, or use our API for 50,000+ records. Our partners can handle the full migration for you.',
}, },
}, },
{ {
@ -43,25 +52,34 @@ export const FAQ_DATA: FaqDataType = {
fontFamily: 'sans', fontFamily: 'sans',
}, },
answer: { answer: {
text: 'Twenty is designed to be customizable without code. For advanced customizations, our API and documentation support developer-led extensions.', text: 'No. Build custom objects, fields, views, and no-code workflows straight from Settings. Unlimited, no extra charge.',
}, },
}, },
{ {
question: { question: {
text: 'Is my data secure?', text: 'Can developers extend Twenty with code?',
fontFamily: 'sans', fontFamily: 'sans',
}, },
answer: { answer: {
text: 'We take security seriously. Your data is encrypted at rest and in transit, and you can self-host to keep full control.', text: 'Yes, with our Apps framework. Scaffold an extension with `npx create-twenty-app` and ship custom objects, server-side logic functions, React components that render inside Twenty\u2019s UI, AI skills and agents, views, and navigation, all in TypeScript, deployable to any workspace.',
}, },
}, },
{ {
question: { question: {
text: 'How does AI usage pricing work?', text: 'Does Twenty work with Claude, ChatGPT, and Cursor?',
fontFamily: 'sans', fontFamily: 'sans',
}, },
answer: { answer: {
text: 'AI features are billed based on usage. See our pricing page for details and current rates.', text: 'Yes. Every Cloud workspace ships with a native MCP server. Connect your AI assistant via OAuth and it can read and write your CRM data in natural language.',
},
},
{
question: {
text: 'What does Twenty cost?',
fontFamily: 'sans',
},
answer: {
text: 'Cloud Pro is $9/user/month (yearly). Organization is $19/user/month and unlocks SSO and row-level permissions for teams needing enterprise-grade security.',
}, },
}, },
], ],

View file

@ -12,9 +12,8 @@ export const FOOTER_DATA: FooterDataType = {
ctas: [], ctas: [],
links: [ links: [
{ label: 'Home', href: '/', external: false }, { label: 'Home', href: '/', external: false },
{ label: 'Product', href: '/product', external: false },
{ label: 'Pricing', href: '/pricing', external: false }, { label: 'Pricing', href: '/pricing', external: false },
{ label: 'Partners', href: '/partner', external: false }, { label: 'Partners', href: '/partners', external: false },
{ label: 'Why Twenty', href: '/why-twenty', external: false }, { label: 'Why Twenty', href: '/why-twenty', external: false },
], ],
}, },

View file

@ -1,11 +1,76 @@
import { getLatestReleasePreview } from '@/lib/releases/get-latest-release-preview';
import type { MenuDataType } from '@/sections/Menu/types'; import type { MenuDataType } from '@/sections/Menu/types';
const releasesPreview = getLatestReleasePreview() ?? {
image: '/images/releases/1.23/1.23.0-easier-layouts.webp',
imageAlt: 'Twenty latest release',
imageScale: 1.04,
title: 'See the latest release',
description:
'Track every release with changelogs, highlights and demos of the newest features.',
};
export const MENU_DATA: MenuDataType = { export const MENU_DATA: MenuDataType = {
navItems: [ navItems: [
{ label: 'Product', href: '/product' }, { label: 'Why', href: '/why-twenty' },
{
label: 'Resources',
children: [
{
label: 'User Guide',
description: 'Learn how to use Twenty',
href: 'https://docs.twenty.com/user-guide/introduction',
external: true,
icon: 'book',
preview: {
image: '/images/product/feature/contacts.webp',
imageAlt: 'Twenty companies list',
title: 'Master every corner of Twenty',
description:
'Step-by-step guides and playbooks to help your team get the most out of their workspace.',
},
},
{
label: 'Developers',
description: 'Create apps on Twenty',
href: 'https://docs.twenty.com/developers/introduction',
external: true,
icon: 'code',
preview: {
image: '/images/shared/menu/developers-preview.png',
imageAlt: 'Blue developer illustration with branching arrows',
imagePosition: 'center',
imageScale: 1.4,
title: 'Build on an open platform',
description:
'APIs, SDKs and webhooks to extend Twenty and ship apps on top of your CRM data.',
},
},
{
label: 'Partners',
description: 'Find a Twenty partner',
href: '/partners',
icon: 'users',
preview: {
image: '/images/partner/hero/hero.webp',
imageAlt: 'Twenty partner ecosystem',
imagePosition: 'center',
title: 'Team up with a Twenty expert',
description:
'Meet the certified agencies and consultants implementing Twenty for teams worldwide.',
},
},
{
label: 'Releases',
description: "Discover what's new",
href: '/releases',
icon: 'tag',
preview: releasesPreview,
},
],
},
{ label: 'Customers', href: '/customers' },
{ label: 'Pricing', href: '/pricing' }, { label: 'Pricing', href: '/pricing' },
{ label: 'Partners', href: '/partner' },
{ label: 'Why Twenty', href: '/why-twenty' },
], ],
socialLinks: [ socialLinks: [
{ {

View file

@ -1,7 +0,0 @@
export type {
CaseStudyCatalogEntry,
CaseStudyContentBlock,
CaseStudyData,
CaseStudyTextBlock,
CaseStudyVisualBlock,
} from './types';

View file

@ -1,200 +0,0 @@
import { MENU_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import type { CaseStudyCatalogEntry } from '@/app/case-studies/_constants/types';
import { LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudyCatalog } from '@/sections/CaseStudyCatalog/components';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { Signoff } from '@/sections/Signoff/components';
import { theme } from '@/theme';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Case Studies — Twenty',
description:
'See how teams use Twenty to build custom CRM workflows and drive real business results.',
};
const HERO_HEADING = [
{ text: 'See how teams build ', fontFamily: 'serif' as const },
{ text: 'with Twenty', fontFamily: 'sans' as const },
];
const HERO_BODY = {
text: 'Real stories from real teams — how they shaped Twenty to fit their workflow and accelerated their growth.',
};
const SIGNOFF_HEADING = [
{ text: 'Ready to build ', fontFamily: 'serif' as const },
{ text: 'your own story?', fontFamily: 'sans' as const },
];
const SIGNOFF_BODY = {
text: 'Join the teams that chose to own their CRM. Start building with Twenty today.',
};
const PLACEHOLDER_HERO = '/images/shared/people/avatars/katherine-adams.jpg';
const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [
{
href: '/case-studies/elevate-consulting',
hero: {
readingTime: '8 min',
title: [
{ text: 'Twenty as the API backbone ', fontFamily: 'serif' },
{ text: 'of a go-to-market stack', fontFamily: 'sans' },
],
author: 'Justin Beadle',
clientIcon: 'elevate-consulting',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'Elevate Consulting uses Twenty as the API backbone connecting billing, Teams, resourcing, and a custom front end around client and opportunity data.',
date: 'Jun 2025',
},
},
{
href: '/case-studies/9dots',
hero: {
readingTime: '9 min',
title: [
{ text: 'A real estate agency on WhatsApp ', fontFamily: 'serif' },
{ text: 'built a CRM around it', fontFamily: 'sans' },
],
author: 'Mike Babiy & Azmat Parveen',
clientIcon: 'nine-dots',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
"Nine Dots put Twenty at the center of Homeseller's stack with APIs, automation, and AI on top of WhatsApp-heavy operations.",
date: 'Jul 2025',
},
},
{
href: '/case-studies/alternative-partners',
hero: {
readingTime: '7 min',
title: [
{ text: 'From Salesforce to ', fontFamily: 'serif' },
{ text: 'self-hosted Twenty', fontFamily: 'sans' },
],
author: 'Benjamin Reynolds',
clientIcon: 'alternative-partners',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'Alternative Partners replaced Salesforce with self-hosted Twenty, using agentic AI to compress migration work.',
date: '2025',
},
},
{
href: '/case-studies/netzero',
hero: {
readingTime: '8 min',
title: [
{ text: 'A CRM that ', fontFamily: 'serif' },
{ text: 'grows with you', fontFamily: 'sans' },
],
author: 'Olivier Reinaud',
clientIcon: 'netzero',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'NetZero uses Twenty as a modular CRM across product lines and countries, with a roadmap into AI-assisted workflows.',
date: '2025',
},
},
{
href: '/case-studies/act-education',
hero: {
readingTime: '7 min',
title: [
{ text: 'A CRM they ', fontFamily: 'serif' },
{ text: 'actually own', fontFamily: 'sans' },
],
author: 'Joseph Chiang',
clientIcon: 'act-education',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'AC&T and Flycoder moved from a dead vendor export to self-hosted Twenty — over 90% lower CRM cost and full control.',
date: '2025',
},
},
{
href: '/case-studies/w3villa',
hero: {
readingTime: '8 min',
title: [
{ text: 'When your CRM ', fontFamily: 'serif' },
{ text: 'is the product', fontFamily: 'sans' },
],
author: 'Amrendra Pratap Singh',
clientIcon: 'w3villa',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'W3villa shipped W3Grads on Twenty — AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.',
date: '2025',
},
},
];
export default async function CaseStudiesCatalogPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<>
<Menu.Root
backgroundColor={theme.colors.primary.background[100]}
scheme="primary"
navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks}
>
<Menu.Logo scheme="primary" />
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
<Menu.Cta scheme="primary" />
</Menu.Root>
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
<Hero.Heading page={Pages.CaseStudies} segments={HERO_HEADING} />
<Hero.Body page={Pages.CaseStudies} body={HERO_BODY} />
</Hero.Root>
<CaseStudyCatalog.Grid entries={CASE_STUDY_CATALOG_ENTRIES} />
<Signoff.Root
backgroundColor={theme.colors.primary.background[100]}
color={theme.colors.primary.text[100]}
>
<Signoff.Heading segments={SIGNOFF_HEADING} />
<Signoff.Body body={SIGNOFF_BODY} />
<Signoff.Cta>
<LinkButton
color="primary"
href="https://app.twenty.com/welcome"
label="Get started"
type="anchor"
variant="contained"
/>
<TalkToUsButton
color="primary"
label="Talk to us"
variant="outlined"
/>
</Signoff.Cta>
</Signoff.Root>
</>
);
}

View file

@ -1,5 +1,7 @@
import { MENU_DATA } from '@/app/_constants'; import { MENU_DATA } from '@/app/_constants';
import type { CaseStudyData } from '@/app/case-studies/_constants/types'; import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette } from '@/app/customers/_constants';
import type { CaseStudyData } from '@/app/customers/_constants/types';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components'; import { CaseStudy } from '@/sections/CaseStudy/components';
@ -7,12 +9,13 @@ import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/shared/people/avatars/katherine-adams.jpg'; const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1733244766159-f58f4184fd38?w=1600&q=80';
const CASE_STUDY: CaseStudyData = { const CASE_STUDY: CaseStudyData = {
meta: { meta: {
title: title:
'Homeseller, WhatsApp, and a CRM built around the business Nine Dots & Twenty', 'Homeseller, WhatsApp, and a CRM built around the business | Nine Dots & Twenty',
description: description:
'How Nine Dots Ventures rebuilt a Singapore real estate agency on Twenty with APIs, n8n, Grafana, and AI on top of 2,000+ WhatsApp messages a day.', 'How Nine Dots Ventures rebuilt a Singapore real estate agency on Twenty with APIs, n8n, Grafana, and AI on top of 2,000+ WhatsApp messages a day.',
}, },
@ -20,52 +23,67 @@ const CASE_STUDY: CaseStudyData = {
readingTime: '9 min', readingTime: '9 min',
title: [ title: [
{ text: 'A real estate agency on WhatsApp ', fontFamily: 'serif' }, { text: 'A real estate agency on WhatsApp ', fontFamily: 'serif' },
{ text: 'built a CRM around it', fontFamily: 'sans' }, { text: 'built a ', fontFamily: 'serif' },
{ text: 'CRM', fontFamily: 'sans', newLine: true },
{ text: ' around it', fontFamily: 'serif' },
], ],
author: 'Mike Babiy & Azmat Parveen', author: 'Mike Babiy & Azmat Parveen',
authorAvatarSrc: '/images/partner/testimonials/mike-babiy.png',
authorRole: 'Founder, Nine Dots Ventures',
clientIcon: 'nine-dots', clientIcon: 'nine-dots',
heroImageSrc: PLACEHOLDER_HERO, heroImageSrc: PLACEHOLDER_HERO,
industry: 'Real Estate',
kpis: [
{ value: '150 hrs', label: 'Saved / month' },
{ value: '2,000+', label: 'Daily messages' },
{ value: 'Q1 2026', label: 'Record quarter' },
],
quote: {
text: 'Twenty lets us build a CRM around the business and not the business around the CRM.',
author: 'Mike Babiy',
role: 'Founder, Nine Dots Ventures',
},
}, },
sections: [ sections: [
{ {
type: 'text', type: 'text',
eyebrow: 'Homeseller', eyebrow: 'Homeseller',
heading: [ heading: [
{ text: 'When the channel ', fontFamily: 'serif' }, { text: 'When the channel is ', fontFamily: 'serif' },
{ text: 'is the business', fontFamily: 'sans' }, { text: 'the business', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
"Homeseller is a high-volume real estate agency in Singapore, founded by one of the country's top-performing property agents. The whole operation runs on WhatsApp: no email, no calendars — group chats, thousands of them, with clients, agents, and leads together.", "Homeseller is a high-volume real estate agency in Singapore, founded by one of the country's top-performing property agents. The whole operation runs on WhatsApp: no email, no calendars, just group chats, thousands of them, with clients, agents, and leads together.",
'That works until you need to understand the business underneath. Which deals are stuck? Where are leads coming from? What is the close rate? With spreadsheets and a legacy custom CRM that could not keep up, those questions were nearly impossible to answer.', 'That works until you need to understand the business underneath. Which deals are stuck? Where are leads coming from? What is the close rate? With spreadsheets and a legacy custom CRM that could not keep up, those questions were nearly impossible to answer.',
'Mike and Azmat from Nine Dots stepped in to fix that not by changing how Homeseller works, but by building a system that finally fit around it.', 'Mike and Azmat from Nine Dots stepped in to fix that, not by changing how Homeseller works, but by building a system that finally fit around it.',
], ],
callout: callout:
'"Twenty lets us build a CRM around the business and not the business around the CRM." Mike Babiy, Founder, Nine Dots Ventures', '"Twenty lets us build a CRM around the business and not the business around the CRM." - Mike Babiy, Founder, Nine Dots Ventures',
}, },
{ {
type: 'text', type: 'text',
eyebrow: 'Architecture', eyebrow: 'Architecture',
heading: [ heading: [
{ text: 'The CRM ', fontFamily: 'serif' }, { text: 'The CRM as a ', fontFamily: 'serif' },
{ text: 'as a control hub', fontFamily: 'sans' }, { text: 'control hub', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
"Nine Dots rebuilt Homeseller's operations on Twenty, with a custom data model shaped around their sales flow. Because Twenty is open and everything is accessible via API, they connected it to what the business actually needed: n8n for automated workflows (in-app workflows were not available at that time), Grafana for live dashboards fed from Twenty, and a custom AI layer to parse and extract structured insights from more than 2,000 WhatsApp messages a day.", "Nine Dots rebuilt Homeseller's operations on Twenty, with a custom data model shaped around their sales flow. Because Twenty is open and everything is accessible via API, they connected it to what the business actually needed: n8n for automated workflows (in-app workflows were not available at that time), Grafana for live dashboards fed from Twenty, and a custom AI layer to parse and extract structured insights from more than 2,000 WhatsApp messages a day.",
'Homeseller kept their habits. WhatsApp stayed WhatsApp. What changed is that everything flowing through those conversations now lands in a structured system tracked, classified, and visible in real time.', 'Homeseller kept their habits. WhatsApp stayed WhatsApp. What changed is that everything flowing through those conversations now lands in a structured system, tracked, classified, and visible in real time.',
], ],
callout: callout:
'"Twenty is the heart of the system. Everything branches from it." Azmat Parveen, Nine Dots Ventures', '"Twenty is the heart of the system. Everything branches from it." - Azmat Parveen, Nine Dots Ventures',
}, },
{ {
type: 'text', type: 'text',
eyebrow: 'The result', eyebrow: 'The result',
heading: [ heading: [
{ text: '150 hours ', fontFamily: 'serif' }, { text: '150 hours', fontFamily: 'sans' },
{ text: 'saved every month', fontFamily: 'sans' }, { text: ' saved every month', fontFamily: 'serif' },
], ],
paragraphs: [ paragraphs: [
'About 150 hours per month saved in manual operations. Real-time metrics for the business owner. Growth readiness without adding operational headcount. A team that can answer questions that used to take days to piece together.', 'About 150 hours per month saved in manual operations. Real-time metrics for the business owner. Growth readiness without adding operational headcount. A team that can answer questions that used to take days to piece together.',
'The full rollout landed in July 2025. Since then, Nine Dots built a Smart Assistant on top of the system nudging agents with tasks, reminders, and on-demand market analysis. Some agents never open Twenty directly, yet they are powered by it, outperforming peers on manual processes alone. By Q1 2026, Homeseller had recorded its best sales quarter ever.', 'The full rollout landed in July 2025. Since then, Nine Dots built a Smart Assistant on top of the system, nudging agents with tasks, reminders, and on-demand market analysis. Some agents never open Twenty directly, yet they are powered by it, outperforming peers on manual processes alone. By Q1 2026, Homeseller had recorded its best sales quarter ever.',
], ],
}, },
], ],
@ -89,6 +107,7 @@ export const metadata: Metadata = {
export default async function NineDotsCaseStudyPage() { export default async function NineDotsCaseStudyPage() {
const stats = await fetchCommunityStats(); const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats); const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
const palette = getCaseStudyPalette('/customers/9dots');
let storySectionIndex = 0; let storySectionIndex = 0;
const sectionBlocks = CASE_STUDY.sections.map((block, index) => { const sectionBlocks = CASE_STUDY.sections.map((block, index) => {
@ -96,10 +115,21 @@ export default async function NineDotsCaseStudyPage() {
const sectionId = `case-study-section-${storySectionIndex}`; const sectionId = `case-study-section-${storySectionIndex}`;
storySectionIndex += 1; storySectionIndex += 1;
return ( return (
<CaseStudy.TextBlock key={index} block={block} sectionId={sectionId} /> <CaseStudy.TextBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
sectionId={sectionId}
/>
); );
} }
return <CaseStudy.VisualBlock key={index} block={block} />; return (
<CaseStudy.VisualBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
/>
);
}); });
return ( return (
@ -116,11 +146,21 @@ export default async function NineDotsCaseStudyPage() {
<Menu.Cta scheme="secondary" /> <Menu.Cta scheme="secondary" />
</Menu.Root> </Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} /> <CaseStudy.Hero
dashColor={palette.dashColor}
hero={CASE_STUDY.hero}
hoverDashColor={palette.hoverDashColor}
/>
{sectionBlocks} <CaseStudy.Highlights
industry={CASE_STUDY.hero.industry}
kpis={CASE_STUDY.hero.kpis}
/>
<CaseStudy.Body>{sectionBlocks}</CaseStudy.Body>
<CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} /> <CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} />
<CustomersCaseStudySignoff />
</> </>
); );
} }

View file

@ -0,0 +1,41 @@
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import { LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages';
import { Signoff } from '@/sections/Signoff/components';
import { theme } from '@/theme';
const SIGNOFF_HEADING = [
{ text: 'Ready to grow\nwith ', fontFamily: 'serif' as const },
{ text: 'Twenty?', fontFamily: 'sans' as const },
];
const SIGNOFF_BODY = {
text: 'Join the teams that chose to own their CRM.\nStart building with Twenty today.',
};
export function CustomersCaseStudySignoff() {
return (
<Signoff.Root
backgroundColor={theme.colors.secondary.background[5]}
color={theme.colors.primary.text[100]}
page={Pages.Partners}
>
<Signoff.Heading page={Pages.Partners} segments={SIGNOFF_HEADING} />
<Signoff.Body body={SIGNOFF_BODY} page={Pages.Partners} />
<Signoff.Cta>
<LinkButton
color="secondary"
href="https://app.twenty.com/welcome"
label="Get started"
type="anchor"
variant="contained"
/>
<TalkToUsButton
color="secondary"
label="Talk to us"
variant="outlined"
/>
</Signoff.Cta>
</Signoff.Root>
);
}

View file

@ -0,0 +1,206 @@
import { theme } from '@/theme';
import type { CaseStudyCatalogEntry } from './types';
const PLACEHOLDER_HERO = '/images/shared/people/avatars/katherine-adams.jpg';
export const CASE_STUDY_HALFTONE_PALETTE: readonly {
dashColor: string;
hoverDashColor: string;
}[] = [
{
dashColor: theme.colors.accent.blue[100],
hoverDashColor: theme.colors.accent.blue[70],
},
{
dashColor: theme.colors.accent.pink[100],
hoverDashColor: theme.colors.accent.pink[70],
},
{
dashColor: theme.colors.accent.yellow[100],
hoverDashColor: theme.colors.accent.yellow[70],
},
{
dashColor: theme.colors.accent.green[100],
hoverDashColor: theme.colors.accent.green[70],
},
];
export function getCaseStudyPalette(href: string) {
const index = CASE_STUDY_CATALOG_ENTRIES.findIndex(
(entry) => entry.href === href,
);
const safeIndex = index >= 0 ? index : 0;
return CASE_STUDY_HALFTONE_PALETTE[
safeIndex % CASE_STUDY_HALFTONE_PALETTE.length
];
}
export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [
{
href: '/customers/9dots',
industry: 'Real Estate',
authorRole: 'Founder, Nine Dots Ventures',
kpis: [
{ value: '150 hrs', label: 'Saved / month' },
{ value: '2,000+', label: 'Daily messages' },
{ value: 'Q1 2026', label: 'Record quarter' },
],
quote: {
text: 'Twenty lets us build a CRM around the business and not the business around the CRM.',
author: 'Mike Babiy',
role: 'Founder, Nine Dots Ventures',
},
hero: {
readingTime: '9 min',
title: [
{ text: 'A real estate agency on WhatsApp ', fontFamily: 'serif' },
{ text: 'built a CRM around it', fontFamily: 'sans' },
],
author: 'Mike Babiy & Azmat Parveen',
authorAvatarSrc: '/images/partner/testimonials/mike-babiy.png',
clientIcon: 'nine-dots',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
"Nine Dots put Twenty at the center of Homeseller's stack with APIs, automation, and AI on top of WhatsApp-heavy operations.",
date: 'Jul 2025',
coverImageSrc:
'https://images.unsplash.com/photo-1733244766159-f58f4184fd38?w=1600&q=80',
},
},
{
href: '/customers/alternative-partners',
industry: 'Consulting',
authorRole: 'Principal and Founder, Alternative Partners',
kpis: [
{ value: 'AI-assisted', label: 'Salesforce migration' },
{ value: 'Self-hosted', label: 'Full ownership' },
],
hero: {
readingTime: '7 min',
title: [
{ text: 'From Salesforce to ', fontFamily: 'serif' },
{ text: 'self-hosted Twenty', fontFamily: 'sans' },
],
author: 'Benjamin Reynolds',
authorAvatarSrc: '/images/partner/testimonials/benjamin-reynolds.webp',
clientIcon: 'alternative-partners',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'Alternative Partners replaced Salesforce with self-hosted Twenty, using agentic AI to compress migration work.',
date: '2025',
coverImageSrc:
'https://images.unsplash.com/photo-1702047149248-a6049168d2a8?w=1600&q=80',
},
},
{
href: '/customers/netzero',
industry: 'Agribusiness',
authorRole: 'Co-founder, NetZero',
kpis: [
{ value: '3 product lines', label: 'On a single CRM' },
{ value: 'No-code', label: 'Customizations' },
],
hero: {
readingTime: '8 min',
title: [
{ text: 'A CRM that ', fontFamily: 'serif' },
{ text: 'grows with you', fontFamily: 'sans' },
],
author: 'Olivier Reinaud',
authorAvatarSrc: '/images/partner/testimonials/olivier-reinaud.jpg',
clientIcon: 'netzero',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'NetZero uses Twenty as a modular CRM across product lines and countries, with a roadmap into AI-assisted workflows.',
date: '2025',
coverImageSrc:
'https://images.unsplash.com/photo-1510524474345-1c4bac68d1d0?w=1600&q=80',
},
},
{
href: '/customers/act-education',
industry: 'Education',
authorRole: 'CRM Engineer, AC&T Education Migration',
kpis: [{ value: '90%+', label: 'Lower CRM cost' }],
hero: {
readingTime: '7 min',
title: [
{ text: 'A CRM they ', fontFamily: 'serif' },
{ text: 'actually own', fontFamily: 'sans' },
],
author: 'Joseph Chiang',
authorAvatarSrc: '/images/partner/testimonials/joseph-chiang.jpg',
clientIcon: 'act-education',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'AC&T and Flycoder moved from a dead vendor export to self-hosted Twenty, with over 90% lower CRM cost and full control.',
date: '2025',
coverImageSrc:
'https://images.unsplash.com/photo-1687600154329-150952c73169?w=1600&q=80',
},
},
{
href: '/customers/w3villa',
industry: 'EdTech',
authorRole: 'VP of Engineering, W3villa Technologies',
kpis: [{ value: 'Zero', label: 'Manual work at core' }],
hero: {
readingTime: '8 min',
title: [
{ text: 'When your CRM ', fontFamily: 'serif' },
{ text: 'is the product', fontFamily: 'sans' },
],
author: 'Amrendra Pratap Singh',
authorAvatarSrc: '/images/partner/testimonials/amrendra-singh.webp',
clientIcon: 'w3villa',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'W3villa shipped W3Grads on Twenty for AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.',
date: '2025',
coverImageSrc:
'https://images.unsplash.com/photo-1756830231350-3b501f63c5c1?w=1600&q=80',
},
},
{
href: '/customers/elevate-consulting',
industry: 'Management Consulting',
authorRole: 'Director of Digital and Information, Elevate Consulting',
kpis: [
{ value: '1 click', label: 'Proposal automation' },
{ value: '4 tools', label: 'Connected via API' },
{ value: 'API-first', label: 'Tool integration' },
],
quote: {
text: 'It is just such a nicer experience than dealing with a Salesforce or a HubSpot. My mission has been to get every tool API-accessible, so everything talks to each other.',
author: 'Justin Beadle',
role: 'Director of Digital and Information, Elevate Consulting',
},
hero: {
readingTime: '8 min',
title: [
{ text: 'Twenty as the API backbone ', fontFamily: 'serif' },
{ text: 'of a go-to-market stack', fontFamily: 'sans' },
],
author: 'Justin Beadle',
clientIcon: 'elevate-consulting',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'Elevate Consulting uses Twenty as the API backbone connecting billing, Teams, resourcing, and a custom front end around client and opportunity data.',
date: 'Jun 2025',
coverImageSrc:
'https://images.unsplash.com/photo-1758873269035-aae0e1fd3422?w=1600&q=80',
},
},
];

View file

@ -0,0 +1,14 @@
export {
CASE_STUDY_CATALOG_ENTRIES,
CASE_STUDY_HALFTONE_PALETTE,
getCaseStudyPalette,
} from './case-study-catalog';
export type {
CaseStudyCatalogEntry,
CaseStudyContentBlock,
CaseStudyData,
CaseStudyKpi,
CaseStudyQuote,
CaseStudyTextBlock,
CaseStudyVisualBlock,
} from './types';

View file

@ -16,25 +16,46 @@ export type CaseStudyVisualBlock = {
export type CaseStudyContentBlock = CaseStudyTextBlock | CaseStudyVisualBlock; export type CaseStudyContentBlock = CaseStudyTextBlock | CaseStudyVisualBlock;
export type CaseStudyKpi = {
value: string;
label: string;
};
export type CaseStudyQuote = {
text: string;
author: string;
role: string;
};
export type CaseStudyData = { export type CaseStudyData = {
meta: { title: string; description: string }; meta: { title: string; description: string };
hero: { hero: {
readingTime: string; readingTime: string;
title: HeadingType[]; title: HeadingType[];
author: string; author: string;
authorAvatarSrc?: string;
clientIcon: string; clientIcon: string;
heroImageSrc: string; heroImageSrc: string;
industry?: string;
authorRole?: string;
kpis?: CaseStudyKpi[];
quote?: CaseStudyQuote;
}; };
sections: CaseStudyContentBlock[]; sections: CaseStudyContentBlock[];
tableOfContents: string[]; tableOfContents: string[];
catalogCard: { catalogCard: {
summary: string; summary: string;
date: string; date: string;
coverImageSrc?: string;
}; };
}; };
export type CaseStudyCatalogEntry = { export type CaseStudyCatalogEntry = {
href: string; href: string;
industry: string;
kpis: CaseStudyKpi[];
authorRole: string;
quote?: CaseStudyQuote;
hero: CaseStudyData['hero']; hero: CaseStudyData['hero'];
catalogCard: CaseStudyData['catalogCard']; catalogCard: CaseStudyData['catalogCard'];
}; };

View file

@ -1,5 +1,7 @@
import { MENU_DATA } from '@/app/_constants'; import { MENU_DATA } from '@/app/_constants';
import type { CaseStudyData } from '@/app/case-studies/_constants/types'; import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette } from '@/app/customers/_constants';
import type { CaseStudyData } from '@/app/customers/_constants/types';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components'; import { CaseStudy } from '@/sections/CaseStudy/components';
@ -7,24 +9,29 @@ import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/shared/people/avatars/katherine-adams.jpg'; const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1687600154329-150952c73169?w=1600&q=80';
const CASE_STUDY: CaseStudyData = { const CASE_STUDY: CaseStudyData = {
meta: { meta: {
title: title:
'Burned by vendor lock-in, AC&T built a CRM they actually own Twenty', 'Burned by vendor lock-in, AC&T built a CRM they actually own | Twenty',
description: description:
'How AC&T Education Migration and Flycoder replaced a shuttered vendor CRM with self-hosted Twenty 90%+ lower cost and full ownership.', 'How AC&T Education Migration and Flycoder replaced a shuttered vendor CRM with self-hosted Twenty, with 90%+ lower cost and full ownership.',
}, },
hero: { hero: {
readingTime: '7 min', readingTime: '7 min',
title: [ title: [
{ text: 'A CRM they ', fontFamily: 'serif' }, { text: 'A CRM they ', fontFamily: 'serif' },
{ text: 'actually own', fontFamily: 'sans' }, { text: 'actually own', fontFamily: 'sans', newLine: true },
], ],
author: 'Joseph Chiang', author: 'Joseph Chiang',
authorAvatarSrc: '/images/partner/testimonials/joseph-chiang.jpg',
authorRole: 'CRM Engineer, AC&T Education Migration',
clientIcon: 'act-education', clientIcon: 'act-education',
heroImageSrc: PLACEHOLDER_HERO, heroImageSrc: PLACEHOLDER_HERO,
industry: 'Education',
kpis: [{ value: '90%+', label: 'Lower CRM cost' }],
}, },
sections: [ sections: [
{ {
@ -35,21 +42,21 @@ const CASE_STUDY: CaseStudyData = {
{ text: 'pulled the plug', fontFamily: 'sans' }, { text: 'pulled the plug', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
'AC&T Education Migration (actimmi.com) is an education agency in Australia. They help international students with applications to education providers and visas. They had been on a previous CRM until the vendor shut the system down leaving nothing but a CSV export.', 'AC&T Education Migration (actimmi.com) is an education agency in Australia. They help international students with applications to education providers and visas. They had been on a previous CRM until the vendor shut the system down, leaving nothing but a CSV export.',
'Whatever came next had to be something they could own.', 'Whatever came next had to be something they could own.',
], ],
callout: callout:
'"They did not want to learn someone else\'s system. They wanted to keep working the way they already did and make it smoother." Joseph Chiang, CRM Engineer, AC&T Education Migration', '"They did not want to learn someone else\'s system. They wanted to keep working the way they already did and make it smoother." - Joseph Chiang, CRM Engineer, AC&T Education Migration',
}, },
{ {
type: 'text', type: 'text',
eyebrow: 'Implementation', eyebrow: 'Implementation',
heading: [ heading: [
{ text: 'No more renting ', fontFamily: 'serif' }, { text: "No more renting someone else's ", fontFamily: 'serif' },
{ text: "someone else's structure", fontFamily: 'sans' }, { text: 'structure', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
'They evaluated Salesforce, Zoho, Pipedrive, and SuiteCRM. Each came with the same tradeoffs: too expensive, too rigid, or too generic and none fixed the underlying problem. They were still renting a structure they did not control.', 'They evaluated Salesforce, Zoho, Pipedrive, and SuiteCRM. Each came with the same tradeoffs: too expensive, too rigid, or too generic, and none fixed the underlying problem. They were still renting a structure they did not control.',
'Flycoder, a full-stack development partner, helped them set up Twenty as a self-hosted instance shaped around how AC&T actually operates. The data model centers on students, not a generic contact-and-deal pipeline. Statuses update automatically: a workflow runs nightly to keep enrollment records current. Automated email reminders cover important dates. Adding a new record takes under a minute.', 'Flycoder, a full-stack development partner, helped them set up Twenty as a self-hosted instance shaped around how AC&T actually operates. The data model centers on students, not a generic contact-and-deal pipeline. Statuses update automatically: a workflow runs nightly to keep enrollment records current. Automated email reminders cover important dates. Adding a new record takes under a minute.',
'The result is a system that fits how AC&T already worked, instead of the other way around.', 'The result is a system that fits how AC&T already worked, instead of the other way around.',
], ],
@ -70,12 +77,12 @@ const CASE_STUDY: CaseStudyData = {
type: 'text', type: 'text',
eyebrow: 'The result', eyebrow: 'The result',
heading: [ heading: [
{ text: 'Costs down ', fontFamily: 'serif' }, { text: 'Costs down more than ', fontFamily: 'serif' },
{ text: 'more than 90%', fontFamily: 'sans' }, { text: '90%', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
'CRM costs dropped by more than 90%. Manual overhead tied to the old system is gone. For the first time, AC&T has a CRM they will not lose again.', 'CRM costs dropped by more than 90%. Manual overhead tied to the old system is gone. For the first time, AC&T has a CRM they will not lose again.',
'They did not just replace a tool — they took back ownership of how their business runs.', 'They did not just replace a tool. They took back ownership of how their business runs.',
], ],
}, },
], ],
@ -87,7 +94,7 @@ const CASE_STUDY: CaseStudyData = {
], ],
catalogCard: { catalogCard: {
summary: summary:
'AC&T and Flycoder moved from a dead vendor export to self-hosted Twenty over 90% lower CRM cost and full control.', 'AC&T and Flycoder moved from a dead vendor export to self-hosted Twenty, with over 90% lower CRM cost and full control.',
date: '2025', date: '2025',
}, },
}; };
@ -100,6 +107,7 @@ export const metadata: Metadata = {
export default async function ActEducationCaseStudyPage() { export default async function ActEducationCaseStudyPage() {
const stats = await fetchCommunityStats(); const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats); const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
const palette = getCaseStudyPalette('/customers/act-education');
let storySectionIndex = 0; let storySectionIndex = 0;
const sectionBlocks = CASE_STUDY.sections.map((block, index) => { const sectionBlocks = CASE_STUDY.sections.map((block, index) => {
@ -107,10 +115,21 @@ export default async function ActEducationCaseStudyPage() {
const sectionId = `case-study-section-${storySectionIndex}`; const sectionId = `case-study-section-${storySectionIndex}`;
storySectionIndex += 1; storySectionIndex += 1;
return ( return (
<CaseStudy.TextBlock key={index} block={block} sectionId={sectionId} /> <CaseStudy.TextBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
sectionId={sectionId}
/>
); );
} }
return <CaseStudy.VisualBlock key={index} block={block} />; return (
<CaseStudy.VisualBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
/>
);
}); });
return ( return (
@ -127,11 +146,21 @@ export default async function ActEducationCaseStudyPage() {
<Menu.Cta scheme="secondary" /> <Menu.Cta scheme="secondary" />
</Menu.Root> </Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} /> <CaseStudy.Hero
dashColor={palette.dashColor}
hero={CASE_STUDY.hero}
hoverDashColor={palette.hoverDashColor}
/>
{sectionBlocks} <CaseStudy.Highlights
industry={CASE_STUDY.hero.industry}
kpis={CASE_STUDY.hero.kpis}
/>
<CaseStudy.Body>{sectionBlocks}</CaseStudy.Body>
<CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} /> <CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} />
<CustomersCaseStudySignoff />
</> </>
); );
} }

View file

@ -1,5 +1,7 @@
import { MENU_DATA } from '@/app/_constants'; import { MENU_DATA } from '@/app/_constants';
import type { CaseStudyData } from '@/app/case-studies/_constants/types'; import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette } from '@/app/customers/_constants';
import type { CaseStudyData } from '@/app/customers/_constants/types';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components'; import { CaseStudy } from '@/sections/CaseStudy/components';
@ -7,24 +9,32 @@ import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/shared/people/avatars/katherine-adams.jpg'; const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1702047149248-a6049168d2a8?w=1600&q=80';
const CASE_STUDY: CaseStudyData = { const CASE_STUDY: CaseStudyData = {
meta: { meta: {
title: title:
'From Salesforce to self-hosted Twenty, powered by AI Alternative Partners', 'From Salesforce to self-hosted Twenty, powered by AI | Alternative Partners',
description: description:
'How Alternative Partners migrated from Salesforce to self-hosted Twenty using agentic AI in the implementation loop fast migration, durable ownership.', 'How Alternative Partners migrated from Salesforce to self-hosted Twenty using agentic AI in the implementation loop: fast migration, durable ownership.',
}, },
hero: { hero: {
readingTime: '7 min', readingTime: '7 min',
title: [ title: [
{ text: 'From Salesforce to ', fontFamily: 'serif' }, { text: 'From Salesforce to ', fontFamily: 'serif' },
{ text: 'self-hosted Twenty', fontFamily: 'sans' }, { text: 'self-hosted Twenty', fontFamily: 'sans', newLine: true },
], ],
author: 'Benjamin Reynolds', author: 'Benjamin Reynolds',
authorAvatarSrc: '/images/partner/testimonials/benjamin-reynolds.webp',
authorRole: 'Principal and Founder, Alternative Partners',
clientIcon: 'alternative-partners', clientIcon: 'alternative-partners',
heroImageSrc: PLACEHOLDER_HERO, heroImageSrc: PLACEHOLDER_HERO,
industry: 'Consulting',
kpis: [
{ value: 'AI-assisted', label: 'Salesforce migration' },
{ value: 'Self-hosted', label: 'Full ownership' },
],
}, },
sections: [ sections: [
{ {
@ -35,8 +45,8 @@ const CASE_STUDY: CaseStudyData = {
{ text: 'migration workflow', fontFamily: 'sans' }, { text: 'migration workflow', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
"Alternative Partners is a consulting firm that moved from Salesforce to a self-hosted Twenty instance. Benjamin Reynolds led the migration — he had already become a Twenty expert implementing Twenty for one of Twenty's first cloud customers.", "Alternative Partners is a consulting firm that moved from Salesforce to a self-hosted Twenty instance. Benjamin Reynolds led the migration. He had already become a Twenty expert implementing Twenty for one of Twenty's first cloud customers.",
'His approach was unconventional. Instead of mapping fields manually, scripting transforms, and validating data step by step, he handed the job to agentic AI tools with a brief: where the data lives, the GitHub repo for the target platform, the Railway deployment — start, and only return if something breaks beyond a 70% confidence fix.', 'His approach was unconventional. Instead of mapping fields manually, scripting transforms, and validating data step by step, he handed the job to agentic AI tools with a brief: where the data lives, the GitHub repo for the target platform, and the Railway deployment. Start, and only return if something breaks beyond a 70% confidence fix.',
'It worked. This is AI-assisted iteration in practice: not AI as a product feature, but as part of implementation work, compressing what would typically be weeks into something one person can oversee without being the bottleneck.', 'It worked. This is AI-assisted iteration in practice: not AI as a product feature, but as part of implementation work, compressing what would typically be weeks into something one person can oversee without being the bottleneck.',
], ],
}, },
@ -71,6 +81,7 @@ export const metadata: Metadata = {
export default async function AlternativePartnersCaseStudyPage() { export default async function AlternativePartnersCaseStudyPage() {
const stats = await fetchCommunityStats(); const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats); const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
const palette = getCaseStudyPalette('/customers/alternative-partners');
let storySectionIndex = 0; let storySectionIndex = 0;
const sectionBlocks = CASE_STUDY.sections.map((block, index) => { const sectionBlocks = CASE_STUDY.sections.map((block, index) => {
@ -78,10 +89,21 @@ export default async function AlternativePartnersCaseStudyPage() {
const sectionId = `case-study-section-${storySectionIndex}`; const sectionId = `case-study-section-${storySectionIndex}`;
storySectionIndex += 1; storySectionIndex += 1;
return ( return (
<CaseStudy.TextBlock key={index} block={block} sectionId={sectionId} /> <CaseStudy.TextBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
sectionId={sectionId}
/>
); );
} }
return <CaseStudy.VisualBlock key={index} block={block} />; return (
<CaseStudy.VisualBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
/>
);
}); });
return ( return (
@ -98,11 +120,21 @@ export default async function AlternativePartnersCaseStudyPage() {
<Menu.Cta scheme="secondary" /> <Menu.Cta scheme="secondary" />
</Menu.Root> </Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} /> <CaseStudy.Hero
dashColor={palette.dashColor}
hero={CASE_STUDY.hero}
hoverDashColor={palette.hoverDashColor}
/>
{sectionBlocks} <CaseStudy.Highlights
industry={CASE_STUDY.hero.industry}
kpis={CASE_STUDY.hero.kpis}
/>
<CaseStudy.Body>{sectionBlocks}</CaseStudy.Body>
<CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} /> <CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} />
<CustomersCaseStudySignoff />
</> </>
); );
} }

View file

@ -1,5 +1,7 @@
import { MENU_DATA } from '@/app/_constants'; import { MENU_DATA } from '@/app/_constants';
import type { CaseStudyData } from '@/app/case-studies/_constants/types'; import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette } from '@/app/customers/_constants';
import type { CaseStudyData } from '@/app/customers/_constants/types';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components'; import { CaseStudy } from '@/sections/CaseStudy/components';
@ -7,55 +9,70 @@ import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/shared/people/avatars/katherine-adams.jpg'; const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1758873269035-aae0e1fd3422?w=1600&q=80';
const CASE_STUDY: CaseStudyData = { const CASE_STUDY: CaseStudyData = {
meta: { meta: {
title: title:
'Twenty as the API backbone of a go-to-market stack Elevate Consulting', 'Twenty as the API backbone of a go-to-market stack | Elevate Consulting',
description: description:
'How Elevate Consulting moved off documents and spreadsheets to Twenty as the API-connected CRM at the center of their stack.', 'How Elevate Consulting moved off documents and spreadsheets to Twenty as the API-connected CRM at the center of their stack.',
}, },
hero: { hero: {
readingTime: '8 min', readingTime: '8 min',
title: [ title: [
{ text: 'Twenty as the API backbone ', fontFamily: 'serif' }, { text: 'Twenty as the ', fontFamily: 'serif' },
{ text: 'of a go-to-market stack', fontFamily: 'sans' }, { text: 'API backbone', fontFamily: 'sans', newLine: true },
{ text: ' of a go-to-market stack', fontFamily: 'serif' },
], ],
author: 'Justin Beadle', author: 'Justin Beadle',
authorRole: 'Director of Digital and Information, Elevate Consulting',
clientIcon: 'elevate-consulting', clientIcon: 'elevate-consulting',
heroImageSrc: PLACEHOLDER_HERO, heroImageSrc: PLACEHOLDER_HERO,
industry: 'Management Consulting',
kpis: [
{ value: '1 click', label: 'Proposal automation' },
{ value: '4 tools', label: 'Connected via API' },
{ value: 'API-first', label: 'Tool integration' },
],
quote: {
text: 'It is just such a nicer experience than dealing with a Salesforce or a HubSpot. My mission has been to get every tool API-accessible, so everything talks to each other.',
author: 'Justin Beadle',
role: 'Director of Digital and Information, Elevate Consulting',
},
}, },
sections: [ sections: [
{ {
type: 'text', type: 'text',
eyebrow: 'The situation', eyebrow: 'The situation',
heading: [ heading: [
{ text: 'From documents ', fontFamily: 'serif' }, { text: 'From documents to ', fontFamily: 'serif' },
{ text: 'to open APIs', fontFamily: 'sans' }, { text: 'open APIs', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
'Elevate Consulting is a management consultancy based in Canada. When Justin Beadle, Director of Digital and Information, joined, the company ran entirely on Word documents, Excel spreadsheets, sticky notes, emails, and reliance on people. There was no CRM, no API-accessible tools — only a patchwork trying to stand in for a single source of truth.', 'Elevate Consulting is a management consultancy based in Canada. When Justin Beadle, Director of Digital and Information, joined, the company ran entirely on Word documents, Excel spreadsheets, sticky notes, emails, and reliance on people. There was no CRM, no API-accessible tools, only a patchwork trying to stand in for a single source of truth.',
'The CEO had resisted bringing in a CRM for years. The business development team had no experience using one, and the licensing costs of well-known CRMs like Salesforce or HubSpot were hard to justify without any guarantee of adoption: CRMs are only as good as the maintenance of the data inside them.', 'The CEO had resisted bringing in a CRM for years. The business development team had no experience using one, and the licensing costs of well-known CRMs like Salesforce or HubSpot were hard to justify without any guarantee of adoption: CRMs are only as good as the maintenance of the data inside them.',
'In June 2025, Justin learned Twenty v1 had shipped. Within two or three days, the CEO asked him to look into setting up a CRM. The shift came from the potential of what could be built on top of fully open APIs. The timing was perfect.', 'In June 2025, Justin learned Twenty v1 had shipped. Within two or three days, the CEO asked him to look into setting up a CRM. The shift came from the potential of what could be built on top of fully open APIs. The timing was perfect.',
], ],
callout: callout:
'"It is just such a nicer experience than dealing with a Salesforce or a HubSpot. My mission has been to get every tool API-accessible, so everything talks to each other. Twenty made that possible in a way older CRM platforms simply do not." Justin Beadle, Director of Digital and Information, Elevate Consulting', '"It is just such a nicer experience than dealing with a Salesforce or a HubSpot. My mission has been to get every tool API-accessible, so everything talks to each other. Twenty made that possible in a way older CRM platforms simply do not." - Justin Beadle, Director of Digital and Information, Elevate Consulting',
}, },
{ {
type: 'text', type: 'text',
eyebrow: 'Integration', eyebrow: 'Integration',
heading: [ heading: [
{ text: 'One API ', fontFamily: 'serif' }, { text: 'One ', fontFamily: 'serif' },
{ text: 'to rule them all', fontFamily: 'sans' }, { text: 'API', fontFamily: 'sans' },
{ text: ' to rule them all', fontFamily: 'serif' },
], ],
paragraphs: [ paragraphs: [
"Justin's broader mission at Elevate has been to move the company off static documents and onto tools with API access. By the end of 2025, that was in place: time billing, resource planning, Microsoft Teams, and project management were all accessible via API, with Twenty at the center holding client and opportunity data. Team members could use that information strategically instead of re-keying it.", "Justin's broader mission at Elevate has been to move the company off static documents and onto tools with API access. By the end of 2025, that was in place: time billing, resource planning, Microsoft Teams, and project management were all accessible via API, with Twenty at the center holding client and opportunity data. Team members could use that information strategically instead of re-keying it.",
'That opened the door to something more powerful. Justin built a custom front end that pulls live data from those systems into a single view, tailored to each role. When a proposal is won, what used to require four separate people manually setting up instances across four different tools now happens in a single click, drawing on data collected in Twenty across the full opportunity lifecycle another shift toward higher-value work for clients.', 'That opened the door to something more powerful. Justin built a custom front end that pulls live data from those systems into a single view, tailored to each role. When a proposal is won, what used to require four separate people manually setting up instances across four different tools now happens in a single click, drawing on data collected in Twenty across the full opportunity lifecycle. It is another shift toward higher-value work for clients.',
'Twenty is not only where CRM data lives. It is the API backbone that makes the rest of the stack possible.', 'Twenty is not only where CRM data lives. It is the API backbone that makes the rest of the stack possible.',
], ],
callout: callout:
'"Because Twenty\'s API is fully open, I could connect it to every other tool in our stack. When a proposal is won, what used to take four people manually setting things up across four different tools now happens in a single click. That is the kind of time saving that only becomes possible when everything is connected." Justin Beadle, Director of Digital and Information, Elevate Consulting', '"Because Twenty\'s API is fully open, I could connect it to every other tool in our stack. When a proposal is won, what used to take four people manually setting things up across four different tools now happens in a single click. That is the kind of time saving that only becomes possible when everything is connected." - Justin Beadle, Director of Digital and Information, Elevate Consulting',
}, },
{ {
type: 'text', type: 'text',
@ -66,7 +83,7 @@ const CASE_STUDY: CaseStudyData = {
], ],
paragraphs: [ paragraphs: [
'The business development team finally had the CRM they had been asking for. Adoption came naturally: their data was already there when they logged in.', 'The business development team finally had the CRM they had been asking for. Adoption came naturally: their data was already there when they logged in.',
'Justin built workflows for notifications across the team, alerting the right people in Teams when a prospect becomes a lead or when project milestones are reached. Forms in Twenty let the business development team log activity without leaving the tool. The impact is real for the organization — the tool has been adaptable from opportunity-level work at a client to executive-level decisions.', 'Justin built workflows for notifications across the team, alerting the right people in Teams when a prospect becomes a lead or when project milestones are reached. Forms in Twenty let the business development team log activity without leaving the tool. The impact is real for the organization. The tool has been adaptable from opportunity-level work at a client to executive-level decisions.',
'The flexibility to wire this together, without outside help and without fighting the platform, is what made it possible for a single person to stand up and maintain a connected stack across an entire consultancy.', 'The flexibility to wire this together, without outside help and without fighting the platform, is what made it possible for a single person to stand up and maintain a connected stack across an entire consultancy.',
], ],
}, },
@ -79,7 +96,7 @@ const CASE_STUDY: CaseStudyData = {
], ],
paragraphs: [ paragraphs: [
"Elevate's CEO was so impressed with Twenty he started recommending it to clients before the internal setup was even complete. The team is exploring bringing Twenty to client projects as part of their consulting practice, including as the backend for custom-built products tailored to specific operational needs.", "Elevate's CEO was so impressed with Twenty he started recommending it to clients before the internal setup was even complete. The team is exploring bringing Twenty to client projects as part of their consulting practice, including as the backend for custom-built products tailored to specific operational needs.",
'For a firm that once ran on sticky notes, this is more than an upgrade — it is a complete transformation.', 'For a firm that once ran on sticky notes, this is more than an upgrade. It is a complete transformation.',
], ],
}, },
], ],
@ -104,6 +121,7 @@ export const metadata: Metadata = {
export default async function ElevateConsultingCaseStudyPage() { export default async function ElevateConsultingCaseStudyPage() {
const stats = await fetchCommunityStats(); const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats); const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
const palette = getCaseStudyPalette('/customers/elevate-consulting');
let storySectionIndex = 0; let storySectionIndex = 0;
const sectionBlocks = CASE_STUDY.sections.map((block, index) => { const sectionBlocks = CASE_STUDY.sections.map((block, index) => {
@ -111,10 +129,21 @@ export default async function ElevateConsultingCaseStudyPage() {
const sectionId = `case-study-section-${storySectionIndex}`; const sectionId = `case-study-section-${storySectionIndex}`;
storySectionIndex += 1; storySectionIndex += 1;
return ( return (
<CaseStudy.TextBlock key={index} block={block} sectionId={sectionId} /> <CaseStudy.TextBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
sectionId={sectionId}
/>
); );
} }
return <CaseStudy.VisualBlock key={index} block={block} />; return (
<CaseStudy.VisualBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
/>
);
}); });
return ( return (
@ -131,11 +160,21 @@ export default async function ElevateConsultingCaseStudyPage() {
<Menu.Cta scheme="secondary" /> <Menu.Cta scheme="secondary" />
</Menu.Root> </Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} /> <CaseStudy.Hero
dashColor={palette.dashColor}
hero={CASE_STUDY.hero}
hoverDashColor={palette.hoverDashColor}
/>
{sectionBlocks} <CaseStudy.Highlights
industry={CASE_STUDY.hero.industry}
kpis={CASE_STUDY.hero.kpis}
/>
<CaseStudy.Body>{sectionBlocks}</CaseStudy.Body>
<CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} /> <CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} />
<CustomersCaseStudySignoff />
</> </>
); );
} }

View file

@ -1,5 +1,7 @@
import { MENU_DATA } from '@/app/_constants'; import { MENU_DATA } from '@/app/_constants';
import type { CaseStudyData } from '@/app/case-studies/_constants/types'; import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette } from '@/app/customers/_constants';
import type { CaseStudyData } from '@/app/customers/_constants/types';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components'; import { CaseStudy } from '@/sections/CaseStudy/components';
@ -7,11 +9,12 @@ import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/shared/people/avatars/katherine-adams.jpg'; const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1510524474345-1c4bac68d1d0?w=1600&q=80';
const CASE_STUDY: CaseStudyData = { const CASE_STUDY: CaseStudyData = {
meta: { meta: {
title: 'A CRM that grows with you NetZero & Twenty', title: 'A CRM that grows with you | NetZero & Twenty',
description: description:
'How NetZero uses Twenty across carbon credits, agricultural products, and franchised industrial systems with a modular CRM and a roadmap toward AI-assisted workflows.', 'How NetZero uses Twenty across carbon credits, agricultural products, and franchised industrial systems with a modular CRM and a roadmap toward AI-assisted workflows.',
}, },
@ -19,11 +22,19 @@ const CASE_STUDY: CaseStudyData = {
readingTime: '8 min', readingTime: '8 min',
title: [ title: [
{ text: 'A CRM that ', fontFamily: 'serif' }, { text: 'A CRM that ', fontFamily: 'serif' },
{ text: 'grows with you', fontFamily: 'sans' }, { text: 'grows', fontFamily: 'sans', newLine: true },
{ text: ' with you', fontFamily: 'serif' },
], ],
author: 'Olivier Reinaud', author: 'Olivier Reinaud',
authorAvatarSrc: '/images/partner/testimonials/olivier-reinaud.jpg',
authorRole: 'Co-founder, NetZero',
clientIcon: 'netzero', clientIcon: 'netzero',
heroImageSrc: PLACEHOLDER_HERO, heroImageSrc: PLACEHOLDER_HERO,
industry: 'Agribusiness',
kpis: [
{ value: '3 product lines', label: 'On a single CRM' },
{ value: 'No-code', label: 'Customizations' },
],
}, },
sections: [ sections: [
{ {
@ -34,44 +45,44 @@ const CASE_STUDY: CaseStudyData = {
{ text: 'foundation', fontFamily: 'sans' }, { text: 'foundation', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
'NetZero works with the agro-industry, serving clients from multinationals to smallholder farmers. They sell carbon credits, agricultural products, and franchised industrial systems — three different product lines across multiple countries and company sizes. When Olivier Reinaud, co-founder of NetZero, started looking at CRMs in late 2024, he was not chasing the most feature-rich platform. He wanted the right foundation.', 'NetZero works with the agro-industry, serving clients from multinationals to smallholder farmers. They sell carbon credits, agricultural products, and franchised industrial systems across three different product lines, multiple countries, and multiple company sizes. When Olivier Reinaud, co-founder of NetZero, started looking at CRMs in late 2024, he was not chasing the most feature-rich platform. He wanted the right foundation.',
], ],
callout: callout:
'"Twenty delivers on what CRMs should have always been: fairly priced software with a fully modular and customizable model, a clean and modern UI, granular permissions, automations, enterprise features. A compelling solution with high potential to rightfully disrupt the CRM market." Olivier Reinaud, co-founder of NetZero', '"Twenty delivers on what CRMs should have always been: fairly priced software with a fully modular and customizable model, a clean and modern UI, granular permissions, automations, enterprise features. A compelling solution with high potential to rightfully disrupt the CRM market." - Olivier Reinaud, co-founder of NetZero',
}, },
{ {
type: 'text', type: 'text',
eyebrow: 'Flexibility', eyebrow: 'Flexibility',
heading: [ heading: [
{ text: 'A business that ', fontFamily: 'serif' }, { text: 'A business that does not fit a ', fontFamily: 'serif' },
{ text: 'does not fit a template', fontFamily: 'sans' }, { text: 'template', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
'What convinced Olivier was the flexibility of the platform and where it was headed. Even when initial needs were basic record-keeping, he still needed a custom data model with granular permissions to manage the wide range of NetZero activities. He also needed a system that could adapt quickly to a fast-iteration company.', 'What convinced Olivier was the flexibility of the platform and where it was headed. Even when initial needs were basic record-keeping, he still needed a custom data model with granular permissions to manage the wide range of NetZero activities. He also needed a system that could adapt quickly to a fast-iteration company.',
'With Twenty, when a new need appears, he can address it himself no developer required, no support ticket.', 'With Twenty, when a new need appears, he can address it himself: no developer required, no support ticket.',
], ],
callout: callout:
'"The flexibility is really what made the difference. Our needs evolve very fast. I discover a new need and in two clicks I can address it. That is a real advantage when you are moving quickly." Olivier Reinaud, co-founder of NetZero', '"The flexibility is really what made the difference. Our needs evolve very fast. I discover a new need and in two clicks I can address it. That is a real advantage when you are moving quickly." - Olivier Reinaud, co-founder of NetZero',
}, },
{ {
type: 'text', type: 'text',
eyebrow: 'Roadmap', eyebrow: 'Roadmap',
heading: [ heading: [
{ text: 'From simple ', fontFamily: 'serif' }, { text: 'From simple to ', fontFamily: 'serif' },
{ text: 'to advanced', fontFamily: 'sans' }, { text: 'advanced', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
"Olivier recognizes that NetZero's current use of Twenty is still relatively simple: workflows and integrations are not yet as deep as he eventually wants, because he prioritized getting foundations right first.", "Olivier recognizes that NetZero's current use of Twenty is still relatively simple: workflows and integrations are not yet as deep as he eventually wants, because he prioritized getting foundations right first.",
'What is planned is significant. NetZero has a data lake, online forms, and multiple internal systems that he wants to connect to Twenty. The pipes are there; the next step is automations that tie them together.', 'What is planned is significant. NetZero has a data lake, online forms, and multiple internal systems that he wants to connect to Twenty. The pipes are there; the next step is automations that tie them together.',
'What is coming in April 2026 is what he has been waiting for: AI-assisted workflow creation describing what he needs and iterating from there instead of building complex logic from scratch. For a founder who runs the CRM himself, that changes what is realistically possible.', 'What is coming in April 2026 is what he has been waiting for: AI-assisted workflow creation, describing what he needs and iterating from there instead of building complex logic from scratch. For a founder who runs the CRM himself, that changes what is realistically possible.',
], ],
}, },
{ {
type: 'text', type: 'text',
eyebrow: 'Results', eyebrow: 'Results',
heading: [ heading: [
{ text: 'The bet ', fontFamily: 'serif' }, { text: 'The bet is ', fontFamily: 'serif' },
{ text: 'is paying off', fontFamily: 'sans' }, { text: 'paying off', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
'While NetZero still runs a second CRM in parallel for WhatsApp-heavy operations with farmers in Brazil, they expect to migrate all of it to Twenty as features and the ecosystem grow. Already, their structured, multinational pipeline is powered by Twenty.', 'While NetZero still runs a second CRM in parallel for WhatsApp-heavy operations with farmers in Brazil, they expect to migrate all of it to Twenty as features and the ecosystem grow. Already, their structured, multinational pipeline is powered by Twenty.',
@ -100,6 +111,7 @@ export const metadata: Metadata = {
export default async function NetZeroCaseStudyPage() { export default async function NetZeroCaseStudyPage() {
const stats = await fetchCommunityStats(); const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats); const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
const palette = getCaseStudyPalette('/customers/netzero');
let storySectionIndex = 0; let storySectionIndex = 0;
const sectionBlocks = CASE_STUDY.sections.map((block, index) => { const sectionBlocks = CASE_STUDY.sections.map((block, index) => {
@ -107,10 +119,21 @@ export default async function NetZeroCaseStudyPage() {
const sectionId = `case-study-section-${storySectionIndex}`; const sectionId = `case-study-section-${storySectionIndex}`;
storySectionIndex += 1; storySectionIndex += 1;
return ( return (
<CaseStudy.TextBlock key={index} block={block} sectionId={sectionId} /> <CaseStudy.TextBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
sectionId={sectionId}
/>
); );
} }
return <CaseStudy.VisualBlock key={index} block={block} />; return (
<CaseStudy.VisualBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
/>
);
}); });
return ( return (
@ -127,11 +150,21 @@ export default async function NetZeroCaseStudyPage() {
<Menu.Cta scheme="secondary" /> <Menu.Cta scheme="secondary" />
</Menu.Root> </Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} /> <CaseStudy.Hero
dashColor={palette.dashColor}
hero={CASE_STUDY.hero}
hoverDashColor={palette.hoverDashColor}
/>
{sectionBlocks} <CaseStudy.Highlights
industry={CASE_STUDY.hero.industry}
kpis={CASE_STUDY.hero.kpis}
/>
<CaseStudy.Body>{sectionBlocks}</CaseStudy.Body>
<CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} /> <CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} />
<CustomersCaseStudySignoff />
</> </>
); );
} }

View file

@ -0,0 +1,168 @@
import { FAQ_DATA, MENU_DATA, TRUSTED_BY_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import { CASE_STUDY_CATALOG_ENTRIES } from '@/app/customers/_constants';
import { Eyebrow, LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudyCatalog } from '@/sections/CaseStudyCatalog/components';
import { Faq } from '@/sections/Faq/components';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { Signoff } from '@/sections/Signoff/components';
import { TrustedBy } from '@/sections/TrustedBy/components';
import { theme } from '@/theme';
import { css } from '@linaria/core';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Customers | Twenty',
description:
'Meet the teams running their business on Twenty. Real customer stories on how they shaped the CRM to fit their workflow.',
alternates: { canonical: '/customers' },
openGraph: {
title: 'Customers | Twenty',
description:
'Meet the teams running their business on Twenty. Real customer stories on how they shaped the CRM to fit their workflow.',
url: '/customers',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Customers | Twenty',
description:
'Meet the teams running their business on Twenty. Real customer stories on how they shaped the CRM to fit their workflow.',
},
};
const HERO_HEADING = [
{ text: 'See how teams ', fontFamily: 'serif' as const },
{ text: 'build ', fontFamily: 'serif' as const, newLine: true },
{ text: 'on Twenty', fontFamily: 'sans' as const },
];
const HERO_BODY = {
text: 'Real stories from real teams about how they shaped Twenty to fit their workflow and accelerated their growth.',
};
const SIGNOFF_HEADING = [
{ text: 'Ready to build\n', fontFamily: 'serif' as const },
{ text: 'your own story?', fontFamily: 'sans' as const },
];
const SIGNOFF_BODY = {
text: 'Join the teams that chose to own their CRM.\nStart building with Twenty today.',
};
const CUSTOMERS_TOP_BACKGROUND_COLOR = '#F4F4F4';
const pageRevealClassName = css`
@keyframes customersPageReveal {
from {
opacity: 0;
transform: translate3d(0, 20px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
background-color: ${CUSTOMERS_TOP_BACKGROUND_COLOR};
& > * {
animation: customersPageReveal 720ms cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: 80ms;
}
@media (prefers-reduced-motion: reduce) {
& > * {
animation: none;
}
}
`;
export default async function CaseStudiesCatalogPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<>
<Menu.Root
backgroundColor={CUSTOMERS_TOP_BACKGROUND_COLOR}
scheme="primary"
navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks}
>
<Menu.Logo scheme="primary" />
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
<Menu.Cta scheme="primary" />
</Menu.Root>
<div className={pageRevealClassName}>
<Hero.Root backgroundColor={CUSTOMERS_TOP_BACKGROUND_COLOR}>
<Hero.Heading page={Pages.CaseStudies} segments={HERO_HEADING} />
<Hero.Body body={HERO_BODY} page={Pages.CaseStudies} />
</Hero.Root>
<TrustedBy.Root
cardBackgroundColor={CUSTOMERS_TOP_BACKGROUND_COLOR}
compactBottom
>
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
<TrustedBy.Logos logos={TRUSTED_BY_DATA.logos} />
<TrustedBy.ClientCount
label={TRUSTED_BY_DATA.clientCountLabel.text}
/>
</TrustedBy.Root>
</div>
<CaseStudyCatalog.Grid compactTop entries={CASE_STUDY_CATALOG_ENTRIES} />
<Signoff.Root
backgroundColor={theme.colors.primary.background[100]}
color={theme.colors.primary.text[100]}
page={Pages.Partners}
>
<Signoff.Heading page={Pages.Partners} segments={SIGNOFF_HEADING} />
<Signoff.Body body={SIGNOFF_BODY} page={Pages.Partners} />
<Signoff.Cta>
<LinkButton
color="secondary"
href="https://app.twenty.com/welcome"
label="Get started"
type="anchor"
variant="contained"
/>
<TalkToUsButton
color="secondary"
label="Talk to us"
variant="outlined"
/>
</Signoff.Cta>
</Signoff.Root>
<Faq.Root illustration={FAQ_DATA.illustration}>
<Faq.Intro>
<Eyebrow colorScheme="secondary" heading={FAQ_DATA.eyebrow.heading} />
<Faq.Heading segments={FAQ_DATA.heading} />
<Faq.Cta>
<LinkButton
color="primary"
href="https://app.twenty.com/welcome"
label="Get started"
type="anchor"
variant="contained"
/>
<TalkToUsButton
color="primary"
label="Talk to us"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
</>
);
}

View file

@ -1,5 +1,7 @@
import { MENU_DATA } from '@/app/_constants'; import { MENU_DATA } from '@/app/_constants';
import type { CaseStudyData } from '@/app/case-studies/_constants/types'; import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette } from '@/app/customers/_constants';
import type { CaseStudyData } from '@/app/customers/_constants/types';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components'; import { CaseStudy } from '@/sections/CaseStudy/components';
@ -7,24 +9,29 @@ import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/shared/people/avatars/katherine-adams.jpg'; const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1756830231350-3b501f63c5c1?w=1600&q=80';
const CASE_STUDY: CaseStudyData = { const CASE_STUDY: CaseStudyData = {
meta: { meta: {
title: title:
'When your CRM is the product: W3Grads on Twenty W3villa Technologies', 'When your CRM is the product: W3Grads on Twenty | W3villa Technologies',
description: description:
'How W3villa Technologies shipped W3Grads, an AI mock interview platform for institutions, on Twenty as the operational backbone.', 'How W3villa Technologies shipped W3Grads, an AI mock interview platform for institutions, on Twenty as the operational backbone.',
}, },
hero: { hero: {
readingTime: '8 min', readingTime: '8 min',
title: [ title: [
{ text: 'When your CRM ', fontFamily: 'serif' }, { text: 'When your CRM is ', fontFamily: 'serif' },
{ text: 'is the product', fontFamily: 'sans' }, { text: 'the product', fontFamily: 'sans', newLine: true },
], ],
author: 'Amrendra Pratap Singh', author: 'Amrendra Pratap Singh',
authorAvatarSrc: '/images/partner/testimonials/amrendra-singh.webp',
authorRole: 'VP of Engineering, W3villa Technologies',
clientIcon: 'w3villa', clientIcon: 'w3villa',
heroImageSrc: PLACEHOLDER_HERO, heroImageSrc: PLACEHOLDER_HERO,
industry: 'EdTech',
kpis: [{ value: 'Zero', label: 'Manual work at core' }],
}, },
sections: [ sections: [
{ {
@ -36,21 +43,21 @@ const CASE_STUDY: CaseStudyData = {
], ],
paragraphs: [ paragraphs: [
'Running mock interview programs for hundreds of students sounds straightforward. In practice, universities and training institutes hit the same wall: registrations entered by hand, interview links sent one by one, faculty reviewing every session without scoring or classification. At real scale, it breaks.', 'Running mock interview programs for hundreds of students sounds straightforward. In practice, universities and training institutes hit the same wall: registrations entered by hand, interview links sent one by one, faculty reviewing every session without scoring or classification. At real scale, it breaks.',
'W3villa Technologies set out to solve it properly not with a workaround, but with a product.', 'W3villa Technologies set out to solve it properly, not with a workaround, but with a product.',
], ],
callout: callout:
'"We did not want to patch over the problem. We wanted to build something institutions could rely on at scale, and that meant starting from a foundation solid enough to support the full complexity of what we had in mind." Amrendra Pratap Singh, VP of Engineering, W3villa Technologies', '"We did not want to patch over the problem. We wanted to build something institutions could rely on at scale, and that meant starting from a foundation solid enough to support the full complexity of what we had in mind." - Amrendra Pratap Singh, VP of Engineering, W3villa Technologies',
}, },
{ {
type: 'text', type: 'text',
eyebrow: 'Architecture', eyebrow: 'Architecture',
heading: [ heading: [
{ text: 'Focus on the use case, ', fontFamily: 'serif' }, { text: 'Focus on the use case, not the ', fontFamily: 'serif' },
{ text: 'not the plumbing', fontFamily: 'sans' }, { text: 'plumbing', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
'W3villa built W3Grads (w3grads.com), an AI-powered mock interview platform for universities and training institutes, using Twenty as its operational backbone.', 'W3villa built W3Grads (w3grads.com), an AI-powered mock interview platform for universities and training institutes, using Twenty as its operational backbone.',
'The key decision was not to build everything from scratch. Twenty covers the data model, permissions, authentication, and workflow engine — the parts that would have taken months to rebuild — so the team could focus on product-specific logic.', 'The key decision was not to build everything from scratch. Twenty covers the data model, permissions, authentication, and workflow engine, the parts that would have taken months to rebuild, so the team could focus on product-specific logic.',
'When a student registers via QR at a campus event, the system assigns a plan, generates an interview session, and sends a link. The AI conducts the interview, scores the candidate, and classifies the result. Faculty see where each student stands without manually reviewing every session. Building and iterating on these workflows was faster with AI in the loop.', 'When a student registers via QR at a campus event, the system assigns a plan, generates an interview session, and sends a link. The AI conducts the interview, scores the candidate, and classifies the result. Faculty see where each student stands without manually reviewing every session. Building and iterating on these workflows was faster with AI in the loop.',
], ],
}, },
@ -58,21 +65,21 @@ const CASE_STUDY: CaseStudyData = {
type: 'text', type: 'text',
eyebrow: 'Scale', eyebrow: 'Scale',
heading: [ heading: [
{ text: 'A platform ', fontFamily: 'serif' }, { text: 'A platform ready to ', fontFamily: 'serif' },
{ text: 'ready to grow', fontFamily: 'sans' }, { text: 'grow', fontFamily: 'sans' },
], ],
paragraphs: [ paragraphs: [
'Because the foundation is solid, W3Grads is architected for what comes next including a payment layer for future paid interview plans and nationwide scale without structural rewrites.', 'Because the foundation is solid, W3Grads is architected for what comes next, including a payment layer for future paid interview plans and nationwide scale without structural rewrites.',
], ],
callout: callout:
'"Twenty gave us the flexibility to model the entire interview lifecycle as custom objects and workflows. We could build something genuinely complex without fighting the platform to do it." Piyush Khandelwal, Director, W3villa Technologies, Partner', '"Twenty gave us the flexibility to model the entire interview lifecycle as custom objects and workflows. We could build something genuinely complex without fighting the platform to do it." - Piyush Khandelwal, Director, W3villa Technologies, Partner',
}, },
{ {
type: 'text', type: 'text',
eyebrow: 'The result', eyebrow: 'The result',
heading: [ heading: [
{ text: 'Zero manual work ', fontFamily: 'serif' }, { text: 'Zero manual work', fontFamily: 'sans' },
{ text: 'at the core', fontFamily: 'sans' }, { text: ' at the core', fontFamily: 'serif' },
], ],
paragraphs: [ paragraphs: [
'Programs that previously needed heavy manual coordination now run end-to-end with automation. Institutions get a scalable, intelligent system; students get faster preparation for interviews that matter; W3villa shipped a product institutions can build revenue around.', 'Programs that previously needed heavy manual coordination now run end-to-end with automation. Institutions get a scalable, intelligent system; students get faster preparation for interviews that matter; W3villa shipped a product institutions can build revenue around.',
@ -88,7 +95,7 @@ const CASE_STUDY: CaseStudyData = {
], ],
catalogCard: { catalogCard: {
summary: summary:
'W3villa shipped W3Grads on Twenty AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.', 'W3villa shipped W3Grads on Twenty for AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.',
date: '2025', date: '2025',
}, },
}; };
@ -101,6 +108,7 @@ export const metadata: Metadata = {
export default async function W3villaCaseStudyPage() { export default async function W3villaCaseStudyPage() {
const stats = await fetchCommunityStats(); const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats); const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
const palette = getCaseStudyPalette('/customers/w3villa');
let storySectionIndex = 0; let storySectionIndex = 0;
const sectionBlocks = CASE_STUDY.sections.map((block, index) => { const sectionBlocks = CASE_STUDY.sections.map((block, index) => {
@ -108,10 +116,21 @@ export default async function W3villaCaseStudyPage() {
const sectionId = `case-study-section-${storySectionIndex}`; const sectionId = `case-study-section-${storySectionIndex}`;
storySectionIndex += 1; storySectionIndex += 1;
return ( return (
<CaseStudy.TextBlock key={index} block={block} sectionId={sectionId} /> <CaseStudy.TextBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
sectionId={sectionId}
/>
); );
} }
return <CaseStudy.VisualBlock key={index} block={block} />; return (
<CaseStudy.VisualBlock
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
/>
);
}); });
return ( return (
@ -128,11 +147,21 @@ export default async function W3villaCaseStudyPage() {
<Menu.Cta scheme="secondary" /> <Menu.Cta scheme="secondary" />
</Menu.Root> </Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} /> <CaseStudy.Hero
dashColor={palette.dashColor}
hero={CASE_STUDY.hero}
hoverDashColor={palette.hoverDashColor}
/>
{sectionBlocks} <CaseStudy.Highlights
industry={CASE_STUDY.hero.industry}
kpis={CASE_STUDY.hero.kpis}
/>
<CaseStudy.Body>{sectionBlocks}</CaseStudy.Body>
<CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} /> <CaseStudy.SectionNav items={CASE_STUDY.tableOfContents} />
<CustomersCaseStudySignoff />
</> </>
); );
} }

View file

@ -2,7 +2,7 @@ import { HalftoneStudio } from '@/app/halftone/_components/HalftoneStudio';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Halftone Generator Twenty', title: 'Halftone Generator | Twenty',
description: 'Interactive halftone generator exported from Twenty.', description: 'Interactive halftone generator exported from Twenty.',
}; };

View file

@ -1,8 +1,6 @@
import { FooterVisibilityGate } from '@/app/_components/FooterVisibilityGate'; import { FooterVisibilityGate } from '@/app/_components/FooterVisibilityGate';
import { FOOTER_DATA } from '@/app/_constants/footer'; import { FOOTER_DATA } from '@/app/_constants/footer';
import { ContactCalModalRoot } from '@/app/components/ContactCalModal'; import { ContactCalModalRoot } from '@/app/components/ContactCalModal';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { Footer } from '@/sections/Footer/components'; import { Footer } from '@/sections/Footer/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
import { cssVariables } from '@/theme/css-variables'; import { cssVariables } from '@/theme/css-variables';
@ -68,19 +66,13 @@ const StyledMain = styled.main`
`; `;
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Twenty Open Source CRM', title: 'Twenty | Open Source CRM',
description: 'Modular, scalable open source CRM for modern teams.', description: 'Modular, scalable open source CRM for modern teams.',
}; };
export default async function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ children: React.ReactNode }>) { }: Readonly<{ children: React.ReactNode }>) {
const stats = await fetchCommunityStats();
const footerSocialLinks = mergeSocialLinkLabels(
FOOTER_DATA.socialLinks,
stats,
);
return ( return (
<html lang="en"> <html lang="en">
<body <body
@ -95,7 +87,7 @@ export default async function RootLayout({
<Footer.Nav groups={FOOTER_DATA.navGroups} /> <Footer.Nav groups={FOOTER_DATA.navGroups} />
<Footer.Bottom <Footer.Bottom
copyright={FOOTER_DATA.bottom.copyright} copyright={FOOTER_DATA.bottom.copyright}
links={footerSocialLinks} links={FOOTER_DATA.socialLinks}
/> />
</Footer.Root> </Footer.Root>
</FooterVisibilityGate> </FooterVisibilityGate>

View file

@ -2,10 +2,10 @@ import type { EngagementBandDataType } from '@/sections/EngagementBand/types';
export const ENGAGEMENT_BAND_DATA: EngagementBandDataType = { export const ENGAGEMENT_BAND_DATA: EngagementBandDataType = {
heading: { heading: {
text: 'Looking for a partner?', text: 'See what they built',
fontFamily: 'serif', fontFamily: 'serif',
}, },
body: { body: {
text: 'Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.', text: 'Discover how our partners implement, customize, and scale Twenty in real-world deployments.',
}, },
}; };

View file

@ -2,8 +2,8 @@ import type { HeroBaseDataType } from '@/sections/Hero/types';
export const HERO_DATA = { export const HERO_DATA = {
heading: [ heading: [
{ text: 'Become a ', fontFamily: 'serif' }, { text: 'Become ', fontFamily: 'serif' },
{ text: 'Twenty Partner', fontFamily: 'sans', newLine: true }, { text: 'our partner', fontFamily: 'sans', newLine: true },
], ],
body: { body: {
text: "We're building the #1 open-source CRM, but we can't do it alone. Join our partner ecosystem and grow with us.", text: "We're building the #1 open-source CRM, but we can't do it alone. Join our partner ecosystem and grow with us.",

View file

@ -19,10 +19,15 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
text: 'Build integrations that connect Twenty with the tools your customers already use. Help us expand the Twenty ecosystem.', text: 'Build integrations that connect Twenty with the tools your customers already use. Help us expand the Twenty ecosystem.',
}, },
benefits: [ benefits: [
{ text: 'Co-marketing opportunities' }, { text: 'Co-marketing opportunities', icon: 'users' },
{ text: 'Listing on Twenty integrations page' }, { text: 'Listing on Twenty integrations page', icon: 'search' },
{ text: 'Soon: earn revenue' }, { text: 'Soon: earn revenue', icon: 'tag' },
], ],
action: {
kind: 'partnerApplication',
label: 'Become a Technology Partner',
programId: 'technology',
},
attribution: undefined, attribution: undefined,
illustration: 'programming', illustration: 'programming',
}, },
@ -32,10 +37,15 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
text: "Share Twenty with your audience and help shape the future of open-source CRM. We're looking for creators, educators, and community builders who want to showcase great software.", text: "Share Twenty with your audience and help shape the future of open-source CRM. We're looking for creators, educators, and community builders who want to showcase great software.",
}, },
benefits: [ benefits: [
{ text: 'Revenue share for referred customers' }, { text: 'Revenue share for referred customers', icon: 'tag' },
{ text: 'Exclusive content collaboration opportunities' }, { text: 'Exclusive content collaboration opportunities', icon: 'edit' },
{ text: 'Marketing assets & brand resources' }, { text: 'Marketing assets & brand resources', icon: 'book' },
], ],
action: {
kind: 'partnerApplication',
label: 'Become a Content Partner',
programId: 'content',
},
attribution: undefined, attribution: undefined,
illustration: 'connect', illustration: 'connect',
}, },
@ -45,10 +55,15 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
text: 'Help customers implement, customize, and succeed with Twenty. Combine sales and services to grow your business.', text: 'Help customers implement, customize, and succeed with Twenty. Combine sales and services to grow your business.',
}, },
benefits: [ benefits: [
{ text: 'Resale discounts & revenue share' }, { text: 'Resale discounts & revenue share', icon: 'tag' },
{ text: 'Marketplace listing' }, { text: 'Marketplace listing', icon: 'search' },
{ text: 'Dedicated partner support' }, { text: 'Dedicated partner support', icon: 'users' },
], ],
action: {
kind: 'partnerApplication',
label: 'Become a Solution Partner',
programId: 'solutions',
},
attribution: undefined, attribution: undefined,
illustration: 'grow', illustration: 'grow',
}, },

View file

@ -28,7 +28,9 @@ export function BecomePartnerButton({
data-color={color} data-color={color}
data-variant={variant} data-variant={variant}
type="button" type="button"
onClick={openPartnerApplicationModal} onClick={() => {
openPartnerApplicationModal();
}}
> >
<BaseButton color={color} label="Become a partner" variant={variant} /> <BaseButton color={color} label="Become a partner" variant={variant} />
</StyledTrigger> </StyledTrigger>

View file

@ -4,8 +4,11 @@ import {
PARTNER_APPLICATION_MODAL_COPY, PARTNER_APPLICATION_MODAL_COPY,
PARTNER_PROGRAM_OPTIONS, PARTNER_PROGRAM_OPTIONS,
type PartnerProgramId, type PartnerProgramId,
} from '@/app/partner/_constants/partner-application-modal'; } from '@/app/partners/_constants/partner-application-modal';
import { buttonBaseStyles } from '@/design-system/components/Button/BaseButton'; import {
BUTTON_HEIGHTS_PX,
buttonBaseStyles,
} from '@/design-system/components/Button/BaseButton';
import { ButtonShape } from '@/design-system/components/Button/ButtonShape'; import { ButtonShape } from '@/design-system/components/Button/ButtonShape';
import { Body, Heading } from '@/design-system/components'; import { Body, Heading } from '@/design-system/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
@ -345,21 +348,30 @@ const FormFields = styled.div`
type PartnerApplicationModalProps = { type PartnerApplicationModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
initialProgramId?: PartnerProgramId;
}; };
export function PartnerApplicationModal({ export function PartnerApplicationModal({
open, open,
onClose, onClose,
initialProgramId = 'technology',
}: PartnerApplicationModalProps) { }: PartnerApplicationModalProps) {
const titleId = useId(); const titleId = useId();
const panelRef = useRef<HTMLDivElement>(null); const panelRef = useRef<HTMLDivElement>(null);
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const [programId, setProgramId] = useState<PartnerProgramId>('technology'); const [programId, setProgramId] =
useState<PartnerProgramId>(initialProgramId);
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null); const [submitError, setSubmitError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (open) {
setProgramId(initialProgramId);
}
}, [open, initialProgramId]);
const handleOverlayPointerDown = useCallback( const handleOverlayPointerDown = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => { (event: React.PointerEvent<HTMLDivElement>) => {
if (event.target === event.currentTarget) { if (event.target === event.currentTarget) {
@ -651,6 +663,7 @@ export function PartnerApplicationModal({
> >
<ButtonShape <ButtonShape
fillColor={theme.colors.primary.background[100]} fillColor={theme.colors.primary.background[100]}
height={BUTTON_HEIGHTS_PX.regular}
strokeColor="none" strokeColor="none"
/> />
<SubmitLabel> <SubmitLabel>

View file

@ -9,10 +9,11 @@ import {
type ReactNode, type ReactNode,
} from 'react'; } from 'react';
import type { PartnerProgramId } from '@/app/partners/_constants/partner-application-modal';
import { PartnerApplicationModal } from './PartnerApplicationModal'; import { PartnerApplicationModal } from './PartnerApplicationModal';
type PartnerApplicationModalContextValue = { type PartnerApplicationModalContextValue = {
openPartnerApplicationModal: () => void; openPartnerApplicationModal: (programId?: PartnerProgramId) => void;
}; };
const PartnerApplicationModalContext = const PartnerApplicationModalContext =
@ -34,10 +35,18 @@ export function PartnerApplicationModalRoot({
children: ReactNode; children: ReactNode;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [initialProgramId, setInitialProgramId] =
useState<PartnerProgramId>('technology');
const openPartnerApplicationModal = useCallback(() => { const openPartnerApplicationModal = useCallback(
setOpen(true); (programId?: PartnerProgramId) => {
}, []); if (programId !== undefined) {
setInitialProgramId(programId);
}
setOpen(true);
},
[],
);
const closePartnerApplicationModal = useCallback(() => { const closePartnerApplicationModal = useCallback(() => {
setOpen(false); setOpen(false);
@ -52,6 +61,7 @@ export function PartnerApplicationModalRoot({
<PartnerApplicationModalContext.Provider value={contextValue}> <PartnerApplicationModalContext.Provider value={contextValue}>
{children} {children}
<PartnerApplicationModal <PartnerApplicationModal
initialProgramId={initialProgramId}
onClose={closePartnerApplicationModal} onClose={closePartnerApplicationModal}
open={open} open={open}
/> />

View file

@ -1,11 +1,18 @@
'use client'; 'use client';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import { BecomePartnerButton } from './BecomePartnerButton'; import { BecomePartnerButton } from './BecomePartnerButton';
export function PartnerHeroCtas() { export function PartnerHeroCtas() {
return ( return (
<> <>
<BecomePartnerButton variant="contained" /> <BecomePartnerButton variant="contained" />
<TalkToUsButton
color="secondary"
label="Find a partner"
variant="outlined"
/>
</> </>
); );
} }

View file

@ -1,22 +1,22 @@
import { FAQ_DATA, MENU_DATA, TRUSTED_BY_DATA } from '@/app/_constants'; import { FAQ_DATA, MENU_DATA, TRUSTED_BY_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal'; import { TalkToUsButton } from '@/app/components/ContactCalModal';
import { CASE_STUDY_CATALOG_ENTRIES } from '@/app/customers/_constants';
import { import {
ENGAGEMENT_BAND_DATA, THREE_CARDS_ILLUSTRATION_DATA,
HERO_DATA, HERO_DATA,
SIGNOFF_DATA, SIGNOFF_DATA,
TESTIMONIALS_DATA, TESTIMONIALS_DATA,
THREE_CARDS_ILLUSTRATION_DATA, } from '@/app/partners/_constants';
} from '@/app/partner/_constants';
import { import {
PartnerApplicationModalRoot, PartnerApplicationModalRoot,
PartnerHeroCtas, PartnerHeroCtas,
PartnerSignoffCtas, PartnerSignoffCtas,
} from '@/app/partner/components/PartnerApplication'; } from '@/app/partners/components/PartnerApplication';
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components'; import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages'; import { Pages } from '@/enums/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { EngagementBand } from '@/sections/EngagementBand/components'; import { CaseStudyCatalog } from '@/sections/CaseStudyCatalog/components';
import { Faq } from '@/sections/Faq/components'; import { Faq } from '@/sections/Faq/components';
import { Hero } from '@/sections/Hero/components'; import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components'; import { Menu } from '@/sections/Menu/components';
@ -24,11 +24,21 @@ import { Signoff } from '@/sections/Signoff/components';
import { Testimonials } from '@/sections/Testimonials/components'; import { Testimonials } from '@/sections/Testimonials/components';
import { ThreeCards } from '@/sections/ThreeCards/components'; import { ThreeCards } from '@/sections/ThreeCards/components';
import { TrustedBy } from '@/sections/TrustedBy/components'; import { TrustedBy } from '@/sections/TrustedBy/components';
import type { ThreeCardsScrollLayoutOptions } from '@/sections/ThreeCards/utils/three-cards-scroll-layout';
import { theme } from '@/theme'; import { theme } from '@/theme';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
const PARTNER_ILLUSTRATION_CARDS_SCROLL_LAYOUT_OPTIONS: ThreeCardsScrollLayoutOptions =
{
endEdgeRatio: 0.28,
initialScale: 0.935,
initialTranslateY: 132,
opacityRamp: 0.28,
stagger: 0.16,
};
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Partners — Twenty', title: 'Partners | Twenty',
description: description:
'Join our partner ecosystem and grow with us as we build the #1 open-source CRM.', 'Join our partner ecosystem and grow with us as we build the #1 open-source CRM.',
}; };
@ -52,47 +62,26 @@ export default async function PartnerPage() {
</Menu.Root> </Menu.Root>
<Hero.Root backgroundColor={theme.colors.primary.background[100]}> <Hero.Root backgroundColor={theme.colors.primary.background[100]}>
<Hero.Heading page={Pages.Partner} segments={HERO_DATA.heading} /> <Hero.Heading page={Pages.Partners} segments={HERO_DATA.heading} />
<Hero.Body page={Pages.Partner} body={HERO_DATA.body} /> <Hero.Body page={Pages.Partners} body={HERO_DATA.body} />
<Hero.Cta> <Hero.Cta>
<PartnerHeroCtas /> <PartnerHeroCtas />
</Hero.Cta> </Hero.Cta>
<Hero.PartnerVisual /> <Hero.PartnerVisual />
</Hero.Root> </Hero.Root>
<TrustedBy.Root> <TrustedBy.Root
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
<TrustedBy.Logos
clientCountLabel={TRUSTED_BY_DATA.clientCountLabel}
logos={TRUSTED_BY_DATA.logos}
/>
</TrustedBy.Root>
<EngagementBand.Root
backgroundColor={theme.colors.primary.background[100]} backgroundColor={theme.colors.primary.background[100]}
> >
<EngagementBand.Strip <TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
fillColor={theme.colors.secondary.background[100]} <TrustedBy.Logos logos={TRUSTED_BY_DATA.logos} />
variant="secondary" <TrustedBy.ClientCount label={TRUSTED_BY_DATA.clientCountLabel.text} />
> </TrustedBy.Root>
<EngagementBand.Copy>
<EngagementBand.Heading segments={ENGAGEMENT_BAND_DATA.heading} /> <CaseStudyCatalog.Promo entries={CASE_STUDY_CATALOG_ENTRIES} />
<EngagementBand.Body body={ENGAGEMENT_BAND_DATA.body} />
</EngagementBand.Copy>
<EngagementBand.Actions>
<LinkButton
color="primary"
href="/case-studies"
label="Read our case studies"
type="link"
variant="contained"
/>
</EngagementBand.Actions>
</EngagementBand.Strip>
</EngagementBand.Root>
<ThreeCards.Root backgroundColor={theme.colors.secondary.background[5]}> <ThreeCards.Root backgroundColor={theme.colors.secondary.background[5]}>
<ThreeCards.Intro page={Pages.Partner} align="left"> <ThreeCards.Intro page={Pages.Partners} align="left">
<Eyebrow <Eyebrow
colorScheme="primary" colorScheme="primary"
heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading} heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading}
@ -106,6 +95,7 @@ export default async function PartnerPage() {
</ThreeCards.Intro> </ThreeCards.Intro>
<ThreeCards.IllustrationCards <ThreeCards.IllustrationCards
illustrationCards={THREE_CARDS_ILLUSTRATION_DATA.illustrationCards} illustrationCards={THREE_CARDS_ILLUSTRATION_DATA.illustrationCards}
layoutOptions={PARTNER_ILLUSTRATION_CARDS_SCROLL_LAYOUT_OPTIONS}
variant="simple" variant="simple"
/> />
</ThreeCards.Root> </ThreeCards.Root>
@ -126,10 +116,10 @@ export default async function PartnerPage() {
<Signoff.Root <Signoff.Root
backgroundColor={theme.colors.primary.background[100]} backgroundColor={theme.colors.primary.background[100]}
color={theme.colors.primary.text[100]} color={theme.colors.primary.text[100]}
page={Pages.Partner} page={Pages.Partners}
> >
<Signoff.Heading page={Pages.Partner} segments={SIGNOFF_DATA.heading} /> <Signoff.Heading page={Pages.Partners} segments={SIGNOFF_DATA.heading} />
<Signoff.Body body={SIGNOFF_DATA.body} page={Pages.Partner} /> <Signoff.Body body={SIGNOFF_DATA.body} page={Pages.Partners} />
<Signoff.Cta> <Signoff.Cta>
<PartnerSignoffCtas /> <PartnerSignoffCtas />
</Signoff.Cta> </Signoff.Cta>

View file

@ -1,7 +1,7 @@
import type { HeroBaseDataType } from '@/sections/Hero/types'; import type { HeroBaseDataType } from '@/sections/Hero/types';
const PRICING_HERO_SUBTAGLINE = { const PRICING_HERO_SUBTAGLINE = {
text: 'Start your free trial today without credit card.', text: 'Start your free trial today\nwithout credit card.',
}; };
export const HERO_DATA = { export const HERO_DATA = {

View file

@ -5,20 +5,16 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
rows: [ rows: [
{ {
featureLabel: 'Price', featureLabel: 'Price',
selfHostTiers: {
organization: { kind: 'text', text: '$19' },
pro: { kind: 'text', text: '$0' },
},
tiers: { tiers: {
organization: { kind: 'text', text: '$19' }, organization: { kind: 'text', text: '$19' },
pro: { kind: 'text', text: '$9' }, pro: { kind: 'text', text: '$9' },
}, },
type: 'row', type: 'row',
}, },
{
featureLabel: 'Self-hosting discount',
tiers: {
organization: { kind: 'text', text: 'Free for up to 20 seats' },
pro: { kind: 'text', text: 'Free for up to 20 seats' },
},
type: 'row',
},
{ {
featureLabel: 'Seats limit', featureLabel: 'Seats limit',
tiers: { tiers: {
@ -55,6 +51,14 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
}, },
type: 'row', type: 'row',
}, },
{
featureLabel: 'View types',
tiers: {
organization: { kind: 'text', text: 'Table, Kanban, Calendar' },
pro: { kind: 'text', text: 'Table, Kanban, Calendar' },
},
type: 'row',
},
{ {
featureLabel: 'Custom layout', featureLabel: 'Custom layout',
tiers: { tiers: {
@ -71,6 +75,22 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
}, },
type: 'row', type: 'row',
}, },
{
featureLabel: 'CSV import & export',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'Languages',
tiers: {
organization: { kind: 'text', text: '30+' },
pro: { kind: 'text', text: '30+' },
},
type: 'row',
},
{ {
title: 'Reports', title: 'Reports',
type: 'category', type: 'category',
@ -131,23 +151,23 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
}, },
type: 'row', type: 'row',
}, },
{
featureLabel: 'Automations credits',
tiers: {
organization: { kind: 'text', text: '2k' },
pro: { kind: 'text', text: '1k' },
},
type: 'row',
},
{ {
title: 'Security', title: 'Security',
type: 'category', type: 'category',
}, },
{
featureLabel: 'Two-factor authentication',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{ {
featureLabel: 'User roles', featureLabel: 'User roles',
tiers: { tiers: {
organization: { kind: 'text', text: 'Unlimited' }, organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'text', text: 'Admin, Members' }, pro: { kind: 'text', text: 'Unlimited' },
}, },
type: 'row', type: 'row',
}, },
@ -155,7 +175,7 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
featureLabel: 'Read/Edit/Delete permissions', featureLabel: 'Read/Edit/Delete permissions',
tiers: { tiers: {
organization: { kind: 'text', text: 'Unlimited' }, organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'dash' }, pro: { kind: 'text', text: 'Unlimited' },
}, },
type: 'row', type: 'row',
}, },
@ -163,7 +183,7 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
featureLabel: 'Field-level permissions', featureLabel: 'Field-level permissions',
tiers: { tiers: {
organization: { kind: 'text', text: 'Unlimited' }, organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'dash' }, pro: { kind: 'text', text: 'Unlimited' },
}, },
type: 'row', type: 'row',
}, },
@ -183,6 +203,14 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
}, },
type: 'row', type: 'row',
}, },
{
featureLabel: 'Audit logs',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'dash' },
},
type: 'row',
},
{ {
featureLabel: 'Environments', featureLabel: 'Environments',
tiers: { tiers: {
@ -221,6 +249,10 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
}, },
{ {
featureLabel: 'Email and Chat', featureLabel: 'Email and Chat',
selfHostTiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'dash' },
},
tiers: { tiers: {
organization: { kind: 'yes', label: 'Yes' }, organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' }, pro: { kind: 'yes', label: 'Yes' },
@ -236,13 +268,25 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
type: 'row', type: 'row',
}, },
{ {
featureLabel: 'Professional services', featureLabel: 'Onboarding Packs',
selfHostTiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'dash' },
},
tiers: { tiers: {
organization: { kind: 'yes', label: 'Yes' }, organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' }, pro: { kind: 'yes', label: 'Yes' },
}, },
type: 'row', type: 'row',
}, },
{
featureLabel: 'Implementation partners',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'dash' },
},
type: 'row',
},
{ {
title: 'Customization', title: 'Customization',
type: 'category', type: 'category',
@ -256,7 +300,17 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
type: 'row', type: 'row',
}, },
{ {
featureLabel: 'Custom domain', appliesTo: 'cloud',
featureLabel: 'Subdomain (yourco.twenty.com)',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
appliesTo: 'cloud',
featureLabel: 'Custom domain (crm.yourco.com)',
tiers: { tiers: {
organization: { kind: 'yes', label: 'Yes' }, organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' }, pro: { kind: 'yes', label: 'Yes' },
@ -268,6 +322,39 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
type: 'category', type: 'category',
}, },
{ {
featureLabel: 'REST & GraphQL API',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'Webhooks',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'MCP server',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'Install shared tarball app',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'dash' },
},
type: 'row',
},
{
appliesTo: 'cloud',
featureLabel: 'API calls', featureLabel: 'API calls',
tiers: { tiers: {
organization: { kind: 'text', text: '200 per minute' }, organization: { kind: 'text', text: '200 per minute' },
@ -275,6 +362,29 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
}, },
type: 'row', type: 'row',
}, },
{
appliesTo: 'selfHost',
title: 'Self-hosting',
type: 'category',
},
{
appliesTo: 'selfHost',
featureLabel: 'Source code access',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
appliesTo: 'selfHost',
featureLabel: 'Commercial license (no AGPL obligations)',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'dash' },
},
type: 'row',
},
], ],
initialVisibleRowCount: 15, initialVisibleRowCount: 15,
seeMoreFeaturesCta: { seeMoreFeaturesCta: {

View file

@ -4,28 +4,39 @@ const ORGANIZATION_HEADING = {
text: 'Organization', text: 'Organization',
}; };
const PRO_BULLETS_DEFAULT = [ const PRO_BULLETS_MONTHLY = [
{ text: 'Full customisation' }, { text: 'Full customization' },
{ text: 'Create custom apps' },
{ text: 'AI Agents with custom skills' }, { text: 'AI Agents with custom skills' },
{ text: '1K automation credits' }, { text: 'Up to 5M automation credits/month' },
{ text: 'Standard support' }, { text: 'Standard support' },
]; ];
const PRO_BULLETS_SELF_HOST_MONTHLY = [ const PRO_BULLETS_YEARLY = [
{ text: 'Full customisation' }, { text: 'Full customization' },
{ text: 'Create custom apps' },
{ text: 'AI Agents with custom skills' },
{ text: 'Up to 50M automation credits/year' },
{ text: 'Standard support' },
];
const PRO_BULLETS_SELF_HOST = [
{ text: 'Full customization' },
{ text: 'Create custom apps' },
{ text: 'Community support' }, { text: 'Community support' },
]; ];
const PRO_BULLETS_SELF_HOST_YEARLY = [ const ORGANIZATION_BULLETS_MONTHLY = [
{ text: 'Full customisation' }, { text: 'Everything in Pro' },
{ text: 'Community support' }, { text: 'Roles & Permissions' },
]; { text: 'SAML/OIDC SSO' },
{ text: 'Priority support' },
const ORGANIZATION_BULLETS_DEFAULT = [ ];
const ORGANIZATION_BULLETS_YEARLY = [
{ text: 'Everything in Pro' }, { text: 'Everything in Pro' },
{ text: 'Roles & Permissions' }, { text: 'Roles & Permissions' },
{ text: 'SAML/OIDC SSO' }, { text: 'SAML/OIDC SSO' },
{ text: '2K automation credits' },
{ text: 'Priority support' }, { text: 'Priority support' },
]; ];
@ -33,7 +44,8 @@ const ORGANIZATION_BULLETS_SELF_HOST = [
{ text: 'Everything in Pro' }, { text: 'Everything in Pro' },
{ text: 'Roles & Permissions' }, { text: 'Roles & Permissions' },
{ text: 'SAML/OIDC SSO' }, { text: 'SAML/OIDC SSO' },
{ text: 'Priority support' }, { text: 'Twenty team support' },
{ text: 'No open-source distribution requirement' },
]; ];
import type { PlansDataType } from '@/sections/Plans/types'; import type { PlansDataType } from '@/sections/Plans/types';
@ -43,14 +55,14 @@ export const PLANS_DATA = {
cells: { cells: {
cloud: { cloud: {
monthly: { monthly: {
featureBullets: ORGANIZATION_BULLETS_DEFAULT, featureBullets: ORGANIZATION_BULLETS_MONTHLY,
price: { price: {
body: { text: '/user/month' }, body: { text: '/user/month' },
heading: { fontFamily: 'sans', text: '$25' }, heading: { fontFamily: 'sans', text: '$25' },
}, },
}, },
yearly: { yearly: {
featureBullets: ORGANIZATION_BULLETS_DEFAULT, featureBullets: ORGANIZATION_BULLETS_YEARLY,
price: { price: {
body: { text: '/user/month' }, body: { text: '/user/month' },
heading: { fontFamily: 'sans', text: '$19' }, heading: { fontFamily: 'sans', text: '$19' },
@ -84,14 +96,14 @@ export const PLANS_DATA = {
cells: { cells: {
cloud: { cloud: {
monthly: { monthly: {
featureBullets: PRO_BULLETS_DEFAULT, featureBullets: PRO_BULLETS_MONTHLY,
price: { price: {
body: { text: '/user/month' }, body: { text: '/user/month' },
heading: { fontFamily: 'sans', text: '$12' }, heading: { fontFamily: 'sans', text: '$12' },
}, },
}, },
yearly: { yearly: {
featureBullets: PRO_BULLETS_DEFAULT, featureBullets: PRO_BULLETS_YEARLY,
price: { price: {
body: { text: '/user/month' }, body: { text: '/user/month' },
heading: { fontFamily: 'sans', text: '$9' }, heading: { fontFamily: 'sans', text: '$9' },
@ -100,14 +112,14 @@ export const PLANS_DATA = {
}, },
selfHost: { selfHost: {
monthly: { monthly: {
featureBullets: PRO_BULLETS_SELF_HOST_MONTHLY, featureBullets: PRO_BULLETS_SELF_HOST,
price: { price: {
body: { text: '/user/month' }, body: { text: '/user/month' },
heading: { fontFamily: 'sans', text: '$0' }, heading: { fontFamily: 'sans', text: '$0' },
}, },
}, },
yearly: { yearly: {
featureBullets: PRO_BULLETS_SELF_HOST_YEARLY, featureBullets: PRO_BULLETS_SELF_HOST,
price: { price: {
body: { text: '/user/month' }, body: { text: '/user/month' },
heading: { fontFamily: 'sans', text: '$0' }, heading: { fontFamily: 'sans', text: '$0' },

View file

@ -15,6 +15,7 @@ import { Faq } from '@/sections/Faq/components';
import { Hero } from '@/sections/Hero/components'; import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components'; import { Menu } from '@/sections/Menu/components';
import { Plans } from '@/sections/Plans/components'; import { Plans } from '@/sections/Plans/components';
import { PricingStateProvider } from '@/sections/Plans/context/PricingStateContext';
import { PlanTable } from '@/sections/PlanTable/components'; import { PlanTable } from '@/sections/PlanTable/components';
import { Salesforce } from '@/sections/Salesforce/components'; import { Salesforce } from '@/sections/Salesforce/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
@ -34,9 +35,9 @@ const PricingBannerContainer = styled.div`
`; `;
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Pricing Twenty', title: 'Pricing | Twenty',
description: description:
'Plans that scale with your team. Compare tiers and see how Twenty stacks up for your open source CRM.', 'Plans that scale with your team. Compare tiers of the #1 open-source CRM.',
}; };
export default async function PricingPage() { export default async function PricingPage() {
@ -59,44 +60,54 @@ export default async function PricingPage() {
<Hero.Root backgroundColor={theme.colors.secondary.background[5]}> <Hero.Root backgroundColor={theme.colors.secondary.background[5]}>
<Hero.Heading page={Pages.Pricing} segments={HERO_DATA.heading} /> <Hero.Heading page={Pages.Pricing} segments={HERO_DATA.heading} />
<Hero.Body page={Pages.Pricing} body={HERO_DATA.body} /> <Hero.Body
body={HERO_DATA.body}
page={Pages.Pricing}
preserveLineBreaks
/>
</Hero.Root> </Hero.Root>
<Plans.Root backgroundColor={theme.colors.secondary.background[5]}> <PricingStateProvider>
<PricingPlansContainer> <Plans.Root backgroundColor={theme.colors.secondary.background[5]}>
<Plans.Content /> <PricingPlansContainer>
</PricingPlansContainer> <Plans.Content />
</Plans.Root> </PricingPlansContainer>
</Plans.Root>
<EngagementBand.Root <EngagementBand.Root
backgroundColor={theme.colors.secondary.background[5]} backgroundColor={theme.colors.secondary.background[5]}
> >
<PricingBannerContainer> <PricingBannerContainer>
<EngagementBand.Strip <EngagementBand.Strip
desktopCopyMaxWidth="60%" desktopCopyMaxWidth="60%"
fillColor={theme.colors.primary.background[100]} fillColor={theme.colors.primary.background[100]}
variant="primary" variant="primary"
> >
<EngagementBand.Copy> <EngagementBand.Copy>
<EngagementBand.Heading segments={ENGAGEMENT_BAND_DATA.heading} /> <EngagementBand.Heading
<EngagementBand.Body body={ENGAGEMENT_BAND_DATA.body} /> segments={ENGAGEMENT_BAND_DATA.heading}
</EngagementBand.Copy> />
<EngagementBand.Actions> <EngagementBand.Body body={ENGAGEMENT_BAND_DATA.body} />
<LinkButton </EngagementBand.Copy>
color="secondary" <EngagementBand.Actions>
href="https://app.twenty.com/welcome" <LinkButton
label="Find a partner" color="secondary"
type="anchor" href="https://app.twenty.com/welcome"
variant="outlined" label="Find a partner"
/> type="anchor"
</EngagementBand.Actions> variant="outlined"
</EngagementBand.Strip> />
</PricingBannerContainer> </EngagementBand.Actions>
</EngagementBand.Root> </EngagementBand.Strip>
</PricingBannerContainer>
</EngagementBand.Root>
<PlanTable.Root backgroundColor={theme.colors.secondary.background[100]}> <PlanTable.Root
<PlanTable.Content data={PLAN_TABLE_DATA} /> backgroundColor={theme.colors.secondary.background[100]}
</PlanTable.Root> >
<PlanTable.Content data={PLAN_TABLE_DATA} />
</PlanTable.Root>
</PricingStateProvider>
<Salesforce.Flow <Salesforce.Flow
backgroundColor={theme.colors.secondary.background[5]} backgroundColor={theme.colors.secondary.background[5]}

View file

@ -8,7 +8,7 @@ import { LegalDocumentPage } from '@/sections/LegalDocument/legal-document-page'
import { PrivacyPolicyDocument } from './_components'; import { PrivacyPolicyDocument } from './_components';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Privacy Policy Twenty', title: 'Privacy Policy | Twenty',
description: description:
'How Twenty collects, uses, safeguards, and discloses information when you use Twenty.com and related services.', 'How Twenty collects, uses, safeguards, and discloses information when you use Twenty.com and related services.',
}; };

View file

@ -1,6 +1,7 @@
export { DEMO_DATA } from './demo'; export { DEMO_DATA } from './demo';
export { FEATURE_DATA } from './feature'; export { FEATURE_DATA } from './feature';
export { HERO_DATA } from './hero'; export { HERO_DATA } from './hero';
export { SIGNOFF_DATA } from './signoff';
export { STEPPER_DATA } from './stepper'; export { STEPPER_DATA } from './stepper';
export { TABS_DATA } from './tabs'; export { TABS_DATA } from './tabs';
export { THREE_CARDS_ILLUSTRATION_DATA } from './three-cards'; export { THREE_CARDS_ILLUSTRATION_DATA } from './three-cards';

View file

@ -0,0 +1,12 @@
import type { SignoffDataType } from '@/sections/Signoff/types';
export const SIGNOFF_DATA: SignoffDataType = {
heading: [
{ text: 'Ready to grow', fontFamily: 'serif' },
{ text: 'with ', fontFamily: 'serif', newLine: true },
{ text: 'Twenty?', fontFamily: 'sans' },
],
body: {
text: 'Join the teams that chose to own their CRM. Start building with Twenty today.',
},
};

View file

@ -1,31 +1,29 @@
import { FAQ_DATA, MENU_DATA, TRUSTED_BY_DATA } from '@/app/_constants'; import { FAQ_DATA, MENU_DATA, TRUSTED_BY_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal'; import { TalkToUsButton } from '@/app/components/ContactCalModal';
import { import {
DEMO_DATA,
FEATURE_DATA, FEATURE_DATA,
HERO_DATA, HERO_DATA,
SIGNOFF_DATA,
STEPPER_DATA, STEPPER_DATA,
TABS_DATA,
THREE_CARDS_ILLUSTRATION_DATA, THREE_CARDS_ILLUSTRATION_DATA,
} from '@/app/product/_constants'; } from '@/app/product/_constants';
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components'; import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages'; import { Pages } from '@/enums/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { Demo } from '@/sections/Demo/components';
import { Faq } from '@/sections/Faq/components'; import { Faq } from '@/sections/Faq/components';
import { Feature } from '@/sections/Feature/components'; import { Feature } from '@/sections/Feature/components';
import { Hero } from '@/sections/Hero/components'; import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components'; import { Menu } from '@/sections/Menu/components';
import { ProductStepper } from '@/sections/ProductStepper/components'; import { ProductStepper } from '@/sections/ProductStepper/components';
import { Tabs } from '@/sections/Tabs/components'; import { Signoff } from '@/sections/Signoff/components';
import { ThreeCards } from '@/sections/ThreeCards/components'; import { ThreeCards } from '@/sections/ThreeCards/components';
import { TrustedBy } from '@/sections/TrustedBy/components'; import { TrustedBy } from '@/sections/TrustedBy/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Product Twenty', title: 'Product | Twenty',
description: description:
'Track relationships, manage pipelines, and take action quickly with a CRM that feels intuitive from day one.', 'Track relationships, manage pipelines, and take action quickly with a CRM that feels intuitive from day one.',
}; };
@ -50,7 +48,7 @@ export default async function ProductPage() {
<Hero.Root backgroundColor={theme.colors.primary.background[100]}> <Hero.Root backgroundColor={theme.colors.primary.background[100]}>
<Hero.Heading page={Pages.Product} segments={HERO_DATA.heading} /> <Hero.Heading page={Pages.Product} segments={HERO_DATA.heading} />
<Hero.Body page={Pages.Product} body={HERO_DATA.body} /> <Hero.Body body={HERO_DATA.body} page={Pages.Product} />
<Hero.Cta> <Hero.Cta>
<LinkButton <LinkButton
color="secondary" color="secondary"
@ -65,19 +63,10 @@ export default async function ProductPage() {
<TrustedBy.Root> <TrustedBy.Root>
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} /> <TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
<TrustedBy.Logos <TrustedBy.Logos logos={TRUSTED_BY_DATA.logos} />
clientCountLabel={TRUSTED_BY_DATA.clientCountLabel} <TrustedBy.ClientCount label={TRUSTED_BY_DATA.clientCountLabel.text} />
logos={TRUSTED_BY_DATA.logos}
/>
</TrustedBy.Root> </TrustedBy.Root>
<Tabs.Root>
<Eyebrow colorScheme="secondary" heading={TABS_DATA.eyebrow.heading} />
<Tabs.Heading segments={TABS_DATA.heading} />
<Tabs.Body body={TABS_DATA.body} />
<Tabs.TabGroup tabs={TABS_DATA.tabs} />
</Tabs.Root>
<Feature.Root backgroundColor={theme.colors.primary.background[100]}> <Feature.Root backgroundColor={theme.colors.primary.background[100]}>
<Feature.Intro align="center" page={Pages.Product}> <Feature.Intro align="center" page={Pages.Product}>
<Eyebrow <Eyebrow
@ -114,20 +103,28 @@ export default async function ProductPage() {
steps={STEPPER_DATA.steps} steps={STEPPER_DATA.steps}
/> />
<Demo.Root> <Signoff.Root
<Eyebrow colorScheme="primary" heading={DEMO_DATA.eyebrow.heading} /> backgroundColor={theme.colors.secondary.background[5]}
<Demo.Heading segments={DEMO_DATA.heading} /> color={theme.colors.primary.text[100]}
<Demo.Cta> page={Pages.Partners}
>
<Signoff.Heading page={Pages.Partners} segments={SIGNOFF_DATA.heading} />
<Signoff.Body body={SIGNOFF_DATA.body} page={Pages.Partners} />
<Signoff.Cta>
<LinkButton <LinkButton
color="secondary" color="secondary"
href="https://app.twenty.com/welcome" href="https://app.twenty.com/welcome"
label="Try twenty cloud" label="Get started"
type="anchor" type="anchor"
variant="contained" variant="contained"
/> />
</Demo.Cta> <TalkToUsButton
<Demo.Screenshot image={DEMO_DATA.image} /> color="secondary"
</Demo.Root> label="Talk to us"
variant="outlined"
/>
</Signoff.Cta>
</Signoff.Root>
<Faq.Root illustration={FAQ_DATA.illustration}> <Faq.Root illustration={FAQ_DATA.illustration}>
<Faq.Intro> <Faq.Intro>

View file

@ -2,10 +2,10 @@ import type { BodyType } from '@/design-system/components/Body/types/Body';
import type { HeadingType } from '@/design-system/components/Heading/types/Heading'; import type { HeadingType } from '@/design-system/components/Heading/types/Heading';
export const RELEASE_NOTES_HERO_HEADING: HeadingType[] = [ export const RELEASE_NOTES_HERO_HEADING: HeadingType[] = [
{ fontFamily: 'sans', text: 'Release ' }, { fontFamily: 'serif', text: 'Latest ' },
{ fontFamily: 'serif', text: 'notes' }, { fontFamily: 'sans', text: 'Releases', newLine: true },
]; ];
export const RELEASE_NOTES_HERO_BODY: BodyType = { export const RELEASE_NOTES_HERO_BODY: BodyType = {
text: 'New features, fixes, and improvements across Twenty. Notes are maintained alongside the main marketing site content.', text: 'Discover the newest features and improvements in Twenty,\nthe #1 open-source CRM.',
}; };

View file

@ -2,9 +2,10 @@ import { MENU_DATA } from '@/app/_constants';
import { import {
RELEASE_NOTES_HERO_BODY, RELEASE_NOTES_HERO_BODY,
RELEASE_NOTES_HERO_HEADING, RELEASE_NOTES_HERO_HEADING,
} from '@/app/release-notes/_constants/hero'; } from '@/app/releases/_constants/hero';
import { LinkButton } from '@/design-system/components'; import { LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages'; import { Pages } from '@/enums/pages';
import { GitHubIcon } from '@/icons';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { fetchLatestGithubReleaseTag } from '@/lib/github/fetch-latest-release-tag'; import { fetchLatestGithubReleaseTag } from '@/lib/github/fetch-latest-release-tag';
@ -18,12 +19,12 @@ import type { Metadata } from 'next';
import { Fragment } from 'react'; import { Fragment } from 'react';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Release notes — Twenty', title: 'Releases | Twenty',
description: description:
'Discover the newest features and improvements in Twenty, the open-source CRM.', 'Discover the newest features and improvements in Twenty, the open-source CRM.',
}; };
export default async function ReleaseNotesPage() { export default async function ReleasesPage() {
const allNotes = loadLocalReleaseNotes(); const allNotes = loadLocalReleaseNotes();
const [latestTag, stats] = await Promise.all([ const [latestTag, stats] = await Promise.all([
fetchLatestGithubReleaseTag(), fetchLatestGithubReleaseTag(),
@ -65,17 +66,19 @@ export default async function ReleaseNotesPage() {
<LinkButton <LinkButton
color="secondary" color="secondary"
href="https://github.com/twentyhq/twenty/releases" href="https://github.com/twentyhq/twenty/releases"
label="GitHub releases" label="Technical notes"
leadingIcon={<GitHubIcon fillColor="currentColor" size={14} />}
type="anchor" type="anchor"
variant="outlined" variant="outlined"
/> />
</Hero.Cta> </Hero.Cta>
<Hero.ReleaseNotesVisual />
</Hero.Root> </Hero.Root>
<ReleaseNotes.Root> <ReleaseNotes.Root>
{allNotes.length === 0 ? ( {allNotes.length === 0 ? (
<ReleaseNotes.EmptyMessage> <ReleaseNotes.EmptyMessage>
Release notes were not found. Add MDX under{' '} Releases were not found. Add MDX under{' '}
<strong>packages/twenty-website-new/src/content/releases</strong>{' '} <strong>packages/twenty-website-new/src/content/releases</strong>{' '}
and images under{' '} and images under{' '}
<strong>packages/twenty-website-new/public/images/releases</strong>. <strong>packages/twenty-website-new/public/images/releases</strong>.

View file

@ -8,7 +8,7 @@ import { LegalDocumentPage } from '@/sections/LegalDocument/legal-document-page'
import { TermsDocument } from './_components'; import { TermsDocument } from './_components';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Terms of Service Twenty', title: 'Terms of Service | Twenty',
description: description:
'Terms of Service for Twenty.com PBC, including use of Twenty.com, sub-domains, and related services.', 'Terms of Service for Twenty.com PBC, including use of Twenty.com, sub-domains, and related services.',
}; };

View file

@ -1,34 +1,36 @@
import { MENU_DATA } from '@/app/_constants'; import { MENU_DATA } from '@/app/_constants';
import { import {
EDITORIAL_FOUR,
EDITORIAL_ONE, EDITORIAL_ONE,
EDITORIAL_THREE,
EDITORIAL_TWO, EDITORIAL_TWO,
HERO_DATA, HERO_DATA,
MARQUEE_DATA, MARQUEE_DATA,
QUOTE_DATA, QUOTE_DATA,
SIGNOFF_DATA,
STATEMENT_ONE,
STATEMENT_TWO,
STEPPER_DATA,
} from '@/app/why-twenty/_constants'; } from '@/app/why-twenty/_constants';
import { LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages'; import { Pages } from '@/enums/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { OverflowProbe } from '@/lib/debug/overflow-probe';
import { Editorial } from '@/sections/Editorial/components'; import { Editorial } from '@/sections/Editorial/components';
import { Hero } from '@/sections/Hero/components'; import { Hero } from '@/sections/Hero/components';
import { Marquee } from '@/sections/Marquee/components'; import { Marquee } from '@/sections/Marquee/components';
import { Menu } from '@/sections/Menu/components'; import { Menu } from '@/sections/Menu/components';
import { Quote } from '@/sections/Quote/components'; import { Quote } from '@/sections/Quote/components';
import { Signoff } from '@/sections/Signoff/components';
import { Statement } from '@/sections/Statement/components';
import { WhyTwentyStepper } from '@/sections/WhyTwentyStepper/components';
import { theme } from '@/theme'; import { theme } from '@/theme';
import { css } from '@linaria/core';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
const editorialOneIntroClass = css`
margin-bottom: ${theme.spacing(4)};
--editorial-heading-max-width: 760px;
--editorial-intro-max-width: 760px;
@media (min-width: ${theme.breakpoints.md}px) {
margin-bottom: ${theme.spacing(8)};
}
`;
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Why Twenty — Twenty', title: 'Why Twenty | Twenty',
description: description:
'Most packaged software makes companies more similar. Learn why the future of CRM is built, not bought.', 'Most packaged software makes companies more similar. Learn why the future of CRM is built, not bought.',
}; };
@ -39,6 +41,7 @@ export default async function WhyTwentyPage() {
return ( return (
<> <>
<OverflowProbe />
<Menu.Root <Menu.Root
backgroundColor={theme.colors.secondary.background[100]} backgroundColor={theme.colors.secondary.background[100]}
scheme="secondary" scheme="secondary"
@ -51,13 +54,16 @@ export default async function WhyTwentyPage() {
<Menu.Cta scheme="secondary" /> <Menu.Cta scheme="secondary" />
</Menu.Root> </Menu.Root>
<Hero.Root backgroundColor={theme.colors.secondary.background[100]}> <Hero.Root
backgroundColor={theme.colors.secondary.background[100]}
colorScheme="secondary"
>
<Hero.Heading <Hero.Heading
page={Pages.WhyTwenty} page={Pages.WhyTwenty}
segments={HERO_DATA.heading} segments={HERO_DATA.heading}
size="xl" size="xl"
/> />
<Hero.Body page={Pages.WhyTwenty} body={HERO_DATA.body} /> <Hero.Body body={HERO_DATA.body} page={Pages.WhyTwenty} />
<Hero.WhyTwentyVisual /> <Hero.WhyTwentyVisual />
</Hero.Root> </Hero.Root>
@ -66,7 +72,7 @@ export default async function WhyTwentyPage() {
color={theme.colors.secondary.text[100]} color={theme.colors.secondary.text[100]}
mutedColor={theme.colors.secondary.text[60]} mutedColor={theme.colors.secondary.text[60]}
> >
<Editorial.Intro> <Editorial.Intro className={editorialOneIntroClass}>
<Editorial.Eyebrow <Editorial.Eyebrow
colorScheme="secondary" colorScheme="secondary"
eyebrow={EDITORIAL_ONE.eyebrow!} eyebrow={EDITORIAL_ONE.eyebrow!}
@ -94,75 +100,6 @@ export default async function WhyTwentyPage() {
color={theme.colors.secondary.text[100]} color={theme.colors.secondary.text[100]}
heading={MARQUEE_DATA.heading} heading={MARQUEE_DATA.heading}
/> />
<Editorial.Root
backgroundColor={theme.colors.secondary.background[100]}
color={theme.colors.secondary.text[100]}
mutedColor={theme.colors.secondary.text[60]}
>
<Editorial.Intro>
<Editorial.Eyebrow
colorScheme="secondary"
eyebrow={EDITORIAL_THREE.eyebrow!}
/>
<Editorial.Heading segments={EDITORIAL_THREE.heading!} />
</Editorial.Intro>
<Editorial.Body body={EDITORIAL_THREE.body} layout="indented" />
</Editorial.Root>
<Statement.Root
backgroundColor={theme.colors.secondary.background[100]}
color={theme.colors.secondary.text[100]}
>
<Statement.Heading segments={STATEMENT_ONE.heading} />
</Statement.Root>
<Statement.Root
backgroundColor={theme.colors.primary.background[100]}
color={theme.colors.primary.text[100]}
>
<Statement.Heading segments={STATEMENT_TWO.heading} />
</Statement.Root>
<Editorial.Root
backgroundColor={theme.colors.primary.background[100]}
color={theme.colors.primary.text[100]}
mutedColor={theme.colors.primary.text[60]}
>
<Editorial.Intro>
<Editorial.Eyebrow
colorScheme="primary"
eyebrow={EDITORIAL_FOUR.eyebrow!}
/>
<Editorial.Heading segments={EDITORIAL_FOUR.heading!} />
</Editorial.Intro>
<Editorial.Body body={EDITORIAL_FOUR.body} layout="two-column" />
</Editorial.Root>
<WhyTwentyStepper.Flow
body={STEPPER_DATA.body}
heading={STEPPER_DATA.heading}
illustration={STEPPER_DATA.illustration}
/>
<Signoff.Root
backgroundColor={theme.colors.primary.text[10]}
color={theme.colors.secondary.text[100]}
variant="shaped"
shapeFillColor={theme.colors.secondary.background[100]}
>
<Signoff.Heading segments={SIGNOFF_DATA.heading} />
<Signoff.Body body={SIGNOFF_DATA.body} />
<Signoff.Cta>
<LinkButton
color="primary"
href="https://app.twenty.com/welcome"
label="Get started"
type="anchor"
variant="contained"
/>
</Signoff.Cta>
</Signoff.Root>
</> </>
); );
} }

View file

@ -0,0 +1,10 @@
---
release: 1.13.0
Date: 2025-12-17
---
# Stop Workflow Button
Take control of your running workflows with the new Stop Workflow button. When a workflow is in progress, you can now immediately halt its execution, giving you the flexibility to cancel operations that are no longer needed or troubleshoot issues in real-time.
![](/images/releases/1.13/1.13.0-stop-workflow-button.png)

View file

@ -0,0 +1,10 @@
---
release: 1.14.0
Date: 2025-12-20
---
# Resize navbar and side panel
You can now resize the side panel and navigation menu to view content more easily. This is especially useful on record pages with long content.
![](/images/releases/1.14/1.14.0-resize-navbar-and-side-panel.png)

View file

@ -0,0 +1,10 @@
---
release: 1.15.0
Date: 2026-01-08
---
# Updated by
You can now see who last updated a record
![](/images/releases/1.15/1.15.0-updated-by-official.webp)

View file

@ -0,0 +1,16 @@
---
release: 1.16.0
Date: 2026-01-23
---
# Files in records
Add files directly to records so documents, screenshots, and other assets stay attached to the right work.
![](/images/releases/1.16/1.16.0-files-in-records.webp)
# Flexible relations
Create more flexible relationships between objects, including more advanced many-to-many setups.
![](/images/releases/1.16/1.16.0-flexible-relations.webp)

View file

@ -0,0 +1,14 @@
---
release: 1.17.0
Date: 2026-02-10
---
# AI chat
AI Chat is easier to use with a cleaner experience and clearer model choices.
![](/images/releases/1.17/1.17.0-ai-chat.webp)
# Custom sidebar
Favorites now live in the navigation menu, making the sidebar easier to organize and customize.

View file

@ -0,0 +1,16 @@
---
release: 1.18.0
Date: 2026-02-18
---
# Sidebar items
Create and organize sidebar items more easily with a cleaner navigation setup.
![](/images/releases/1.18/1.18.0-sidebar-items.webp)
# Live updates
Some teammate changes now appear right away, so collaboration feels faster and more up to date.
![](/images/releases/1.18/1.18.0-live-updates.webp)

Some files were not shown because too many files have changed in this diff Show more