From ffd27c04b7716ad2a9c6395a6786a5a8dd54469e Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 16:16:34 -0700 Subject: [PATCH 1/2] refactor(landing): buildLandingMetadata helper + derive pricing credit cells - Add lib/landing/seo.ts buildLandingMetadata(), the single source of truth for the shared landing OpenGraph/Twitter/robots/canonical chrome; migrate 13 static landing pages to it (drop ~470 lines of duplicated literals). Output is identical per page (verified: rendered head tags unchanged, render mode unchanged); partners additionally gains the standard authors/creator/publisher it was missing. - Drop the now-dead PAGE_URL const + SITE_URL import from the 12 migrated pages that no longer reference them (partners keeps both for its JSON-LD). - comparison-data.ts: derive monthly/daily credit cells from CREDIT_TIERS, DEFAULT_FREE_CREDITS, DAILY_REFRESH_RATE and a new CREDITS_PER_DOLLAR constant instead of hardcoding, so they can't drift (values byte-identical). --- apps/sim/app/(landing)/changelog/page.tsx | 53 ++--------- apps/sim/app/(landing)/demo/page.tsx | 53 ++--------- apps/sim/app/(landing)/enterprise/page.tsx | 51 ++--------- apps/sim/app/(landing)/partners/page.tsx | 46 ++-------- apps/sim/app/(landing)/pricing/page.tsx | 53 ++--------- apps/sim/app/(landing)/privacy/page.tsx | 53 ++--------- .../(landing)/solutions/compliance/page.tsx | 51 ++--------- .../(landing)/solutions/engineering/page.tsx | 51 ++--------- .../app/(landing)/solutions/finance/page.tsx | 51 ++--------- apps/sim/app/(landing)/solutions/hr/page.tsx | 51 ++--------- apps/sim/app/(landing)/solutions/it/page.tsx | 51 ++--------- apps/sim/app/(landing)/terms/page.tsx | 53 ++--------- apps/sim/app/(landing)/workflows/page.tsx | 53 ++--------- .../comparison-table/comparison-data.ts | 30 ++++++- apps/sim/lib/billing/constants.ts | 6 ++ apps/sim/lib/landing/seo.ts | 89 +++++++++++++++++++ 16 files changed, 195 insertions(+), 600 deletions(-) create mode 100644 apps/sim/lib/landing/seo.ts diff --git a/apps/sim/app/(landing)/changelog/page.tsx b/apps/sim/app/(landing)/changelog/page.tsx index 4f49f6064c3..46180cfc7c0 100644 --- a/apps/sim/app/(landing)/changelog/page.tsx +++ b/apps/sim/app/(landing)/changelog/page.tsx @@ -1,60 +1,17 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import Changelog from '@/app/(landing)/changelog/changelog' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/changelog` const TITLE = 'Changelog | Sim, the AI Workspace' const DESCRIPTION = 'Every new feature, improvement, and fix in Sim, the open-source AI workspace, with release notes straight from GitHub.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'Changelog | Sim, the AI Workspace', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { - url: '/logo/426-240/reverse/small.png', - alt: 'Changelog | Sim, the AI Workspace', - }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + path: '/changelog', +}) export default function Page() { return diff --git a/apps/sim/app/(landing)/demo/page.tsx b/apps/sim/app/(landing)/demo/page.tsx index b91d228dfd5..5c197227422 100644 --- a/apps/sim/app/(landing)/demo/page.tsx +++ b/apps/sim/app/(landing)/demo/page.tsx @@ -1,60 +1,17 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import Demo from '@/app/(landing)/demo/demo' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/demo` const TITLE = 'Book a Demo | Sim, the AI Workspace' const DESCRIPTION = 'Book a demo of Sim, the AI agent workspace where teams build, deploy, and manage AI agents and workflows that connect 1,000+ integrations and every major LLM.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'Book a Demo | Sim, the AI Workspace', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { - url: '/logo/426-240/reverse/small.png', - alt: 'Book a Demo | Sim, the AI Workspace', - }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + path: '/demo', +}) export default function Page() { return diff --git a/apps/sim/app/(landing)/enterprise/page.tsx b/apps/sim/app/(landing)/enterprise/page.tsx index a8ff30e2e76..8fa995cc5e0 100644 --- a/apps/sim/app/(landing)/enterprise/page.tsx +++ b/apps/sim/app/(landing)/enterprise/page.tsx @@ -1,59 +1,20 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import EnterprisePage from '@/app/(landing)/enterprise/enterprise' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/enterprise` const TITLE = 'Enterprise AI Agent Platform | Sim AI' const DESCRIPTION = 'Build, deploy, and govern enterprise AI agents in one workspace with security, approvals, observability, and collaboration.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, + path: '/enterprise', keywords: 'enterprise ai agents, enterprise ai agent, enterprise ai agent platform, enterprise workflow agents', - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: TITLE, - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { url: '/logo/426-240/reverse/small.png', alt: 'Sim' }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + twitterImageAlt: 'Sim', +}) export default function Page() { return diff --git a/apps/sim/app/(landing)/partners/page.tsx b/apps/sim/app/(landing)/partners/page.tsx index e41c3a88817..05c25ed87f8 100644 --- a/apps/sim/app/(landing)/partners/page.tsx +++ b/apps/sim/app/(landing)/partners/page.tsx @@ -1,54 +1,20 @@ import { ChipLink } from '@sim/emcn' -import type { Metadata } from 'next' import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' const PAGE_URL = `${SITE_URL}/partners` const TITLE = 'Partner Program | Sim' const DESCRIPTION = "Join the Sim partner program. Build, deploy, and sell AI agent solutions powered by Sim's AI workspace, and earn your certification through Sim Academy." -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, + path: '/partners', keywords: 'Sim partner program, AI agent partners, AI workspace partners, Sim Academy certification, AI agent reseller, co-marketing', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: TITLE, - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { url: '/logo/426-240/reverse/small.png', alt: 'Sim' }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + twitterImageAlt: 'Sim', +}) const partnersJsonLd = { '@context': 'https://schema.org', diff --git a/apps/sim/app/(landing)/pricing/page.tsx b/apps/sim/app/(landing)/pricing/page.tsx index 144c7cae205..97ada5ec6b2 100644 --- a/apps/sim/app/(landing)/pricing/page.tsx +++ b/apps/sim/app/(landing)/pricing/page.tsx @@ -1,64 +1,21 @@ -import type { Metadata } from 'next' import type { SearchParams } from 'nuqs/server' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import Pricing from '@/app/(landing)/pricing/pricing' import { pricingSearchParamsCache } from '@/app/(landing)/pricing/search-params' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/pricing` const TITLE = 'Pricing | Sim, the AI Workspace' const DESCRIPTION = 'Pricing for Sim, the open-source AI workspace for building, deploying, and managing AI agents. Compare the Free, Pro, Max, and Enterprise plans. Start free.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, + path: '/pricing', keywords: 'Sim pricing, AI workspace pricing, AI agent platform pricing, build AI agents, Pro plan, Max plan, Enterprise plan, open-source AI agents, LLM pricing', - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'Pricing | Sim, the AI Workspace', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { - url: '/logo/426-240/reverse/small.png', - alt: 'Pricing | Sim, the AI Workspace', - }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} +}) export default async function Page({ searchParams }: { searchParams: Promise }) { await pricingSearchParamsCache.parse(searchParams) diff --git a/apps/sim/app/(landing)/privacy/page.tsx b/apps/sim/app/(landing)/privacy/page.tsx index c99a7a8c64a..81c2708154f 100644 --- a/apps/sim/app/(landing)/privacy/page.tsx +++ b/apps/sim/app/(landing)/privacy/page.tsx @@ -1,60 +1,17 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import Privacy from '@/app/(landing)/privacy/privacy' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/privacy` const TITLE = 'Privacy Policy | Sim, the AI Workspace' const DESCRIPTION = 'How Sim, the open-source AI workspace, collects, uses, and protects your data, including data obtained from Google APIs, and the controls you have over it.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'Privacy Policy | Sim, the AI Workspace', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { - url: '/logo/426-240/reverse/small.png', - alt: 'Privacy Policy | Sim, the AI Workspace', - }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + path: '/privacy', +}) export default function Page() { return diff --git a/apps/sim/app/(landing)/solutions/compliance/page.tsx b/apps/sim/app/(landing)/solutions/compliance/page.tsx index 9c8467f83f4..f3c7b914933 100644 --- a/apps/sim/app/(landing)/solutions/compliance/page.tsx +++ b/apps/sim/app/(landing)/solutions/compliance/page.tsx @@ -1,59 +1,20 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import ComplianceSolution from '@/app/(landing)/solutions/compliance/compliance' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/solutions/compliance` const TITLE = 'AI Agents for Continuous Compliance & Audit | Sim' const DESCRIPTION = 'Compliance teams use Sim, the open-source AI workspace, to build and deploy AI agents that automate evidence collection, control monitoring, and reporting.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, + path: '/solutions/compliance', keywords: 'AI workspace, AI agents for compliance, compliance automation, evidence collection, control monitoring, audit readiness, open-source AI agent platform', - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'AI Agents for Continuous Compliance & Audit | Sim', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { url: '/logo/426-240/reverse/small.png', alt: 'Sim' }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + twitterImageAlt: 'Sim', +}) export default function Page() { return diff --git a/apps/sim/app/(landing)/solutions/engineering/page.tsx b/apps/sim/app/(landing)/solutions/engineering/page.tsx index 1eba1af08e0..7b52ba1bf5f 100644 --- a/apps/sim/app/(landing)/solutions/engineering/page.tsx +++ b/apps/sim/app/(landing)/solutions/engineering/page.tsx @@ -1,59 +1,20 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import EngineeringSolution from '@/app/(landing)/solutions/engineering/engineering' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/solutions/engineering` const TITLE = 'AI Agents for Code Review & On-Call | Sim' const DESCRIPTION = 'Engineering teams use Sim, the open-source AI workspace, to build and deploy AI agents that automate code review, on-call triage, and documentation.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, + path: '/solutions/engineering', keywords: 'AI workspace, AI agents for engineering, automated code review, on-call automation, CI/CD agents, developer automation, open-source AI agent platform', - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'AI Agents for Code Review & On-Call | Sim', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { url: '/logo/426-240/reverse/small.png', alt: 'Sim' }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + twitterImageAlt: 'Sim', +}) export default function Page() { return diff --git a/apps/sim/app/(landing)/solutions/finance/page.tsx b/apps/sim/app/(landing)/solutions/finance/page.tsx index 5a1754a0339..38a1e7e2196 100644 --- a/apps/sim/app/(landing)/solutions/finance/page.tsx +++ b/apps/sim/app/(landing)/solutions/finance/page.tsx @@ -1,59 +1,20 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import FinanceSolution from '@/app/(landing)/solutions/finance/finance' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/solutions/finance` const TITLE = 'AI Agents for Invoice Processing & Reconciliation | Sim' const DESCRIPTION = 'Finance teams use Sim, the open-source AI workspace, to build and deploy AI agents that automate reconciliation, invoice processing, and reporting.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, + path: '/solutions/finance', keywords: 'AI workspace, AI agents for finance, finance automation, invoice processing, account reconciliation, financial reporting, open-source AI agent platform', - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'AI Agents for Invoice Processing & Reconciliation | Sim', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { url: '/logo/426-240/reverse/small.png', alt: 'Sim' }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + twitterImageAlt: 'Sim', +}) export default function Page() { return diff --git a/apps/sim/app/(landing)/solutions/hr/page.tsx b/apps/sim/app/(landing)/solutions/hr/page.tsx index b593f7be9f5..ca278dd0825 100644 --- a/apps/sim/app/(landing)/solutions/hr/page.tsx +++ b/apps/sim/app/(landing)/solutions/hr/page.tsx @@ -1,59 +1,20 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import HrSolution from '@/app/(landing)/solutions/hr/hr' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/solutions/hr` const TITLE = 'AI Agents for Onboarding & People Operations | Sim' const DESCRIPTION = 'HR teams use Sim, the open-source AI workspace, to build and deploy AI agents that automate onboarding, employee questions, and approvals.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, + path: '/solutions/hr', keywords: 'AI workspace, AI agents for HR, HR automation, employee onboarding, people operations, HRIS automation, open-source AI agent platform', - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'AI Agents for Onboarding & People Operations | Sim', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { url: '/logo/426-240/reverse/small.png', alt: 'Sim' }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + twitterImageAlt: 'Sim', +}) export default function Page() { return diff --git a/apps/sim/app/(landing)/solutions/it/page.tsx b/apps/sim/app/(landing)/solutions/it/page.tsx index 8a17bb383e3..f3d01a5a274 100644 --- a/apps/sim/app/(landing)/solutions/it/page.tsx +++ b/apps/sim/app/(landing)/solutions/it/page.tsx @@ -1,59 +1,20 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import ItSolution from '@/app/(landing)/solutions/it/it' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/solutions/it` const TITLE = 'AI Agents for Ticket Triage & Access | Sim' const DESCRIPTION = 'IT teams use Sim, the open-source AI workspace, to build and deploy AI agents that automate ticket triage, access provisioning, and monitoring.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, + path: '/solutions/it', keywords: 'AI workspace, IT automation, AI agents for IT, IT service desk automation, access provisioning, infrastructure monitoring, open-source AI agent platform', - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'AI Agents for Ticket Triage & Access | Sim', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { url: '/logo/426-240/reverse/small.png', alt: 'Sim' }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + twitterImageAlt: 'Sim', +}) export default function Page() { return diff --git a/apps/sim/app/(landing)/terms/page.tsx b/apps/sim/app/(landing)/terms/page.tsx index 84949e8df53..1cad66ef3c0 100644 --- a/apps/sim/app/(landing)/terms/page.tsx +++ b/apps/sim/app/(landing)/terms/page.tsx @@ -1,60 +1,17 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import Terms from '@/app/(landing)/terms/terms' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/terms` const TITLE = 'Terms of Service | Sim, the AI Workspace' const DESCRIPTION = 'The terms and conditions for using Sim, the open-source AI workspace: subscription plans, data ownership, acceptable use, and your rights.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'Terms of Service | Sim, the AI Workspace', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { - url: '/logo/426-240/reverse/small.png', - alt: 'Terms of Service | Sim, the AI Workspace', - }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} + path: '/terms', +}) export default function Page() { return diff --git a/apps/sim/app/(landing)/workflows/page.tsx b/apps/sim/app/(landing)/workflows/page.tsx index 242f44ecf4b..532feccdc5b 100644 --- a/apps/sim/app/(landing)/workflows/page.tsx +++ b/apps/sim/app/(landing)/workflows/page.tsx @@ -1,62 +1,19 @@ -import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' import Workflows from '@/app/(landing)/workflows/workflows' export const revalidate = 3600 -const PAGE_URL = `${SITE_URL}/workflows` const TITLE = 'Workflows | The Visual Builder in Sim, the AI Workspace' const DESCRIPTION = 'Workflows is the visual builder in Sim, the open-source AI workspace. Connect blocks, every major LLM, and 1,000+ integrations into agent logic.' -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { absolute: TITLE }, +export const metadata = buildLandingMetadata({ + title: TITLE, description: DESCRIPTION, + path: '/workflows', keywords: 'AI workspace, visual workflow builder, build AI agents, AI agent workflow builder, LLM orchestration, AI integrations, open-source AI agent platform, agentic workflows', - authors: [{ name: 'Sim' }], - creator: 'Sim', - publisher: 'Sim', - openGraph: { - title: TITLE, - description: DESCRIPTION, - type: 'website', - url: PAGE_URL, - siteName: 'Sim', - locale: 'en_US', - images: [ - { - url: '/logo/426-240/reverse/small.png', - width: 2130, - height: 1200, - alt: 'Workflows | The Visual Builder in Sim, the AI Workspace', - type: 'image/png', - }, - ], - }, - twitter: { - card: 'summary_large_image', - site: '@simdotai', - creator: '@simdotai', - title: TITLE, - description: DESCRIPTION, - images: { - url: '/logo/426-240/reverse/small.png', - alt: 'Workflows | The Visual Builder in Sim, the AI Workspace', - }, - }, - alternates: { - canonical: PAGE_URL, - languages: { 'en-US': PAGE_URL, 'x-default': PAGE_URL }, - }, - robots: { - index: true, - follow: true, - googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, - }, - category: 'technology', -} +}) export default function Page() { return diff --git a/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data.ts b/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data.ts index 9b058852a94..277f2aae24f 100644 --- a/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data.ts +++ b/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data.ts @@ -1,3 +1,19 @@ +import { + CREDIT_TIERS, + CREDITS_PER_DOLLAR, + DAILY_REFRESH_RATE, + DEFAULT_FREE_CREDITS, +} from '@/lib/billing/constants' + +const [PRO_TIER, MAX_TIER] = CREDIT_TIERS + +/** Formats a credit count with thousands separators (e.g. 25000 → "25,000"). */ +const formatCredits = (credits: number): string => credits.toLocaleString('en-US') + +/** Daily refresh credits for a plan: 1% of plan dollars/day, in credits. */ +const dailyRefreshCredits = (dollars: number): number => + Math.round(dollars * DAILY_REFRESH_RATE * CREDITS_PER_DOLLAR) + /** A brand icon rendered in a cell instead of a check/em-dash/text. */ export interface CellIcon { /** Icon identifier resolved to a component by the table renderer. */ @@ -58,11 +74,21 @@ export const COMPARISON_SECTIONS: ComparisonSection[] = [ rows: [ { label: 'Monthly credits', - values: ['1,000', '6,000', '25,000', 'Custom'], + values: [ + formatCredits(DEFAULT_FREE_CREDITS * CREDITS_PER_DOLLAR), + formatCredits(PRO_TIER.credits), + formatCredits(MAX_TIER.credits), + 'Custom', + ], }, { label: 'Daily refresh', - values: [false, '+50', '+200', 'Custom'], + values: [ + false, + `+${formatCredits(dailyRefreshCredits(PRO_TIER.dollars))}`, + `+${formatCredits(dailyRefreshCredits(MAX_TIER.dollars))}`, + 'Custom', + ], }, ], }, diff --git a/apps/sim/lib/billing/constants.ts b/apps/sim/lib/billing/constants.ts index ce20115db8b..d4e60cd74ee 100644 --- a/apps/sim/lib/billing/constants.ts +++ b/apps/sim/lib/billing/constants.ts @@ -50,6 +50,12 @@ export const CREDIT_TIERS = [ export type CreditTier = (typeof CREDIT_TIERS)[number] +/** + * Credits granted per dollar of plan spend. A credit is $0.005, so a dollar + * buys 200 — the conversion behind both free-tier and daily-refresh credits. + */ +export const CREDITS_PER_DOLLAR = 200 + /** * Daily refresh rate: 1% of plan cost per day. * E.g. $25 plan => $0.25/day => 50 credits/day included usage. diff --git a/apps/sim/lib/landing/seo.ts b/apps/sim/lib/landing/seo.ts new file mode 100644 index 00000000000..d0f04f50caf --- /dev/null +++ b/apps/sim/lib/landing/seo.ts @@ -0,0 +1,89 @@ +import type { Metadata } from 'next' +import { SITE_URL } from '@/lib/core/utils/urls' + +/** Shared OpenGraph/Twitter card image for landing pages. */ +const OG_IMAGE_URL = '/logo/426-240/reverse/small.png' +const OG_IMAGE_WIDTH = 2130 +const OG_IMAGE_HEIGHT = 1200 + +interface LandingMetadataInput { + /** Absolute ``, rendered as-is (no site template applied). */ + title: string + description: string + /** Path under the site root, e.g. `/pricing`. Use `''` for the home page. */ + path: string + /** Optional comma-separated keywords; omitted from the output when absent. */ + keywords?: string + /** OpenGraph image alt text. Defaults to {@link title}. */ + imageAlt?: string + /** Twitter card image alt text. Defaults to {@link imageAlt}. */ + twitterImageAlt?: string +} + +/** + * Builds the canonical metadata for a static landing page — the single source of + * truth for the OpenGraph/Twitter/robots/canonical chrome shared across the + * landing family, so per-page files declare only what actually varies (title, + * description, keywords, path, and any non-default image alt). + * + * Note: dynamic and image-bearing routes that resolve their card from a sibling + * `opengraph-image` (integrations, models) and the blog family (`buildPostMetadata` + * in `lib/blog/seo`) intentionally do not use this helper. + */ +export function buildLandingMetadata({ + title, + description, + path, + keywords, + imageAlt, + twitterImageAlt, +}: LandingMetadataInput): Metadata { + const url = `${SITE_URL}${path}` + const ogAlt = imageAlt ?? title + const twitterAlt = twitterImageAlt ?? ogAlt + + return { + metadataBase: new URL(SITE_URL), + title: { absolute: title }, + description, + ...(keywords ? { keywords } : {}), + authors: [{ name: 'Sim' }], + creator: 'Sim', + publisher: 'Sim', + openGraph: { + title, + description, + type: 'website', + url, + siteName: 'Sim', + locale: 'en_US', + images: [ + { + url: OG_IMAGE_URL, + width: OG_IMAGE_WIDTH, + height: OG_IMAGE_HEIGHT, + alt: ogAlt, + type: 'image/png', + }, + ], + }, + twitter: { + card: 'summary_large_image', + site: '@simdotai', + creator: '@simdotai', + title, + description, + images: { url: OG_IMAGE_URL, alt: twitterAlt }, + }, + alternates: { + canonical: url, + languages: { 'en-US': url, 'x-default': url }, + }, + robots: { + index: true, + follow: true, + googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1 }, + }, + category: 'technology', + } +} From 6fa67036985efbec981599cf764e3c5767e587ab Mon Sep 17 00:00:00 2001 From: waleed <walif6@gmail.com> Date: Tue, 30 Jun 2026 16:27:52 -0700 Subject: [PATCH 2/2] refactor(billing): bind comparison columns to credit tiers by name Look up Pro/Max tiers by name instead of array position so the comparison table can't silently bind to the wrong tier if CREDIT_TIERS is reordered or a tier is inserted (addresses Greptile P2). Values unchanged. --- .../components/comparison-table/comparison-data.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data.ts b/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data.ts index 277f2aae24f..88ddfde1922 100644 --- a/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data.ts +++ b/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data.ts @@ -5,7 +5,18 @@ import { DEFAULT_FREE_CREDITS, } from '@/lib/billing/constants' -const [PRO_TIER, MAX_TIER] = CREDIT_TIERS +/** + * Looks up a credit tier by name, so a column binds to the right tier even if + * {@link CREDIT_TIERS} is reordered or a tier is inserted. + */ +function tierByName(name: (typeof CREDIT_TIERS)[number]['name']) { + const tier = CREDIT_TIERS.find((t) => t.name === name) + if (!tier) throw new Error(`comparison-data: no credit tier named "${name}"`) + return tier +} + +const PRO_TIER = tierByName('Pro') +const MAX_TIER = tierByName('Max') /** Formats a credit count with thousands separators (e.g. 25000 → "25,000"). */ const formatCredits = (credits: number): string => credits.toLocaleString('en-US')