Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ const allIntegrations = INTEGRATIONS
const INTEGRATION_COUNT = allIntegrations.length
const baseUrl = SITE_URL

/**
* High-connectivity integrations (e.g. Slack, used as a notification target
* across hundreds of unrelated templates via `alsoIntegrations`) can match
* many hundreds of templates, ballooning this page's HTML/RSC payload well
* past Googlebot's 2MB crawl limit. Cap to a bounded, still-generous set,
* consistent with the related-integrations section's own cap below.
*
* `getTemplatesForBlock` returns matches in registry insertion order, not
* relevance - sorted `featured` first, then owned-by-the-viewing-integration,
* so the cap can't hide a curated `featured` template (owned or reached via
* `alsoIntegrations`) behind a larger pile of non-featured owned templates,
* nor behind unrelated `alsoIntegrations` matches that happen to iterate
* earlier in the registry.
*/
const MAX_TEMPLATES_SHOWN = 12

/** 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]))
Expand Down Expand Up @@ -380,6 +396,12 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
relatedIntegrations.map((i) => i.name)
)
const matchingTemplates = getTemplatesForBlock(integration.type)
.sort(
(a, b) =>
Number(b.featured ?? false) - Number(a.featured ?? false) ||
Number(b.isOwner) - Number(a.isOwner)
)
.slice(0, MAX_TEMPLATES_SHOWN)

const breadcrumbJsonLd = {
'@context': 'https://schema.org',
Expand Down
10 changes: 6 additions & 4 deletions apps/sim/app/(landing)/integrations/(shell)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
INTEGRATIONS,
type Integration,
POPULAR_WORKFLOWS,
toIntegrationSummary,
} from '@/lib/integrations'
import { withFilteredNoindex } from '@/lib/landing/seo'
import { JsonLd } from '@/app/(landing)/components/json-ld'
Expand All @@ -17,6 +18,7 @@ import { RequestIntegrationModal } from '@/app/(landing)/integrations/components
import { integrationsSearchParamsCache } from '@/app/(landing)/integrations/search-params'

const allIntegrations = INTEGRATIONS
const integrationSummaries = allIntegrations.map(toIntegrationSummary)
const INTEGRATION_COUNT = allIntegrations.length
const OAUTH_COUNT = allIntegrations.filter((i) => i.authType === 'oauth').length
const TRIGGER_INTEGRATION_COUNT = allIntegrations.filter((i) => i.triggerCount > 0).length
Expand Down Expand Up @@ -75,9 +77,9 @@ const INTEGRATIONS_BREADCRUMB_JSON_LD = {
const FEATURED_SLUGS = ['slack', 'notion', 'github', 'gmail'] as const

const bySlug = new Map(allIntegrations.map((i) => [i.slug, i]))
const featured = FEATURED_SLUGS.map((s) => bySlug.get(s)).filter(
(i): i is Integration => i !== undefined
)
const featured = FEATURED_SLUGS.map((s) => bySlug.get(s))
.filter((i): i is Integration => i !== undefined)
.map(toIntegrationSummary)

/**
* `q`/`category` render a genuinely different server-rendered list (see
Expand Down Expand Up @@ -215,7 +217,7 @@ export default async function IntegrationsPage({
All Integrations
</h2>
</div>
<IntegrationGrid integrations={allIntegrations} />
<IntegrationGrid integrations={integrationSummaries} />
</section>

{/* FAQ */}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { ComponentType, SVGProps } from 'react'
import { memo } from 'react'
import Link from 'next/link'
import type { Integration } from '@/lib/integrations'
import type { IntegrationSummary } from '@/lib/integrations'
import { ChevronArrow } from '@/app/(landing)/components/chevron-arrow'
import { IntegrationIcon } from '@/app/(landing)/integrations/components/integration-icon'

const HOVER_BG = 'transition-colors hover:bg-[var(--surface-hover)]' as const

interface IntegrationItemProps {
integration: Integration
integration: IntegrationSummary
IconComponent?: ComponentType<SVGProps<SVGSVGElement>>
}

