diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 440333adea..19f650044c 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4140,7 +4140,7 @@ export function IncidentioIcon(props: SVGProps) { export function InfisicalIcon(props: SVGProps) { return ( - + ) { fill='currentColor' width='800px' height='800px' - viewBox='0 0 32 32' + viewBox='-1 9.5 34 13' version='1.1' xmlns='http://www.w3.org/2000/svg' > diff --git a/apps/docs/content/docs/en/tools/microsoft_ad.mdx b/apps/docs/content/docs/en/tools/microsoft_ad.mdx index 10095e0888..3f16dfd31a 100644 --- a/apps/docs/content/docs/en/tools/microsoft_ad.mdx +++ b/apps/docs/content/docs/en/tools/microsoft_ad.mdx @@ -5,7 +5,7 @@ description: Manage users and groups in Azure AD (Microsoft Entra ID) import { BlockInfoCard } from "@/components/ui/block-info-card" - diff --git a/apps/docs/content/docs/en/tools/okta.mdx b/apps/docs/content/docs/en/tools/okta.mdx index 09b2f4ec0a..04eaa51532 100644 --- a/apps/docs/content/docs/en/tools/okta.mdx +++ b/apps/docs/content/docs/en/tools/okta.mdx @@ -5,7 +5,7 @@ description: Manage users and groups in Okta import { BlockInfoCard } from "@/components/ui/block-info-card" - @@ -29,6 +29,7 @@ In Sim, the Okta integration enables your agents to automate identity management If you encounter issues with the Okta integration, contact us at [help@sim.ai](mailto:help@sim.ai) {/* MANUAL-CONTENT-END */} + ## Usage Instructions Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership. diff --git a/apps/sim/app/(home)/components/enterprise/enterprise.tsx b/apps/sim/app/(home)/components/enterprise/enterprise.tsx index 17c36b9b17..63f37fad98 100644 --- a/apps/sim/app/(home)/components/enterprise/enterprise.tsx +++ b/apps/sim/app/(home)/components/enterprise/enterprise.tsx @@ -612,7 +612,9 @@ export default function Enterprise() { Ready for growth?

Book a demo diff --git a/apps/sim/app/(home)/components/pricing/pricing.tsx b/apps/sim/app/(home)/components/pricing/pricing.tsx index cc803a4af3..77c4b9b46f 100644 --- a/apps/sim/app/(home)/components/pricing/pricing.tsx +++ b/apps/sim/app/(home)/components/pricing/pricing.tsx @@ -78,7 +78,7 @@ const PRICING_TIERS: PricingTier[] = [ 'SSO & SCIM · SOC2 & HIPAA', 'Self hosting · Dedicated support', ], - cta: { label: 'Book a demo', href: '/contact' }, + cta: { label: 'Book a demo', href: 'https://form.typeform.com/to/jqCO12pF' }, }, ] @@ -125,12 +125,14 @@ function PricingCard({ tier }: PricingCardProps) {

{isEnterprise ? ( - {tier.cta.label} - + ) : isPro ? ( pricing (Pricing) -> testimonials (Testimonials). */ export default async function Landing() { - const allPosts = await getAllPostMeta() - const featuredPost = allPosts.find((p) => p.featured) ?? allPosts[0] - const recentPosts = allPosts.filter((p) => p !== featuredPost).slice(0, 4) - const blogPosts = [featuredPost, ...recentPosts] - .filter(Boolean) - .map((p) => ({ slug: p.slug, title: p.title, ogImage: p.ogImage })) + const blogPosts = await getNavBlogPosts() return (
diff --git a/apps/sim/app/(landing)/blog/layout.tsx b/apps/sim/app/(landing)/blog/layout.tsx index 0387f7ea60..59f7adaf70 100644 --- a/apps/sim/app/(landing)/blog/layout.tsx +++ b/apps/sim/app/(landing)/blog/layout.tsx @@ -1,7 +1,9 @@ +import { getNavBlogPosts } from '@/lib/blog/registry' import Footer from '@/app/(home)/components/footer/footer' import Navbar from '@/app/(home)/components/navbar/navbar' -export default function StudioLayout({ children }: { children: React.ReactNode }) { +export default async function StudioLayout({ children }: { children: React.ReactNode }) { + const blogPosts = await getNavBlogPosts() const orgJsonLd = { '@context': 'https://schema.org', '@type': 'Organization', @@ -34,7 +36,7 @@ export default function StudioLayout({ children }: { children: React.ReactNode } dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }} />
- +
{children}
diff --git a/apps/sim/app/(landing)/components/external-redirect.tsx b/apps/sim/app/(landing)/components/external-redirect.tsx new file mode 100644 index 0000000000..9557e5a888 --- /dev/null +++ b/apps/sim/app/(landing)/components/external-redirect.tsx @@ -0,0 +1,18 @@ +'use client' + +import { useEffect } from 'react' + +interface ExternalRedirectProps { + url: string +} + +/** Redirects to an external URL when it is configured via an environment variable. */ +export default function ExternalRedirect({ url }: ExternalRedirectProps) { + useEffect(() => { + if (url?.startsWith('http')) { + window.location.href = url + } + }, [url]) + + return null +} diff --git a/apps/sim/app/(landing)/components/index.ts b/apps/sim/app/(landing)/components/index.ts index 78f1d14cc4..483d59ef82 100644 --- a/apps/sim/app/(landing)/components/index.ts +++ b/apps/sim/app/(landing)/components/index.ts @@ -1,4 +1,5 @@ import Background from '@/app/(landing)/components/background/background' +import ExternalRedirect from '@/app/(landing)/components/external-redirect' import Footer from '@/app/(landing)/components/footer/footer' import Hero from '@/app/(landing)/components/hero/hero' import Integrations from '@/app/(landing)/components/integrations/integrations' @@ -20,4 +21,5 @@ export { Footer, StructuredData, LegalLayout, + ExternalRedirect, } diff --git a/apps/sim/app/(landing)/components/legal-layout.tsx b/apps/sim/app/(landing)/components/legal-layout.tsx index 2e115c0a28..552bb5dd9e 100644 --- a/apps/sim/app/(landing)/components/legal-layout.tsx +++ b/apps/sim/app/(landing)/components/legal-layout.tsx @@ -1,3 +1,4 @@ +import { getNavBlogPosts } from '@/lib/blog/registry' import { isHosted } from '@/lib/core/config/feature-flags' import Footer from '@/app/(home)/components/footer/footer' import Navbar from '@/app/(home)/components/navbar/navbar' @@ -7,11 +8,13 @@ interface LegalLayoutProps { children: React.ReactNode } -export default function LegalLayout({ title, children }: LegalLayoutProps) { +export default async function LegalLayout({ title, children }: LegalLayoutProps) { + const blogPosts = await getNavBlogPosts() + return (
- +
diff --git a/apps/sim/app/(landing)/integrations/[slug]/page.tsx b/apps/sim/app/(landing)/integrations/[slug]/page.tsx index 693e0fee68..743fc2f006 100644 --- a/apps/sim/app/(landing)/integrations/[slug]/page.tsx +++ b/apps/sim/app/(landing)/integrations/[slug]/page.tsx @@ -5,7 +5,6 @@ import { TEMPLATES } from '@/app/workspace/[workspaceId]/home/components/templat import { IntegrationIcon } from '../components/integration-icon' import { blockTypeToIconMap } from '../data/icon-mapping' import integrations from '../data/integrations.json' -import { POPULAR_WORKFLOWS } from '../data/popular-workflows' import type { AuthType, FAQItem, Integration } from '../data/types' import { IntegrationFAQ } from './components/integration-faq' import { TemplateCardButton } from './components/template-card-button' @@ -14,44 +13,52 @@ const allIntegrations = integrations as Integration[] const INTEGRATION_COUNT = allIntegrations.length /** Fast O(1) lookups — avoids repeated linear scans inside render loops. */ -const byName = new Map(allIntegrations.map((i) => [i.name, i])) const bySlug = new Map(allIntegrations.map((i) => [i.slug, i])) const byType = new Map(allIntegrations.map((i) => [i.type, i])) -/** Returns workflow pairs that feature the given integration on either side. */ -function getPairsFor(name: string) { - return POPULAR_WORKFLOWS.filter((p) => p.from === name || p.to === name) -} - /** * Returns up to `limit` related integration slugs. * - * Scoring: - * +100 — integration appears as a workflow pair partner (explicit editorial signal) - * +N — N operation names shared with the current integration (semantic similarity) + * Scoring (additive): + * +3 per shared operation name — strongest signal (same capability) + * +2 per shared operation word — weaker signal (e.g. both have "create" ops) + * +1 same auth type — comparable setup experience * - * This means genuine partners always rank first; operation-similar integrations - * (e.g. Slack → Teams → Discord for "Send Message") fill the rest organically. + * Every integration gets a score, so the sidebar always has suggestions. + * Ties are broken by alphabetical slug order for determinism. */ function getRelatedSlugs( - name: string, slug: string, operations: Integration['operations'], + authType: AuthType, limit = 6 ): string[] { - const partners = new Set(getPairsFor(name).map((p) => (p.from === name ? p.to : p.from))) - const currentOps = new Set(operations.map((o) => o.name.toLowerCase())) + 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 .filter((i) => i.slug !== slug) - .map((i) => ({ - slug: i.slug, - score: - (partners.has(i.name) ? 100 : 0) + - i.operations.filter((o) => currentOps.has(o.name.toLowerCase())).length, - })) - .filter(({ score }) => score > 0) - .sort((a, b) => b.score - a.score) + .map((i) => { + 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 sameAuth = i.authType === authType ? 1 : 0 + return { slug: i.slug, score: sharedNames * 3 + sharedWords * 2 + sameAuth } + }) + .sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug)) .slice(0, limit) .map(({ slug: s }) => s) } @@ -70,7 +77,6 @@ function buildFAQs(integration: Integration): FAQItem[] { const { name, description, operations, triggers, authType } = integration const topOps = operations.slice(0, 5) const topOpNames = topOps.map((o) => o.name) - const pairs = getPairsFor(name) const authStep = AUTH_STEP[authType] const faqs: FAQItem[] = [ @@ -89,6 +95,10 @@ function buildFAQs(integration: Integration): FAQItem[] { question: `How do I connect ${name} to Sim?`, answer: `Getting started takes under five minutes: (1) Create a free account at sim.ai. (2) Open a new workflow. (3) Drag a ${name} block onto the canvas. (4) ${authStep} (5) Choose the tool you want to use, wire it to the inputs you need, and click Run. Your automation is live.`, }, + { + question: `Can I use ${name} as a tool inside an AI agent in Sim?`, + answer: `Yes — this is one of Sim's core capabilities. Instead of hard-coding when and how ${name} is used, you give an AI agent access to ${name} tools and describe the goal in plain language. The agent decides which tools to call, in what order, and how to handle the results. This means your automation adapts to context rather than breaking when inputs change.`, + }, ...(topOpNames.length >= 2 ? [ { @@ -97,19 +107,15 @@ function buildFAQs(integration: Integration): FAQItem[] { }, ] : []), - ...(pairs.length > 0 + ...(triggers.length > 0 ? [ { - question: `Can I connect ${name} to ${pairs[0].from === name ? pairs[0].to : pairs[0].from} with Sim?`, - answer: `Yes. ${pairs[0].description} In Sim, you set this up by adding both a ${name} block and a ${pairs[0].from === name ? pairs[0].to : pairs[0].from} block to the same workflow and connecting them through an AI agent that orchestrates the logic between them.`, + question: `How do I trigger a Sim workflow from ${name} automatically?`, + answer: `In your Sim workflow, switch the ${name} block to Trigger mode and copy the generated webhook URL. Paste that URL into ${name}'s webhook settings and select the events you want to listen for (${triggers.map((t) => t.name).join(', ')}). From that point on, every matching event in ${name} instantly fires your workflow — no polling, no delay.`, }, - ] - : []), - ...(triggers.length > 0 - ? [ { - question: `Can ${name} trigger a Sim workflow automatically?`, - answer: `Yes. ${name} supports ${triggers.length} webhook trigger${triggers.length === 1 ? '' : 's'} that can instantly start a Sim workflow: ${triggers.map((t) => t.name).join(', ')}. No polling needed — the workflow fires the moment the event occurs in ${name}.`, + question: `What data does Sim receive when a ${name} event triggers a workflow?`, + answer: `When ${name} fires a webhook, Sim receives the full event payload that ${name} sends — typically the record or object that changed, along with metadata like the event type and timestamp. Inside your workflow, every field from that payload is available as a variable you can pass to AI agents, conditions, or other integrations.`, }, ] : []), @@ -190,11 +196,10 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl const IconComponent = blockTypeToIconMap[integration.type] const faqs = buildFAQs(integration) - const relatedSlugs = getRelatedSlugs(name, slug, operations) + const relatedSlugs = getRelatedSlugs(slug, operations, authType) const relatedIntegrations = relatedSlugs .map((s) => bySlug.get(s)) .filter((i): i is Integration => i !== undefined) - const featuredPairs = getPairsFor(name) const baseType = integration.type.replace(/_v\d+$/, '') const matchingTemplates = TEMPLATES.filter( (t) => @@ -420,15 +425,18 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl {triggers.length > 0 && (

- Triggers + Real-time triggers

-

- These events in {name} can automatically start a Sim workflow — no polling - required. +

+ Connect a {name} webhook to Sim and your workflow fires the instant an event + happens — no polling, no delay. Sim receives the full event payload and makes + every field available as a variable inside your workflow.

+ + {/* Event cards */}
    {triggers.map((trigger) => (
  • - Trigger + Event

{trigger.name}

@@ -462,73 +470,6 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl )} - {/* Popular workflows featuring this integration */} - {featuredPairs.length > 0 && ( -
-

- Popular workflows with {name} -

-

- Common automation patterns teams build on Sim using {name}. -

-
    - {featuredPairs.map(({ from, to, headline, description: desc }) => { - const fromInt = byName.get(from) - const toInt = byName.get(to) - const FromIcon = fromInt ? blockTypeToIconMap[fromInt.type] : undefined - const ToIcon = toInt ? blockTypeToIconMap[toInt.type] : undefined - const fromBg = fromInt?.bgColor ?? '#6B7280' - const toBg = toInt?.bgColor ?? '#6B7280' - - return ( -
  • -
    -
    - - {FromIcon && ( - - - - {ToIcon && ( - -
    -

    {headline}

    -

    {desc}

    -
    -
  • - ) - })} -
-
- )} - {/* Workflow templates */} {matchingTemplates.length > 0 && (
@@ -539,7 +480,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl Ready-to-use workflows featuring {name}. Click any to build it instantly.

    {matchingTemplates.map((template) => { @@ -551,34 +492,49 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl return (
  • - {/* Integration icons */} -
    - {allTypes.map((bt) => { - const int = byType.get(bt) + {/* Integration pills row */} +
    + {allTypes.map((bt, idx) => { + // Templates may use unversioned keys (e.g. "notion") while the + // icon map has versioned keys ("notion_v2") — fall back to _v2. + const resolvedBt = byType.get(bt) + ? bt + : byType.get(`${bt}_v2`) + ? `${bt}_v2` + : bt + const int = byType.get(resolvedBt) const intName = int?.name ?? bt return ( - + + {idx > 0 && ( + + )} + + + ) })}
    -

    +

    {template.title}

    - +

    Try this workflow → - +

  • ) @@ -660,40 +616,34 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
    Free to start
- - Get started free - -
- - {/* Docs */} -
-

Documentation

-

- Full API reference, authentication setup, and usage examples for {name}. -

- - docs.sim.ai - - + Get started free + + + View docs + + +
{/* Related integrations — internal linking for SEO */} @@ -738,6 +688,43 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl aria-labelledby='cta-heading' className='mt-20 rounded-xl border border-[#2A2A2A] bg-[#242424] p-8 text-center sm:p-12' > + {/* Logo pair: Sim × Integration */} +
+ Sim +
+
+

{ bgColor: string @@ -33,9 +32,6 @@ export function IntegrationIcon({ as: Tag = 'div', ...rest }: IntegrationIconProps) { - const isLight = isLightBg(bgColor) - const fgColor = isLight ? 'text-[#1C1C1C]' : 'text-white' - return ( {Icon ? ( - + ) : ( - + {name.charAt(0)} )} diff --git a/apps/sim/app/(landing)/integrations/components/request-integration-modal.tsx b/apps/sim/app/(landing)/integrations/components/request-integration-modal.tsx new file mode 100644 index 0000000000..a5acd60fe0 --- /dev/null +++ b/apps/sim/app/(landing)/integrations/components/request-integration-modal.tsx @@ -0,0 +1,179 @@ +'use client' + +import { useCallback, useState } from 'react' +import { + Button, + Input, + Label, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Textarea, +} from '@/components/emcn' + +type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error' + +export function RequestIntegrationModal() { + const [open, setOpen] = useState(false) + const [status, setStatus] = useState('idle') + + const [integrationName, setIntegrationName] = useState('') + const [email, setEmail] = useState('') + const [useCase, setUseCase] = useState('') + + const resetForm = useCallback(() => { + setIntegrationName('') + setEmail('') + setUseCase('') + setStatus('idle') + }, []) + + const handleOpenChange = useCallback( + (nextOpen: boolean) => { + setOpen(nextOpen) + if (!nextOpen) resetForm() + }, + [resetForm] + ) + + const handleSubmit = useCallback( + async (e: React.FormEvent) => { + e.preventDefault() + if (!integrationName.trim() || !email.trim()) return + + setStatus('submitting') + + try { + const res = await fetch('/api/help/integration-request', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + integrationName: integrationName.trim(), + email: email.trim(), + useCase: useCase.trim() || undefined, + }), + }) + + if (!res.ok) throw new Error('Request failed') + + setStatus('success') + setTimeout(() => setOpen(false), 1500) + } catch { + setStatus('error') + } + }, + [integrationName, email, useCase] + ) + + const canSubmit = integrationName.trim() && email.trim() && status === 'idle' + + return ( + <> + + + + + Request an Integration + + {status === 'success' ? ( + +
+
+ + + +
+

+ Request submitted — we'll follow up at{' '} + {email}. +

+
+
+ ) : ( +
+ +
+
+ + setIntegrationName(e.target.value)} + maxLength={200} + autoComplete='off' + required + /> +
+ +
+ + setEmail(e.target.value)} + autoComplete='email' + required + /> +
+ +
+ +