Skip to content

Commit bcdf198

Browse files
kylemclarenclaude
andcommitted
Generate API sidebar dynamically from schema with version-aware links
- Add sidebar transformation utilities to api-versions.ts: - rewriteApiUrl() rewrites API URLs to target version - transformSidebarForVersion() recursively transforms sidebar entries - Type definitions for SidebarEntry, SidebarLink, SidebarGroup - Update Sidebar.astro to transform sidebar at render time: - Detects current API version from URL path - Rewrites all /api/ links to match current version - Sidebar links now match the version being viewed - Modify generate-api-docs.ts to output src/lib/api-sidebar.ts: - New generateUnifiedSidebarFile() function - Uses versionToSlug() for Astro-compatible paths - Auto-generated from API schema at build time - Update sidebar.ts to import generated config: - Remove 150+ lines of hardcoded API Reference section - Import apiSidebarConfig from generated file Benefits: - Sidebar auto-updates when API schema changes - New endpoints appear automatically - Version switching shows correct links for each version Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent c01d475 commit bcdf198

File tree

5 files changed

+387
-163
lines changed

5 files changed

+387
-163
lines changed

scripts/generate-api-docs.ts

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
API_VERSIONS,
2222
type APIVersion,
2323
DEFAULT_VERSION,
24+
versionToSlug,
2425
} from '../src/lib/api-versions';
2526

