feat(slack): add install + privacy section to integration landing page (#4799)

* feat(slack): add install + privacy section to integration landing page

Adds a hand-authored, slug-keyed landing-content module (separate from the generated integrations.json so it survives regeneration) and renders an install walkthrough + privacy-policy link on integration pages when present. Also refreshes generated docs (data-enrichment entry, icon mappings, tool mdx).

* fix(landing): render privacy section independently, align CTA analytics label

* docs(landing): clarify the Slack install button is behind sign-in

* refactor(landing): bake integration landing content into generated json via docs-gen

Moves landing content (install walkthrough + privacy) out of a render-time augment and into the generation pipeline: generate-docs reads the pure-data content map and writes landingContent into integrations.json, so the page reads a single source (integration.landingContent). Canonical types live in integrations/data/types.ts.
This commit is contained in:
Waleed
2026-05-29 16:33:30 -07:00
committed by GitHub
parent 925dd871e8
commit e1e773f487
13 changed files with 254 additions and 32 deletions
+17 -1
View File
@@ -1,6 +1,22 @@
import type { SVGProps } from 'react'
import { useId } from 'react'
export function EnrichmentIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='0 0 24 24'
fill='currentColor'
role='img'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M12 2.5l1.9 4.6 4.6 1.9-4.6 1.9L12 15.5l-1.9-4.6L5.5 9l4.6-1.9L12 2.5z' />
<path d='M18.5 14l.95 2.3 2.3.95-2.3.95L18.5 20.5l-.95-2.3-2.3-.95 2.3-.95.95-2.3z' />
<path d='M5.5 14.5l.7 1.7 1.7.7-1.7.7-.7 1.7-.7-1.7-1.7-.7 1.7-.7.7-1.7z' />
</svg>
)
}
export function AgentMailIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 350 363' fill='none' xmlns='http://www.w3.org/2000/svg'>
@@ -4731,7 +4747,7 @@ export function ZoomInfoIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='0 0 65 65'
viewBox='0.272461 0.272461 64 64'
fill='none'
xmlns='http://www.w3.org/2000/svg'
aria-hidden='true'
+3 -2
View File
@@ -20,7 +20,6 @@ import {
AshbyIcon,
AthenaIcon,
AttioIcon,
AzureDevOpsIcon,
AzureIcon,
BoxCompanyIcon,
BrainIcon,
@@ -53,6 +52,7 @@ import {
ElasticsearchIcon,
ElevenLabsIcon,
EmailBisonIcon,
EnrichmentIcon,
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
@@ -232,7 +232,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
ashby: AshbyIcon,
athena: AthenaIcon,
attio: AttioIcon,
azure_devops: AzureDevOpsIcon,
azure_devops: AzureIcon,
box: BoxCompanyIcon,
brandfetch: BrandfetchIcon,
brightdata: BrightDataIcon,
@@ -265,6 +265,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
elevenlabs: ElevenLabsIcon,
emailbison: EmailBisonIcon,
enrich: EnrichSoIcon,
enrichment: EnrichmentIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
extend: ExtendIcon,
+4 -3
View File
@@ -252,7 +252,7 @@ Update an existing contact in your Apollo database
| `mobile_phone` | string | No | Mobile phone number |
| `home_phone` | string | No | Home phone number |
| `other_phone` | string | No | Alternative phone number |
| `typed_custom_fields` | json | No | Custom field values keyed by custom field ID \(accepted by Apollo but not officially documented for PATCH /contacts/\{id\}\) |
| `typed_custom_fields` | json | No | Custom field values keyed by custom field ID |
#### Output
@@ -440,6 +440,7 @@ Update up to 1000 existing accounts at once in your Apollo database (higher limi
| `account_ids` | array | No | Array of account IDs to update with the same values \(max 1000\). Use with name/owner_id for uniform updates. Use either this OR account_attributes. |
| `name` | string | No | When using account_ids, apply this name to all accounts |
| `owner_id` | string | No | When using account_ids, apply this owner to all accounts |
| `account_stage_id` | string | No | When using account_ids, apply this account stage to all accounts |
| `account_attributes` | json | No | Array of account objects with individual updates \(each must include id\). Example: \[\{"id": "acc1", "name": "Acme", "owner_id": "u1", "account_stage_id": "s1", "typed_custom_fields": \{"field_id": "value"\}\}\] |
| `async` | boolean | No | When true, processes the update asynchronously. Only supported when using account_ids; returns 422 if used with account_attributes. |
@@ -447,9 +448,9 @@ Update up to 1000 existing accounts at once in your Apollo database (higher limi
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `accounts` | json | Updated accounts \(synchronous response, ≤100 accounts\): \[\{id, account_stage_id, ...\}\] |
| `accounts` | json | Updated accounts \(synchronous response\): \[\{id, account_stage_id, ...\}\] |
| `account_ids` | json | IDs of accounts that were updated |
| `entity_progress_job` | json | Async job descriptor \(&gt;100 accounts or async=true\) |
| `entity_progress_job` | json | Async job descriptor \(when async=true is passed with account_ids\) |
| `job_id` | string | Async job ID extracted from entity_progress_job |
| `message` | string | Optional confirmation message from Apollo |
+2 -11
View File
@@ -1,5 +1,5 @@
---
title: Enrichment
title: Data Enrichment
description: Enrich data with a Sim enrichment
---
@@ -31,18 +31,9 @@ Run a Sim enrichment (e.g. Work Email, Phone Number) and return its outputs
#### Output
The exact fields depend on which enrichment ran. `matched` and `provider` are always present.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `matched` | boolean | Whether the enrichment found a result |
| `provider` | string | Provider whose result was returned (e.g. "Hunter", "People Data Labs"); `null` on no match |
| `email` | string | Work email address (Work Email enrichment) |
| `phone` | string | Phone number (Phone Number enrichment) |
| `domain` | string | Website domain (Company Domain enrichment) |
| `industry` | string | Industry (Company Info enrichment) |
| `employeeCount` | number | Employee count (Company Info enrichment) |
| `foundedYear` | number | Founded year (Company Info enrichment) |
| `description` | string | Company description (Company Info enrichment) |
| `provider` | string | Provider whose result was returned \(e.g. "Hunter", "People Data Labs"\) |
+20 -7
View File
@@ -80,17 +80,19 @@ Retrieve details of a previously sent email by its ID
| --------- | ---- | ----------- |
| `id` | string | Email ID |
| `from` | string | Sender email address |
| `to` | json | Recipient email addresses |
| `to` | array | Recipient email addresses |
| `subject` | string | Email subject |
| `html` | string | HTML email content |
| `text` | string | Plain text email content |
| `cc` | json | CC email addresses |
| `bcc` | json | BCC email addresses |
| `replyTo` | json | Reply-to email addresses |
| `cc` | array | CC email addresses |
| `bcc` | array | BCC email addresses |
| `replyTo` | array | Reply-to email addresses |
| `lastEvent` | string | Last event status \(e.g., delivered, bounced\) |
| `createdAt` | string | Email creation timestamp |
| `scheduledAt` | string | Scheduled send timestamp |
| `tags` | json | Email tags as name-value pairs |
| `tags` | array | Email tags as name-value pairs |
| ↳ `name` | string | Tag name |
| ↳ `value` | string | Tag value |
### `resend_create_contact`
@@ -126,7 +128,13 @@ List all contacts in Resend
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `contacts` | json | Array of contacts with id, email, first_name, last_name, created_at, unsubscribed |
| `contacts` | array | Array of contacts |
| ↳ `id` | string | Contact ID |
| ↳ `email` | string | Contact email address |
| ↳ `first_name` | string | Contact first name |
| ↳ `last_name` | string | Contact last name |
| ↳ `created_at` | string | Contact creation timestamp |
| ↳ `unsubscribed` | boolean | Whether the contact is unsubscribed |
| `hasMore` | boolean | Whether there are more contacts to retrieve |
### `resend_get_contact`
@@ -203,7 +211,12 @@ List all verified domains in your Resend account
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `domains` | json | Array of domains with id, name, status, region, and createdAt |
| `domains` | array | Array of domains |
| ↳ `id` | string | Domain ID |
| ↳ `name` | string | Domain name |
| ↳ `status` | string | Domain verification status |
| ↳ `region` | string | Region the domain is configured in |
| ↳ `createdAt` | string | Domain creation timestamp |
| `hasMore` | boolean | Whether there are more domains to retrieve |
+1 -1
View File
@@ -149,7 +149,7 @@ Enrich a company by name, domain, LinkedIn ID, or LinkedIn slug with detailed fi
| `company_region` | string | State/region |
| `company_postal_code` | string | Postal code |
| `company_country` | string | Country |
| `credits` | json | Credits deducted for this enrichment \(api_credits: \{ total, email_credits, phone_credits, scrape_credits \}\) |
| `credits` | json | Credits deducted for this enrichment \(api_credits: \{ total, company_credits \}\) |
### `wiza_start_individual_reveal`
+2 -2
View File
@@ -119,7 +119,7 @@ Enrich up to 25 companies in one request with detailed firmographics, industry,
| `clientId` | string | Yes | ZoomInfo OAuth client ID |
| `clientSecret` | string | Yes | ZoomInfo OAuth client secret |
| `matchCompanyInput` | string | Yes | JSON array \(1-25 items\) of company matching criteria, e.g. \[\{"companyName":"Acme","companyWebsite":"acme.com"\}\] |
| `outputFields` | string | No | JSON array or comma-separated list of fields to return \(e.g. \["id","name","website","revenue","employeeCount"\]\) |
| `outputFields` | string | No | JSON array or comma-separated list of fields to return \(e.g. \["id","name","website","revenue","employeeCount"\]\). Defaults to a standard firmographic set if omitted. |
#### Output
@@ -138,7 +138,7 @@ Enrich up to 25 contacts in one request with verified emails, phone numbers, job
| `clientId` | string | Yes | ZoomInfo OAuth client ID |
| `clientSecret` | string | Yes | ZoomInfo OAuth client secret |
| `matchPersonInput` | string | Yes | JSON array \(1-25 items\) of contact matching criteria, e.g. \[\{"firstName":"Jane","lastName":"Doe","companyName":"Acme"\}\] |
| `outputFields` | string | No | JSON array or comma-separated list of fields to return \(e.g. \["id","firstName","email","phone","jobTitle"\]\) |
| `outputFields` | string | No | JSON array or comma-separated list of fields to return \(e.g. \["id","firstName","email","phone","jobTitle"\]\). Defaults to a standard contact set if omitted. |
| `requiredFields` | string | No | JSON array or comma-separated list of fields that must exist in results \(e.g. \["email"\]\) |
#### Output
@@ -217,6 +217,8 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
const { name, description, longDescription, bgColor, docsUrl, operations, triggers, authType } =
integration
const landingContent = integration.landingContent
const IconComponent = blockTypeToIconMap[integration.type]
const faqs = buildFAQs(integration)
const relatedSlugs = getRelatedSlugs(slug, operations, authType)
@@ -444,6 +446,77 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
</>
)}
{/* Install / Add to workspace (integration-specific) */}
{landingContent?.install && (
<>
<section aria-labelledby='install-heading' className='px-6 py-10'>
<h2
id='install-heading'
className='mb-4 text-[20px] text-white leading-[100%] tracking-[-0.02em]'
>
{landingContent.install.heading}
</h2>
<p className='mb-6 max-w-[700px] text-[15px] text-[var(--landing-text-body)] leading-[150%] tracking-[0.02em]'>
{landingContent.install.intro}
</p>
<ol className='space-y-4' aria-label={`Steps to add ${name}`}>
{landingContent.install.steps.map((item, index) => (
<li key={item.title} className='flex gap-4'>
<span
className='mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full border border-[var(--landing-border-strong)] font-martian-mono text-[11px] text-[var(--landing-text-subtle)]'
aria-hidden='true'
>
{String(index + 1).padStart(2, '0')}
</span>
<div>
<h3 className='mb-1 text-[15px] text-white tracking-[-0.02em]'>
{item.title}
</h3>
<p className='text-[14px] text-[var(--landing-text-body)] leading-[150%] tracking-[0.02em]'>
{item.body}
</p>
</div>
</li>
))}
</ol>
<div className='mt-8 flex flex-wrap gap-2'>
<IntegrationCtaButton
label={`Add to ${name}`}
className='inline-flex h-[32px] items-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Add to {name}
</IntegrationCtaButton>
</div>
</section>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
)}
{/* Privacy & data (integration-specific) */}
{landingContent?.privacy && (
<>
<section aria-labelledby='privacy-heading' className='px-6 py-10'>
<h2
id='privacy-heading'
className='mb-4 text-[20px] text-white leading-[100%] tracking-[-0.02em]'
>
Privacy & data
</h2>
<p className='max-w-[700px] text-[15px] text-[var(--landing-text-body)] leading-[150%] tracking-[0.02em]'>
{landingContent.privacy.body}{' '}
<Link
href={landingContent.privacy.href}
className='text-[var(--landing-text)] underline underline-offset-2 hover:text-white'
>
Privacy Policy
</Link>
.
</p>
</section>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
)}
{/* How to automate */}
<section aria-labelledby='how-it-works-heading' className='px-6 py-10'>
<h2
@@ -20,7 +20,6 @@ import {
AshbyIcon,
AthenaIcon,
AttioIcon,
AzureDevOpsIcon,
AzureIcon,
BoxCompanyIcon,
BrainIcon,
@@ -53,6 +52,7 @@ import {
ElasticsearchIcon,
ElevenLabsIcon,
EmailBisonIcon,
EnrichmentIcon,
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
@@ -231,7 +231,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
ashby: AshbyIcon,
athena: AthenaIcon,
attio: AttioIcon,
azure_devops: AzureDevOpsIcon,
azure_devops: AzureIcon,
box: BoxCompanyIcon,
brandfetch: BrandfetchIcon,
brightdata: BrightDataIcon,
@@ -262,6 +262,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
elevenlabs: ElevenLabsIcon,
emailbison: EmailBisonIcon,
enrich: EnrichSoIcon,
enrichment: EnrichmentIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
extend_v2: ExtendIcon,
@@ -1889,7 +1889,7 @@
"description": "Interact with Azure DevOps pipelines, builds, and work items",
"longDescription": "Integrate Azure DevOps into your workflow. List and inspect pipelines and builds, query and manage work items, and add or read comments.",
"bgColor": "#0078D4",
"iconName": "AzureDevOpsIcon",
"iconName": "AzureIcon",
"docsUrl": "https://docs.sim.ai/tools/azure_devops",
"operations": [
{
@@ -3137,6 +3137,24 @@
"integrationTypes": ["analytics", "developer-tools"],
"tags": ["data-analytics", "automation"]
},
{
"type": "enrichment",
"slug": "data-enrichment",
"name": "Data Enrichment",
"description": "Enrich data with a Sim enrichment",
"longDescription": "Run a Sim enrichment to look up data — work email, phone number, company domain, company info, and more — from the fields you map in. Uses the same provider cascade as table enrichments.",
"bgColor": "#9333EA",
"iconName": "EnrichmentIcon",
"docsUrl": "https://docs.sim.ai/tools/enrichment",
"operations": [],
"operationCount": 0,
"triggers": [],
"triggerCount": 0,
"authType": "none",
"category": "tools",
"integrationTypes": ["sales"],
"tags": ["enrichment"]
},
{
"type": "databricks",
"slug": "databricks",
@@ -13288,7 +13306,35 @@
"authType": "oauth",
"category": "tools",
"integrationTypes": ["communication", "developer-tools"],
"tags": ["messaging", "webhooks", "automation"]
"tags": ["messaging", "webhooks", "automation"],
"landingContent": {
"install": {
"heading": "Add Sim to your Slack workspace",
"intro": "Sim connects to Slack through Slack’s official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in) — connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.",
"steps": [
{
"title": "Create your free Sim account",
"body": "Sign up at sim.ai — no credit card required."
},
{
"title": "Add a Slack block",
"body": "Open a workflow, drag in a Slack block, and open its credential dropdown."
},
{
"title": "Connect Slack",
"body": "Click Connect Slack, choose your workspace, and approve the requested permissions. This installs the Sim bot in your Slack workspace."
},
{
"title": "Invite the bot and build",
"body": "Invite the Sim bot to the channels it should act in, pick a Slack action, wire it into your agent, and run."
}
]
},
"privacy": {
"body": "Sim requests only the Slack permissions its actions and triggers need, and never shows private channel names or messages to people who are not members of those channels in Slack.",
"href": "/privacy"
}
}
},
{
"type": "smtp",
@@ -0,0 +1,41 @@
/**
* Hand-authored, integration-specific landing content, keyed by integration
* slug. This is a pure-data generation input: `scripts/generate-docs.ts` reads
* it and bakes the matching entry into `integrations.json`, so the landing page
* consumes a single source (`integration.landingContent`) with no render-time
* augmentation. Has no app imports so the build script can import it safely.
*/
import type { IntegrationLandingContent } from '@/app/(landing)/integrations/data/types'
export const INTEGRATION_LANDING_CONTENT: Record<string, IntegrationLandingContent> = {
slack: {
install: {
heading: 'Add Sim to your Slack workspace',
intro:
'Sim connects to Slack through Slack’s official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in) — connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.',
steps: [
{
title: 'Create your free Sim account',
body: 'Sign up at sim.ai — no credit card required.',
},
{
title: 'Add a Slack block',
body: 'Open a workflow, drag in a Slack block, and open its credential dropdown.',
},
{
title: 'Connect Slack',
body: 'Click Connect Slack, choose your workspace, and approve the requested permissions. This installs the Sim bot in your Slack workspace.',
},
{
title: 'Invite the bot and build',
body: 'Invite the Sim bot to the channels it should act in, pick a Slack action, wire it into your agent, and run.',
},
],
},
privacy: {
body: 'Sim requests only the Slack permissions its actions and triggers need, and never shows private channel names or messages to people who are not members of those channels in Slack.',
href: '/privacy',
},
},
}
@@ -19,6 +19,29 @@ export interface FAQItem {
answer: string
}
export interface IntegrationInstallStep {
title: string
body: string
}
export interface IntegrationLandingContent {
/**
* Install walkthrough for OAuth apps whose connection lives behind sign-in.
* Provides the "Add to {app}" instructions that app marketplaces require
* when the install button sits behind a login.
*/
install?: {
heading: string
intro: string
steps: IntegrationInstallStep[]
}
/** Short data-handling summary shown next to a privacy-policy link. */
privacy?: {
body: string
href: string
}
}
export interface Integration {
type: string
slug: string
@@ -36,4 +59,5 @@ export interface Integration {
category: string
integrationTypes?: string[]
tags?: string[]
landingContent?: IntegrationLandingContent
}
+16 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env ts-node
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { fileURLToPath, pathToFileURL } from 'url'
import { glob } from 'glob'
console.log('Starting documentation generator...')
@@ -183,6 +183,7 @@ interface IntegrationEntry {
category: string
integrationTypes?: string[]
tags?: string[]
landingContent?: Record<string, unknown>
}
/**
@@ -665,6 +666,19 @@ async function writeIntegrationsJson(iconMapping: Record<string, string>): Promi
const triggerRegistry = await buildTriggerRegistry()
const { desc: toolDescMap, name: toolNameMap } = await buildToolDescriptionMap()
// Hand-authored, integration-specific landing content (install walkthrough,
// privacy blurb), keyed by slug. Imported as pure data — its only import is
// type-only and erased at runtime — and baked into the entries below so the
// landing page reads a single source instead of augmenting at render time.
const landingContentModule = await import(
pathToFileURL(path.join(LANDING_INTEGRATIONS_DATA_PATH, 'landing-content.ts')).href
)
const landingContentMap = (landingContentModule.INTEGRATION_LANDING_CONTENT ?? {}) as Record<
string,
Record<string, unknown>
>
const integrations: IntegrationEntry[] = []
const seenBaseTypes = new Set<string>()
const blockFiles = (await glob(`${BLOCKS_PATH}/*.ts`)).sort()
@@ -778,6 +792,7 @@ async function writeIntegrationsJson(iconMapping: Record<string, string>): Promi
}
: {}),
...(config.tags ? { tags: config.tags } : {}),
...(landingContentMap[slug] ? { landingContent: landingContentMap[slug] } : {}),
})
}
}