Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,76 +131,106 @@ jest.mock('../../../../../../../utils/apiIndex/get', () => ({
}),
}))

/**
* Mock fetchApiIndex to return the same data as getApiIndex
*/
jest.mock('../../../../../../../utils/apiIndex/fetch', () => ({
fetchApiIndex: jest.fn().mockResolvedValue({
versions: ['v6'],
sections: {
v6: ['components'],
},
pages: {
'v6::components': ['alert'],
},
tabs: {
'v6::components::alert': ['react', 'html', 'react-demos'],
},
}),
}))

beforeEach(() => {
jest.clearAllMocks()
})

it('returns markdown/MDX content as plain text', async () => {
it('redirects to /text endpoint', async () => {
const mockRedirect = jest.fn((path: string) => new Response(null, { status: 302, headers: { Location: path } }))
const response = await GET({
params: {
version: 'v6',
section: 'components',
page: 'alert',
tab: 'react',
},
url: new URL('http://localhost/api/v6/components/alert/react'),
redirect: mockRedirect,
} as any)
const body = await response.text()

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

it('returns different content for different tabs', async () => {
it('redirects to /text endpoint for different tabs', async () => {
const mockRedirect = jest.fn((path: string) => new Response(null, { status: 302, headers: { Location: path } }))

const reactResponse = await GET({
params: {
version: 'v6',
section: 'components',
page: 'alert',
tab: 'react',
},
url: new URL('http://localhost/api/v6/components/alert/react'),
redirect: mockRedirect,
} as any)
const reactBody = await reactResponse.text()

expect(reactResponse.status).toBe(302)
expect(mockRedirect).toHaveBeenCalledWith('/api/v6/components/alert/react/text')

mockRedirect.mockClear()
const htmlResponse = await GET({
params: {
version: 'v6',
section: 'components',
page: 'alert',
tab: 'html',
},
url: new URL('http://localhost/api/v6/components/alert/html'),
redirect: mockRedirect,
} as any)
const htmlBody = await htmlResponse.text()

expect(reactBody).toContain('React Alert')
expect(htmlBody).toContain('HTML')
expect(reactBody).not.toEqual(htmlBody)
expect(htmlResponse.status).toBe(302)
expect(mockRedirect).toHaveBeenCalledWith('/api/v6/components/alert/html/text')
})

it('returns demo content for demos tabs', async () => {
it('redirects demos tabs to /text endpoint', async () => {
const mockRedirect = jest.fn((path: string) => new Response(null, { status: 302, headers: { Location: path } }))
const response = await GET({
params: {
version: 'v6',
section: 'components',
page: 'alert',
tab: 'react-demos',
},
url: new URL('http://localhost/api/v6/components/alert/react-demos'),
redirect: mockRedirect,
} as any)
const body = await response.text()

expect(response.status).toBe(200)
expect(body).toContain('demos')
expect(response.status).toBe(302)
expect(mockRedirect).toHaveBeenCalledWith('/api/v6/components/alert/react-demos/text')
})

it('returns 404 error for nonexistent version', async () => {
const mockRedirect = jest.fn()
const response = await GET({
params: {
version: 'v99',
section: 'components',
page: 'alert',
tab: 'react',
},
url: new URL('http://localhost/api/v99/components/alert/react'),
redirect: mockRedirect,
} as any)
const body = await response.json()

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

it('returns 404 error for nonexistent section', async () => {
const mockRedirect = jest.fn()
const response = await GET({
params: {
version: 'v6',
section: 'invalid',
page: 'alert',
tab: 'react',
},
url: new URL('http://localhost/api/v6/invalid/alert/react'),
redirect: mockRedirect,
} as any)
const body = await response.json()

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

it('returns 404 error for nonexistent page', async () => {
const mockRedirect = jest.fn()
const response = await GET({
params: {
version: 'v6',
section: 'components',
page: 'nonexistent',
tab: 'react',
},
url: new URL('http://localhost/api/v6/components/nonexistent/react'),
redirect: mockRedirect,
} as any)
const body = await response.json()

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

it('returns 404 error for nonexistent tab', async () => {
const mockRedirect = jest.fn()
const response = await GET({
params: {
version: 'v6',
section: 'components',
page: 'alert',
tab: 'nonexistent',
},
url: new URL('http://localhost/api/v6/components/alert/nonexistent'),
redirect: mockRedirect,
} as any)
const body = await response.json()

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

it('returns 400 error when required parameters are missing', async () => {
const mockRedirect = jest.fn()
const response = await GET({
params: {
version: 'v6',
section: 'components',
page: 'alert',
},
url: new URL('http://localhost/api/v6/components/alert'),
redirect: mockRedirect,
} as any)
const body = await response.json()

Expand Down
2 changes: 1 addition & 1 deletion src/pages/[section]/[page]/[tab].astro
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function getStaticPaths() {
.filter((entry) => entry.data.tab) // only pages with a tab should match this route
.map((entry) => {
// Build tabs dictionary
const tab = addDemosOrDeprecated(entry.data.tab, entry.id)
const tab = addDemosOrDeprecated(entry.data.tab, entry.filePath)
buildTab(entry, tab)
if (entry.data.tabName) {
tabNames[entry.data.tab] = entry.data.tabName
Expand Down
112 changes: 8 additions & 104 deletions src/pages/api/[version]/[section]/[page]/[tab].ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,10 @@
/* eslint-disable no-console */
import type { APIRoute, GetStaticPaths } from 'astro'
import type { CollectionEntry, CollectionKey } from 'astro:content'
import { getCollection } from 'astro:content'
import { content } from '../../../../../content'
import {
kebabCase,
addDemosOrDeprecated,
getDefaultTabForApi,
} from '../../../../../utils'
import { generateAndWriteApiIndex } from '../../../../../utils/apiIndex/generate'
import { getApiIndex } from '../../../../../utils/apiIndex/get'
import {
createJsonResponse,
createTextResponse,
createIndexKey,
} from '../../../../../utils/apiHelpers'
import type { APIRoute } from 'astro'
import { fetchApiIndex } from '../../../../../utils/apiIndex/fetch'
import { createJsonResponse, createIndexKey } from '../../../../../utils/apiHelpers'

export const prerender = true
export const prerender = false

type ContentEntry = CollectionEntry<
'core-docs' | 'quickstarts-docs' | 'react-component-docs'
>

export const getStaticPaths: GetStaticPaths = async () => {
// Generate index file for server-side routes to use
// This runs once during build when getCollection() is available
const index = await generateAndWriteApiIndex()

const paths: {
params: { version: string; section: string; page: string; tab: string }
}[] = []

// Build paths from index structure
for (const version of index.versions) {
for (const section of index.sections[version] || []) {
const sectionKey = createIndexKey(version, section)
for (const page of index.pages[sectionKey] || []) {
const pageKey = createIndexKey(version, section, page)
for (const tab of index.tabs[pageKey] || []) {
paths.push({ params: { version, section, page, tab } })
}
}
}
}

// This shouldn't happen since we have a fallback tab value, but if it somehow does we need to alert the user
paths.forEach((path) => {
if (!path.params.tab) {
console.warn(`[API Warning] Tab not found for path: ${path.params.version}/${path.params.section}/${path.params.page}`)
}
})

// 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
return paths.filter((path) => !!path.params.tab)
}

export const GET: APIRoute = async ({ params }) => {
export const GET: APIRoute = async ({ params, redirect, url }) => {
const { version, section, page, tab } = params

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

// Validate using index first (fast path for 404s)
const index = await getApiIndex()
const index = await fetchApiIndex(url)

// Check if version exists
if (!index.versions.includes(version)) {
Expand Down Expand Up @@ -103,51 +52,6 @@ export const GET: APIRoute = async ({ params }) => {
)
}

// Path is valid, now fetch the actual content
const collectionsToFetch = content
.filter((entry) => entry.version === version)
.map((entry) => entry.name as CollectionKey)

const collections = await Promise.all(
collectionsToFetch.map((name) => getCollection(name)),
)

const flatEntries = collections.flat().map(({ data, filePath, ...rest }) => ({
filePath,
...rest,
data: {
...data,
tab: data.tab || data.source || getDefaultTabForApi(filePath),
},
}))

// Find the matching entry
const matchingEntry = flatEntries.find((entry: ContentEntry) => {
const entryTab = addDemosOrDeprecated(entry.data.tab, entry.id)
return (
entry.data.section === section &&
kebabCase(entry.data.id) === page &&
entryTab === tab
)
})

// This shouldn't happen since we validated with index, but handle it anyway
if (!matchingEntry) {
// Log warning - indicates index/content mismatch
console.warn(
`[API Warning] Index exists but content not found: ${version}/${section}/${page}/${tab}. ` +
'This may indicate a mismatch between index generation and actual content.',
)
return createJsonResponse(
{
error: `Content not found for tab '${tab}' in page '${page}', section '${section}', version '${version}'`,
},
404,
)
}

// Get the raw body content (markdown/mdx text)
const textContent = matchingEntry.body || ''

return createTextResponse(textContent)
// Redirect to the text endpoint
return redirect(`${url.pathname}/text`)
}
34 changes: 34 additions & 0 deletions src/pages/api/[version]/[section]/[page]/[tab]/examples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { APIRoute } from 'astro'
import { createJsonResponse, createIndexKey } from '../../../../../../utils/apiHelpers'
import { fetchApiIndex } from '../../../../../../utils/apiIndex/fetch'

export const prerender = false

export const GET: APIRoute = async ({ params, url }) => {
const { version, section, page, tab } = params

if (!version || !section || !page || !tab) {
return createJsonResponse(
{ error: 'Version, section, page, and tab parameters are required' },
400,
)
}

// Get examples with titles directly from the index
try {
const index = await fetchApiIndex(url)
const tabKey = createIndexKey(version, section, page, tab)
const examples = index.examples[tabKey] || []

return createJsonResponse(examples)
} catch (error) {
const details = error instanceof Error ? error.message : String(error)
return createJsonResponse(
{ error: 'Failed to load API index', details },
500,
)
}
}



Loading