Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages#3388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
b7e377e
v0.5.91: docs i18n, turborepo upgrade
waleedlatif1 da46a38
v0.5.92: shortlinks, copilot scrolling stickiness, pagination
waleedlatif1 fdca736
v0.5.93: NextJS config changes, MCP and Blocks whitelisting, copilot …
waleedlatif1 15ace5e
v0.5.94: vercel integration, folder insertion, migrated tracking redi…
waleedlatif1 67aa4bb
v0.5.95: gemini 3.1 pro, cloudflare, dataverse, revenuecat, redis, up…
waleedlatif1 34d92fa
v0.5.96: sim oauth provider, slack ephemeral message tool and blockki…
waleedlatif1 115f04e
v0.5.97: oidc discovery for copilot mcp
waleedlatif1 0d86ea0
v0.5.98: change detection improvements, rate limit and code execution…
waleedlatif1 af59234
v0.5.99: local dev improvements, live workflow logs in terminal
waleedlatif1 67f8a68
v0.5.100: multiple credentials, 40% speedup, gong, attio, audit log i…
waleedlatif1 4fd0989
v0.5.101: circular dependency mitigation, confluence enhancements, go…
waleedlatif1 0d2e6ff
v0.5.102: new integrations, new tools, ci speedups, memory leak instr…
waleedlatif1 d81055d
feat(docs): add API reference with OpenAPI spec and auto-generated en…
waleedlatif1 d26c8cc
multiline curl
waleedlatif1 ee0745d
random improvements
waleedlatif1 0207d61
cleanup
waleedlatif1 5534b00
update docs copy
waleedlatif1 9d2d237
fix build
waleedlatif1 5734f2c
cast
waleedlatif1 dc8a2e3
fix builg
waleedlatif1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| 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, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page' | ||
| @@ -12,36 +15,83 @@ import { LLMCopyButton } from '@/components/page-actions' | ||
| 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' | ||
| const SUPPORTED_LANGUAGES: Set<string> = new Set(i18n.languages) | ||
| const BASE_URL = 'https://docs.sim.ai' | ||
| 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.paremeters} | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| {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 page = source.getPage(params.slug, params.lang) | ||
| const { lang, slug } = resolveLangAndSlug(params) | ||
| const page = source.getPage(slug, lang) | ||
| if (!page) notFound() | ||
| const data = page.data as PageData | ||
| const MDX = data.body | ||
| const baseUrl = 'https://docs.sim.ai' | ||
| const markdownContent = await data.getText('processed') | ||
| 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 | ||
| const pageTreeRecord = source.pageTree as Record<string, any> | ||
| const pageTree = | ||
| pageTreeRecord[params.lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0] | ||
| const neighbours = pageTree ? findNeighbour(pageTree, page.url) : null | ||
| 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 | ||
| const neighbours = isApiReference | ||
| ? { | ||
| previous: rawNeighbours?.previous?.url.includes('/api-reference/') | ||
| ? rawNeighbours.previous | ||
| : undefined, | ||
| next: rawNeighbours?.next?.url.includes('/api-reference/') ? rawNeighbours.next : undefined, | ||
| } | ||
| : rawNeighbours | ||
| const generateBreadcrumbs = () => { | ||
| const breadcrumbs: Array<{ name: string; url: string }> = [ | ||
| { | ||
| name: 'Home', | ||
| url: baseUrl, | ||
| url: BASE_URL, | ||
| }, | ||
| ] | ||
| const urlParts = page.url.split('/').filter(Boolean) | ||
| let currentPath = '' | ||
| urlParts.forEach((part, index) => { | ||
| if (index === 0 && ['en', 'es', 'fr', 'de', 'ja', 'zh'].includes(part)) { | ||
| if (index === 0 && SUPPORTED_LANGUAGES.has(part)) { | ||
| currentPath = `/${part}` | ||
| return | ||
| } | ||
| @@ -56,12 +106,12 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l | ||
| if (index === urlParts.length - 1) { | ||
| breadcrumbs.push({ | ||
| name: data.title, | ||
| url: `${baseUrl}${page.url}`, | ||
| url: `${BASE_URL}${page.url}`, | ||
| }) | ||
| } else { | ||
| breadcrumbs.push({ | ||
| name: name, | ||
| url: `${baseUrl}${currentPath}`, | ||
| url: `${BASE_URL}${currentPath}`, | ||
| }) | ||
| } | ||
| }) | ||
| @@ -73,7 +123,6 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l | ||
| const CustomFooter = () => ( | ||
| <div className='mt-12'> | ||
| {/* Navigation links */} | ||
| <div className='flex items-center justify-between py-8'> | ||
| {neighbours?.previous ? ( | ||
| <Link | ||
| @@ -100,10 +149,8 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l | ||
| )} | ||
| </div> | ||
| {/* Divider line */} | ||
| <div className='border-border border-t' /> | ||
| {/* Social icons */} | ||
| <div className='flex items-center gap-4 py-6'> | ||
| <Link | ||
| href='https://x.com/simdotai' | ||
| @@ -169,13 +216,69 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l | ||
| </div> | ||
| ) | ||
| 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: <CustomFooter />, | ||
| }} | ||
| > | ||
| <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>{data.title}</DocsTitle> | ||
| <DocsDescription>{data.description}</DocsDescription> | ||
| </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={`${baseUrl}${page.url}`} | ||
| lang={params.lang} | ||
| url={`${BASE_URL}${page.url}`} | ||
| lang={lang} | ||
| breadcrumb={breadcrumbs} | ||
| /> | ||
| <DocsPage | ||
| @@ -252,14 +355,14 @@ export async function generateMetadata(props: { | ||
| params: Promise<{ slug?: string[]; lang: string }> | ||
| }) { | ||
| const params = await props.params | ||
| const page = source.getPage(params.slug, params.lang) | ||
| const { lang, slug } = resolveLangAndSlug(params) | ||
| const page = source.getPage(slug, lang) | ||
| if (!page) notFound() | ||
| const data = page.data as PageData | ||
| const baseUrl = 'https://docs.sim.ai' | ||
| const fullUrl = `${baseUrl}${page.url}` | ||
| const data = page.data as unknown as PageData | ||
| const fullUrl = `${BASE_URL}${page.url}` | ||
| const ogImageUrl = `${baseUrl}/api/og?title=${encodeURIComponent(data.title)}` | ||
| const ogImageUrl = `${BASE_URL}/api/og?title=${encodeURIComponent(data.title)}` | ||
| return { | ||
| title: data.title, | ||
| @@ -286,10 +389,10 @@ export async function generateMetadata(props: { | ||
| url: fullUrl, | ||
| siteName: 'Sim Documentation', | ||
| type: 'article', | ||
| locale: params.lang === 'en' ? 'en_US' : `${params.lang}_${params.lang.toUpperCase()}`, | ||
| locale: lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`, | ||
| alternateLocale: ['en', 'es', 'fr', 'de', 'ja', 'zh'] | ||
| .filter((lang) => lang !== params.lang) | ||
| .map((lang) => (lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`)), | ||
| .filter((l) => l !== lang) | ||
| .map((l) => (l === 'en' ? 'en_US' : `${l}_${l.toUpperCase()}`)), | ||
| images: [ | ||
| { | ||
| url: ogImageUrl, | ||
| @@ -323,13 +426,13 @@ export async function generateMetadata(props: { | ||
| alternates: { | ||
| canonical: fullUrl, | ||
| languages: { | ||
| 'x-default': `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`, | ||
| en: `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`, | ||
| es: `${baseUrl}/es${page.url.replace(`/${params.lang}`, '')}`, | ||
| fr: `${baseUrl}/fr${page.url.replace(`/${params.lang}`, '')}`, | ||
| de: `${baseUrl}/de${page.url.replace(`/${params.lang}`, '')}`, | ||
| ja: `${baseUrl}/ja${page.url.replace(`/${params.lang}`, '')}`, | ||
| zh: `${baseUrl}/zh${page.url.replace(`/${params.lang}`, '')}`, | ||
| '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}`, '')}`, | ||
| }, | ||
| }, | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.