Expand Down
35 changes: 13 additions & 22 deletions apps/sim/app/(landing)/integrations/components/integration-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import { useMemo } from 'react'
import { ChipInput, Search } from '@sim/emcn'
import { debounce, useQueryStates } from 'nuqs'
import { blockTypeToIconMap, formatIntegrationType, type Integration } from '@/lib/integrations'
import {
blockTypeToIconMap,
formatIntegrationType,
type IntegrationSummary,
} from '@/lib/integrations'
import { IntegrationRow } from '@/app/(landing)/integrations/components/integration-card'
import {
integrationsParsers,
Expand All @@ -19,7 +23,7 @@ const PILL_ACTIVE = 'bg-[var(--surface-active)]' as const
const PILL_INACTIVE = 'hover:bg-[var(--surface-hover)]' as const

interface IntegrationGridProps {
integrations: readonly Integration[]
integrations: readonly IntegrationSummary[]
}

export function IntegrationGrid({ integrations }: IntegrationGridProps) {
Expand All @@ -29,30 +33,17 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
)
const activeCategory = category || null

// Category facets and a per-integration lowercased search index, derived once
// from the (stable) integration list instead of rebuilt on every keystroke.
// The index keeps each searchable field as its own entry so matching stays
// identical to a per-field `includes` (no cross-field boundary matches).
const { availableCategories, searchIndex } = useMemo(() => {
/** Category facets, derived once from the (stable) integration list. */
const availableCategories = useMemo(() => {
const counts = new Map<string, number>()
const searchIndex = new Map<string, string[]>()
for (const i of integrations) {
if (i.integrationType) {
counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
}
searchIndex.set(i.type, [
i.name.toLowerCase(),
i.description.toLowerCase(),
...i.operations.flatMap((op) => [op.name.toLowerCase(), op.description.toLowerCase()]),
...i.triggers.map((t) => t.name.toLowerCase()),
])
}
return {
availableCategories: Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.map(([key]) => key),
searchIndex,
}
return Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.map(([key]) => key)
}, [integrations])

const q = query.trim().toLowerCase()
Expand All @@ -61,9 +52,9 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
integrations.filter((i) => {
if (activeCategory && i.integrationType !== activeCategory) return false
if (!q) return true
return searchIndex.get(i.type)?.some((field) => field.includes(q)) ?? false
return i.searchFields.some((field) => field.includes(q))
}),
[integrations, searchIndex, q, activeCategory]
[integrations, q, activeCategory]
)

return (
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/blocks/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ export function getAllBlockMeta(): Record<string, BlockMeta> {
export interface ScopedBlockTemplate extends BlockTemplate {
/** Block types (base form) to render alongside the viewing block in the icon cluster. */
otherBlockTypes: readonly string[]
/** Whether the viewing block owns this template, vs matching only via `alsoIntegrations`. */
isOwner: boolean
}

/**
Expand All @@ -208,7 +210,7 @@ export function getTemplatesForBlock(type: string): ScopedBlockTemplate[] {
const alsoBase = stripVersionSuffix(also)
if (alsoBase !== base && !others.includes(alsoBase)) others.push(alsoBase)
}
collected.push({ ...template, otherBlockTypes: others })
collected.push({ ...template, otherBlockTypes: others, isOwner: isOwnerMatch })
}
}
return collected
Expand Down
35 changes: 33 additions & 2 deletions apps/sim/lib/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import { stripVersionSuffix } from '@sim/utils/string'
import integrationsJson from '@/lib/integrations/integrations.json'
import type { Integration } from '@/lib/integrations/types'
import type { Integration, IntegrationSummary } from '@/lib/integrations/types'
import { getAllBlockMeta } from '@/blocks/registry'

/** All integrations surfaced in the catalog, ordered by `scripts/generate-docs.ts`. */
Expand Down Expand Up @@ -67,13 +67,44 @@ export const POPULAR_WORKFLOWS: readonly PopularWorkflow[] = (() => {
return pairs
})()

/**
* Projects a full `Integration` down to the fields the `/integrations`
* catalog grid renders and searches by, replacing the full `operations`/
* `triggers` arrays with a precomputed, lowercased `searchFields` index
* (name, description, every operation's name and description, every
* trigger's name) - the exact same fields the original per-field search
* over `Integration[]` matched against. See {@link IntegrationSummary} for
* why this exists.
*/
export function toIntegrationSummary(integration: Integration): IntegrationSummary {
const searchFields = [
integration.name.toLowerCase(),
integration.description.toLowerCase(),
...integration.operations.flatMap((op) => [
op.name.toLowerCase(),
op.description.toLowerCase(),
]),
...integration.triggers.map((t) => t.name.toLowerCase()),
]

return {
Comment thread
waleedlatif1 marked this conversation as resolved.
type: integration.type,
slug: integration.slug,
name: integration.name,
description: integration.description,
bgColor: integration.bgColor,
integrationType: integration.integrationType,
searchFields,
}
Comment thread
waleedlatif1 marked this conversation as resolved.
}

export { blockTypeToIconMap } from '@/lib/integrations/icon-mapping'
export {
type OAuthServiceMatch,
resolveOAuthServiceForIntegration,
resolveOAuthServiceForSlug,
} from '@/lib/integrations/oauth-service'
export type { AuthType, FAQItem, Integration } from '@/lib/integrations/types'
export type { AuthType, FAQItem, Integration, IntegrationSummary } from '@/lib/integrations/types'
export { getAllBlockMeta, getBlockMeta, getTemplatesForBlock } from '@/blocks/registry'
export type { BlockMeta, BlockTemplate } from '@/blocks/types'
export { formatIntegrationType } from '@/blocks/types'
25 changes: 25 additions & 0 deletions apps/sim/lib/integrations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,28 @@ export interface Integration {
/** Hand-authored landing content baked in at generation time (see `landing-content.ts`). */
landingContent?: IntegrationLandingContent
}

/**
* The fields the `/integrations` catalog grid actually renders and searches
* by, plus a precomputed, lowercased `searchFields` index (name, description,
* every operation's name and description, every trigger's name) in place of
* the full `operations`/`triggers` arrays. Shipping the full `Integration[]`
* to that page's client component embeds every integration's complete field
* set - including data the grid never renders (`tags`, `docsUrl`,
* `landingContent`, ...) - in the initial HTML/RSC payload.
*
* `searchFields` stays an array, one entry per source field, rather than a
* single joined string: matching must still require the query to fall
* entirely within one field (as the original per-field `Integration[]`
* search did), not span a field boundary a single concatenated string would
* silently allow.
*/
export interface IntegrationSummary {
type: Integration['type']
slug: string
name: Integration['name']
description: Integration['description']
bgColor: Integration['bgColor']
integrationType: Integration['integrationType']
searchFields: readonly string[]
}
Loading