diff --git a/packages/twenty-website/next.config.ts b/packages/twenty-website/next.config.ts index 679df7d271..1fc51e3310 100644 --- a/packages/twenty-website/next.config.ts +++ b/packages/twenty-website/next.config.ts @@ -90,16 +90,22 @@ const nextConfig: LinariaConfig = { ]; }, async rewrites() { + const localeAlternation = DEPLOYED_LOCALE_URL_SEGMENTS.join('|'); + return { beforeFiles: [ // Root rewrites to the source locale. { source: '/', destination: '/en' }, - // Any path that isn't already locale-prefixed (en/, fr/), an internal - // Next.js path, a static asset folder, or a file with an extension - // rewrites to the source locale prefix. Mirrors proxy.ts Rule 4. + // Multi-segment paths (e.g. /customers/9dots, /articles/my-post). + // The Worker's path-to-regexp does not allow :param(regex) to match + // across "/" boundaries, so we split multi-segment into :first/:rest+. { - source: - '/:rest((?!en$|en/|fr$|fr/|api|_next/static|_next/image|favicon\\.ico|robots\\.txt|sitemap\\.xml|illustrations|lottie|fonts|.+\\..+).+)', + source: `/:first((?!(?:${localeAlternation}|api|_next|images|illustrations|halftone|lottie|fonts)(?=/))[^/.]+)/:rest+`, + destination: '/en/:first/:rest+', + }, + // Single-segment paths (e.g. /pricing, /customers, /why-twenty). + { + source: `/:rest((?!${DEPLOYED_LOCALE_URL_SEGMENTS.map((s) => `${s}$|${s}/`).join('|')}|api|_next/static|_next/image|favicon\\.ico|robots\\.txt|sitemap\\.xml|images|illustrations|lottie|fonts|.+\\..+).+)`, destination: '/en/:rest', }, ], diff --git a/packages/twenty-website/public/images/customers/case-studies/act-education.webp b/packages/twenty-website/public/images/customers/case-studies/act-education.webp new file mode 100644 index 0000000000..cc9544ea33 Binary files /dev/null and b/packages/twenty-website/public/images/customers/case-studies/act-education.webp differ diff --git a/packages/twenty-website/public/images/customers/case-studies/alternative-partners.webp b/packages/twenty-website/public/images/customers/case-studies/alternative-partners.webp new file mode 100644 index 0000000000..f4bdce553a Binary files /dev/null and b/packages/twenty-website/public/images/customers/case-studies/alternative-partners.webp differ diff --git a/packages/twenty-website/public/images/customers/case-studies/elevate-consulting.webp b/packages/twenty-website/public/images/customers/case-studies/elevate-consulting.webp new file mode 100644 index 0000000000..616b7e7ec2 Binary files /dev/null and b/packages/twenty-website/public/images/customers/case-studies/elevate-consulting.webp differ diff --git a/packages/twenty-website/public/images/customers/case-studies/netzero.webp b/packages/twenty-website/public/images/customers/case-studies/netzero.webp new file mode 100644 index 0000000000..2b1c985a5f Binary files /dev/null and b/packages/twenty-website/public/images/customers/case-studies/netzero.webp differ diff --git a/packages/twenty-website/public/images/customers/case-studies/nine-dots.webp b/packages/twenty-website/public/images/customers/case-studies/nine-dots.webp new file mode 100644 index 0000000000..f663a1aca1 Binary files /dev/null and b/packages/twenty-website/public/images/customers/case-studies/nine-dots.webp differ diff --git a/packages/twenty-website/public/images/customers/case-studies/w3villa.webp b/packages/twenty-website/public/images/customers/case-studies/w3villa.webp new file mode 100644 index 0000000000..b8b280e27b Binary files /dev/null and b/packages/twenty-website/public/images/customers/case-studies/w3villa.webp differ diff --git a/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx b/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx index 35c916ccfa..4ade697420 100644 --- a/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx @@ -1,15 +1,14 @@ -import { msg } from '@lingui/core/macro'; -import { type CaseStudyData } from '@/lib/customers'; +import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; +import { Heading, HeadingPart } from '@/design-system/components'; +import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; import { getRouteI18n, type LocaleRouteParams, } from '@/lib/i18n/utils/get-route-i18n'; import { buildLocalizedMetadata } from '@/lib/seo'; -import { Heading, HeadingPart } from '@/design-system/components'; -import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; +import { msg } from '@lingui/core/macro'; -const PLACEHOLDER_HERO = - 'https://images.unsplash.com/photo-1733244766159-f58f4184fd38?w=1600&q=80'; +const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.nineDots; const META_TITLE = msg`Homeseller, WhatsApp, and a CRM built around the business | Nine Dots & Twenty`; const META_DESCRIPTION = msg`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.`; @@ -51,7 +50,7 @@ export default async function NineDotsCaseStudyPage({ authorAvatarSrc: '/images/partner/testimonials/mike-babiy.webp', authorRole: msg`Founder, Nine Dots Ventures`, clientIcon: 'nine-dots', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: HERO_IMAGE, industry: msg`Real Estate`, kpis: [ { value: msg`150 hrs`, label: msg`Saved / month` }, diff --git a/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx b/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx index 103e451adf..81941d6cfe 100644 --- a/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx @@ -1,5 +1,5 @@ import { msg } from '@lingui/core/macro'; -import { type CaseStudyData } from '@/lib/customers'; +import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; import { getRouteI18n, type LocaleRouteParams, @@ -8,8 +8,7 @@ import { buildLocalizedMetadata } from '@/lib/seo'; import { Heading, HeadingPart } from '@/design-system/components'; import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; -const PLACEHOLDER_HERO = - 'https://images.unsplash.com/photo-1687600154329-150952c73169?w=1600&q=80'; +const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.actEducation; const META_TITLE = msg`Burned by vendor lock-in, AC&T built a CRM they actually own | Twenty`; const META_DESCRIPTION = msg`How AC&T Education Migration and Flycoder replaced a shuttered vendor CRM with self-hosted Twenty, with 90%+ lower cost and full ownership.`; @@ -51,7 +50,7 @@ export default async function ActEducationCaseStudyPage({ authorAvatarSrc: '/images/partner/testimonials/joseph-chiang.webp', authorRole: msg`CRM Engineer, AC&T Education Migration`, clientIcon: 'act-education', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: HERO_IMAGE, industry: msg`Education`, kpis: [{ value: msg`90%+`, label: msg`Lower CRM cost` }], }, diff --git a/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx b/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx index 5b9ecc9b89..932ded4797 100644 --- a/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx @@ -1,6 +1,6 @@ import { msg } from '@lingui/core/macro'; import { Heading, HeadingPart } from '@/design-system/components'; -import type { CaseStudyData } from '@/lib/customers'; +import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; import { getRouteI18n, type LocaleRouteParams, @@ -8,8 +8,7 @@ import { import { buildLocalizedMetadata } from '@/lib/seo'; import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; -const PLACEHOLDER_HERO = - 'https://images.unsplash.com/photo-1702047149248-a6049168a2a8?w=1600&q=80'; +const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.alternativePartners; const META_TITLE = msg`From Salesforce to self-hosted Twenty, powered by AI | Alternative Partners`; const META_DESCRIPTION = msg`How Alternative Partners migrated from Salesforce to self-hosted Twenty using agentic AI in the implementation loop: fast migration, durable ownership.`; @@ -51,7 +50,7 @@ export default async function AlternativePartnersCaseStudyPage({ authorAvatarSrc: '/images/partner/testimonials/benjamin-reynolds.webp', authorRole: msg`Principal and Founder, Alternative Partners`, clientIcon: 'alternative-partners', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: HERO_IMAGE, industry: msg`Consulting`, kpis: [ { value: msg`AI-assisted`, label: msg`Salesforce migration` }, diff --git a/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx b/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx index e9213f44b1..d4880c51d7 100644 --- a/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx @@ -1,5 +1,5 @@ import { msg } from '@lingui/core/macro'; -import { type CaseStudyData } from '@/lib/customers'; +import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; import { getRouteI18n, type LocaleRouteParams, @@ -8,8 +8,7 @@ import { buildLocalizedMetadata } from '@/lib/seo'; import { Heading, HeadingPart } from '@/design-system/components'; import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; -const PLACEHOLDER_HERO = - 'https://images.unsplash.com/photo-1758873269035-aae0e1fd3422?w=1600&q=80'; +const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.elevateConsulting; const META_TITLE = msg`Twenty as the API backbone of a go-to-market stack | Elevate Consulting`; const META_DESCRIPTION = msg`How Elevate Consulting moved off documents and spreadsheets to Twenty as the API-connected CRM at the center of their stack.`; @@ -53,7 +52,7 @@ export default async function ElevateConsultingCaseStudyPage({ author: 'Justin Beadle', authorRole: msg`Director of Digital and Information, Elevate Consulting`, clientIcon: 'elevate-consulting', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: HERO_IMAGE, industry: msg`Management Consulting`, kpis: [ { value: msg`1 click`, label: msg`Proposal automation` }, diff --git a/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx b/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx index ea9ac5ba20..7b0f751983 100644 --- a/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx @@ -1,5 +1,5 @@ import { msg } from '@lingui/core/macro'; -import { type CaseStudyData } from '@/lib/customers'; +import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; import { getRouteI18n, type LocaleRouteParams, @@ -8,8 +8,7 @@ import { buildLocalizedMetadata } from '@/lib/seo'; import { Heading, HeadingPart } from '@/design-system/components'; import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; -const PLACEHOLDER_HERO = - 'https://images.unsplash.com/photo-1744830343976-ce690ba2a67c?w=1600&q=80'; +const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.netzero; const META_TITLE = msg`A CRM that grows with you | NetZero & Twenty`; const META_DESCRIPTION = msg`How NetZero uses Twenty across carbon credits, agricultural products, and franchised industrial systems with a modular CRM and a roadmap toward AI-assisted workflows.`; @@ -50,7 +49,7 @@ export default async function NetZeroCaseStudyPage({ authorAvatarSrc: '/images/partner/testimonials/olivier-reinaud.webp', authorRole: msg`Co-founder, NetZero`, clientIcon: 'netzero', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: HERO_IMAGE, industry: msg`Agribusiness`, kpis: [ { value: msg`3 product lines`, label: msg`On a single CRM` }, diff --git a/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx b/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx index 6ae2884076..c7ad5f1576 100644 --- a/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx @@ -1,5 +1,5 @@ import { msg } from '@lingui/core/macro'; -import { type CaseStudyData } from '@/lib/customers'; +import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; import { getRouteI18n, type LocaleRouteParams, @@ -8,8 +8,7 @@ import { buildLocalizedMetadata } from '@/lib/seo'; import { Heading, HeadingPart } from '@/design-system/components'; import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; -const PLACEHOLDER_HERO = - 'https://images.unsplash.com/photo-1756830231350-3b501f63c5c1?w=1600&q=80'; +const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.w3villa; const META_TITLE = msg`When your CRM is the product: W3Grads on Twenty | W3villa Technologies`; const META_DESCRIPTION = msg`How W3villa Technologies shipped W3Grads, an AI mock interview platform for institutions, on Twenty as the operational backbone.`; @@ -51,7 +50,7 @@ export default async function W3villaCaseStudyPage({ authorAvatarSrc: '/images/partner/testimonials/amrendra-singh.webp', authorRole: msg`VP of Engineering, W3villa Technologies`, clientIcon: 'w3villa', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: HERO_IMAGE, industry: msg`EdTech`, kpis: [{ value: msg`Zero`, label: msg`Manual work at core` }], }, diff --git a/packages/twenty-website/src/lib/customers/case-study-catalog.ts b/packages/twenty-website/src/lib/customers/case-study-catalog.ts index d5c0f4cf4d..c333fd3b30 100644 --- a/packages/twenty-website/src/lib/customers/case-study-catalog.ts +++ b/packages/twenty-website/src/lib/customers/case-study-catalog.ts @@ -2,7 +2,16 @@ import { msg } from '@lingui/core/macro'; import type { CaseStudyCatalogEntry } from '@/lib/customers/types'; import { theme } from '@/theme'; -const PLACEHOLDER_HERO = '/images/shared/people/avatars/katherine-adams.webp'; +const CASE_STUDY_IMAGE_BASE = '/images/customers/case-studies'; + +export const CASE_STUDY_HERO_IMAGES = { + nineDots: `${CASE_STUDY_IMAGE_BASE}/nine-dots.webp`, + alternativePartners: `${CASE_STUDY_IMAGE_BASE}/alternative-partners.webp`, + netzero: `${CASE_STUDY_IMAGE_BASE}/netzero.webp`, + actEducation: `${CASE_STUDY_IMAGE_BASE}/act-education.webp`, + w3villa: `${CASE_STUDY_IMAGE_BASE}/w3villa.webp`, + elevateConsulting: `${CASE_STUDY_IMAGE_BASE}/elevate-consulting.webp`, +} as const; export const CASE_STUDY_HALFTONE_PALETTE: readonly { dashColor: string; @@ -57,13 +66,12 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [ author: 'Mike Babiy & Azmat Parveen', authorAvatarSrc: '/images/partner/testimonials/mike-babiy.webp', clientIcon: 'nine-dots', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: CASE_STUDY_HERO_IMAGES.nineDots, }, catalogCard: { summary: msg`Nine Dots put Twenty at the center of Homeseller's stack with APIs, automation, and AI on top of WhatsApp-heavy operations.`, date: msg`Jul 2025`, - coverImageSrc: - 'https://images.unsplash.com/photo-1733244766159-f58f4184fd38?w=1600&q=80', + coverImageSrc: CASE_STUDY_HERO_IMAGES.nineDots, }, }, { @@ -80,13 +88,12 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [ author: 'Benjamin Reynolds', authorAvatarSrc: '/images/partner/testimonials/benjamin-reynolds.webp', clientIcon: 'alternative-partners', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: CASE_STUDY_HERO_IMAGES.alternativePartners, }, catalogCard: { summary: msg`Alternative Partners replaced Salesforce with self-hosted Twenty, using agentic AI to compress migration work.`, date: msg`2025`, - coverImageSrc: - 'https://images.unsplash.com/photo-1702047149248-a6049168d2a8?w=1600&q=80', + coverImageSrc: CASE_STUDY_HERO_IMAGES.alternativePartners, }, }, { @@ -103,13 +110,12 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [ author: 'Olivier Reinaud', authorAvatarSrc: '/images/partner/testimonials/olivier-reinaud.webp', clientIcon: 'netzero', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: CASE_STUDY_HERO_IMAGES.netzero, }, catalogCard: { summary: msg`NetZero uses Twenty as a modular CRM across product lines and countries, with a roadmap into AI-assisted workflows.`, date: msg`2025`, - coverImageSrc: - 'https://images.unsplash.com/photo-1744830343976-ce690ba2a67c?w=1600&q=80', + coverImageSrc: CASE_STUDY_HERO_IMAGES.netzero, }, }, { @@ -123,13 +129,12 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [ author: 'Joseph Chiang', authorAvatarSrc: '/images/partner/testimonials/joseph-chiang.webp', clientIcon: 'act-education', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: CASE_STUDY_HERO_IMAGES.actEducation, }, catalogCard: { summary: msg`AC&T and Flycoder moved from a dead vendor export to self-hosted Twenty, with over 90% lower CRM cost and full control.`, date: msg`2025`, - coverImageSrc: - 'https://images.unsplash.com/photo-1687600154329-150952c73169?w=1600&q=80', + coverImageSrc: CASE_STUDY_HERO_IMAGES.actEducation, }, }, { @@ -143,13 +148,12 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [ author: 'Amrendra Pratap Singh', authorAvatarSrc: '/images/partner/testimonials/amrendra-singh.webp', clientIcon: 'w3villa', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: CASE_STUDY_HERO_IMAGES.w3villa, }, catalogCard: { summary: msg`W3villa shipped W3Grads on Twenty for AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.`, date: msg`2025`, - coverImageSrc: - 'https://images.unsplash.com/photo-1756830231350-3b501f63c5c1?w=1600&q=80', + coverImageSrc: CASE_STUDY_HERO_IMAGES.w3villa, }, }, { @@ -171,13 +175,12 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [ title: msg`Twenty as the API backbone of a go-to-market stack`, author: 'Justin Beadle', clientIcon: 'elevate-consulting', - heroImageSrc: PLACEHOLDER_HERO, + heroImageSrc: CASE_STUDY_HERO_IMAGES.elevateConsulting, }, catalogCard: { summary: msg`Elevate Consulting uses Twenty as the API backbone connecting billing, Teams, resourcing, and a custom front end around client and opportunity data.`, date: msg`Jun 2025`, - coverImageSrc: - 'https://images.unsplash.com/photo-1758873269035-aae0e1fd3422?w=1600&q=80', + coverImageSrc: CASE_STUDY_HERO_IMAGES.elevateConsulting, }, }, ]; diff --git a/packages/twenty-website/src/lib/customers/index.ts b/packages/twenty-website/src/lib/customers/index.ts index c21ad19003..519ac8a10f 100644 --- a/packages/twenty-website/src/lib/customers/index.ts +++ b/packages/twenty-website/src/lib/customers/index.ts @@ -11,5 +11,6 @@ export type { export { CASE_STUDY_CATALOG_ENTRIES, CASE_STUDY_HALFTONE_PALETTE, + CASE_STUDY_HERO_IMAGES, getCaseStudyPalette, } from './case-study-catalog';