Skip to content

Commit f2e933b

Browse files
dawnhoclaude
andcommitted
fix: match live site navigation layout with 3 tabs
Rewrite SUMMARY.md parser to produce correct Mintlify navigation: - Guides tab with sidebar groups (Getting Started, Use Cases, Core Concepts, etc.) - API Reference tab with groups per resource (Access Codes, Devices, etc.) - Brand Guides tab with groups per manufacturer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7b692c8 commit f2e933b

2 files changed

Lines changed: 908 additions & 884 deletions

File tree

migrate-to-mintlify.mjs

Lines changed: 127 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -353,100 +353,132 @@ function convertFrontmatter(content, filePath) {
353353

354354
// ─── SUMMARY.md → docs.json navigation ─────────────────────────────────────
355355

356-
function parseSummary(summaryPath) {
356+
/**
357+
* Parse a single SUMMARY.md file into sections.
358+
* Returns an array of { name: string, pages: string[] } where each section
359+
* corresponds to a `## Header` in the SUMMARY.md. Pages before the first
360+
* `## Header` go into a section named "Getting Started".
361+
*
362+
* Page paths have .md stripped but README is preserved
363+
* (e.g. "use-cases/granting-access/README").
364+
*/
365+
function parseSummary(summaryPath, spacePrefix) {
357366
const content = fs.readFileSync(summaryPath, "utf-8");
358367
const lines = content.split("\n");
359368

360-
const sections = []; // [{name, items: [{title, path, indent, children}]}]
369+
const sections = []; // [{name, pages: [string]}]
361370
let currentSection = null;
362371

363372
for (const line of lines) {
364373
// Section headers like ## Core Concepts
365374
const sectionMatch = line.match(/^## (.+?)(?:\s*<a[^>]*>.*<\/a>)?$/);
366375
if (sectionMatch) {
367-
currentSection = { name: sectionMatch[1].trim(), items: [] };
376+
currentSection = { name: sectionMatch[1].trim(), pages: [] };
368377
sections.push(currentSection);
369378
continue;
370379
}
371380

372-
// Page entries: * [Title](path.md)
373-
const pageMatch = line.match(/^(\s*)\*\s+\[([^\]]+)\]\(([^)]+)\)/);
381+
// Page entries at any indent level: * [Title](path.md)
382+
const pageMatch = line.match(/^\s*\*\s+\[([^\]]+)\]\(([^)]+)\)/);
374383
if (pageMatch) {
375-
const indent = pageMatch[1].length;
376-
const title = pageMatch[2];
377-
let pagePath = pageMatch[3];
384+
let pagePath = pageMatch[2];
378385

386+
// Skip external links and broken references
379387
if (pagePath.startsWith("http")) continue;
380388
if (pagePath.includes("broken-reference") || pagePath.includes("/broken/")) continue;
381389

382-
pagePath = pagePath
383-
.replace(/\.md$/, "")
384-
.replace(/\/README$/, "");
390+
// Strip .md extension (Mintlify uses .mdx, referenced without extension)
391+
pagePath = pagePath.replace(/\.md$/, "");
392+
393+
// Prefix with space name
394+
pagePath = `${spacePrefix}/${pagePath}`;
385395

386396
if (!currentSection) {
387-
currentSection = { name: "Getting Started", items: [] };
397+
currentSection = { name: "Getting Started", pages: [] };
388398
sections.unshift(currentSection);
389399
}
390400

391-
currentSection.items.push({ title, path: pagePath, indent });
401+
currentSection.pages.push(pagePath);
392402
}
393403
}
394404

