diff --git a/packages/twenty-website/public/images/pricing/plans/enterprise-icon.webp b/packages/twenty-website/public/images/pricing/plans/enterprise-icon.webp new file mode 100644 index 0000000000..7f3a6ac8ed Binary files /dev/null and b/packages/twenty-website/public/images/pricing/plans/enterprise-icon.webp differ diff --git a/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/hubspot/page.tsx b/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/hubspot/page.tsx new file mode 100644 index 0000000000..3634b70735 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/hubspot/page.tsx @@ -0,0 +1,31 @@ +import { getCommunityStats } from '@/platform/community'; +import { + getRouteI18n, + type LocaleRouteParams, +} from '@/platform/i18n/get-route-i18n'; +import { buildRouteMetadata } from '@/platform/seo'; +import { ComparePage } from '@/sections/compare/ComparePage'; +import { COMPARISONS } from '@/sections/compare/compare-data'; +import { Menu } from '@/sections/menu'; + +export const generateMetadata = buildRouteMetadata('comparePricingHubspot'); + +export default async function CompareHubspotPricingPage({ + params, +}: { + params: Promise; +}) { + const [, communityStats] = await Promise.all([ + getRouteI18n(params), + getCommunityStats(), + ]); + + return ( + <> + +
+ +
+ + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/microsoft-dynamics/page.tsx b/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/microsoft-dynamics/page.tsx new file mode 100644 index 0000000000..fcfca58480 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/microsoft-dynamics/page.tsx @@ -0,0 +1,31 @@ +import { getCommunityStats } from '@/platform/community'; +import { + getRouteI18n, + type LocaleRouteParams, +} from '@/platform/i18n/get-route-i18n'; +import { buildRouteMetadata } from '@/platform/seo'; +import { ComparePage } from '@/sections/compare/ComparePage'; +import { COMPARISONS } from '@/sections/compare/compare-data'; +import { Menu } from '@/sections/menu'; + +export const generateMetadata = buildRouteMetadata('comparePricingDynamics'); + +export default async function CompareMicrosoftDynamicsPricingPage({ + params, +}: { + params: Promise; +}) { + const [, communityStats] = await Promise.all([ + getRouteI18n(params), + getCommunityStats(), + ]); + + return ( + <> + +
+ +
+ + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/pipedrive/page.tsx b/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/pipedrive/page.tsx new file mode 100644 index 0000000000..6cb99fe78a --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/pipedrive/page.tsx @@ -0,0 +1,31 @@ +import { getCommunityStats } from '@/platform/community'; +import { + getRouteI18n, + type LocaleRouteParams, +} from '@/platform/i18n/get-route-i18n'; +import { buildRouteMetadata } from '@/platform/seo'; +import { ComparePage } from '@/sections/compare/ComparePage'; +import { COMPARISONS } from '@/sections/compare/compare-data'; +import { Menu } from '@/sections/menu'; + +export const generateMetadata = buildRouteMetadata('comparePricingPipedrive'); + +export default async function ComparePipedrivePricingPage({ + params, +}: { + params: Promise; +}) { + const [, communityStats] = await Promise.all([ + getRouteI18n(params), + getCommunityStats(), + ]); + + return ( + <> + +
+ +
+ + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/salesforce/page.tsx b/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/salesforce/page.tsx new file mode 100644 index 0000000000..020e58c568 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/salesforce/page.tsx @@ -0,0 +1,31 @@ +import { getCommunityStats } from '@/platform/community'; +import { + getRouteI18n, + type LocaleRouteParams, +} from '@/platform/i18n/get-route-i18n'; +import { buildRouteMetadata } from '@/platform/seo'; +import { ComparePage } from '@/sections/compare/ComparePage'; +import { COMPARISONS } from '@/sections/compare/compare-data'; +import { Menu } from '@/sections/menu'; + +export const generateMetadata = buildRouteMetadata('comparePricingSalesforce'); + +export default async function CompareSalesforcePricingPage({ + params, +}: { + params: Promise; +}) { + const [, communityStats] = await Promise.all([ + getRouteI18n(params), + getCommunityStats(), + ]); + + return ( + <> + +
+ +
+ + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/sap-sales-cloud/page.tsx b/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/sap-sales-cloud/page.tsx new file mode 100644 index 0000000000..f7a0921bf1 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(site)/compare-pricing/sap-sales-cloud/page.tsx @@ -0,0 +1,31 @@ +import { getCommunityStats } from '@/platform/community'; +import { + getRouteI18n, + type LocaleRouteParams, +} from '@/platform/i18n/get-route-i18n'; +import { buildRouteMetadata } from '@/platform/seo'; +import { ComparePage } from '@/sections/compare/ComparePage'; +import { COMPARISONS } from '@/sections/compare/compare-data'; +import { Menu } from '@/sections/menu'; + +export const generateMetadata = buildRouteMetadata('comparePricingSap'); + +export default async function CompareSapPricingPage({ + params, +}: { + params: Promise; +}) { + const [, communityStats] = await Promise.all([ + getRouteI18n(params), + getCommunityStats(), + ]); + + return ( + <> + +
+ +
+ + ); +} diff --git a/packages/twenty-website/src/platform/routing/static-website-routes.ts b/packages/twenty-website/src/platform/routing/static-website-routes.ts index 5062661b25..a8f3b59ce6 100644 --- a/packages/twenty-website/src/platform/routing/static-website-routes.ts +++ b/packages/twenty-website/src/platform/routing/static-website-routes.ts @@ -39,6 +39,51 @@ export const STATIC_WEBSITE_ROUTES: readonly WebsiteRoute[] = [ priority: 0.9, title: msg`Twenty CRM Pricing — Plans from $9 per User per Month`, }, + { + changeFrequency: 'monthly', + description: msg`HubSpot gates custom objects behind its $150/seat Enterprise tier. See the full price comparison, with sources, and what Twenty includes from $9 per user.`, + id: 'comparePricingHubspot', + indexed: true, + path: '/compare-pricing/hubspot', + priority: 0.7, + title: msg`Twenty vs HubSpot Pricing — The Real Cost Compared`, + }, + { + changeFrequency: 'monthly', + description: msg`Sales Cloud Enterprise lists at $175/user/month before support, sandboxes, and AI add-ons. Compare the real cost with Twenty, from $9 per user.`, + id: 'comparePricingSalesforce', + indexed: true, + path: '/compare-pricing/salesforce', + priority: 0.7, + title: msg`Twenty vs Salesforce Pricing — The Real Cost Compared`, + }, + { + changeFrequency: 'monthly', + description: msg`SAP Sales Cloud lists at $134/user/month before a year-one implementation project that often exceeds the licenses. Compare with Twenty, from $9 per user.`, + id: 'comparePricingSap', + indexed: true, + path: '/compare-pricing/sap-sales-cloud', + priority: 0.7, + title: msg`Twenty vs SAP Sales Cloud Pricing — The Real Cost Compared`, + }, + { + changeFrequency: 'monthly', + description: msg`Pipedrive caps automations per company and keeps SSO for its top tier. Compare real per-seat costs with Twenty, from $9 per user with no caps.`, + id: 'comparePricingPipedrive', + indexed: true, + path: '/compare-pricing/pipedrive', + priority: 0.7, + title: msg`Twenty vs Pipedrive Pricing — The Real Cost Compared`, + }, + { + changeFrequency: 'monthly', + description: msg`Dynamics 365 Sales runs $105/user/month plus $40/GB database storage. See the full comparison with Twenty, from $9 per user, open source.`, + id: 'comparePricingDynamics', + indexed: true, + path: '/compare-pricing/microsoft-dynamics', + priority: 0.7, + title: msg`Twenty vs Microsoft Dynamics Pricing — The Real Cost Compared`, + }, { changeFrequency: 'monthly', description: msg`Find a certified Twenty partner to migrate, customise, and operate your open source CRM, or join the ecosystem and grow your practice with us.`, diff --git a/packages/twenty-website/src/platform/routing/website-route.ts b/packages/twenty-website/src/platform/routing/website-route.ts index 656d73e4c3..04f17d5ddc 100644 --- a/packages/twenty-website/src/platform/routing/website-route.ts +++ b/packages/twenty-website/src/platform/routing/website-route.ts @@ -3,6 +3,11 @@ import { type MetadataRoute } from 'next'; export type WebsiteRouteId = | 'apps' + | 'comparePricingDynamics' + | 'comparePricingHubspot' + | 'comparePricingPipedrive' + | 'comparePricingSalesforce' + | 'comparePricingSap' | 'customers' | 'enterpriseActivate' | 'halftone' diff --git a/packages/twenty-website/src/sections/compare/CompareCostTable.tsx b/packages/twenty-website/src/sections/compare/CompareCostTable.tsx new file mode 100644 index 0000000000..66e22ddd09 --- /dev/null +++ b/packages/twenty-website/src/sections/compare/CompareCostTable.tsx @@ -0,0 +1,209 @@ +import { msg } from '@lingui/core/macro'; +import { styled } from '@linaria/react'; + +import { INFORMATIVE_MARKS } from '@/icons'; +import { getServerI18n } from '@/platform/i18n/get-server-i18n'; +import { + color, + FONT_WEIGHT, + fontFamily, + fontSize, + mediaUp, + semanticColor, + spacing, +} from '@/tokens'; +import { ExternalLink } from '@/ui'; + +import { type CompetitorComparison } from './compare-data'; + +const CheckMark = INFORMATIVE_MARKS.check; + +const REPORT_INACCURACY_URL = + 'https://github.com/twentyhq/twenty/issues/new/choose'; + +// On narrow viewports the three-column grid scrolls horizontally instead of +// squeezing the price cells. +const TableScroller = styled.div` + overflow-x: auto; + width: 100%; +`; + +const TableScope = styled.div` + color: ${semanticColor.ink}; + min-width: 640px; + width: 100%; +`; + +const GridRow = styled.div` + display: grid; + grid-template-columns: 1.2fr 1fr 1fr; + width: 100%; +`; + +const HeadCell = styled.div` + border-bottom: 1px solid ${semanticColor.line}; + font-family: ${fontFamily('sans')}; + font-size: ${fontSize(5.5)}; + font-weight: ${FONT_WEIGHT.medium}; + line-height: ${spacing(7)}; + padding: ${spacing(4)}; + + &[data-tier] { + border-left: 1px solid ${semanticColor.line}; + } +`; + +const FeatureCell = styled.div` + border-bottom: 1px solid ${semanticColor.line}; + display: flex; + flex-direction: column; + gap: ${spacing(1)}; + justify-content: center; + min-height: ${spacing(18)}; + padding: ${spacing(4)}; +`; + +const FeatureName = styled.span` + font-family: ${fontFamily('sans')}; + font-size: ${fontSize(4)}; + font-weight: ${FONT_WEIGHT.medium}; +`; + +const FeatureDescription = styled.span` + color: ${semanticColor.inkMuted}; + display: none; + font-family: ${fontFamily('sans')}; + font-size: ${fontSize(3.5)}; + + ${mediaUp('md')} { + display: block; + } +`; + +const ValueCell = styled.div` + border-bottom: 1px solid ${semanticColor.line}; + border-left: 1px solid ${semanticColor.line}; + display: flex; + flex-direction: column; + gap: ${spacing(1)}; + justify-content: center; + min-height: ${spacing(18)}; + padding: ${spacing(4)}; +`; + +const CompetitorPrice = styled.span` + font-family: ${fontFamily('sans')}; + font-size: ${fontSize(4)}; + font-weight: ${FONT_WEIGHT.medium}; +`; + +const CompetitorDetail = styled.span` + color: ${semanticColor.inkMuted}; + font-family: ${fontFamily('sans')}; + font-size: ${fontSize(3.5)}; +`; + +const SourceLink = styled(ExternalLink)` + color: ${semanticColor.inkMuted}; + font-family: ${fontFamily('sans')}; + font-size: ${fontSize(3)}; + text-decoration: underline; + text-underline-offset: 2px; + width: fit-content; + + &:hover { + color: ${semanticColor.ink}; + } +`; + +const IncludedRow = styled.div` + align-items: center; + column-gap: ${spacing(2.5)}; + display: flex; +`; + +const IncludedText = styled.span` + font-family: ${fontFamily('sans')}; + font-size: ${fontSize(4)}; +`; + +const SourceNote = styled.p` + color: ${semanticColor.inkMuted}; + font-family: ${fontFamily('sans')}; + font-size: ${fontSize(3.5)}; + line-height: ${spacing(5.5)}; + margin-top: ${spacing(6)}; + max-width: 640px; +`; + +const ReportLink = styled(ExternalLink)` + color: ${semanticColor.inkMuted}; + text-decoration: underline; + text-underline-offset: 2px; + + &:hover { + color: ${semanticColor.ink}; + } +`; + +export function CompareCostTable({ + comparison, +}: { + comparison: CompetitorComparison; +}) { + const i18n = getServerI18n(); + + return ( +
+ + + + {i18n._(msg`Feature`)} + + {i18n._(comparison.competitorColumnLabel)} + + {i18n._(msg`On Twenty`)} + + {comparison.rows.map((row) => ( + + + {i18n._(row.feature)} + + {i18n._(row.description)} + + + + + {i18n._(row.competitor.price)} + + + {i18n._(row.competitor.detail)} + + {row.competitor.sourceUrl !== undefined ? ( + + {i18n._(msg`source`)} + + ) : null} + + + + + {i18n._(row.twenty.detail)} + + + + ))} + + + + {i18n._(comparison.sourceNote)}{' '} + + {i18n._(msg`Spotted an inaccuracy? Tell us and we will fix it.`)} + + +
+ ); +} diff --git a/packages/twenty-website/src/sections/compare/ComparePage.tsx b/packages/twenty-website/src/sections/compare/ComparePage.tsx new file mode 100644 index 0000000000..b0f9a7b88f --- /dev/null +++ b/packages/twenty-website/src/sections/compare/ComparePage.tsx @@ -0,0 +1,137 @@ +import { msg } from '@lingui/core/macro'; +import { styled } from '@linaria/react'; + +import { getServerI18n } from '@/platform/i18n/get-server-i18n'; +import { GRADIENT, mediaUp, spacing } from '@/tokens'; +import { + Body, + Button, + EngagementBand, + Eyebrow, + Heading, + HeadingPair, + SectionShell, +} from '@/ui'; + +import { CompareCostTable } from './CompareCostTable'; +import { CompareReceipt } from './CompareReceipt'; +import { type CompetitorComparison } from './compare-data'; + +const GradientBackdrop = styled.div` + background: ${GRADIENT.heroGlow}; + inset: 0 -20%; + position: absolute; +`; + +const HeroStack = styled.div` + align-items: center; + display: flex; + flex-direction: column; + margin-inline: auto; + text-align: center; + width: 100%; +`; + +const HeadingMeasure = styled.div` + white-space: pre-line; + + ${mediaUp('md')} { + max-width: 720px; + } +`; + +const IntroMeasure = styled.div` + white-space: pre-line; + + ${mediaUp('md')} { + max-width: 560px; + } +`; + +const CtaRow = styled.div` + display: flex; + gap: ${spacing(3)}; + justify-content: center; + margin-top: ${spacing(8)}; +`; + +const SectionHeading = styled.div` + margin-bottom: ${spacing(8)}; + text-align: center; +`; + +export function ComparePage({ + comparison, +}: { + comparison: CompetitorComparison; +}) { + const i18n = getServerI18n(); + + return ( + <> + } + rhythm="hero" + scheme="muted" + > + + + {i18n._(comparison.eyebrow)} + + + {i18n._(comparison.heading)} + + + + + {i18n._(comparison.intro)} + + + + +