Skip to content

Commit 41ef5cb

Browse files
committed
feat(shell-docs): resolve framework-variant URL slugs end-to-end
Registry slugs don't always match the integrations/<folder>/ name on disk. Three LangChain/LangGraph variants (langgraph-python, langgraph- typescript, langgraph-fastapi) read from the single langgraph/ tree, ms-agent-dotnet and ms-agent-python share microsoft-agent-framework/, and google-adk/strands are legacy renames that point at adk/ and aws-strands/ respectively. Before: the framework-scoped router, the sidebar-override nav builder, and the "not available for this framework" fallback all used the URL slug directly as a folder name, so any of the seven mismatched slugs showed empty sidebars, 404s on framework-unique pages (/langgraph-python/auth, /ms-agent-dotnet/auth), and missing 'available in other integrations' matches. After: lib/registry exposes getDocsFolder(slug) backed by a small DOCS_FOLDER_OVERRIDES table. Callers resolve the URL slug to its actual folder before touching disk; findFrameworksWithPage takes the resolver as a parameter so docs-render stays registry-free. Per-page variant selectors authored as <Tabs groupId="..." default="Python"> now open with the URL-matching tab preselected instead of the author's hardcoded default. getTabDefault(slug, groupId) reads TAB_DEFAULTS_BY_SLUG; a wrapper in DocsPageView's MDX components map injects the resolved value into <Tabs> via a 'default' prop alias. /langgraph-typescript/configurable opens TypeScript, /ms-agent-dotnet/ auth opens .NET, /langgraph-fastapi/deep-agents opens FastAPI. Slugs and groupIds without a mapping fall through to the existing behavior (author default, then first items label).
1 parent 2eced38 commit 41ef5cb

5 files changed

Lines changed: 198 additions & 49 deletions

File tree

showcase/shell-docs/src/app/[framework]/[[...slug]]/page.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ import {
2929
loadDoc,
3030
type NavNode,
3131
} from "@/lib/docs-render";
32-
import { getIntegration, getIntegrations, type Integration } from "@/lib/registry";
32+
import {
33+
getDocsFolder,
34+
getIntegration,
35+
getIntegrations,
36+
type Integration,
37+
} from "@/lib/registry";
3338
import { RESERVED_ROUTE_SLUGS } from "@/app/layout";
3439
import demoContent from "@/data/demo-content.json";
3540

