Skip to content

Commit 9d6dfa2

Browse files
feat(api): add example API endpoints
1 parent 14af6cb commit 9d6dfa2

File tree

10 files changed

+1080
-104
lines changed

10 files changed

+1080
-104
lines changed
Lines changed: 8 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,11 @@
11
/* eslint-disable no-console */
2-
import type { APIRoute, GetStaticPaths } from 'astro'
3-
import type { CollectionEntry, CollectionKey } from 'astro:content'
4-
import { getCollection } from 'astro:content'
5-
import { content } from '../../../../../content'
6-
import {
7-
kebabCase,
8-
addDemosOrDeprecated,
9-
getDefaultTabForApi,
10-
} from '../../../../../utils'
11-
import { generateAndWriteApiIndex } from '../../../../../utils/apiIndex/generate'
12-
import { getApiIndex } from '../../../../../utils/apiIndex/get'
13-
import {
14-
createJsonResponse,
15-
createTextResponse,
16-
createIndexKey,
17-
} from '../../../../../utils/apiHelpers'
2+
import type { APIRoute } from 'astro'
3+
import { fetchApiIndex } from '../../../../../utils/apiIndex/fetch'
4+
import { createJsonResponse, createIndexKey } from '../../../../../utils/apiHelpers'
185

19-
export const prerender = true
6+
export const prerender = false
207

