Skip to content

Commit 6bc1580

Browse files
Kyle McLarenclaude
andcommitted
Add nested sidebar with method badges for API endpoints
- Add collapsible groups for each API category (Sprites, Checkpoints, etc.) - Show individual endpoints as sub-items with HTTP method badges - Badges use color variants: GET=blue, POST=green, PUT=orange, DELETE=red, WSS=purple - Add anchor IDs to endpoint sections for direct linking - Update generator to output nested sidebar structure with link (not slug) for anchors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7faa2f4 commit 6bc1580

File tree

3 files changed

+305
-30
lines changed

3 files changed

+305
-30
lines changed

scripts/generate-api-docs.ts

Lines changed: 149 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,14 @@ function generateEndpointSection(
745745
? generateWebSocketMessagesDocs(endpoint.messages, websocketMessages, types)
746746
: '';
747747

748+
// Generate anchor ID from endpoint name
749+
const anchorId = endpoint.name
750+
.toLowerCase()
751+
.replace(/[^a-z0-9]+/g, '-')
752+
.replace(/^-|-$/g, '');
753+
748754
let content = `
755+
<div id="${anchorId}">
749756
<MethodPage client:load>
750757
<MethodPageLeft client:load>
751758
<MethodHeader
@@ -817,6 +824,7 @@ ${streamingEventsDocs}
817824
/>
818825
</MethodPageRight>
819826
</MethodPage>
827+
</div>
820828
`;
821829

822830
return content;
@@ -842,11 +850,30 @@ function getCategoryTitle(category: string): string {
842850
}
843851

844852
// Manual pages that are not generated from schema but should be included
845-
const MANUAL_PAGES = [
853+
interface ManualEndpoint {
854+
method: string;
855+
title: string;
856+
}
857+
858+
interface ManualPage {
859+
category: string;
860+
title: string;
861+
description: string;
862+
endpoints?: ManualEndpoint[];
863+
}
864+
865+
const MANUAL_PAGES: ManualPage[] = [
846866
{
847867
category: 'sprites',
848868
title: 'Sprites',
849869
description: 'Create, list, update, and delete Sprites',
870+
endpoints: [
871+
{ method: 'POST', title: 'Create Sprite' },
872+
{ method: 'GET', title: 'List Sprites' },
873+
{ method: 'GET', title: 'Get Sprite' },
874+
{ method: 'PUT', title: 'Update Sprite' },
875+
{ method: 'DELETE', title: 'Delete Sprite' },
876+
],
850877
},
851878
];
852879

@@ -1147,42 +1174,149 @@ ${JSON.stringify(msg.example, null, 2)}
11471174
// Sidebar config generation
11481175
// ============================================================================
11491176

1150-
interface SidebarItem {
1177+
interface SidebarBadge {
1178+
text: string;
1179+
variant: 'note' | 'tip' | 'caution' | 'danger' | 'success' | 'default';
1180+
class?: string;
1181+
}
1182+
1183+
interface SidebarLink {
11511184
label: string;
1152-
slug: string;
1185+
slug?: string;
1186+
link?: string;
1187+
badge?: SidebarBadge;
11531188
attrs?: Record<string, string>;
11541189
}
11551190

1191+
interface SidebarGroup {
1192+
label: string;
1193+
collapsed?: boolean;
1194+
items: (SidebarLink | SidebarGroup)[];
1195+
}
1196+
1197+
type SidebarItem = SidebarLink | SidebarGroup;
1198+
1199+
function getMethodBadge(method: string): SidebarBadge {
1200+
const variants: Record<string, SidebarBadge['variant']> = {
1201+
GET: 'note',
1202+
POST: 'success',
1203+
PUT: 'caution',
1204+
PATCH: 'caution',
1205+
DELETE: 'danger',
1206+
WSS: 'tip',
1207+
};
1208+
return {
1209+
text: method,
1210+
variant: variants[method.toUpperCase()] || 'default',
1211+
class: `sidebar-method-${method.toLowerCase()}`,
1212+
};
1213+
}
1214+
1215+
function slugifyEndpoint(title: string): string {
1216+
return title
1217+
.toLowerCase()
1218+
.replace(/[^a-z0-9]+/g, '-')
1219+
.replace(/^-|-$/g, '');
1220+
}
1221+
11561222
function generateSidebarItems(
11571223
categories: string[],
1158-
_endpointsByCategory: Record<string, APIEndpoint[]>,
1224+
endpointsByCategory: Record<string, APIEndpoint[]>,
11591225
versionId: string,
11601226
): SidebarItem[] {
11611227
const items: SidebarItem[] = [
11621228
{ label: 'Overview', slug: `api/${versionId}` },
11631229
];
11641230

1165-
// Add manual pages first
1231+
// Add manual pages (Sprites) with nested endpoint items
11661232
for (const page of MANUAL_PAGES) {
1167-
items.push({
1168-
label: page.title,
1169-
slug: `api/${versionId}/${page.category}`,
1170-
});
1233+
if (page.endpoints && page.endpoints.length > 0) {
1234+
// Create a group with nested endpoint items
1235+
const endpointItems: SidebarLink[] = page.endpoints.map((ep) => ({
1236+
label: ep.title,
1237+
link: `/api/${versionId}/${page.category}#${slugifyEndpoint(ep.title)}`,
1238+
badge: getMethodBadge(ep.method),
1239+
}));
1240+
items.push({
1241+
label: page.title,
1242+
collapsed: true,
1243+
items: endpointItems,
1244+
});
1245+
} else {
1246+
items.push({
1247+
label: page.title,
1248+
slug: `api/${versionId}/${page.category}`,
1249+
});
1250+
}
11711251
}
11721252

1173-
// Add generated categories
1253+
// Add generated categories with nested endpoint items
11741254
for (const category of categories) {
1175-
items.push({
1176-
label: getCategoryTitle(category),
1177-
slug: `api/${versionId}/${category}`,
1178-
});
1255+
const endpoints = endpointsByCategory[category] || [];
1256+
if (endpoints.length > 0) {
1257+
const endpointItems: SidebarLink[] = endpoints.map((ep) => ({
1258+
label: ep.name,
1259+
link: `/api/${versionId}/${category}#${slugifyEndpoint(ep.name)}`,
1260+
badge: getMethodBadge(ep.method),
1261+
}));
1262+
items.push({
1263+
label: getCategoryTitle(category),
1264+
collapsed: true,
1265+
items: endpointItems,
1266+
});
1267+
} else {
1268+
items.push({
1269+
label: getCategoryTitle(category),
1270+
slug: `api/${versionId}/${category}`,
1271+
});
1272+
}
11791273
}
11801274

11811275
items.push({ label: 'Type Definitions', slug: `api/${versionId}/types` });
11821276

11831277
return items;
11841278
}
11851279

1280+
function serializeSidebarItem(item: SidebarItem, indent: number): string {
1281+
const pad = ' '.repeat(indent);
1282+
1283+
// Check if it's a group (has items array)
1284+
if ('items' in item) {
1285+
const group = item as SidebarGroup;
1286+
const nestedItems = group.items
1287+
.map((i) => serializeSidebarItem(i, indent + 1))
1288+
.join(',\n');
1289+
return `${pad}{
1290+
${pad} label: '${group.label}',
1291+
${pad} collapsed: ${group.collapsed ?? false},
1292+
${pad} items: [
1293+
${nestedItems}
1294+
${pad} ]
1295+
${pad}}`;
1296+
}
1297+
1298+
// It's a link
1299+
const sidebarLink = item as SidebarLink;
1300+
const parts = [`label: '${sidebarLink.label}'`];
1301+
if (sidebarLink.link) {
1302+
parts.push(`link: '${sidebarLink.link}'`);
1303+
} else if (sidebarLink.slug) {
1304+
parts.push(`slug: '${sidebarLink.slug}'`);
1305+
}
1306+
if (sidebarLink.badge) {
1307+
parts.push(
1308+
`badge: { text: '${sidebarLink.badge.text}', variant: '${sidebarLink.badge.variant}'${sidebarLink.badge.class ? `, class: '${sidebarLink.badge.class}'` : ''} }`,
1309+
);
1310+
}
1311+
if (sidebarLink.attrs) {
1312+
const attrsStr = Object.entries(sidebarLink.attrs)
1313+
.map(([k, v]) => `'${k}': '${v}'`)
1314+
.join(', ');
1315+
parts.push(`attrs: { ${attrsStr} }`);
1316+
}
1317+
return `${pad}{ ${parts.join(', ')} }`;
1318+
}
1319+
11861320
function generateSidebarConfig(
11871321
categories: string[],
11881322
endpointsByCategory: Record<string, APIEndpoint[]>,
@@ -1195,16 +1329,7 @@ function generateSidebarConfig(
11951329
);
11961330

11971331
const itemsStr = items
1198-
.map((item) => {
1199-
const parts = [`label: '${item.label}'`, `slug: '${item.slug}'`];
1200-
if (item.attrs) {
1201-
const attrsStr = Object.entries(item.attrs)
1202-
.map(([k, v]) => `'${k}': '${v}'`)
1203-
.join(', ');
1204-
parts.push(`attrs: { ${attrsStr} }`);
1205-
}
1206-
return ` { ${parts.join(', ')} }`;
1207-
})
1332+
.map((item) => serializeSidebarItem(item, 3))
12081333
.join(',\n');
12091334

12101335
return `

src/content/docs/api/dev-latest/sprites.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import CodeSnippets from '@/components/CodeSnippets.astro';
1616

1717
<div className="api-full-width">
1818

19+
<div id="create-sprite">
1920
<MethodPage client:load>
2021
<MethodPageLeft client:load>
2122
<MethodHeader
@@ -105,9 +106,11 @@ IO.puts("Sprite '\#{sprite_name}' created")` }
105106
/>
106107
</MethodPageRight>
107108
</MethodPage>
109+
</div>
108110

109111
---
110112

113+
<div id="list-sprites">
111114
<MethodPage client:load>
112115
<MethodPageLeft client:load>
113116
<MethodHeader
@@ -193,9 +196,11 @@ sprites |> Jason.encode!(pretty: true) |> IO.puts()` }
193196
/>
194197
</MethodPageRight>
195198
</MethodPage>
199+
</div>
196200

197201
---
198202

203+
<div id="get-sprite">
199204
<MethodPage client:load>
200205
<MethodPageLeft client:load>
201206
<MethodHeader
@@ -273,9 +278,11 @@ sprite |> Jason.encode!(pretty: true) |> IO.puts()` }
273278
/>
274279
</MethodPageRight>
275280
</MethodPage>
281+
</div>
276282

277283
---
278284

285+
<div id="update-sprite">
279286
<MethodPage client:load>
280287
<MethodPageLeft client:load>
281288
<MethodHeader
@@ -367,9 +374,11 @@ IO.puts("URL settings updated")` }
367374
/>
368375
</MethodPageRight>
369376
</MethodPage>
377+
</div>
370378

371379
---
372380

381+
<div id="delete-sprite">
373382
<MethodPage client:load>
374383
<MethodPageLeft client:load>
375384
<MethodHeader
@@ -435,5 +444,6 @@ IO.puts("Sprite '\#{sprite_name}' destroyed")` }
435444
/>
436445
</MethodPageRight>
437446
</MethodPage>
447+
</div>
438448

439449
</div>

0 commit comments

Comments
 (0)