395-
// Build nested groups from items based on indent levels
396-
function buildGroups(items) {
397-
const groups = [];
398-
let currentGroup = null;
399-
400-
for (let i = 0; i < items.length; i++) {
401-
const item = items[i];
402-
403-
if (item.indent <= 2) {
404-
// Check if next items are children (indent > 2)
405-
const children = [];
406-
let j = i + 1;
407-
while (j < items.length && items[j].indent > 2) {
408-
children.push(items[j]);
409-
j++;
410-
}
405+
return sections;
406+
}
411407

412-
if (children.length > 0) {
413-
// This is a group parent
414-
const subPages = [item.path];
415-
for (const child of children) {
416-
subPages.push(child.path);
417-
}
418-
groups.push({
419-
group: item.title,
420-
pages: subPages,
421-
});
422-
i = j - 1; // skip children
423-
} else {
424-
// Standalone page — add to a catch-all group if needed
425-
if (!currentGroup || currentGroup.group !== "__standalone__") {
426-
currentGroup = { group: "__standalone__", pages: [] };
427-
groups.push(currentGroup);
428-
}
429-
currentGroup.pages.push(item.path);
430-
}
431-
}
408+
/**
409+
* Parse an API-style SUMMARY.md (api-reference or brand-guides) where
410+
* top-level items with children become groups, and top-level items
411+
* without children go into an "Overview" group.
412+
*
413+
* Returns an array of { group: string, pages: string[] }.
414+
*/
415+
function parseSummaryAsGroups(summaryPath, spacePrefix) {
416+
const content = fs.readFileSync(summaryPath, "utf-8");
417+
const lines = content.split("\n");
418+
419+
// Parse into a flat list with indent levels
420+
const items = []; // [{title, path, indent}]
421+
for (const line of lines) {
422+
const pageMatch = line.match(/^(\s*)\*\s+\[([^\]]+)\]\(([^)]+)\)/);
423+
if (!pageMatch) continue;
424+
425+
const indent = pageMatch[1].length;
426+
const title = pageMatch[2];
427+
let pagePath = pageMatch[3];
428+
429+
if (pagePath.startsWith("http")) continue;
430+
if (pagePath.includes("broken-reference") || pagePath.includes("/broken/")) continue;
431+
432+
pagePath = pagePath.replace(/\.md$/, "");
433+
pagePath = `${spacePrefix}/${pagePath}`;
434+
435+
items.push({ title, path: pagePath, indent });
436+
}
437+
438+
// Group: top-level items (indent 0) with children become named groups.
439+
// Top-level items without children go into "Overview".
440+
const groups = [];
441+
let overviewGroup = null;
442+
443+
for (let i = 0; i < items.length; i++) {
444+
const item = items[i];
445+
if (item.indent > 0) continue; // skip — already consumed as child
446+
447+
// Collect all children (anything with indent > 0 that follows)
448+
const children = [];
449+
let j = i + 1;
450+
while (j < items.length && items[j].indent > 0) {
451+
children.push(items[j]);
452+
j++;
432453
}
433454

434-
// Merge standalone groups and rename
435-
return groups.map((g) => {
436-
if (g.group === "__standalone__") {
437-
return { ...g, group: "Overview" };
455+
if (children.length > 0) {
456+
// This top-level item + its children form a group
457+
const pages = [item.path, ...children.map((c) => c.path)];
458+
groups.push({ group: item.title, pages });
459+
i = j - 1; // skip past children
460+
} else {
461+
// Standalone top-level page → "Overview" group
462+
if (!overviewGroup) {
463+
overviewGroup = { group: "Overview", pages: [] };
464+
// Insert Overview at the front
465+
groups.unshift(overviewGroup);
438466
}
439-
return g;
440-
});
467+
overviewGroup.pages.push(item.path);
468+
}
441469
}
442470

443-
return { sections, buildGroups };
471+
return groups;
444472
}
445473

