d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
356 lines
12 KiB
TypeScript
356 lines
12 KiB
TypeScript
import type React from 'react'
|
|
import type { Root } from 'fumadocs-core/page-tree'
|
|
import { findNeighbour } from 'fumadocs-core/page-tree'
|
|
import type { ApiPageProps } from 'fumadocs-openapi/ui'
|
|
import { createAPIPage } from 'fumadocs-openapi/ui'
|
|
import { Pre } from 'fumadocs-ui/components/codeblock'
|
|
import defaultMdxComponents from 'fumadocs-ui/mdx'
|
|
import { DocsBody, DocsPage, DocsTitle } from 'fumadocs-ui/page'
|
|
import { notFound } from 'next/navigation'
|
|
import { PageFooter } from '@/components/docs-layout/page-footer'
|
|
import { PageNavigationArrows } from '@/components/docs-layout/page-navigation-arrows'
|
|
import { LLMCopyButton } from '@/components/page-actions'
|
|
import { PageTypeBadge } from '@/components/page-type-badge'
|
|
import { StructuredData } from '@/components/structured-data'
|
|
import { CodeBlock } from '@/components/ui/code-block'
|
|
import { Heading } from '@/components/ui/heading'
|
|
import { ResponseSection } from '@/components/ui/response-section'
|
|
import { i18n } from '@/lib/i18n'
|
|
import { getApiSpecContent, openapi } from '@/lib/openapi'
|
|
import { type PageData, source } from '@/lib/source'
|
|
import { DOCS_BASE_URL } from '@/lib/urls'
|
|
|
|
const SUPPORTED_LANGUAGES: Set<string> = new Set(i18n.languages)
|
|
const BASE_URL = DOCS_BASE_URL
|
|
|
|
const OG_LOCALE_MAP: Record<string, string> = {
|
|
en: 'en_US',
|
|
es: 'es_ES',
|
|
fr: 'fr_FR',
|
|
de: 'de_DE',
|
|
ja: 'ja_JP',
|
|
zh: 'zh_CN',
|
|
}
|
|
|
|
function resolveLangAndSlug(params: { slug?: string[]; lang: string }) {
|
|
const isValidLang = SUPPORTED_LANGUAGES.has(params.lang)
|
|
const lang = isValidLang ? params.lang : 'en'
|
|
const slug = isValidLang ? params.slug : [params.lang, ...(params.slug ?? [])]
|
|
return { lang, slug }
|
|
}
|
|
|
|
const APIPage = createAPIPage(openapi, {
|
|
playground: { enabled: false },
|
|
content: {
|
|
renderOperationLayout: async (slots) => {
|
|
return (
|
|
<div className='flex @4xl:flex-row flex-col @4xl:items-start gap-x-6 gap-y-4'>
|
|
<div className='min-w-0 flex-1'>
|
|
{slots.header}
|
|
{slots.apiPlayground}
|
|
{slots.authSchemes && <div className='api-section-divider'>{slots.authSchemes}</div>}
|
|
{slots.parameters}
|
|
{slots.body && <div className='api-section-divider'>{slots.body}</div>}
|
|
<ResponseSection>{slots.responses}</ResponseSection>
|
|
{slots.callbacks}
|
|
</div>
|
|
<div className='@4xl:sticky @4xl:top-[calc(var(--fd-docs-row-1,2rem)+1rem)] @4xl:w-[400px]'>
|
|
{slots.apiExample}
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
})
|
|
|
|
export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) {
|
|
const params = await props.params
|
|
const { lang, slug } = resolveLangAndSlug(params)
|
|
const page = source.getPage(slug, lang)
|
|
if (!page) notFound()
|
|
|
|
const data = page.data as unknown as PageData & {
|
|
_openapi?: { method?: string }
|
|
getAPIPageProps?: () => ApiPageProps
|
|
}
|
|
const isOpenAPI = '_openapi' in data && data._openapi != null
|
|
const isApiReference = slug?.some((s) => s === 'api-reference') ?? false
|
|
// Academy lessons are video-first: drop the "On this page" TOC and go full
|
|
// width so the lesson hero/video gets the room (chapters live in-page instead).
|
|
const isAcademy = slug?.[0] === 'academy'
|
|
|
|
const pageTreeRecord = source.pageTree as Record<string, Root>
|
|
const pageTree = pageTreeRecord[lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
|
|
const rawNeighbours = pageTree ? findNeighbour(pageTree, page.url) : null
|
|
// Academy and API Reference are self-contained sections; keep prev/next inside
|
|
// the section instead of spilling into the main documentation tree. Match both
|
|
// the section's pages (`/<slug>/...`) and its index (`/<slug>`).
|
|
const sectionSlug = isApiReference ? 'api-reference' : isAcademy ? 'academy' : null
|
|
const inSection = (url?: string) =>
|
|
url != null && (url.includes(`/${sectionSlug}/`) || url.endsWith(`/${sectionSlug}`))
|
|
const neighbours = sectionSlug
|
|
? {
|
|
previous: inSection(rawNeighbours?.previous?.url) ? rawNeighbours?.previous : undefined,
|
|
next: inSection(rawNeighbours?.next?.url) ? rawNeighbours?.next : undefined,
|
|
}
|
|
: rawNeighbours
|
|
|
|
const generateBreadcrumbs = () => {
|
|
const breadcrumbs: Array<{ name: string; url: string }> = [
|
|
{
|
|
name: 'Home',
|
|
url: BASE_URL,
|
|
},
|
|
]
|
|
|
|
const urlParts = page.url.split('/').filter(Boolean)
|
|
let currentPath = ''
|
|
|
|
urlParts.forEach((part, index) => {
|
|
if (index === 0 && SUPPORTED_LANGUAGES.has(part)) {
|
|
currentPath = `/${part}`
|
|
return
|
|
}
|
|
|
|
currentPath += `/${part}`
|
|
|
|
const name = part
|
|
.split('-')
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ')
|
|
|
|
if (index === urlParts.length - 1) {
|
|
breadcrumbs.push({
|
|
name: data.title,
|
|
url: `${BASE_URL}${page.url}`,
|
|
})
|
|
} else {
|
|
breadcrumbs.push({
|
|
name: name,
|
|
url: `${BASE_URL}${currentPath}`,
|
|
})
|
|
}
|
|
})
|
|
|
|
return breadcrumbs
|
|
}
|
|
|
|
const breadcrumbs = generateBreadcrumbs()
|
|
const footer = <PageFooter previous={neighbours?.previous} next={neighbours?.next} />
|
|
|
|
if (isOpenAPI && data.getAPIPageProps) {
|
|
const apiProps = data.getAPIPageProps()
|
|
const apiPageContent = getApiSpecContent(
|
|
data.title,
|
|
data.description,
|
|
apiProps.operations ?? []
|
|
)
|
|
|
|
return (
|
|
<>
|
|
<StructuredData
|
|
title={data.title}
|
|
description={data.description || ''}
|
|
url={`${BASE_URL}${page.url}`}
|
|
lang={lang}
|
|
breadcrumb={breadcrumbs}
|
|
/>
|
|
<DocsPage
|
|
toc={data.toc}
|
|
breadcrumb={{
|
|
enabled: false,
|
|
}}
|
|
tableOfContent={{
|
|
style: 'clerk',
|
|
enabled: false,
|
|
}}
|
|
tableOfContentPopover={{
|
|
style: 'clerk',
|
|
enabled: false,
|
|
}}
|
|
footer={{
|
|
enabled: true,
|
|
component: footer,
|
|
}}
|
|
>
|
|
<div className='api-page-header relative mt-6 sm:mt-0'>
|
|
<div className='absolute top-1 right-0 flex items-center gap-2'>
|
|
<div className='hidden sm:flex'>
|
|
<LLMCopyButton content={apiPageContent} />
|
|
</div>
|
|
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
|
|
</div>
|
|
<DocsTitle className='mb-2'>{data.title}</DocsTitle>
|
|
</div>
|
|
<DocsBody>
|
|
<APIPage {...apiProps} />
|
|
</DocsBody>
|
|
</DocsPage>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const MDX = data.body
|
|
const markdownContent = await data.getText('processed')
|
|
|
|
return (
|
|
<>
|
|
<StructuredData
|
|
title={data.title}
|
|
description={data.description || ''}
|
|
url={`${BASE_URL}${page.url}`}
|
|
lang={lang}
|
|
breadcrumb={breadcrumbs}
|
|
/>
|
|
<DocsPage
|
|
toc={data.toc}
|
|
full={data.full || isAcademy}
|
|
breadcrumb={{
|
|
enabled: false,
|
|
}}
|
|
tableOfContent={{
|
|
style: 'clerk',
|
|
enabled: !isAcademy,
|
|
single: false,
|
|
}}
|
|
tableOfContentPopover={{
|
|
style: 'clerk',
|
|
enabled: !isAcademy,
|
|
}}
|
|
footer={{
|
|
enabled: true,
|
|
component: footer,
|
|
}}
|
|
>
|
|
<div className='relative mt-6 sm:mt-0'>
|
|
<div className='absolute top-1 right-0 flex items-center gap-2'>
|
|
<div className='hidden sm:flex'>
|
|
<LLMCopyButton content={markdownContent} />
|
|
</div>
|
|
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
|
|
</div>
|
|
{data.pageType && <PageTypeBadge type={data.pageType} className='mb-3' />}
|
|
<DocsTitle className='mb-2'>{data.title}</DocsTitle>
|
|
</div>
|
|
<DocsBody>
|
|
<MDX
|
|
components={{
|
|
...defaultMdxComponents,
|
|
pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
|
|
<CodeBlock {...props}>
|
|
<Pre>{props.children}</Pre>
|
|
</CodeBlock>
|
|
),
|
|
h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
|
<Heading as='h1' {...props} />
|
|
),
|
|
h2: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
|
<Heading as='h2' {...props} />
|
|
),
|
|
h3: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
|
<Heading as='h3' {...props} />
|
|
),
|
|
h4: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
|
<Heading as='h4' {...props} />
|
|
),
|
|
h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
|
<Heading as='h5' {...props} />
|
|
),
|
|
h6: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
|
<Heading as='h6' {...props} />
|
|
),
|
|
}}
|
|
/>
|
|
</DocsBody>
|
|
</DocsPage>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export async function generateStaticParams() {
|
|
return source.generateParams()
|
|
}
|
|
|
|
export async function generateMetadata(props: {
|
|
params: Promise<{ slug?: string[]; lang: string }>
|
|
}) {
|
|
const params = await props.params
|
|
const { lang, slug } = resolveLangAndSlug(params)
|
|
const page = source.getPage(slug, lang)
|
|
if (!page) notFound()
|
|
|
|
const data = page.data as unknown as PageData
|
|
const fullUrl = `${BASE_URL}${page.url}`
|
|
|
|
const ogImageUrl = `${BASE_URL}/api/og?title=${encodeURIComponent(data.title)}`
|
|
|
|
return {
|
|
title: data.title,
|
|
description:
|
|
data.description ||
|
|
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
|
|
keywords: [
|
|
'AI agents',
|
|
'AI workspace',
|
|
'AI agent builder',
|
|
'build AI agents',
|
|
'LLM orchestration',
|
|
'AI automation',
|
|
'knowledge base',
|
|
'AI integrations',
|
|
data.title?.toLowerCase().split(' '),
|
|
]
|
|
.flat()
|
|
.filter(Boolean),
|
|
authors: [{ name: 'Sim Team' }],
|
|
category: 'Developer Tools',
|
|
openGraph: {
|
|
title: data.title,
|
|
description:
|
|
data.description ||
|
|
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
|
|
url: fullUrl,
|
|
siteName: 'Sim Documentation',
|
|
type: 'article',
|
|
locale: OG_LOCALE_MAP[lang] ?? 'en_US',
|
|
alternateLocale: i18n.languages.reduce<string[]>((locales, l) => {
|
|
if (l !== lang) {
|
|
locales.push(OG_LOCALE_MAP[l] ?? 'en_US')
|
|
}
|
|
return locales
|
|
}, []),
|
|
images: [
|
|
{
|
|
url: ogImageUrl,
|
|
width: 1200,
|
|
height: 675,
|
|
alt: data.title,
|
|
},
|
|
],
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: data.title,
|
|
description:
|
|
data.description ||
|
|
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
|
|
images: [ogImageUrl],
|
|
creator: '@simdotai',
|
|
site: '@simdotai',
|
|
},
|
|
canonical: fullUrl,
|
|
alternates: {
|
|
canonical: fullUrl,
|
|
languages: {
|
|
'x-default': `${BASE_URL}${page.url.replace(`/${lang}`, '')}`,
|
|
en: `${BASE_URL}${page.url.replace(`/${lang}`, '')}`,
|
|
es: `${BASE_URL}/es${page.url.replace(`/${lang}`, '')}`,
|
|
fr: `${BASE_URL}/fr${page.url.replace(`/${lang}`, '')}`,
|
|
de: `${BASE_URL}/de${page.url.replace(`/${lang}`, '')}`,
|
|
ja: `${BASE_URL}/ja${page.url.replace(`/${lang}`, '')}`,
|
|
zh: `${BASE_URL}/zh${page.url.replace(`/${lang}`, '')}`,
|
|
},
|
|
},
|
|
}
|
|
}
|