11import type React from 'react'
22import { findNeighbour } from 'fumadocs-core/page-tree'
3+ import { createAPIPage } from 'fumadocs-openapi/ui'
34import { Pre } from 'fumadocs-ui/components/codeblock'
45import defaultMdxComponents from 'fumadocs-ui/mdx'
56import { DocsBody , DocsDescription , DocsPage , DocsTitle } from 'fumadocs-ui/page'
@@ -12,22 +13,65 @@ import { LLMCopyButton } from '@/components/page-actions'
1213import { StructuredData } from '@/components/structured-data'
1314import { CodeBlock } from '@/components/ui/code-block'
1415import { Heading } from '@/components/ui/heading'
16+ import { ResponseSection } from '@/components/ui/response-section'
17+ import { getApiSpecContent , openapi } from '@/lib/openapi'
1518import { 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+
1747export 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 }
0 commit comments