446-
function buildDocsJson(parsedSummary) {
447-
const { sections, buildGroups } = parsedSummary;
448-
449-
const docsJson = {
474+
/**
475+
* Build the full docs.json object with 3 tabs:
476+
* - Guides (default tab, sidebar groups from ## sections)
477+
* - API Reference (groups per API resource)
478+
* - Brand Guides (groups per brand)
479+
*/
480+
function buildDocsJson(guidesSections, apiGroups, brandGroups) {
481+
return {
450482
$schema: "https://mintlify.com/docs.json",
451483
name: "Seam",
452484
theme: "mint",
@@ -460,7 +492,25 @@ function buildDocsJson(parsedSummary) {
460492
light: "#60A5FA",
461493
dark: "#1D4ED8",
462494
},
463-
navigation: {},
495+
navigation: {
496+
tabs: [
497+
{
498+
tab: "Guides",
499+
groups: guidesSections.map((s) => ({
500+
group: s.name,
501+
pages: s.pages,
502+
})),
503+
},
504+
{
505+
tab: "API Reference",
506+
groups: apiGroups,
507+
},
508+
{
509+
tab: "Brand Guides",
510+
groups: brandGroups,
511+
},
512+
],
513+
},
464514
topbar: {
465515
links: [
466516
{
@@ -479,27 +529,6 @@ function buildDocsJson(parsedSummary) {
479529
baseUrl: "https://connect.getseam.com",
480530
},
481531
};
482-
483-
if (sections.length <= 1) {
484-
// Simple: just groups
485-
const groups = sections.length > 0 ? buildGroups(sections[0].items) : [];
486-
docsJson.navigation.groups = groups;
487-
} else {
488-
// First section becomes the main sidebar groups, rest become tabs
489-
const firstSection = sections[0];
490-
docsJson.navigation.groups = buildGroups(firstSection.items);
491-
492-
docsJson.navigation.tabs = [];
493-
for (let i = 1; i < sections.length; i++) {
494-
const section = sections[i];
495-
docsJson.navigation.tabs.push({
496-
tab: section.name,
497-
groups: buildGroups(section.items),
498-
});
499-
}
500-
}
501-
502-
return docsJson;
503532
}
504533

505534
// ─── Main ───────────────────────────────────────────────────────────────────
@@ -596,61 +625,23 @@ function main() {
596625

597626
// 4. Generate docs.json from all SUMMARY.md files
598627
console.log("📋 Generating docs.json...");
599-
const allSections = [];
600-
for (const space of spaces) {
601-
const summaryPath = path.join(SRC, space, "SUMMARY.md");
602-
if (fs.existsSync(summaryPath)) {
603-
const parsed = parseSummary(summaryPath);
604-
// Prefix all page paths with the space name
605-
for (const section of parsed.sections) {
606-
for (const item of section.items) {
607-
item.path = `${space}/${item.path}`;
608-
}
609-
}
610-
allSections.push(...parsed.sections);
611-
}
612-
}
613-
614-
const docsJson = buildDocsJson({
615-
sections: allSections,
616-
buildGroups: function (items) {
617-
const groups = [];
618-
let currentGroup = null;
619-
620-
for (let i = 0; i < items.length; i++) {
621-
const item = items[i];
622-
623-
if (item.indent <= 2) {
624-
const children = [];
625-
let j = i + 1;
626-
while (j < items.length && items[j].indent > 2) {
627-
children.push(items[j]);
628-
j++;
629-
}
630-
631-
if (children.length > 0) {
632-
const subPages = [item.path];
633-
for (const child of children) {
634-
subPages.push(child.path);
635-
}
636-
groups.push({ group: item.title, pages: subPages });
637-
i = j - 1;
638-
} else {
639-
if (!currentGroup || currentGroup.group !== "__standalone__") {
640-
currentGroup = { group: "__standalone__", pages: [] };
641-
groups.push(currentGroup);
642-
}
643-
currentGroup.pages.push(item.path);
644-
}
645-
}
646-
}
647628

648-
return groups.map((g) => {
649-
if (g.group === "__standalone__") return { ...g, group: "Overview" };
650-
return g;
651-
});
652-
},
653-
});
629+
// Parse each space's SUMMARY.md with the appropriate strategy
630+
const guidesSummary = path.join(SRC, "guides", "SUMMARY.md");
631+
const apiSummary = path.join(SRC, "api-reference", "SUMMARY.md");
632+
const brandSummary = path.join(SRC, "brand-guides", "SUMMARY.md");
633+
634+
const guidesSections = fs.existsSync(guidesSummary)
635+
? parseSummary(guidesSummary, "guides")
636+
: [];
637+
const apiGroups = fs.existsSync(apiSummary)
638+
? parseSummaryAsGroups(apiSummary, "api-reference")
639+
: [];
640+
const brandGroups = fs.existsSync(brandSummary)
641+
? parseSummaryAsGroups(brandSummary, "brand-guides")
642+
: [];
643+
644+
const docsJson = buildDocsJson(guidesSections, apiGroups, brandGroups);
654645
fs.writeFileSync(
655646
path.join(DEST, "docs.json"),
656647
JSON.stringify(docsJson, null, 2)

0 commit comments

Comments
 (0)