2627
const OUTPUT_BASE_DIR = './src/content/docs/api';
@@ -1217,8 +1218,11 @@ function generateSidebarItems(
12171218
endpointsByCategory: Record<string, APIEndpoint[]>,
12181219
versionId: string,
12191220
): SidebarItem[] {
1221+
// Use slugified version for Astro routing (dots removed)
1222+
const versionSlug = versionToSlug(versionId);
1223+
12201224
const items: SidebarItem[] = [
1221-
{ label: 'Overview', slug: `api/${versionId}` },
1225+
{ label: 'Overview', slug: `api/${versionSlug}` },
12221226
];
12231227

12241228
// Add manual pages (Sprites) with nested endpoint items
@@ -1227,7 +1231,7 @@ function generateSidebarItems(
12271231
// Create a group with nested endpoint items
12281232
const endpointItems: SidebarLink[] = page.endpoints.map((ep) => ({
12291233
label: ep.title,
1230-
link: `/api/${versionId}/${page.category}#${slugifyEndpoint(ep.title)}`,
1234+
link: `/api/${versionSlug}/${page.category}#${slugifyEndpoint(ep.title)}`,
12311235
attrs: getMethodAttrs(ep.method),
12321236
}));
12331237
items.push({
@@ -1238,7 +1242,7 @@ function generateSidebarItems(
12381242
} else {
12391243
items.push({
12401244
label: page.title,
1241-
slug: `api/${versionId}/${page.category}`,
1245+
slug: `api/${versionSlug}/${page.category}`,
12421246
});
12431247
}
12441248
}
@@ -1249,7 +1253,7 @@ function generateSidebarItems(
12491253
if (endpoints.length > 0) {
12501254
const endpointItems: SidebarLink[] = endpoints.map((ep) => ({
12511255
label: ep.name,
1252-
link: `/api/${versionId}/${category}#${slugifyEndpoint(ep.name)}`,
1256+
link: `/api/${versionSlug}/${category}#${slugifyEndpoint(ep.name)}`,
12531257
attrs: getMethodAttrs(ep.method),
12541258
}));
12551259
items.push({
@@ -1260,12 +1264,12 @@ function generateSidebarItems(
12601264
} else {
12611265
items.push({
12621266
label: getCategoryTitle(category),
1263-
slug: `api/${versionId}/${category}`,
1267+
slug: `api/${versionSlug}/${category}`,
12641268
});
12651269
}
12661270
}
12671271

1268-
items.push({ label: 'Type Definitions', slug: `api/${versionId}/types` });
1272+
items.push({ label: 'Type Definitions', slug: `api/${versionSlug}/types` });
12691273

12701274
return items;
12711275
}
@@ -1334,6 +1338,48 @@ ${itemsStr}
13341338
`;
13351339
}
13361340

1341+
/**
1342+
* Generate the unified API sidebar config file for use in sidebar.ts.
1343+
* This uses the DEFAULT_VERSION and is imported by the main sidebar config.
1344+
*/
1345+
function generateUnifiedSidebarFile(
1346+
categories: string[],
1347+
endpointsByCategory: Record<string, APIEndpoint[]>,
1348+
versionId: string,
1349+
): string {
1350+
const items = generateSidebarItems(
1351+
categories,
1352+
endpointsByCategory,
1353+
versionId,
1354+
);
1355+
1356+
// Generate TypeScript with proper typing
1357+
const itemsStr = items
1358+
.map((item) => serializeSidebarItem(item, 1))
1359+
.join(',\n');
1360+
1361+
const versionSlug = versionToSlug(versionId);
1362+
1363+
return `/**
1364+
* Auto-generated API sidebar configuration.
1365+
* Generated by scripts/generate-api-docs.ts
1366+
* DO NOT EDIT MANUALLY - changes will be overwritten.
1367+
*
1368+
* This file uses the default API version (${versionId}${versionSlug}) for sidebar links.
1369+
* The Sidebar.astro component dynamically rewrites these links based on
1370+
* the current URL's version, so users see version-appropriate links.
1371+
*/
1372+
1373+
import type { StarlightUserConfig } from '@astrojs/starlight/types';
1374+
1375+
type SidebarItem = NonNullable<StarlightUserConfig['sidebar']>[number];
1376+
1377+
export const apiSidebarConfig: SidebarItem[] = [
1378+
${itemsStr}
1379+
];
1380+
`;
1381+
}
1382+
13371383
// ============================================================================
13381384
// Manual page handling - shared across all versions
13391385
// ============================================================================
@@ -1482,6 +1528,8 @@ async function generateVersionDocs(version: APIVersion): Promise<{
14821528
// Main
14831529
// ============================================================================
14841530

1531+
const API_SIDEBAR_OUTPUT_PATH = './src/lib/api-sidebar.ts';
1532+
14851533
async function main() {
14861534
console.log(
14871535
'🚀 Generating API documentation (versioned, double-pane layout)...',
@@ -1507,9 +1555,20 @@ async function main() {
15071555
await mkdir(OUTPUT_BASE_DIR, { recursive: true });
15081556
}
15091557

1558+
// Store default version data for unified sidebar generation
1559+
let defaultVersionData: {
1560+
categories: string[];
1561+
endpointsByCategory: Record<string, APIEndpoint[]>;
1562+
} | null = null;
1563+
15101564
// Generate docs for each version
15111565
for (const version of API_VERSIONS) {
1512-
await generateVersionDocs(version);
1566+
const versionData = await generateVersionDocs(version);
1567+
1568+
// Save default version data for sidebar generation
1569+
if (version.id === DEFAULT_VERSION.id) {
1570+
defaultVersionData = versionData;
1571+
}
15131572

15141573
// Copy shared manual pages to this version
15151574
await copyManualPagesToVersion(version.id);
@@ -1520,6 +1579,18 @@ async function main() {
15201579
const redirectContent = await generateRootRedirectPage(DEFAULT_VERSION);
15211580
await writeFile(join(OUTPUT_BASE_DIR, 'index.mdx'), redirectContent);
15221581

1582+
// Generate unified API sidebar config
1583+
if (defaultVersionData) {
1584+
console.log('\n📝 Generating unified API sidebar config...');
1585+
const sidebarContent = generateUnifiedSidebarFile(
1586+
defaultVersionData.categories,
1587+
defaultVersionData.endpointsByCategory,
1588+
DEFAULT_VERSION.id,
1589+
);
1590+
await writeFile(API_SIDEBAR_OUTPUT_PATH, sidebarContent);
1591+
console.log(` ✅ Generated ${API_SIDEBAR_OUTPUT_PATH}`);
1592+
}
1593+
15231594
console.log('\n🎉 All versions generated successfully!');
15241595
console.log(` Default version: ${DEFAULT_VERSION.id}`);
15251596
}

src/components/Sidebar.astro

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
---
22
import StarlightSidebar from '@astrojs/starlight/components/Sidebar.astro';
33
import { VersionSelector } from './react/api/VersionSelector';
4-
import { getVersionFromPath, API_VERSIONS } from '@/lib/api-versions';
4+
import {
5+
getVersionFromPath,
6+
API_VERSIONS,
7+
transformSidebarForVersion,
8+
type SidebarEntry,
9+
} from '@/lib/api-versions';
510
611
const currentPath = Astro.url.pathname;
712
813
// Check if this is a versioned API page (or any page where we want to show the selector)
914
const versionId = getVersionFromPath(currentPath);
1015
const showVersionSelector = versionId !== null && API_VERSIONS.length > 1;
16+
17+
// Transform sidebar links to use the current API version
18+
// This ensures sidebar links match the version the user is viewing
19+
if (versionId && Astro.locals.starlightRoute?.sidebar) {
20+
const originalSidebar = Astro.locals.starlightRoute.sidebar as SidebarEntry[];
21+
Astro.locals.starlightRoute.sidebar = transformSidebarForVersion(
22+
originalSidebar,
23+
versionId,
24+
);
25+
}
1126
---
1227

1328
<StarlightSidebar {...Astro.props}>

0 commit comments

Comments
 (0)