Skip to content

Commit d81055d

Browse files
committed
feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages
1 parent 0d2e6ff commit d81055d

Some content is hidden

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

47 files changed

+16130
-69
lines changed

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

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type React from 'react'
22
import { findNeighbour } from 'fumadocs-core/page-tree'
3+
import { createAPIPage } from 'fumadocs-openapi/ui'
34
import { Pre } from 'fumadocs-ui/components/codeblock'
45
import defaultMdxComponents from 'fumadocs-ui/mdx'
56
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'
@@ -12,22 +13,65 @@ import { LLMCopyButton } from '@/components/page-actions'
1213
import { StructuredData } from '@/components/structured-data'
1314
import { CodeBlock } from '@/components/ui/code-block'
1415
import { Heading } from '@/components/ui/heading'
16+
import { ResponseSection } from '@/components/ui/response-section'
17+
import { getApiSpecContent, openapi } from '@/lib/openapi'
1518
import { type PageData, source } from '@/lib/source'
1619

20+
const SUPPORTED_LANGUAGES = new Set(['en', 'es', 'fr', 'de', 'ja', 'zh'])
21+
22+
const APIPage = createAPIPage(openapi, {
23+
playground: { enabled: false },
24+
content: {
25+
renderOperationLayout: async (slots) => {
26+
return (
27+
<div className='flex @4xl:flex-row flex-col @4xl:items-start gap-x-6 gap-y-4'>
28+
<div className='min-w-0 flex-1'>
29+
{slots.header}
30+
{slots.apiPlayground}
31+
{slots.description}
32+
{slots.authSchemes && <div className='api-section-divider'>{slots.authSchemes}</div>}
33+
{slots.paremeters}
34+
{slots.body && <div className='api-section-divider'>{slots.body}</div>}
35+
<ResponseSection>{slots.responses}</ResponseSection>
36+
{slots.callbacks}
37+
</div>
38+
<div className='@4xl:sticky @4xl:top-[calc(var(--fd-docs-row-1,2rem)+1rem)] @4xl:w-[400px]'>
39+
{slots.apiExample}
40+
</div>
41+
</div>
42+
)
43+
},
44+
},
45+
})
46+
1747
export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) {
1848
const params = await props.params
19-
const page = source.getPage(params.slug, params.lang)
49+
const isValidLang = SUPPORTED_LANGUAGES.has(params.lang)
50+
const lang = isValidLang ? params.lang : 'en'
51+
const slug = isValidLang ? params.slug : [params.lang, ...(params.slug ?? [])]
52+
const page = source.getPage(slug, lang)
2053
if (!page) notFound()
2154

22-
const data = page.data as PageData
23-
const MDX = data.body
55+
const data = page.data as PageData & {
56+
_openapi?: { method?: string }
57+
getAPIPageProps?: () => any
58+
}
59+
const isOpenAPI = '_openapi' in data && data._openapi != null
60+
const isApiReference = slug?.some((s) => s === 'api-reference') ?? false
2461
const baseUrl = 'https://docs.sim.ai'
25-
const markdownContent = await data.getText('processed')
2662

2763
const pageTreeRecord = source.pageTree as Record<string, any>
2864
const pageTree =
2965
pageTreeRecord[params.lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
30-
const neighbours = pageTree ? findNeighbour(pageTree, page.url) : null
66+
const rawNeighbours = pageTree ? findNeighbour(pageTree, page.url) : null
67+
const neighbours = isApiReference
68+
? {
69+
previous: rawNeighbours?.previous?.url.includes('/api-reference/')
70+
? rawNeighbours.previous
71+
: undefined,
72+
next: rawNeighbours?.next?.url.includes('/api-reference/') ? rawNeighbours.next : undefined,
73+
}
74+
: rawNeighbours
3175

3276
const generateBreadcrumbs = () => {
3377
const breadcrumbs: Array<{ name: string; url: string }> = [
@@ -169,6 +213,62 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
169213
</div>
170214
)
171215

216+
if (isOpenAPI && data.getAPIPageProps) {
217+
const apiProps = data.getAPIPageProps()
218+
const apiPageContent = getApiSpecContent(
219+
data.title,
220+
data.description,
221+
apiProps.operations ?? []
222+
)
223+
224+
return (
225+
<>
226+
<StructuredData
227+
title={data.title}
228+
description={data.description || ''}
229+
url={`${baseUrl}${page.url}`}
230+
lang={params.lang}
231+
breadcrumb={breadcrumbs}
232+
/>
233+
<DocsPage
234+
toc={data.toc}
235+
breadcrumb={{
236+
enabled: false,
237+
}}
238+
tableOfContent={{
239+
style: 'clerk',
240+
enabled: false,
241+
}}
242+
tableOfContentPopover={{
243+
style: 'clerk',
244+
enabled: false,
245+
}}
246+
footer={{
247+
enabled: true,
248+
component: <CustomFooter />,
249+
}}
250+
>
251+
<div className='api-page-header relative mt-6 sm:mt-0'>
252+
<div className='absolute top-1 right-0 flex items-center gap-2'>
253+
<div className='hidden sm:flex'>
254+
<LLMCopyButton content={apiPageContent} />
255+
</div>
256+
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
257+
</div>
258+
<DocsTitle>{data.title}</DocsTitle>
259+
<DocsDescription>{data.description}</DocsDescription>
260+
</div>
261+
<DocsBody>
262+
<APIPage {...apiProps} />
263+
</DocsBody>
264+
</DocsPage>
265+
</>
266+
)
267+
}
268+
269+
const MDX = (data as PageData).body
270+
const markdownContent = await (data as PageData).getText('processed')
271+
172272
return (
173273
<>
174274
<StructuredData
@@ -252,7 +352,10 @@ export async function generateMetadata(props: {
252352
params: Promise<{ slug?: string[]; lang: string }>
253353
}) {
254354
const params = await props.params
255-
const page = source.getPage(params.slug, params.lang)
355+
const isValidLang = SUPPORTED_LANGUAGES.has(params.lang)
356+
const lang = isValidLang ? params.lang : 'en'
357+
const slug = isValidLang ? params.slug : [params.lang, ...(params.slug ?? [])]
358+
const page = source.getPage(slug, lang)
256359
if (!page) notFound()
257360

258361
const data = page.data as PageData
@@ -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': `${baseUrl}${page.url.replace(`/${lang}`, '')}`,
430+
en: `${baseUrl}${page.url.replace(`/${lang}`, '')}`,
431+
es: `${baseUrl}/es${page.url.replace(`/${lang}`, '')}`,
432+
fr: `${baseUrl}/fr${page.url.replace(`/${lang}`, '')}`,
433+
de: `${baseUrl}/de${page.url.replace(`/${lang}`, '')}`,
434+
ja: `${baseUrl}/ja${page.url.replace(`/${lang}`, '')}`,
435+
zh: `${baseUrl}/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 = new Set(['en', 'es', 'fr', 'de', 'ja', 'zh'])
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)