21-
type ContentEntry = CollectionEntry<
22-
'core-docs' | 'quickstarts-docs' | 'react-component-docs'
23-
>
24-
25-
export const getStaticPaths: GetStaticPaths = async () => {
26-
// Generate index file for server-side routes to use
27-
// This runs once during build when getCollection() is available
28-
const index = await generateAndWriteApiIndex()
29-
30-
const paths: {
31-
params: { version: string; section: string; page: string; tab: string }
32-
}[] = []
33-
34-
// Build paths from index structure
35-
for (const version of index.versions) {
36-
for (const section of index.sections[version] || []) {
37-
const sectionKey = createIndexKey(version, section)
38-
for (const page of index.pages[sectionKey] || []) {
39-
const pageKey = createIndexKey(version, section, page)
40-
for (const tab of index.tabs[pageKey] || []) {
41-
paths.push({ params: { version, section, page, tab } })
42-
}
43-
}
44-
}
45-
}
46-
47-
// This shouldn't happen since we have a fallback tab value, but if it somehow does we need to alert the user
48-
paths.forEach((path) => {
49-
if (!path.params.tab) {
50-
console.warn(`[API Warning] Tab not found for path: ${path.params.version}/${path.params.section}/${path.params.page}`)
51-
}
52-
})
53-
54-
// Again, this shouldn't happen since we have a fallback tab value, but if it somehow does and we don't filter out tabless paths it will crash the build
55-
return paths.filter((path) => !!path.params.tab)
56-
}
57-
58-
export const GET: APIRoute = async ({ params }) => {
8+
export const GET: APIRoute = async ({ params, redirect, url }) => {
599
const { version, section, page, tab } = params
6010

6111
if (!version || !section || !page || !tab) {
@@ -66,7 +16,7 @@ export const GET: APIRoute = async ({ params }) => {
6616
}
6717

6818
// Validate using index first (fast path for 404s)
69-
const index = await getApiIndex()
19+
const index = await fetchApiIndex(url)
7020

7121
// Check if version exists
7222
if (!index.versions.includes(version)) {
@@ -103,51 +53,6 @@ export const GET: APIRoute = async ({ params }) => {
10353
)
10454
}
10555

106-
// Path is valid, now fetch the actual content
107-
const collectionsToFetch = content
108-
.filter((entry) => entry.version === version)
109-
.map((entry) => entry.name as CollectionKey)
110-
111-
const collections = await Promise.all(
112-
collectionsToFetch.map((name) => getCollection(name)),
113-
)
114-
115-
const flatEntries = collections.flat().map(({ data, filePath, ...rest }) => ({
116-
filePath,
117-
...rest,
118-
data: {
119-
...data,
120-
tab: data.tab || data.source || getDefaultTabForApi(filePath),
121-
},
122-
}))
123-
124-
// Find the matching entry
125-
const matchingEntry = flatEntries.find((entry: ContentEntry) => {
126-
const entryTab = addDemosOrDeprecated(entry.data.tab, entry.id)
127-
return (
128-
entry.data.section === section &&
129-
kebabCase(entry.data.id) === page &&
130-
entryTab === tab
131-
)
132-
})
133-
134-
// This shouldn't happen since we validated with index, but handle it anyway
135-
if (!matchingEntry) {
136-
// Log warning - indicates index/content mismatch
137-
console.warn(
138-
`[API Warning] Index exists but content not found: ${version}/${section}/${page}/${tab}. ` +
139-
'This may indicate a mismatch between index generation and actual content.',
140-
)
141-
return createJsonResponse(
142-
{
143-
error: `Content not found for tab '${tab}' in page '${page}', section '${section}', version '${version}'`,
144-
},
145-
404,
146-
)
147-
}
148-
149-
// Get the raw body content (markdown/mdx text)
150-
const textContent = matchingEntry.body || ''
151-
152-
return createTextResponse(textContent)
56+
// Redirect to the text endpoint
57+
return redirect(`${url.pathname}/text`)
15358
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { APIRoute, GetStaticPaths } from 'astro'
2+
import { createJsonResponse, createIndexKey } from '../../../../../../utils/apiHelpers'
3+
import { getApiIndex } from '../../../../../../utils/apiIndex/get'
4+
import { fetchApiIndex } from '../../../../../../utils/apiIndex/fetch'
5+
6+
export const prerender = false
7+
8+
export const getStaticPaths: GetStaticPaths = async () => {
9+
// Use the pre-generated index file
10+
const index = await getApiIndex()
11+
12+
const paths: { params: { version: string; section: string; page: string; tab: string } }[] = []
13+
14+
// Build paths from index structure
15+
for (const version of index.versions) {
16+
for (const section of index.sections[version] || []) {
17+
const sectionKey = createIndexKey(version, section)
18+
for (const page of index.pages[sectionKey] || []) {
19+
const pageKey = createIndexKey(version, section, page)
20+
for (const tab of index.tabs[pageKey] || []) {
21+
// Only create paths for tabs that have examples
22+
const tabKey = createIndexKey(version, section, page, tab)
23+
if (index.examples[tabKey] && index.examples[tabKey].length > 0) {
24+
paths.push({ params: { version, section, page, tab } })
25+
}
26+
}
27+
}
28+
}
29+
}
30+
31+
return paths
32+
}
33+
34+
export const GET: APIRoute = async ({ params, url }) => {
35+
const { version, section, page, tab } = params
36+
37+
if (!version || !section || !page || !tab) {
38+
return createJsonResponse(
39+
{ error: 'Version, section, page, and tab parameters are required' },
40+
400,
41+
)
42+
}
43+
44+
// Get examples with titles directly from the index
45+
const index = await fetchApiIndex(url)
46+
const tabKey = createIndexKey(version, section, page, tab)
47+
const examples = index.examples[tabKey] || []
48+
49+
return createJsonResponse(examples)
50+
}
51+
52+
53+
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { APIRoute, GetStaticPaths } from 'astro'
2+
import type { CollectionEntry, CollectionKey } from 'astro:content'
3+
import { getCollection } from 'astro:content'
4+
import { readFile } from 'fs/promises'
5+
import { resolve } from 'path'
6+
import { content } from '../../../../../../../content'
7+
import { kebabCase, getDefaultTab, addDemosOrDeprecated } from '../../../../../../../utils'
8+
import { createJsonResponse, createTextResponse, createIndexKey } from '../../../../../../../utils/apiHelpers'
9+
import { generateAndWriteApiIndex } from '../../../../../../../utils/apiIndex/generate'
10+
11+
export const prerender = true
12+
13+
export const getStaticPaths: GetStaticPaths = async () => {
14+
// Generate index file (will be cached if already generated)
15+
const index = await generateAndWriteApiIndex()
16+
17+
const paths: {
18+
params: {
19+
version: string
20+
section: string
21+
page: string
22+
tab: string
23+
example: string
24+
}
25+
}[] = []
26+
27+
// Build paths from index structure
28+
for (const version of index.versions) {
29+
for (const section of index.sections[version] || []) {
30+
const sectionKey = createIndexKey(version, section)
31+
for (const page of index.pages[sectionKey] || []) {
32+
const pageKey = createIndexKey(version, section, page)
33+
for (const tab of index.tabs[pageKey] || []) {
34+
const tabKey = createIndexKey(version, section, page, tab)
35+
36+
// Get all examples for this tab
37+
const examples = index.examples[tabKey] || []
38+
for (const example of examples) {
39+
paths.push({
40+
params: {
41+
version,
42+
section,
43+
page,
44+
tab,
45+
example: example.exampleName,
46+
},
47+
})
48+
}
49+
}
50+
}
51+
}
52+
}
53+
54+
return paths
55+
}
56+
57+
function getImports(fileContent: string) {
58+
const importRegex = /import.*from.*['"]\..*\/[\w\.\?]*['"]/gm
59+
const matches = fileContent.match(importRegex)
60+
return matches
61+
}
62+
63+
function getExampleFilePath(imports: string[], exampleName: string) {
64+
const exampleImport = imports.find((imp) => imp.includes(exampleName))
65+
if (!exampleImport) {
66+
console.error('No import path found for example', exampleName)
67+
return null
68+
}
69+
const match = exampleImport.match(/['"]\..*\/[\w\.]*\?/)
70+
if (!match) {
71+
return null
72+
}
73+
return match[0].replace(/['"?]/g, '')
74+
}
75+
76+
async function getCollections(version: string) {
77+
const collectionsToFetch = content
78+
.filter((entry) => entry.version === version)
79+
.map((entry) => entry.name as CollectionKey)
80+
const collections = await Promise.all(
81+
collectionsToFetch.map(async (name) => await getCollection(name)),
82+
)
83+
return collections.flat().map(({ data, filePath, ...rest }) => ({
84+
filePath,
85+
...rest,
86+
data: {
87+
...data,
88+
tab: data.tab || data.source || getDefaultTab(filePath),
89+
},
90+
}))
91+
}
92+
93+
async function getContentEntryFilePath(collections: CollectionEntry<'core-docs' | 'quickstarts-docs' | 'react-component-docs'>[], section: string, page: string, tab: string) {
94+
const contentEntry = collections.find((entry) => entry.data.section === section && kebabCase(entry.data.id) === page && entry.data.tab === tab)
95+
if (!contentEntry) {
96+
console.error('No content entry found for section', section, 'page', page, 'tab', tab)
97+
return null
98+
}
99+
if (typeof contentEntry.filePath !== 'string') {
100+
console.error('No file path found for content entry', contentEntry.id)
101+
return null
102+
}
103+
return contentEntry.filePath
104+
}
105+
106+
export const GET: APIRoute = async ({ params }) => {
107+
const { version, section, page, tab, example } = params
108+
if (!version || !section || !page || !tab || !example) {
109+
return createJsonResponse({ error: 'Version, section, page, tab, and example parameters are required' }, 400)
110+
}
111+
112+
const collections = await getCollections(version)
113+
const contentEntryFilePath = await getContentEntryFilePath(collections, section, page, tab)
114+
115+
if (!contentEntryFilePath) {
116+
return createJsonResponse({ error: 'Content entry not found' }, 404)
117+
}
118+
119+
const contentEntryFileContent = await readFile(contentEntryFilePath, 'utf8')
120+
121+
const contentEntryImports = getImports(contentEntryFileContent)
122+
if (!contentEntryImports) {
123+
return createJsonResponse({ error: 'Content entry imports not found' }, 404)
124+
}
125+
126+
const relativeExampleFilePath = getExampleFilePath(contentEntryImports, example)
127+
if (!relativeExampleFilePath) {
128+
return createJsonResponse({ error: 'Example file path not found' }, 404)
129+
}
130+
131+
const absoluteExampleFilePath = resolve(contentEntryFilePath, '../', relativeExampleFilePath)
132+
const exampleFileContent = await readFile(absoluteExampleFilePath, 'utf8')
133+
134+
return createTextResponse(exampleFileContent)
135+
}

0 commit comments

Comments
 (0)