@@ -166,18 +171,26 @@ export default async function FrameworkScopedDocsPage({
166171
// render a "not available for <framework>" fallback inside the
167172
// docs shell (handled below, after the nav is built).
168173
// 4. Otherwise 404.
174+
// Most registry slugs map 1:1 to a folder under `integrations/`, but
175+
// language/runtime variants share a single docs folder:
176+
// langgraph-python/typescript/fastapi → `langgraph/`, ms-agent-dotnet/
177+
// python → `microsoft-agent-framework/`, plus legacy renames for
178+
// google-adk → `adk/` and strands → `aws-strands/`. Resolve the URL
179+
// slug to its docs folder before touching disk.
180+
const docsFolder = getDocsFolder(framework);
181+
169182
let contentSlugPath: string = slugPath;
170183
let doc = loadDoc(slugPath);
171184
if (!doc) {
172-
const fallbackPath = `integrations/${framework}/${slugPath}`;
185+
const fallbackPath = `integrations/${docsFolder}/${slugPath}`;
173186
doc = loadDoc(fallbackPath);
174187
if (doc) contentSlugPath = fallbackPath;
175188
}
176189

177190
// Sidebar nav needs to render on both the happy path and the
178191
// "not available" fallback, so build it before branching.
179192
const rootNav = buildNavTree(CONTENT_DIR);
180-
const overrideNav = buildFrameworkOverridesNav(framework);
193+
const overrideNav = buildFrameworkOverridesNav(docsFolder);
181194
const navTree: NavNode[] = mergeFrameworkNav(
182195
rootNav,
183196
overrideNav,
@@ -192,7 +205,11 @@ export default async function FrameworkScopedDocsPage({
192205
// user keeps their framework context and gets a clear path
193206
// forward. Only 404 when the slug is unknown everywhere.
194207
const allFrameworkSlugs = getIntegrations().map((i) => i.slug);
195-
const availableIn = findFrameworksWithPage(slugPath, allFrameworkSlugs);
208+
const availableIn = findFrameworksWithPage(
209+
slugPath,
210+
allFrameworkSlugs,
211+
getDocsFolder,
212+
);
196213
if (availableIn.length > 0) {
197214
return (
198215
<NotAvailableForFrameworkPage
@@ -287,9 +304,10 @@ function FrameworkLandingPage({ framework }: { framework: string }) {
287304
const integration = getIntegration(framework);
288305
if (!integration) notFound();
289306

290-
// Same nav merge as the scoped-page route.
307+
// Same nav merge as the scoped-page route. Resolve the URL slug to
308+
// its docs folder — see comment in FrameworkScopedDocsPage above.
291309
const rootNav = buildNavTree(CONTENT_DIR);
292-
const overrideNav = buildFrameworkOverridesNav(framework);
310+
const overrideNav = buildFrameworkOverridesNav(getDocsFolder(framework));
293311
const tree = mergeFrameworkNav(rootNav, overrideNav, integration.name);
294312

295313
return (

showcase/shell-docs/src/components/docs-page-view.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import { SidebarLink } from "@/components/sidebar-link";
1717
import { SidebarFrameworkSelector } from "@/components/sidebar-framework-selector";
1818
import { Snippet } from "@/components/snippet";
1919
import { DocsToc } from "@/components/docs-toc";
20+
import { Tabs as DocsTabs } from "@/components/docs-tabs";
2021
import { docsComponents } from "@/lib/mdx-registry";
22+
import { getTabDefault } from "@/lib/registry";
2123
import {
2224
NavNode,
2325
buildBreadcrumbs,
@@ -235,6 +237,37 @@ export async function DocsPageView({
235237
defaultCell={defaultCell}
236238
/>
237239
),
240+
// MDX pages author in-page variant selectors as
241+
// `<Tabs groupId="language_langgraph_agent" default="Python">`.
242+
// When the URL scope is a specific variant (e.g.
243+
// `/langgraph-typescript/*`), pre-select the
244+
// matching tab instead of the author's hardcoded
245+
// default so the code visible on arrival matches
246+
// the URL the user followed. Slugs without a
247+
// mapping (or tabs whose groupId isn't listed in
248+
// TAB_DEFAULTS_BY_SLUG) fall through to the MDX
249+
// `default` and the component's first-label
250+
// fallback unchanged.
251+
Tabs: (props: {
252+
groupId?: string;
253+
default?: string;
254+
items?: string[];
255+
children?: React.ReactNode;
256+
persist?: boolean;
257+
}) => {
258+
const urlDefault = getTabDefault(
259+
frameworkOverride ?? null,
260+
props.groupId,
261+
);
262+
return (
263+
<DocsTabs
264+
{...props}
265+
default={urlDefault ?? props.default}
266+
>
267+
{props.children}
268+
</DocsTabs>
269+
);
270+
},
238271
InlineDemo: (props: Record<string, unknown>) => {
239272
const InlineDemoComp = docsComponents.InlineDemo;
240273
return (

showcase/shell-docs/src/components/docs-tabs.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,26 @@ import React, { useState, useMemo, Children, isValidElement } from "react";
1717

1818
interface TabsProps {
1919
items?: string[];
20+
/**
21+
* Initial active tab label. MDX authors write `default="Python"`
22+
* (fumadocs convention); we also accept `defaultValue` for
23+
* programmatic callers. Either name resolves the same initial tab.
24+
*/
25+
default?: string;
2026
defaultValue?: string;
27+
/**
28+
* Author-supplied tab-group identifier used to key tab state across
29+
* pages (e.g. "language_langgraph_agent"). Accepted for MDX source
30+
* compatibility; the per-page `<Tabs>` override injected by
31+
* DocsPageView reads it to compute URL-variant-driven defaults.
32+
* Currently no persistence implemented.
33+
*/
34+
groupId?: string;
35+
/**
36+
* Accepted for MDX source compatibility — fumadocs' persistent-tab
37+
* feature. Not yet implemented here.
38+
*/
39+
persist?: boolean;
2140
children: React.ReactNode;
2241
}
2342

@@ -27,7 +46,12 @@ interface TabProps {
2746
children?: React.ReactNode;
2847
}
2948

30-
export function Tabs({ items, defaultValue, children }: TabsProps) {
49+
export function Tabs({
50+
items,
51+
default: defaultProp,
52+
defaultValue,
53+
children,
54+
}: TabsProps) {
3155
// Discover tab labels from children when `items` isn't provided.
3256
const kids = useMemo(() => {
3357
const list: { label: string; content: React.ReactNode }[] = [];
@@ -42,7 +66,7 @@ export function Tabs({ items, defaultValue, children }: TabsProps) {
4266

4367
const labels = items ?? kids.map((k) => k.label);
4468
const [active, setActive] = useState<string>(
45-
defaultValue ?? labels[0] ?? "Tab",
69+
defaultValue ?? defaultProp ?? labels[0] ?? "Tab",
4670
);
4771

4872
return (

showcase/shell-docs/src/lib/docs-render.tsx

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -289,37 +289,34 @@ export function buildNavTreeFromFilesystem(
289289
}
290290

291291
/**
292-
* Walk `content/docs/integrations/<framework>/` and return NavNodes for
293-
* pages that have NO root equivalent. These are framework-specific topics
294-
* (e.g. Built-in Agent's `copilot-runtime`, `server-tools`) that live
295-
* only in the per-framework tree and need their own sidebar entries.
296-
* Pages that duplicate root files are skipped — root wins, and the
297-
* framework view already renders the root MDX with a framework override.
292+
* Walk `content/docs/integrations/<folder>/` and return NavNodes for
293+
* pages that have NO root equivalent. These are framework-specific
294+
* topics (e.g. Built-in Agent's `copilot-runtime`, LangGraph's `auth`
295+
* and `subgraphs`) that live only in the per-framework tree and need
296+
* their own sidebar entries. Pages that duplicate root files are
297+
* skipped — root wins, and the framework view already renders the
298+
* root MDX with a framework override.
299+
*
300+
* Takes the resolved folder name (not the URL slug). Callers should
301+
* use `getDocsFolder(slug)` from lib/registry to map language/runtime
302+
* variants to their shared folder (e.g. langgraph-python / typescript
303+
* / fastapi → `langgraph/`).
298304
*/
299-
export function buildFrameworkOverridesNav(framework: string): NavNode[] {
300-
const frameworkDir = path.join(
301-
CONTENT_DIR,
302-
"integrations",
303-
framework,
304-
);
305+
export function buildFrameworkOverridesNav(folder: string): NavNode[] {
306+
const frameworkDir = path.join(CONTENT_DIR, "integrations", folder);
305307
if (!fs.existsSync(frameworkDir)) return [];
306-
const nodes = buildNavTree(
307-
frameworkDir,
308-
`integrations/${framework}`,
309-
);
308+
const nodes = buildNavTree(frameworkDir, `integrations/${folder}`);
310309

311310
// Drop entries whose equivalent root file exists. Root wins when
312311
// both are present — the per-framework tree is only an escape hatch
313312
// for framework-specific topics, not an alternative rendering.
313+
const prefix = `integrations/${folder}/`;
314314
const filtered: NavNode[] = [];
315315
for (const node of nodes) {
316316
if (node.type === "page") {
317-
// node.slug looks like `integrations/<framework>/<topic>`.
318-
// Strip the prefix to check the root-level equivalent.
319-
const rootSlug = node.slug.replace(
320-
`integrations/${framework}/`,
321-
"",
322-
);
317+
// node.slug looks like `integrations/<folder>/<topic>`. Strip
318+
// the prefix to check the root-level equivalent.
319+
const rootSlug = node.slug.replace(prefix, "");
323320
const rootMdx = path.join(CONTENT_DIR, `${rootSlug}.mdx`);
324321
const rootIndex = path.join(CONTENT_DIR, rootSlug, "index.mdx");
325322
if (fs.existsSync(rootMdx) || fs.existsSync(rootIndex)) continue;
@@ -328,23 +325,19 @@ export function buildFrameworkOverridesNav(framework: string): NavNode[] {
328325
filtered.push({ ...node, slug: rootSlug });
329326
} else if (node.type === "group") {
330327
// Recursively filter children of a group.
331-
const children = node.children.filter((c) => {
332-
if (c.type !== "page") return true;
333-
const rootSlug = c.slug.replace(
334-
`integrations/${framework}/`,
335-
"",
328+
const children = node.children
329+
.filter((c) => {
330+
if (c.type !== "page") return true;
331+
const rootSlug = c.slug.replace(prefix, "");
332+
const rootMdx = path.join(CONTENT_DIR, `${rootSlug}.mdx`);
333+
const rootIndex = path.join(CONTENT_DIR, rootSlug, "index.mdx");
334+
return !fs.existsSync(rootMdx) && !fs.existsSync(rootIndex);
335+
})
336+
.map((c) =>
337+
c.type === "page"
338+
? { ...c, slug: c.slug.replace(prefix, "") }
339+
: c,
336340
);
337-
const rootMdx = path.join(CONTENT_DIR, `${rootSlug}.mdx`);
338-
const rootIndex = path.join(CONTENT_DIR, rootSlug, "index.mdx");
339-
return !fs.existsSync(rootMdx) && !fs.existsSync(rootIndex);
340-
}).map((c) =>
341-
c.type === "page"
342-
? {
343-
...c,
344-
slug: c.slug.replace(`integrations/${framework}/`, ""),
345-
}
346-
: c,
347-
);
348341
if (children.length > 0) {
349342
filtered.push({ ...node, children });
350343
}
@@ -360,29 +353,38 @@ export function buildFrameworkOverridesNav(framework: string): NavNode[] {
360353
}
361354

362355
/**
363-
* Return the list of framework slugs whose `integrations/<framework>/`
356+
* Return the list of framework slugs whose `integrations/<folder>/`
364357
* tree contains an MDX file for `slugPath`. Matches either
365358
* `<slug>.mdx` or `<slug>/index.mdx`. Used by the framework-scoped
366359
* router to detect that a topic is available in *some* framework but
367360
* not the one the user is currently viewing, so we can render a
368361
* helpful "not available for <X>" page instead of a bare 404.
362+
*
363+
* Most slugs map 1:1 to their folder, but language/runtime variants
364+
* share one folder (langgraph-python/typescript/fastapi → `langgraph/`,
365+
* ms-agent-dotnet/python → `microsoft-agent-framework/`) and two
366+
* legacy slugs were renamed after the folder existed (google-adk →
367+
* `adk/`, strands → `aws-strands/`). The caller supplies the
368+
* slug→folder resolver so this module stays registry-free.
369369
*/
370370
export function findFrameworksWithPage(
371371
slugPath: string,
372372
integrationSlugs: readonly string[],
373+
slugToFolder: (slug: string) => string,
373374
): string[] {
374375
const matches: string[] = [];
375376
for (const slug of integrationSlugs) {
377+
const folder = slugToFolder(slug);
376378
const mdx = path.join(
377379
CONTENT_DIR,
378380
"integrations",
379-
slug,
381+
folder,
380382
`${slugPath}.mdx`,
381383
);
382384
const indexMdx = path.join(
383385
CONTENT_DIR,
384386
"integrations",
385-
slug,
387+
folder,
386388
slugPath,
387389
"index.mdx",
388390
);

showcase/shell-docs/src/lib/registry.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,78 @@ export function getIntegration(slug: string): Integration | undefined {
7373
return registry.integrations.find((i) => i.slug === slug);
7474
}
7575

76+
/**
77+
* Maps a registry slug to the `integrations/<folder>/` directory name
78+
* that holds its framework-unique docs content. Most slugs match their
79+
* folder 1:1. These overrides exist because:
80+
*
81+
* - Three LangChain/LangGraph URL variants (`-python`, `-typescript`,
82+
* `-fastapi`) read from one shared `langgraph/` tree, with
83+
* in-page `<Tabs>` and `<TailoredContent>` handling the per-variant
84+
* code examples. The URL slug determines which tab opens by default
85+
* (see TAB_DEFAULTS_BY_SLUG below).
86+
* - `microsoft-agent-framework/` serves both `ms-agent-dotnet` and
87+
* `ms-agent-python`, same in-page-tabs pattern.
88+
* - `google-adk` / `strands` are legacy renames — the slug changed in
89+
* the registry but the docs folder still uses the earlier name.
90+
*
91+
* Unlisted slugs default to the slug itself, so registry entries
92+
* whose folder already matches their slug need no entry here.
93+
*/
94+
const DOCS_FOLDER_OVERRIDES: Record<string, string> = {
95+
"langgraph-python": "langgraph",
96+
"langgraph-typescript": "langgraph",
97+
"langgraph-fastapi": "langgraph",
98+
"google-adk": "adk",
99+
"strands": "aws-strands",
100+
"ms-agent-dotnet": "microsoft-agent-framework",
101+
"ms-agent-python": "microsoft-agent-framework",
102+
};
103+
104+
export function getDocsFolder(slug: string): string {
105+
return DOCS_FOLDER_OVERRIDES[slug] ?? slug;
106+
}
107+
108+
/**
109+
* Per-slug default-tab selections for the in-page `<Tabs>` component,
110+
* keyed by the MDX `groupId` prop. When a page renders under one of
111+
* these slugs, any tab whose `groupId` matches opens with the mapped
112+
* value pre-selected — so `/langgraph-typescript/configurable` opens
113+
* the TypeScript tab, `/ms-agent-dotnet/auth` opens .NET, etc.
114+
*
115+
* The MDX still authors `items={['Python', 'TypeScript']}` once; only
116+
* the initially-selected label differs per URL variant. Unmapped
117+
* slugs, or tabs with a `groupId` not listed here, fall back to the
118+
* component's existing behavior (first label wins).
119+
*/
120+
const TAB_DEFAULTS_BY_SLUG: Record<string, Record<string, string>> = {
121+
"langgraph-python": {
122+
language_langgraph_agent: "Python",
123+
deployment_method: "LangSmith",
124+
},
125+
"langgraph-typescript": {
126+
language_langgraph_agent: "TypeScript",
127+
},
128+
"langgraph-fastapi": {
129+
language_langgraph_agent: "Python",
130+
deployment_method: "FastAPI",
131+
},
132+
"ms-agent-dotnet": {
133+
"language_microsoft-agent-framework_agent": ".NET",
134+
},
135+
"ms-agent-python": {
136+
"language_microsoft-agent-framework_agent": "Python",
137+
},
138+
};
139+
140+
export function getTabDefault(
141+
slug: string | null | undefined,
142+
groupId: string | undefined,
143+
): string | undefined {
144+
if (!slug || !groupId) return undefined;
145+
return TAB_DEFAULTS_BY_SLUG[slug]?.[groupId];
146+
}
147+
76148
export function getFeatures(): Feature[] {
77149
return registry.feature_registry.features;
78150
}

0 commit comments

Comments
 (0)