import { ChipLink } from '@sim/emcn' import { truncate } from '@sim/utils/string' import type { Metadata } from 'next' import Image from 'next/image' import Link from 'next/link' import { notFound } from 'next/navigation' import { SITE_URL } from '@/lib/core/utils/urls' import { type AuthType, blockTypeToIconMap, type FAQItem, formatIntegrationType, INTEGRATIONS, INTEGRATIONS_UPDATED_AT, type Integration, } from '@/lib/integrations' import { BackLink } from '@/app/(landing)/components' import { JsonLd } from '@/app/(landing)/components/json-ld' import { LandingFAQ } from '@/app/(landing)/components/landing-faq' import { ShareButton } from '@/app/(landing)/components/share-button' import { IntegrationCtaButton } from '@/app/(landing)/integrations/(shell)/[slug]/components/integration-cta-button' import { TemplateCardButton } from '@/app/(landing)/integrations/(shell)/[slug]/components/template-card-button' import { IntegrationIcon } from '@/app/(landing)/integrations/components/integration-icon' import { INTEGRATION_SEO } from '@/app/(landing)/integrations/data/seo-content' import { getTemplatesForBlock } from '@/blocks/registry' const allIntegrations = INTEGRATIONS const INTEGRATION_COUNT = allIntegrations.length const baseUrl = SITE_URL /** Fast O(1) lookups - avoids repeated linear scans inside render loops. */ const bySlug = new Map(allIntegrations.map((i) => [i.slug, i])) const byType = new Map(allIntegrations.map((i) => [i.type, i])) export const dynamicParams = false /** * Returns up to `limit` related integration slugs. * * Scoring (additive): * +3 per shared operation name - strongest signal (same capability) * +2 per shared operation word - weaker signal (e.g. both have "create" ops) * +2 same integration category - topical relevance (both CRMs, both devops) * +1 same auth type - comparable setup experience * * Every integration gets a score, so the sidebar always has suggestions. * Ties are broken by alphabetical slug order for determinism. */ function getRelatedSlugs( slug: string, operations: Integration['operations'], authType: AuthType, integrationType: Integration['integrationType'], limit = 6 ): string[] { const currentOpNames = new Set(operations.map((o) => o.name.toLowerCase())) const currentOpWords = new Set( operations.flatMap((o) => o.name .toLowerCase() .split(/\s+/) .filter((w) => w.length > 3) ) ) return allIntegrations .reduce>((scored, i) => { if (i.slug === slug) return scored const sharedNames = i.operations.filter((o) => currentOpNames.has(o.name.toLowerCase()) ).length const sharedWords = i.operations.filter((o) => o.name .toLowerCase() .split(/\s+/) .some((w) => w.length > 3 && currentOpWords.has(w)) ).length const sameCategory = i.integrationType === integrationType ? 2 : 0 const sameAuth = i.authType === authType ? 1 : 0 scored.push({ slug: i.slug, score: sharedNames * 3 + sharedWords * 2 + sameCategory + sameAuth, }) return scored }, []) .sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug)) .slice(0, limit) .map(({ slug: s }) => s) } const AUTH_STEP: Record string> = { oauth: (name) => `Connect your ${name} account with one-click OAuth, with no credentials to copy.`, 'api-key': (name) => `Paste your ${name} API key to authenticate. You can find it in your ${name} account settings.`, none: () => 'No authentication is needed, so the block works as soon as you drop it in.', } /** Human-readable catalog refresh date for the visible last-updated line. */ const UPDATED_AT_DISPLAY = new Date(`${INTEGRATIONS_UPDATED_AT}T00:00:00Z`).toLocaleDateString( 'en-US', { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' } ) /** * Ensures autogenerated prose can be safely composed with a following sentence. */ function sentenceWithTerminalPunctuation(value: string): string { const trimmedValue = value.trim() return /[.!?]$/.test(trimmedValue) ? trimmedValue : `${trimmedValue}.` } function escapeRegex(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } /** * Server-side rewrite of bare integration names in a curated template prompt * to `@`-mention form (`Slack` → `@Slack`) so the prompt chips with brand * icons once it is populated into the Mothership home input after signup - * the home auto-mention pipeline only chips token-starting `@` mentions, so * curated prompts must opt in. * * Unlike the workspace surface, which calls `mentionifyIntegrations` from * `@/blocks/integration-matcher`, this runs only over the handful of names a * template actually references (its owner + `otherBlockTypes`) and lives in a * Server Component, so it never pulls the full block/tool/icon registry into * the landing client bundle. Whole-token, longest-first matching with * lookarounds mirrors the canonical matcher; idempotent on already-prefixed * names. */ function mentionifyPromptForNames(prompt: string, names: readonly string[]): string { const unique = Array.from(new Set(names.filter((n) => n.trim().length >= 2))).sort( (a, b) => b.length - a.length ) if (unique.length === 0) return prompt const regex = new RegExp( `(? `@${match}`) } /** Lowercases only the first character so acronyms in tool names survive. */ function lowercaseFirst(value: string): string { return value.charAt(0).toLowerCase() + value.slice(1) } /** * The ordered integration-icon chain for a template card - one tile per block * type in flow order, separated by arrows. Resolves each type through its * versioned aliases so v2/v3 blocks reuse the base icon and name. */ function TemplateIconRow({ allTypes }: { allTypes: string[] }) { return ( <> {allTypes.map((bt, idx) => { const resolvedBt = byType.get(bt) ? bt : byType.get(`${bt}_v2`) ? `${bt}_v2` : byType.get(`${bt}_v3`) ? `${bt}_v3` : bt const int = byType.get(resolvedBt) const ToolIcon = blockTypeToIconMap[resolvedBt] return ( {idx > 0 && ( )} ) })} ) } /** Joins items into readable prose: "a", "a and b", or "a, b, and c". */ function toProseList(items: string[]): string { if (items.length <= 1) return items[0] ?? '' if (items.length === 2) return `${items[0]} and ${items[1]}` return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}` } /** "a" vs "an" for a service name; U-names read as "you", so they take "a". */ function articleFor(name: string): string { return /^[aeio]/i.test(name) ? 'an' : 'a' } /** * Generates the per-integration FAQ. Answers lead with a direct answer and * carry integration-specific facts; catalog-generic questions live once on * the /integrations index FAQ instead of repeating across every page. */ function buildFAQs(integration: Integration, relatedNames: string[]): FAQItem[] { const { name, description, operations, triggers, authType } = integration const faqDescription = sentenceWithTerminalPunctuation(description) const opCount = operations.length const triggerCount = triggers.length const topOpNames = operations.slice(0, 5).map((o) => o.name) const firstOp = operations[0] const firstTrigger = triggers[0] const pairings = relatedNames.slice(0, 2) const toolsPhrase = `${opCount} ${name} tool${opCount === 1 ? '' : 's'}` const triggersPhrase = `${triggerCount} real-time trigger${triggerCount === 1 ? '' : 's'}` const capabilityPhrase = [ opCount > 0 ? toolsPhrase : null, triggerCount > 0 ? triggersPhrase : null, ] .filter((part): part is string => part !== null) .join(' and ') const triggerNames = triggers.map((t) => t.name) const triggerListPhrase = triggerCount > 6 ? `${triggerNames.slice(0, 6).join(', ')}, and ${triggerCount - 6} more` : toProseList(triggerNames) const firstTriggerWhen = firstTrigger?.description.match(/^trigger workflow (when .+)$/i)?.[1] const connectFinalStep = firstOp ? `Pick a tool such as "${firstOp.name}", wire up its inputs, and click Run, and your agent is live.` : triggerCount > 0 ? `Choose the ${name} event you want to listen for, and your agent runs automatically from then on.` : `Configure the block's inputs and click Run, and your agent is live.` const faqs: FAQItem[] = [ { question: `What is Sim's ${name} integration?`, answer: `Sim's ${name} integration ${capabilityPhrase ? `adds ${capabilityPhrase} to` : `connects ${name} to`} the AI agents you build in Sim's visual workflow builder — you build it all visually. ${faqDescription}${ pairings.length === 2 ? ` Teams often pair ${name} with ${pairings[0]} and ${pairings[1]} in the same agent.` : '' }`, }, ...(opCount > 0 ? [ { question: `What can I automate with ${name} in Sim?`, answer: `You can ${toProseList(topOpNames.map(lowercaseFirst))} with ${name} in Sim${ opCount > 5 ? `, plus ${opCount - 5} more ${name} tools listed on this page` : '' }. ${opCount === 1 ? 'It runs' : 'Each runs'} as a tool inside an AI agent block, so an agent can chain ${name} with ${ pairings.length === 2 ? `services like ${pairings[0]} and ${pairings[1]}` : 'any other connected service' } and apply LLM reasoning between steps.`, }, ] : []), { question: `How do I connect ${name} to Sim?`, answer: `Connecting ${name} takes about five minutes: (1) Create a free account at sim.ai. (2) Create an agent in your workspace. (3) Drag ${articleFor(name)} ${name} block onto the workflow builder. (4) ${AUTH_STEP[authType](name)} (5) ${connectFinalStep}`, }, ...(firstOp && opCount >= 2 ? [ { question: `How do I ${lowercaseFirst(firstOp.name)} with ${name} in Sim?`, answer: `Add ${articleFor(name)} ${name} block to your agent and select "${firstOp.name}" as the tool.${ firstOp.description ? ` ${sentenceWithTerminalPunctuation(firstOp.description)}` : '' } Fill in the required fields. Inputs can reference outputs from earlier steps, such as text generated by an AI block or data fetched from another integration, and you build it all visually.`, }, ] : []), ...(triggerCount > 0 ? [ { question: `How do I trigger a Sim agent from ${name} automatically?`, answer: `Add ${articleFor(name)} ${name} trigger block to your agent and copy its generated webhook URL into ${name}'s webhook settings. Sim supports ${triggersPhrase} for ${name}: ${triggerListPhrase}. Once configured, every matching ${name} event starts your agent instantly, no polling, no delay.`, }, { question: `What data does Sim receive when a ${name} event triggers an agent?`, answer: `Sim receives the full event payload ${name} sends, typically the record or object that changed, plus metadata like the event type and timestamp.${ firstTriggerWhen ? ` For example, the "${firstTrigger.name}" trigger fires ${sentenceWithTerminalPunctuation(firstTriggerWhen)}` : '' } Every field in the payload is available as a variable you can pass to AI blocks, conditions, or other integrations.`, }, ] : []), ] return faqs } export async function generateStaticParams() { return allIntegrations.map((i) => ({ slug: i.slug })) } export async function generateMetadata({ params, }: { params: Promise<{ slug: string }> }): Promise { const { slug } = await params const integration = bySlug.get(slug) if (!integration) return {} const { name, description, operations } = integration const opSample = operations .slice(0, 3) .map((o) => o.name) .join(', ') const categoryLabel = formatIntegrationType(integration.integrationType) const seo = INTEGRATION_SEO[slug] const metaDesc = seo?.description ?? `Automate ${name} with AI agents in Sim. ${sentenceWithTerminalPunctuation(truncate(description, 100))} Free to start.` return { // A hand-authored SEO title is rendered verbatim (it carries its own brand // suffix); otherwise the bare name flows through the root `%s | Sim` template. title: seo?.title ? { absolute: seo.title } : `${name} Integration`, description: metaDesc, keywords: seo?.keywords ?? [ `${name} automation`, `${name} integration`, `automate ${name}`, `connect ${name}`, `${name} AI agent`, `${name} AI automation`, ...(opSample ? [`${name} ${opSample}`] : []), `${categoryLabel} integration`, ...(integration.tags ?? []).map((tag) => `${name} ${tag.replace(/-/g, ' ')}`), ...(integration.triggerCount > 0 ? [`${name} webhook`, `${name} trigger`] : []), 'AI workspace integrations', 'AI agent integrations', 'AI agent builder', ], // og:image/twitter:image come from the sibling opengraph-image.tsx - // Next serves it at a hash-suffixed URL, so hardcoding it here 404s. openGraph: { title: seo?.title ?? `${name} Integration | Sim AI Workspace`, description: seo?.description ?? `Connect ${name} to ${INTEGRATION_COUNT - 1}+ tools using AI agents. ${sentenceWithTerminalPunctuation(truncate(description, 100))}`, url: `${baseUrl}/integrations/${slug}`, type: 'website', }, twitter: { card: 'summary_large_image', title: seo?.title ?? `${name} Integration | Sim`, description: seo?.description ?? `Automate ${name} with AI agents in Sim. Connect to ${INTEGRATION_COUNT - 1}+ tools. Free to start.`, }, alternates: { canonical: `${baseUrl}/integrations/${slug}` }, } } export default async function IntegrationPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params const integration = bySlug.get(slug) if (!integration) notFound() const { name, description, longDescription, bgColor, docsUrl, operations, triggers, authType } = integration const landingContent = integration.landingContent const seo = INTEGRATION_SEO[slug] const overviewBody = seo?.overview ?? longDescription const IconComponent = blockTypeToIconMap[integration.type] const categoryLabel = formatIntegrationType(integration.integrationType) const relatedSlugs = getRelatedSlugs(slug, operations, authType, integration.integrationType) const relatedIntegrations = relatedSlugs .map((s) => bySlug.get(s)) .filter((i): i is Integration => i !== undefined) const faqs = buildFAQs( integration, relatedIntegrations.map((i) => i.name) ) const matchingTemplates = getTemplatesForBlock(integration.type) const breadcrumbJsonLd = { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: 'Home', item: baseUrl }, { '@type': 'ListItem', position: 2, name: 'Integrations', item: `${baseUrl}/integrations`, }, { '@type': 'ListItem', position: 3, name, item: `${baseUrl}/integrations/${slug}` }, ], } const softwareAppJsonLd = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: `${name} Integration`, description, url: `${baseUrl}/integrations/${slug}`, applicationCategory: 'BusinessApplication', applicationSubCategory: categoryLabel, operatingSystem: 'Web', featureList: operations.map((o) => o.name), ...(integration.tags?.length ? { keywords: integration.tags.map((tag) => tag.replace(/-/g, ' ')).join(', ') } : {}), dateModified: INTEGRATIONS_UPDATED_AT, offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, } const faqJsonLd = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faqs.map(({ question, answer }) => ({ '@type': 'Question', name: question, acceptedAnswer: { '@type': 'Answer', text: answer }, })), } return (
{/* Hero */}
{/* Hero content */}

{seo?.tagline ?? description}

{name} is a {categoryLabel} integration for Sim, the AI workspace where teams build and deploy AI agents. Sim's {name} integration provides{' '} {[ operations.length > 0 ? `${operations.length} ${name} tool${operations.length === 1 ? '' : 's'}` : null, triggers.length > 0 ? `${triggers.length} real-time trigger${triggers.length === 1 ? '' : 's'}` : null, ] .filter((part): part is string => part !== null) .join(' and ') || `a ${name} connection`}{' '} that AI agents can use inside Sim's visual workflow builder.{' '} {authType === 'oauth' ? `${name} connects with one-click OAuth.` : authType === 'api-key' ? `${name} connects with an API key.` : `${name} requires no authentication.`}{' '} Free to start at sim.ai.

{/* CTAs */}
Start building free View docs

Last updated

{/* Full-width divider */}
{/* Border-railed content */}
{/* Overview */} {overviewBody && ( <>

Overview

{overviewBody}

)} {/* Install / Add to workspace (integration-specific) */} {landingContent?.install && ( <>

{landingContent.install.heading}

{landingContent.install.intro}

    {landingContent.install.steps.map((item, index) => (
  1. {item.title}

    {item.body}

  2. ))}
Add to {name}
)} {/* Privacy & data (integration-specific) */} {landingContent?.privacy && ( <>

Privacy & data

{landingContent.privacy.body}{' '} Privacy Policy .

)} {/* AI-generated content disclaimer (integration-specific) */} {landingContent?.aiDisclaimer && ( <>

AI-generated content

{landingContent.aiDisclaimer}

)} {/* How to automate */}

How to automate {name} with Sim

    {[ { step: '01', title: 'Create a free account', body: 'Sign up at sim.ai in seconds. No credit card required. Your workspace is ready immediately.', }, { step: '02', title: `Add ${articleFor(name)} ${name} block`, body: authType === 'oauth' ? `Open your workspace, drag ${articleFor(name)} ${name} block onto the workflow builder, and connect your account with one-click OAuth.` : authType === 'api-key' ? `Open your workspace, drag ${articleFor(name)} ${name} block onto the workflow builder, and paste in your ${name} API key.` : `Open your workspace, drag ${articleFor(name)} ${name} block onto the workflow builder. No authentication is needed.`, }, { step: '03', title: 'Configure, connect, and run', body: `Pick the tool you need, wire in an AI agent for reasoning or data transformation, and run. Your ${name} automation is live.`, }, ].map(({ step, title, body }) => (
  1. {title}

    {body}

  2. ))}
{/* Triggers - rows */} {triggers.length > 0 && (

{seo?.triggersIntro ?? ( <> Connect {articleFor(name)} {name} webhook to Sim and your agent runs the instant an event happens, no polling, no delay. )}

{triggers.map((trigger) => (

{trigger.name}

{trigger.description && (

{trigger.description}

)}
))}
)} {/* Workflow templates - horizontal cards */} {matchingTemplates.length > 0 && (

Agent templates

{seo?.templatesIntro ?? `Ready-to-use templates featuring ${name}. Click any to build it instantly.`}

{(() => { const isOdd = matchingTemplates.length % 2 === 1 const pairedTemplates = isOdd ? matchingTemplates.slice(0, -1) : matchingTemplates const lastTemplate = isOdd ? matchingTemplates[matchingTemplates.length - 1] : null const resolveTypes = (template: (typeof matchingTemplates)[number]) => [ integration.type, ...template.otherBlockTypes, ] const resolveDisplayName = (bt: string): string | null => { const resolvedBt = byType.get(bt) ? bt : byType.get(`${bt}_v2`) ? `${bt}_v2` : byType.get(`${bt}_v3`) ? `${bt}_v3` : bt return byType.get(resolvedBt)?.name ?? null } /** * The curated template prompt rewritten so the integrations it * references chip in the home input after signup. Computed * server-side from the template's own integration set - never the * full registry - so the visible card text stays raw while the * stored prompt opts into mention treatment. */ const storedPrompt = (template: (typeof matchingTemplates)[number]) => mentionifyPromptForNames( template.prompt, resolveTypes(template) .map(resolveDisplayName) .filter((n): n is string => n !== null) ) return ( <> {/* Paired rows of 2 */} {Array.from({ length: Math.ceil(pairedTemplates.length / 2) }, (_, rowIdx) => { const row = pairedTemplates.slice(rowIdx * 2, rowIdx * 2 + 2) return (
) })} {/* Last template as a full-width row when odd */} {lastTemplate && ( <>

{lastTemplate.title}

{lastTemplate.prompt}

)} ) })()}
)} {/* Supported tools - rows */} {operations.length > 0 && (

Supported tools

{operations.length} {name} tool{operations.length === 1 ? '' : 's'} available in Sim {seo?.toolsSubtitleSuffix ?? ''}

{operations.map((op) => (

{op.name}

{op.description && (

{op.description}

)}
))}
)} {/* FAQ - full width */}

Frequently asked questions

{/* Related integrations - horizontal cards with vertical dividers (blog featured pattern) */} {relatedIntegrations.length > 0 && ( <>
)} {/* Bottom CTA */}
Sim

Start automating {name} today

Build your first AI agent with {name} in minutes. Connect to every tool your team uses. Free to start, no credit card required.

Build for free
{/* Closing full-width divider */}
) }