11import type React from 'react'
2+ import type { Root } from 'fumadocs-core/page-tree'
23import { findNeighbour } from 'fumadocs-core/page-tree'
4+ import type { ApiPageProps } from 'fumadocs-openapi/ui'
5+ import { createAPIPage } from 'fumadocs-openapi/ui'
36import { Pre } from 'fumadocs-ui/components/codeblock'
47import defaultMdxComponents from 'fumadocs-ui/mdx'
58import { DocsBody , DocsDescription , DocsPage , DocsTitle } from 'fumadocs-ui/page'
@@ -12,36 +15,83 @@ import { LLMCopyButton } from '@/components/page-actions'
1215import { StructuredData } from '@/components/structured-data'
1316import { CodeBlock } from '@/components/ui/code-block'
1417import { 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'
1521import { 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+
1757export 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 }
0 commit comments