From 5395dec55989bbca8ddf39a3840b2ca8c32fcc48 Mon Sep 17 00:00:00 2001 From: Mike Odnis Date: Sun, 10 May 2026 03:58:21 -0400 Subject: [PATCH] fix(nav): de-duplicate package landing pages in splice After the multi-package TS refactor (#58), each package showed up twice in the TypeScript sub-group: - once as a standalone PAGE entry (e.g. \`analytics\`) - once as a collapsible GROUP entry (\`{group: \"analytics\", pages: [\"analytics/index/...\"]}\`) Root cause: the splice's \`/index\` strip turned \`analytics/index\` into the bare path \`analytics\`, which got registered as a leaf file at the parent level. Then \`analytics/index/classes/Analytics\` (and siblings) created a sibling \`analytics/\` directory tree. Both ended up in the parent group's pages list. Fix: introduce \`insert_landing()\` which stores the landing page id on the dir node itself (\`_landing\`) rather than as a sibling \`_files\` leaf. \`to_mintlify\` emits \`_landing\` first inside the dir's group, before any \`_files\` or sub-\`_dirs\`. Result: each package gets exactly one collapsible group, with its landing page as the first child. Applied to both: - automation/source-repo-templates/api-docs.typescript.yml (used at doc-generation time) - scripts/splice-sdk-nav.py (used for local re-splicing across all languages: TS, .NET, Python, Rust, C++) docs.json re-spliced locally to clear the duplicates already on main from PR #60. --- .../api-docs.typescript.yml | 35 ++- docs.json | 220 ++++++++++-------- scripts/splice-sdk-nav.py | 18 +- 3 files changed, 169 insertions(+), 104 deletions(-) diff --git a/automation/source-repo-templates/api-docs.typescript.yml b/automation/source-repo-templates/api-docs.typescript.yml index 472db502..a3d857ac 100644 --- a/automation/source-repo-templates/api-docs.typescript.yml +++ b/automation/source-repo-templates/api-docs.typescript.yml @@ -453,34 +453,53 @@ jobs: tree: dict = {} - def insert(node, parts, full_id): + def insert_file(node, parts, full_id): if len(parts) == 1: node.setdefault("_files", []).append((parts[0], full_id)) return head, *rest = parts - insert(node.setdefault("_dirs", {}).setdefault(head, {}), rest, full_id) + insert_file( + node.setdefault("_dirs", {}).setdefault(head, {}), + rest, + full_id, + ) + + def insert_landing(node, parts, full_id): + # Drill down to the dir node for `parts` and store the + # landing page id there. Stored on the dir itself rather + # than as a sibling _files leaf so the directory and its + # landing aren't both rendered in the parent group + # (which produced the duplicate-entries bug we hit on + # multi-package nav: each package showed up once as a + # standalone page and once as a collapsible group). + cur = node + for part in parts: + cur = cur.setdefault("_dirs", {}).setdefault(part, {}) + cur["_landing"] = full_id for p in raw: if p in ("README", "index"): continue # Each subdir's landing page is `/index.md` (after # the rename step above) which Mintlify natively - # resolves from page id ``. Strip the `/index` - # suffix when building the registered path so the URL - # stays clean (`/` not `//index`). Same - # principle applied to legacy `/README` entries in - # case any downstream tool still emits them. + # resolves from page id ``. Register it as the + # landing of that dir's group so the URL stays clean + # (`/` not `//index`) and the dir's + # collapsible nav group opens to that page when clicked. if p.endswith("/index") or p.endswith("/README"): bare = p.rsplit("/", 1)[0] full_id = f"{PREFIX}/{bare}" parts = bare.split("/") + insert_landing(tree, parts, full_id) else: full_id = f"{PREFIX}/{p}" parts = p.split("/") - insert(tree, parts, full_id) + insert_file(tree, parts, full_id) def to_mintlify(node, group_name): pages = [] + if "_landing" in node: + pages.append(node["_landing"]) for fname, full_id in sorted(node.get("_files", [])): pages.append(full_id) for dname, sub in sorted(node.get("_dirs", {}).items()): diff --git a/docs.json b/docs.json index 083c4965..00b03a59 100644 --- a/docs.json +++ b/docs.json @@ -72,23 +72,14 @@ "group": "TypeScript", "pages": [ "sdks/typescript/api/README", - "sdks/typescript/api/analytics", - "sdks/typescript/api/decorators", - "sdks/typescript/api/dsa", - "sdks/typescript/api/http", - "sdks/typescript/api/logger", - "sdks/typescript/api/rate-limiting", - "sdks/typescript/api/security", { "group": "analytics", "pages": [ - "sdks/typescript/api/analytics/index", - "sdks/typescript/api/analytics/next", - "sdks/typescript/api/analytics/react", - "sdks/typescript/api/analytics/resq", + "sdks/typescript/api/analytics", { "group": "index", "pages": [ + "sdks/typescript/api/analytics/index", { "group": "classes", "pages": [ @@ -126,6 +117,7 @@ { "group": "next", "pages": [ + "sdks/typescript/api/analytics/next", { "group": "functions", "pages": [ @@ -144,6 +136,7 @@ { "group": "react", "pages": [ + "sdks/typescript/api/analytics/react", { "group": "functions", "pages": [ @@ -163,6 +156,7 @@ { "group": "resq", "pages": [ + "sdks/typescript/api/analytics/resq", { "group": "functions", "pages": [ @@ -184,33 +178,15 @@ { "group": "decorators", "pages": [ - "sdks/typescript/api/decorators/after", - "sdks/typescript/api/decorators/before", - "sdks/typescript/api/decorators/bind", - "sdks/typescript/api/decorators/debounce", - "sdks/typescript/api/decorators/delay", - "sdks/typescript/api/decorators/delegate", - "sdks/typescript/api/decorators/exec-time", - "sdks/typescript/api/decorators/execute", - "sdks/typescript/api/decorators/index", - "sdks/typescript/api/decorators/memoize", - "sdks/typescript/api/decorators/memoize-async", - "sdks/typescript/api/decorators/observer", - "sdks/typescript/api/decorators/rate-limit", - "sdks/typescript/api/decorators/readonly", - "sdks/typescript/api/decorators/throttle", - "sdks/typescript/api/decorators/throttle-async", - "sdks/typescript/api/decorators/types", - "sdks/typescript/api/decorators/utils", + "sdks/typescript/api/decorators", { "group": "after", "pages": [ - "sdks/typescript/api/decorators/after/after", - "sdks/typescript/api/decorators/after/after.fn", - "sdks/typescript/api/decorators/after/after.types", + "sdks/typescript/api/decorators/after", { "group": "after", "pages": [ + "sdks/typescript/api/decorators/after/after", { "group": "functions", "pages": [ @@ -222,6 +198,7 @@ { "group": "after.fn", "pages": [ + "sdks/typescript/api/decorators/after/after.fn", { "group": "functions", "pages": [ @@ -233,6 +210,7 @@ { "group": "after.types", "pages": [ + "sdks/typescript/api/decorators/after/after.types", { "group": "interfaces", "pages": [ @@ -253,12 +231,11 @@ { "group": "before", "pages": [ - "sdks/typescript/api/decorators/before/before", - "sdks/typescript/api/decorators/before/before.fn", - "sdks/typescript/api/decorators/before/before.types", + "sdks/typescript/api/decorators/before", { "group": "before", "pages": [ + "sdks/typescript/api/decorators/before/before", { "group": "functions", "pages": [ @@ -270,6 +247,7 @@ { "group": "before.fn", "pages": [ + "sdks/typescript/api/decorators/before/before.fn", { "group": "functions", "pages": [ @@ -281,6 +259,7 @@ { "group": "before.types", "pages": [ + "sdks/typescript/api/decorators/before/before.types", { "group": "interfaces", "pages": [ @@ -294,12 +273,11 @@ { "group": "bind", "pages": [ - "sdks/typescript/api/decorators/bind/bind", - "sdks/typescript/api/decorators/bind/bind.fn", - "sdks/typescript/api/decorators/bind/bind.types", + "sdks/typescript/api/decorators/bind", { "group": "bind", "pages": [ + "sdks/typescript/api/decorators/bind/bind", { "group": "functions", "pages": [ @@ -311,6 +289,7 @@ { "group": "bind.fn", "pages": [ + "sdks/typescript/api/decorators/bind/bind.fn", { "group": "functions", "pages": [ @@ -322,6 +301,7 @@ { "group": "bind.types", "pages": [ + "sdks/typescript/api/decorators/bind/bind.types", { "group": "interfaces", "pages": [ @@ -335,11 +315,11 @@ { "group": "debounce", "pages": [ - "sdks/typescript/api/decorators/debounce/debounce", - "sdks/typescript/api/decorators/debounce/debounce.fn", + "sdks/typescript/api/decorators/debounce", { "group": "debounce", "pages": [ + "sdks/typescript/api/decorators/debounce/debounce", { "group": "functions", "pages": [ @@ -351,6 +331,7 @@ { "group": "debounce.fn", "pages": [ + "sdks/typescript/api/decorators/debounce/debounce.fn", { "group": "functions", "pages": [ @@ -364,11 +345,11 @@ { "group": "delay", "pages": [ - "sdks/typescript/api/decorators/delay/delay", - "sdks/typescript/api/decorators/delay/delay.fn", + "sdks/typescript/api/decorators/delay", { "group": "delay", "pages": [ + "sdks/typescript/api/decorators/delay/delay", { "group": "functions", "pages": [ @@ -380,6 +361,7 @@ { "group": "delay.fn", "pages": [ + "sdks/typescript/api/decorators/delay/delay.fn", { "group": "functions", "pages": [ @@ -393,12 +375,11 @@ { "group": "delegate", "pages": [ - "sdks/typescript/api/decorators/delegate/delegate", - "sdks/typescript/api/decorators/delegate/delegate.fn", - "sdks/typescript/api/decorators/delegate/delegate.types", + "sdks/typescript/api/decorators/delegate", { "group": "delegate", "pages": [ + "sdks/typescript/api/decorators/delegate/delegate", { "group": "functions", "pages": [ @@ -410,6 +391,7 @@ { "group": "delegate.fn", "pages": [ + "sdks/typescript/api/decorators/delegate/delegate.fn", { "group": "functions", "pages": [ @@ -421,6 +403,7 @@ { "group": "delegate.types", "pages": [ + "sdks/typescript/api/decorators/delegate/delegate.types", { "group": "type-aliases", "pages": [ @@ -434,12 +417,11 @@ { "group": "exec-time", "pages": [ - "sdks/typescript/api/decorators/exec-time/exec-time", - "sdks/typescript/api/decorators/exec-time/exec-time.fn", - "sdks/typescript/api/decorators/exec-time/exec-time.types", + "sdks/typescript/api/decorators/exec-time", { "group": "exec-time", "pages": [ + "sdks/typescript/api/decorators/exec-time/exec-time", { "group": "functions", "pages": [ @@ -451,6 +433,7 @@ { "group": "exec-time.fn", "pages": [ + "sdks/typescript/api/decorators/exec-time/exec-time.fn", { "group": "functions", "pages": [ @@ -462,6 +445,7 @@ { "group": "exec-time.types", "pages": [ + "sdks/typescript/api/decorators/exec-time/exec-time.types", { "group": "interfaces", "pages": [ @@ -482,10 +466,11 @@ { "group": "execute", "pages": [ - "sdks/typescript/api/decorators/execute/execute", + "sdks/typescript/api/decorators/execute", { "group": "execute", "pages": [ + "sdks/typescript/api/decorators/execute/execute", { "group": "functions", "pages": [ @@ -496,15 +481,20 @@ } ] }, + { + "group": "index", + "pages": [ + "sdks/typescript/api/decorators/index" + ] + }, { "group": "memoize", "pages": [ - "sdks/typescript/api/decorators/memoize/memoize", - "sdks/typescript/api/decorators/memoize/memoize.fn", - "sdks/typescript/api/decorators/memoize/memoize.types", + "sdks/typescript/api/decorators/memoize", { "group": "memoize", "pages": [ + "sdks/typescript/api/decorators/memoize/memoize", { "group": "functions", "pages": [ @@ -516,6 +506,7 @@ { "group": "memoize.fn", "pages": [ + "sdks/typescript/api/decorators/memoize/memoize.fn", { "group": "functions", "pages": [ @@ -527,6 +518,7 @@ { "group": "memoize.types", "pages": [ + "sdks/typescript/api/decorators/memoize/memoize.types", { "group": "interfaces", "pages": [ @@ -548,12 +540,11 @@ { "group": "memoize-async", "pages": [ - "sdks/typescript/api/decorators/memoize-async/memoize-async", - "sdks/typescript/api/decorators/memoize-async/memoize-async.fn", - "sdks/typescript/api/decorators/memoize-async/memoize-async.types", + "sdks/typescript/api/decorators/memoize-async", { "group": "memoize-async", "pages": [ + "sdks/typescript/api/decorators/memoize-async/memoize-async", { "group": "functions", "pages": [ @@ -565,6 +556,7 @@ { "group": "memoize-async.fn", "pages": [ + "sdks/typescript/api/decorators/memoize-async/memoize-async.fn", { "group": "functions", "pages": [ @@ -576,6 +568,7 @@ { "group": "memoize-async.types", "pages": [ + "sdks/typescript/api/decorators/memoize-async/memoize-async.types", { "group": "interfaces", "pages": [ @@ -596,11 +589,11 @@ { "group": "observer", "pages": [ - "sdks/typescript/api/decorators/observer/observer", - "sdks/typescript/api/decorators/observer/observer.types", + "sdks/typescript/api/decorators/observer", { "group": "observer", "pages": [ + "sdks/typescript/api/decorators/observer/observer", { "group": "functions", "pages": [ @@ -612,6 +605,7 @@ { "group": "observer.types", "pages": [ + "sdks/typescript/api/decorators/observer/observer.types", { "group": "type-aliases", "pages": [ @@ -625,13 +619,11 @@ { "group": "rate-limit", "pages": [ - "sdks/typescript/api/decorators/rate-limit/rate-limit", - "sdks/typescript/api/decorators/rate-limit/rate-limit.fn", - "sdks/typescript/api/decorators/rate-limit/rate-limit.types", - "sdks/typescript/api/decorators/rate-limit/simple-rate-limit-counter", + "sdks/typescript/api/decorators/rate-limit", { "group": "rate-limit", "pages": [ + "sdks/typescript/api/decorators/rate-limit/rate-limit", { "group": "functions", "pages": [ @@ -643,6 +635,7 @@ { "group": "rate-limit.fn", "pages": [ + "sdks/typescript/api/decorators/rate-limit/rate-limit.fn", { "group": "functions", "pages": [ @@ -654,6 +647,7 @@ { "group": "rate-limit.types", "pages": [ + "sdks/typescript/api/decorators/rate-limit/rate-limit.types", { "group": "interfaces", "pages": [ @@ -673,6 +667,7 @@ { "group": "simple-rate-limit-counter", "pages": [ + "sdks/typescript/api/decorators/rate-limit/simple-rate-limit-counter", { "group": "classes", "pages": [ @@ -686,11 +681,11 @@ { "group": "readonly", "pages": [ - "sdks/typescript/api/decorators/readonly/readonly", - "sdks/typescript/api/decorators/readonly/readonly.types", + "sdks/typescript/api/decorators/readonly", { "group": "readonly", "pages": [ + "sdks/typescript/api/decorators/readonly/readonly", { "group": "functions", "pages": [ @@ -702,6 +697,7 @@ { "group": "readonly.types", "pages": [ + "sdks/typescript/api/decorators/readonly/readonly.types", { "group": "type-aliases", "pages": [ @@ -715,11 +711,11 @@ { "group": "throttle", "pages": [ - "sdks/typescript/api/decorators/throttle/throttle", - "sdks/typescript/api/decorators/throttle/throttle.fn", + "sdks/typescript/api/decorators/throttle", { "group": "throttle", "pages": [ + "sdks/typescript/api/decorators/throttle/throttle", { "group": "functions", "pages": [ @@ -731,6 +727,7 @@ { "group": "throttle.fn", "pages": [ + "sdks/typescript/api/decorators/throttle/throttle.fn", { "group": "functions", "pages": [ @@ -744,12 +741,11 @@ { "group": "throttle-async", "pages": [ - "sdks/typescript/api/decorators/throttle-async/throttle-async", - "sdks/typescript/api/decorators/throttle-async/throttle-async-executor", - "sdks/typescript/api/decorators/throttle-async/throttle-async.fn", + "sdks/typescript/api/decorators/throttle-async", { "group": "throttle-async", "pages": [ + "sdks/typescript/api/decorators/throttle-async/throttle-async", { "group": "functions", "pages": [ @@ -761,6 +757,7 @@ { "group": "throttle-async-executor", "pages": [ + "sdks/typescript/api/decorators/throttle-async/throttle-async-executor", { "group": "classes", "pages": [ @@ -772,6 +769,7 @@ { "group": "throttle-async.fn", "pages": [ + "sdks/typescript/api/decorators/throttle-async/throttle-async.fn", { "group": "functions", "pages": [ @@ -785,6 +783,7 @@ { "group": "types", "pages": [ + "sdks/typescript/api/decorators/types", { "group": "type-aliases", "pages": [ @@ -799,6 +798,7 @@ { "group": "utils", "pages": [ + "sdks/typescript/api/decorators/utils", { "group": "classes", "pages": [ @@ -828,20 +828,11 @@ { "group": "dsa", "pages": [ - "sdks/typescript/api/dsa/bloom", - "sdks/typescript/api/dsa/count-min", - "sdks/typescript/api/dsa/distance", - "sdks/typescript/api/dsa/graph", - "sdks/typescript/api/dsa/heap", - "sdks/typescript/api/dsa/index", - "sdks/typescript/api/dsa/lru-cache", - "sdks/typescript/api/dsa/priority-queue", - "sdks/typescript/api/dsa/queue", - "sdks/typescript/api/dsa/schemas", - "sdks/typescript/api/dsa/trie", + "sdks/typescript/api/dsa", { "group": "bloom", "pages": [ + "sdks/typescript/api/dsa/bloom", { "group": "classes", "pages": [ @@ -853,6 +844,7 @@ { "group": "count-min", "pages": [ + "sdks/typescript/api/dsa/count-min", { "group": "classes", "pages": [ @@ -864,6 +856,7 @@ { "group": "distance", "pages": [ + "sdks/typescript/api/dsa/distance", { "group": "classes", "pages": [ @@ -890,10 +883,10 @@ { "group": "dsa", "pages": [ - "sdks/typescript/api/dsa/dsa/rabin-karp", { "group": "rabin-karp", "pages": [ + "sdks/typescript/api/dsa/dsa/rabin-karp", { "group": "classes", "pages": [ @@ -921,6 +914,7 @@ { "group": "graph", "pages": [ + "sdks/typescript/api/dsa/graph", { "group": "classes", "pages": [ @@ -949,6 +943,7 @@ { "group": "heap", "pages": [ + "sdks/typescript/api/dsa/heap", { "group": "classes", "pages": [ @@ -963,9 +958,16 @@ } ] }, + { + "group": "index", + "pages": [ + "sdks/typescript/api/dsa/index" + ] + }, { "group": "lru-cache", "pages": [ + "sdks/typescript/api/dsa/lru-cache", { "group": "classes", "pages": [ @@ -983,6 +985,7 @@ { "group": "priority-queue", "pages": [ + "sdks/typescript/api/dsa/priority-queue", { "group": "classes", "pages": [ @@ -1018,6 +1021,7 @@ { "group": "queue", "pages": [ + "sdks/typescript/api/dsa/queue", { "group": "classes", "pages": [ @@ -1035,6 +1039,7 @@ { "group": "schemas", "pages": [ + "sdks/typescript/api/dsa/schemas", { "group": "functions", "pages": [ @@ -1080,6 +1085,7 @@ { "group": "trie", "pages": [ + "sdks/typescript/api/dsa/trie", { "group": "classes", "pages": [ @@ -1105,12 +1111,11 @@ { "group": "http", "pages": [ - "sdks/typescript/api/http/fetcher", - "sdks/typescript/api/http/index", - "sdks/typescript/api/http/security", + "sdks/typescript/api/http", { "group": "fetcher", "pages": [ + "sdks/typescript/api/http/fetcher", { "group": "classes", "pages": [ @@ -1150,9 +1155,16 @@ } ] }, + { + "group": "index", + "pages": [ + "sdks/typescript/api/http/index" + ] + }, { "group": "security", "pages": [ + "sdks/typescript/api/http/security", { "group": "functions", "pages": [ @@ -1167,13 +1179,17 @@ { "group": "logger", "pages": [ - "sdks/typescript/api/logger/index", - "sdks/typescript/api/logger/logger", - "sdks/typescript/api/logger/logger.decorators", - "sdks/typescript/api/logger/logger.types", + "sdks/typescript/api/logger", + { + "group": "index", + "pages": [ + "sdks/typescript/api/logger/index" + ] + }, { "group": "logger", "pages": [ + "sdks/typescript/api/logger/logger", { "group": "classes", "pages": [ @@ -1210,6 +1226,7 @@ { "group": "logger.decorators", "pages": [ + "sdks/typescript/api/logger/logger.decorators", { "group": "functions", "pages": [ @@ -1224,6 +1241,7 @@ { "group": "logger.types", "pages": [ + "sdks/typescript/api/logger/logger.types", { "group": "interfaces", "pages": [ @@ -1251,12 +1269,17 @@ { "group": "rate-limiting", "pages": [ - "sdks/typescript/api/rate-limiting/index", - "sdks/typescript/api/rate-limiting/rate-limit", - "sdks/typescript/api/rate-limiting/throttle", + "sdks/typescript/api/rate-limiting", + { + "group": "index", + "pages": [ + "sdks/typescript/api/rate-limiting/index" + ] + }, { "group": "rate-limit", "pages": [ + "sdks/typescript/api/rate-limiting/rate-limit", { "group": "classes", "pages": [ @@ -1290,6 +1313,7 @@ { "group": "throttle", "pages": [ + "sdks/typescript/api/rate-limiting/throttle", { "group": "classes", "pages": [ @@ -1332,13 +1356,11 @@ { "group": "security", "pages": [ - "sdks/typescript/api/security/crypto", - "sdks/typescript/api/security/index", - "sdks/typescript/api/security/sanitize", - "sdks/typescript/api/security/validators", + "sdks/typescript/api/security", { "group": "crypto", "pages": [ + "sdks/typescript/api/security/crypto", { "group": "functions", "pages": [ @@ -1353,9 +1375,16 @@ } ] }, + { + "group": "index", + "pages": [ + "sdks/typescript/api/security/index" + ] + }, { "group": "sanitize", "pages": [ + "sdks/typescript/api/security/sanitize", { "group": "functions", "pages": [ @@ -1411,6 +1440,7 @@ { "group": "validators", "pages": [ + "sdks/typescript/api/security/validators", { "group": "functions", "pages": [ @@ -2464,4 +2494,4 @@ "website": "https://resq.software" } } -} \ No newline at end of file +} diff --git a/scripts/splice-sdk-nav.py b/scripts/splice-sdk-nav.py index 7a0f12d9..5fbda9a4 100644 --- a/scripts/splice-sdk-nav.py +++ b/scripts/splice-sdk-nav.py @@ -29,9 +29,24 @@ def insert(tree: dict, parts: list[str], full_id: str) -> None: insert(sub, rest, full_id) +def insert_landing(tree: dict, parts: list[str], full_id: str) -> None: + """Mark `full_id` as the landing page for the dir at `parts`. + Stored on the dir node itself rather than as a sibling _files + leaf, so the directory and its landing aren't both rendered in + the parent group (which produced a duplicate-entries bug on + multi-package nav: each package showed up once as a standalone + page and once as a collapsible group).""" + cur = tree + for part in parts: + cur = cur.setdefault("_dirs", {}).setdefault(part, {}) + cur["_landing"] = full_id + + def to_mintlify(tree: dict, group_name: str | None) -> dict | list: """Convert internal tree to Mintlify groups/pages structure.""" pages: list = [] + if "_landing" in tree: + pages.append(tree["_landing"]) for fname, full_id in sorted(tree.get("_files", [])): pages.append(full_id) for dname, subtree in sorted(tree.get("_dirs", {}).items()): @@ -114,10 +129,11 @@ def build_lang_group(language: str, prefix: str, pages_path: pathlib.Path, bare = p.rsplit("/", 1)[0] full_id = f"{prefix}/{bare}" parts = bare.split("/") + insert_landing(tree, parts, full_id) else: full_id = f"{prefix}/{p}" parts = p.split("/") - insert(tree, parts, full_id) + insert(tree, parts, full_id) pages = [readme_id] + to_mintlify(tree, None) # Post-process: collapse .NET namespace dirs into class-grouped form