Skip to content

Commit 79bb4e5

Browse files
waleedlatif1claudelakeesivicecrasher321
authored
feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages (#3388)
* feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages * multiline curl * random improvements * cleanup * update docs copy * fix build * cast * fix builg --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com> Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai> Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
1 parent ee20e11 commit 79bb4e5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+16235
-171
lines changed

apps/docs/app/[lang]/[[...slug]]/page.tsx

Lines changed: 136 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type React from 'react'
2+
import type { Root } from 'fumadocs-core/page-tree'
23
import { findNeighbour } from 'fumadocs-core/page-tree'
4+
import type { ApiPageProps } from 'fumadocs-openapi/ui'
5+
import { createAPIPage } from 'fumadocs-openapi/ui'
36
import { Pre } from 'fumadocs-ui/components/codeblock'
47
import defaultMdxComponents from 'fumadocs-ui/mdx'
58
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'
@@ -12,36 +15,83 @@ import { LLMCopyButton } from '@/components/page-actions'
1215
import { StructuredData } from '@/components/structured-data'
1316
import { CodeBlock } from '@/components/ui/code-block'
1417
import { Heading } from '@/components/ui/heading'
18+
import { ResponseSection } from '@/components/ui/response-section'
19+
import { i18n } from '@/lib/i18n'
20+
import { getApiSpecContent, openapi } from '@/lib/openapi'
1521
import { type PageData, source } from '@/lib/source'
1622

23+
const SUPPORTED_LANGUAGES: Set<string> = new Set(i18n.languages)
24+
const BASE_URL = 'https://docs.sim.ai'
25+
26+
function resolveLangAndSlug(params: { slug?: string[]; lang: string }) {
27+
const isValidLang = SUPPORTED_LANGUAGES.has(params.lang)
28+
const lang = isValidLang ? params.lang : 'en'
29+
const slug = isValidLang ? params.slug : [params.lang, ...(params.slug ?? [])]
30+
return { lang, slug }
31+
}
32+
33+
const APIPage = createAPIPage(openapi, {
34+
playground: { enabled: false },
35+
content: {
36+
renderOperationLayout: async (slots) => {
37+
return (
38+
<div className='flex @4xl:flex-row flex-col @4xl:items-start gap-x-6 gap-y-4'>
39+
<div className='min-w-0 flex-1'>
40+
{slots.header}
41+
{slots.apiPlayground}
42+
{slots.authSchemes && <div className='api-section-divider'>{slots.authSchemes}</div>}
43+
{slots.paremeters}
44+
{slots.body && <div className='api-section-divider'>{slots.body}</div>}
45+
<ResponseSection>{slots.responses}</ResponseSection>
46+
{slots.callbacks}
47+
</div>
48+
<div className='@4xl:sticky @4xl:top-[calc(var(--fd-docs-row-1,2rem)+1rem)] @4xl:w-[400px]'>
49+
{slots.apiExample}
50+
</div>
51+
</div>
52+
)
53+
},
54+
},
55+
})
56+
1757
export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) {
1858
const params = await props.params
19-
const page = source.getPage(params.slug, params.lang)
59+
const { lang, slug } = resolveLangAndSlug(params)
60+
const page = source.getPage(slug, lang)
2061
if (!page) notFound()
2162

22-
const data = page.data as PageData
23-
const MDX = data.body
24-
const baseUrl = 'https://docs.sim.ai'
25-
const markdownContent = await data.getText('processed')
63+
const data = page.data as unknown as PageData & {
64+
_openapi?: { method?: string }
65+
getAPIPageProps?: () => ApiPageProps
66+
}
67+
const isOpenAPI = '_openapi' in data && data._openapi != null
68+
const isApiReference = slug?.some((s) => s === 'api-reference') ?? false
2669

27-
const pageTreeRecord = source.pageTree as Record<string, any>
28-
const pageTree =
29-
pageTreeRecord[params.lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
30-
const neighbours = pageTree ? findNeighbour(pageTree, page.url) : null
70+
const pageTreeRecord = source.pageTree as Record<string, Root>
71+
const pageTree = pageTreeRecord[lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
72+
const rawNeighbours = pageTree ? findNeighbour(pageTree, page.url) : null
73+
const neighbours = isApiReference
74+
? {
75+
previous: rawNeighbours?.previous?.url.includes('/api-reference/')
76+
? rawNeighbours.previous
77+
: undefined,
78+
next: rawNeighbours?.next?.url.includes('/api-reference/') ? rawNeighbours.next : undefined,
79+
}
80+
: rawNeighbours
3181

3282
const generateBreadcrumbs = () => {
3383
const breadcrumbs: Array<{ name: string; url: string }> = [
3484
{
3585
name: 'Home',
36-
url: baseUrl,
86+
url: BASE_URL,
3787
},
3888
]
3989

4090
const urlParts = page.url.split('/').filter(Boolean)
4191
let currentPath = ''
4292

4393
urlParts.forEach((part, index) => {
44-
if (index === 0 && ['en', 'es', 'fr', 'de', 'ja', 'zh'].includes(part)) {
94+
if (index === 0 && SUPPORTED_LANGUAGES.has(part)) {
4595
currentPath = `/${part}`
4696
return
4797
}
@@ -56,12 +106,12 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
56106
if (index === urlParts.length - 1) {
57107
breadcrumbs.push({
58108
name: data.title,
59-
url: `${baseUrl}${page.url}`,
109+
url: `${BASE_URL}${page.url}`,
60110
})
61111
} else {
62112
breadcrumbs.push({
63113
name: name,
64-
url: `${baseUrl}${currentPath}`,
114+
url: `${BASE_URL}${currentPath}`,
65115
})
66116
}
67117
})
@@ -73,7 +123,6 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
73123

74124
const CustomFooter = () => (
75125
<div className='mt-12'>
76-
{/* Navigation links */}
77126
<div className='flex items-center justify-between py-8'>
78127
{neighbours?.previous ? (
79128
<Link
@@ -100,10 +149,8 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
100149
)}
101150
</div>
102151

103-
{/* Divider line */}
104152
<div className='border-border border-t' />
105153

106-
{/* Social icons */}
107154
<div className='flex items-center gap-4 py-6'>
108155
<Link
109156
href='https://x.com/simdotai'
@@ -169,13 +216,69 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
169216
</div>
170217
)
171218

219+
if (isOpenAPI && data.getAPIPageProps) {
220+
const apiProps = data.getAPIPageProps()
221+
const apiPageContent = getApiSpecContent(
222+
data.title,
223+
data.description,
224+
apiProps.operations ?? []
225+
)
226+
227+
return (
228+
<>
229+
<StructuredData
230+
title={data.title}
231+
description={data.description || ''}
232+
url={`${BASE_URL}${page.url}`}
233+
lang={lang}
234+
breadcrumb={breadcrumbs}
235+
/>
236+
<DocsPage
237+
toc={data.toc}
238+
breadcrumb={{
239+
enabled: false,
240+
}}
241+
tableOfContent={{
242+
style: 'clerk',
243+
enabled: false,
244+
}}
245+
tableOfContentPopover={{
246+
style: 'clerk',
247+
enabled: false,
248+
}}
249+
footer={{
250+
enabled: true,
251+
component: <CustomFooter />,
252+
}}
253+
>
254+
<div className='api-page-header relative mt-6 sm:mt-0'>
255+
<div className='absolute top-1 right-0 flex items-center gap-2'>
256+
<div className='hidden sm:flex'>
257+
<LLMCopyButton content={apiPageContent} />
258+
</div>
259+
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
260+
</div>
261+
<DocsTitle>{data.title}</DocsTitle>
262+
<DocsDescription>{data.description}</DocsDescription>
263+
</div>
264+
<DocsBody>
265+
<APIPage {...apiProps} />
266+
</DocsBody>
267+
</DocsPage>
268+
</>
269+
)
270+
}
271+
272+
const MDX = data.body
273+
const markdownContent = await data.getText('processed')
274+
172275
return (
173276
<>
174277
<StructuredData
175278
title={data.title}
176279
description={data.description || ''}
177-
url={`${baseUrl}${page.url}`}
178-
lang={params.lang}
280+
url={`${BASE_URL}${page.url}`}
281+
lang={lang}
179282
breadcrumb={breadcrumbs}
180283
/>
181284
<DocsPage
@@ -252,14 +355,14 @@ export async function generateMetadata(props: {
252355
params: Promise<{ slug?: string[]; lang: string }>
253356
}) {
254357
const params = await props.params
255-
const page = source.getPage(params.slug, params.lang)
358+
const { lang, slug } = resolveLangAndSlug(params)
359+
const page = source.getPage(slug, lang)
256360
if (!page) notFound()
257361

258-
const data = page.data as PageData
259-
const baseUrl = 'https://docs.sim.ai'
260-
const fullUrl = `${baseUrl}${page.url}`
362+
const data = page.data as unknown as PageData
363+
const fullUrl = `${BASE_URL}${page.url}`
261364

262-
const ogImageUrl = `${baseUrl}/api/og?title=${encodeURIComponent(data.title)}`
365+
const ogImageUrl = `${BASE_URL}/api/og?title=${encodeURIComponent(data.title)}`
263366

264367
return {
265368
title: data.title,
@@ -286,10 +389,10 @@ export async function generateMetadata(props: {
286389
url: fullUrl,
287390
siteName: 'Sim Documentation',
288391
type: 'article',
289-
locale: params.lang === 'en' ? 'en_US' : `${params.lang}_${params.lang.toUpperCase()}`,
392+
locale: lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`,
290393
alternateLocale: ['en', 'es', 'fr', 'de', 'ja', 'zh']
291-
.filter((lang) => lang !== params.lang)
292-
.map((lang) => (lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`)),
394+
.filter((l) => l !== lang)
395+
.map((l) => (l === 'en' ? 'en_US' : `${l}_${l.toUpperCase()}`)),
293396
images: [
294397
{
295398
url: ogImageUrl,
@@ -323,13 +426,13 @@ export async function generateMetadata(props: {
323426
alternates: {
324427
canonical: fullUrl,
325428
languages: {
326-
'x-default': `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`,
327-
en: `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`,
328-
es: `${baseUrl}/es${page.url.replace(`/${params.lang}`, '')}`,
329-
fr: `${baseUrl}/fr${page.url.replace(`/${params.lang}`, '')}`,
330-
de: `${baseUrl}/de${page.url.replace(`/${params.lang}`, '')}`,
331-
ja: `${baseUrl}/ja${page.url.replace(`/${params.lang}`, '')}`,
332-
zh: `${baseUrl}/zh${page.url.replace(`/${params.lang}`, '')}`,
429+
'x-default': `${BASE_URL}${page.url.replace(`/${lang}`, '')}`,
430+
en: `${BASE_URL}${page.url.replace(`/${lang}`, '')}`,
431+
es: `${BASE_URL}/es${page.url.replace(`/${lang}`, '')}`,
432+
fr: `${BASE_URL}/fr${page.url.replace(`/${lang}`, '')}`,
433+
de: `${BASE_URL}/de${page.url.replace(`/${lang}`, '')}`,
434+
ja: `${BASE_URL}/ja${page.url.replace(`/${lang}`, '')}`,
435+
zh: `${BASE_URL}/zh${page.url.replace(`/${lang}`, '')}`,
333436
},
334437
},
335438
}

apps/docs/app/[lang]/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,11 @@ type LayoutProps = {
5555
params: Promise<{ lang: string }>
5656
}
5757

58+
const SUPPORTED_LANGUAGES: Set<string> = new Set(i18n.languages)
59+
5860
export default async function Layout({ children, params }: LayoutProps) {
59-
const { lang } = await params
61+
const { lang: rawLang } = await params
62+
const lang = SUPPORTED_LANGUAGES.has(rawLang) ? rawLang : 'en'
6063

6164
const structuredData = {
6265
'@context': 'https://schema.org',
@@ -107,6 +110,7 @@ export default async function Layout({ children, params }: LayoutProps) {
107110
title: <SimLogoFull className='h-7 w-auto' />,
108111
}}
109112
sidebar={{
113+
tabs: false,
110114
defaultOpenLevel: 0,
111115
collapsible: false,
112116
footer: null,

0 commit comments

Comments
 (0)