Skip to content

Commit e8cabb8

Browse files
Merge pull request #190 from patternfly/add-example-api
feat(api): add example API endpoints
2 parents 11156f9 + 07fc9ad commit e8cabb8

File tree

12 files changed

+1293
-124
lines changed

12 files changed

+1293
-124
lines changed

src/__tests__/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -131,76 +131,106 @@ jest.mock('../../../../../../../utils/apiIndex/get', () => ({
131131
}),
132132
}))
133133

134+
/**
135+
* Mock fetchApiIndex to return the same data as getApiIndex
136+
*/
137+
jest.mock('../../../../../../../utils/apiIndex/fetch', () => ({
138+
fetchApiIndex: jest.fn().mockResolvedValue({
139+
versions: ['v6'],
140+
sections: {
141+
v6: ['components'],
142+
},
143+
pages: {
144+
'v6::components': ['alert'],
145+
},
146+
tabs: {
147+
'v6::components::alert': ['react', 'html', 'react-demos'],
148+
},
149+
}),
150+
}))
151+
134152
beforeEach(() => {
135153
jest.clearAllMocks()
136154
})
137155

138-
it('returns markdown/MDX content as plain text', async () => {
156+
it('redirects to /text endpoint', async () => {
157+
const mockRedirect = jest.fn((path: string) => new Response(null, { status: 302, headers: { Location: path } }))
139158
const response = await GET({
140159
params: {
141160
version: 'v6',
142161
section: 'components',
143162
page: 'alert',
144163
tab: 'react',
145164
},
165+
url: new URL('http://localhost/api/v6/components/alert/react'),
166+
redirect: mockRedirect,
146167
} as any)
147-
const body = await response.text()
148168

149-
expect(response.status).toBe(200)
150-
expect(response.headers.get('Content-Type')).toBe('text/plain; charset=utf-8')
151-
expect(typeof body).toBe('string')
152-
expect(body).toContain('Alert Component')
169+
expect(mockRedirect).toHaveBeenCalledWith('/api/v6/components/alert/react/text')
170+
expect(response.status).toBe(302)
153171
})
154172

155-
it('returns different content for different tabs', async () => {
173+
it('redirects to /text endpoint for different tabs', async () => {
174+
const mockRedirect = jest.fn((path: string) => new Response(null, { status: 302, headers: { Location: path } }))
175+
156176
const reactResponse = await GET({
157177
params: {
158178
version: 'v6',
159179
section: 'components',
160180
page: 'alert',
161181
tab: 'react',
162182
},
183+
url: new URL('http://localhost/api/v6/components/alert/react'),
184+
redirect: mockRedirect,
163185
} as any)
164-
const reactBody = await reactResponse.text()
165186

187+
expect(reactResponse.status).toBe(302)
188+
expect(mockRedirect).toHaveBeenCalledWith('/api/v6/components/alert/react/text')
189+
190+
mockRedirect.mockClear()
166191
const htmlResponse = await GET({
167192
params: {
168193
version: 'v6',
169194
section: 'components',
170195
page: 'alert',
171196
tab: 'html',
172197
},
198+
url: new URL('http://localhost/api/v6/components/alert/html'),
199+
redirect: mockRedirect,
173200
} as any)
174-
const htmlBody = await htmlResponse.text()
175201

176-
expect(reactBody).toContain('React Alert')
177-
expect(htmlBody).toContain('HTML')
178-
expect(reactBody).not.toEqual(htmlBody)
202+
expect(htmlResponse.status).toBe(302)
203+
expect(mockRedirect).toHaveBeenCalledWith('/api/v6/components/alert/html/text')
179204
})
180205

181-
it('returns demo content for demos tabs', async () => {
206+
it('redirects demos tabs to /text endpoint', async () => {
207+
const mockRedirect = jest.fn((path: string) => new Response(null, { status: 302, headers: { Location: path } }))
182208
const response = await GET({
183209
params: {
184210
version: 'v6',
185211
section: 'components',
186212
page: 'alert',
187213
tab: 'react-demos',
188214
},
215+
url: new URL('http://localhost/api/v6/components/alert/react-demos'),
216+
redirect: mockRedirect,
189217
} as any)
190-
const body = await response.text()
191218

192-
expect(response.status).toBe(200)
193-
expect(body).toContain('demos')
219+
expect(response.status).toBe(302)
220+
expect(mockRedirect).toHaveBeenCalledWith('/api/v6/components/alert/react-demos/text')
194221
})
195222

196223
it('returns 404 error for nonexistent version', async () => {
224+
const mockRedirect = jest.fn()
197225
const response = await GET({
198226
params: {
199227
version: 'v99',
200228
section: 'components',
201229
page: 'alert',
202230
tab: 'react',
203231
},
232+
url: new URL('http://localhost/api/v99/components/alert/react'),
233+
redirect: mockRedirect,
204234
} as any)
205235
const body = await response.json()
206236

@@ -210,13 +240,16 @@ it('returns 404 error for nonexistent version', async () => {
210240
})
211241

212242
it('returns 404 error for nonexistent section', async () => {
243+
const mockRedirect = jest.fn()
213244
const response = await GET({
214245
params: {
215246
version: 'v6',
216247
section: 'invalid',
217248
page: 'alert',
218249
tab: 'react',
219250
},
251+
url: new URL('http://localhost/api/v6/invalid/alert/react'),
252+
redirect: mockRedirect,
220253
} as any)
221254
const body = await response.json()
222255

@@ -225,13 +258,16 @@ it('returns 404 error for nonexistent section', async () => {
225258
})
226259

227260
it('returns 404 error for nonexistent page', async () => {
261+
const mockRedirect = jest.fn()
228262
const response = await GET({
229263
params: {
230264
version: 'v6',
231265
section: 'components',
232266
page: 'nonexistent',
233267
tab: 'react',
234268
},
269+
url: new URL('http://localhost/api/v6/components/nonexistent/react'),
270+
redirect: mockRedirect,
235271
} as any)
236272
const body = await response.json()
237273

@@ -241,13 +277,16 @@ it('returns 404 error for nonexistent page', async () => {
241277
})
242278

243279
it('returns 404 error for nonexistent tab', async () => {
280+
const mockRedirect = jest.fn()
244281
const response = await GET({
245282
params: {
246283
version: 'v6',
247284
section: 'components',
248285
page: 'alert',
249286
tab: 'nonexistent',
250287
},
288+
url: new URL('http://localhost/api/v6/components/alert/nonexistent'),
289+
redirect: mockRedirect,
251290
} as any)
252291
const body = await response.json()
253292

@@ -257,12 +296,15 @@ it('returns 404 error for nonexistent tab', async () => {
257296
})
258297

259298
it('returns 400 error when required parameters are missing', async () => {
299+
const mockRedirect = jest.fn()
260300
const response = await GET({
261301
params: {
262302
version: 'v6',
263303
section: 'components',
264304
page: 'alert',
265305
},
306+
url: new URL('http://localhost/api/v6/components/alert'),
307+
redirect: mockRedirect,
266308
} as any)
267309
const body = await response.json()
268310

src/pages/[section]/[page]/[tab].astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export async function getStaticPaths() {
5050
.filter((entry) => entry.data.tab) // only pages with a tab should match this route
5151
.map((entry) => {
5252
// Build tabs dictionary
53-
const tab = addDemosOrDeprecated(entry.data.tab, entry.id)
53+
const tab = addDemosOrDeprecated(entry.data.tab, entry.filePath)
5454
buildTab(entry, tab)
5555
if (entry.data.tabName) {
5656
tabNames[entry.data.tab] = entry.data.tabName
Lines changed: 8 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,10 @@
1-
/* 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'
1+
import type { APIRoute } from 'astro'
2+
import { fetchApiIndex } from '../../../../../utils/apiIndex/fetch'
3+
import { createJsonResponse, createIndexKey } from '../../../../../utils/apiHelpers'
184

19-
export const prerender = true
5+
export const prerender = false
206

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 }) => {
7+
export const GET: APIRoute = async ({ params, redirect, url }) => {
598
const { version, section, page, tab } = params
609

6110
if (!version || !section || !page || !tab) {
@@ -66,7 +15,7 @@ export const GET: APIRoute = async ({ params }) => {
6615
}
6716

6817
// Validate using index first (fast path for 404s)
69-
const index = await getApiIndex()
18+
const index = await fetchApiIndex(url)
7019

7120
// Check if version exists
7221
if (!index.versions.includes(version)) {
@@ -103,51 +52,6 @@ export const GET: APIRoute = async ({ params }) => {
10352
)
10453
}
10554

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)
55+
// Redirect to the text endpoint
56+
return redirect(`${url.pathname}/text`)
15357
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { APIRoute } from 'astro'
2+
import { createJsonResponse, createIndexKey } from '../../../../../../utils/apiHelpers'
3+
import { fetchApiIndex } from '../../../../../../utils/apiIndex/fetch'
4+
5+
export const prerender = false
6+
7+
export const GET: APIRoute = async ({ params, url }) => {
8+
const { version, section, page, tab } = params
9+
10+
if (!version || !section || !page || !tab) {
11+
return createJsonResponse(
12+
{ error: 'Version, section, page, and tab parameters are required' },
13+
400,
14+
)
15+
}
16+
17+
// Get examples with titles directly from the index
18+
try {
19+
const index = await fetchApiIndex(url)
20+
const tabKey = createIndexKey(version, section, page, tab)
21+
const examples = index.examples[tabKey] || []
22+
23+
return createJsonResponse(examples)
24+
} catch (error) {
25+
const details = error instanceof Error ? error.message : String(error)
26+
return createJsonResponse(
27+
{ error: 'Failed to load API index', details },
28+
500,
29+
)
30+
}
31+
}
32+
33+
34+

0 commit comments

Comments
 (0)