From e221aa94dcf2a11c3ca9fd416d5ff1829fae756f Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Tue, 2 Jul 2024 13:14:31 -0400 Subject: [PATCH] Improve the sidebar generator Improve the sidebar generator to accommodate the reorganized docs site. - Alphabetize auto-generated sidebar entries. The exception is any page that includes "Introduction" or "introduction" in the title. Elevate these to the first entry to avoid a confusing sidebar order. - Show third-level category pages in the sidebar without showing their contents. This way, we can add content one level beyond the level permitted in the sidebar while still showing the category page for that content in the sidebar. - Allow for a category page to be at the same level as its associated subdirectory. This is to accommodate the Terraform Provider reference which, because of the way it's generated, can only include a category page outside of its associated subdirectory. This does not abide by the Docusaurus convention, so we will need to figure out a solution eventually, but this is a quick alternative to tide us over. --- server/pages-helpers.ts | 95 ++++++++++++---- uvu-tests/config-docs.test.ts | 204 ++++++++++++++++++++++++++++++++-- 2 files changed, 269 insertions(+), 30 deletions(-) diff --git a/server/pages-helpers.ts b/server/pages-helpers.ts index b755648f09..4322e53e9f 100644 --- a/server/pages-helpers.ts +++ b/server/pages-helpers.ts @@ -72,6 +72,39 @@ const getEntryForPath = (fs, filePath) => { }; }; +const sortByTitle = (a, b) => { + switch (true) { + case a.title.toLowerCase().includes("introduction"): + return -1; + break; + case b.title.toLowerCase().includes("introduction"): + return 1; + break; + default: + return a.title < b.title ? -1 : 1; + } +}; + +// categoryPagePathForDir looks for a category page at the same directory level +// as its associated directory OR within the associated directory. Throws an +// error if there is no category page for the directory. +const categoryPagePathForDir = (fs, dirPath) => { + const { name } = parse(dirPath); + + const outerCategoryPage = join(dirname(dirPath), name + ".mdx"); + const innerCategoryPage = join(dirPath, name + ".mdx"); + + if (fs.existsSync(outerCategoryPage)) { + return outerCategoryPage; + } + if (fs.existsSync(innerCategoryPage)) { + return innerCategoryPage; + } + throw new Error( + `subdirectory in generated sidebar section ${dirPath} has no category page ${innerCategoryPage} or ${outerCategoryPage}` + ); +}; + export const generateNavPaths = (fs, dirPath) => { const firstLvl = fs.readdirSync(dirPath, "utf8"); let result = []; @@ -86,16 +119,12 @@ export const generateNavPaths = (fs, dirPath) => { } firstLvlFiles.add(fullPath); }); - let sectionIntros = new Set(); - firstLvlDirs.forEach((d: string) => { - const { name } = parse(d); - const asFile = join(d, name + ".mdx"); - if (!fs.existsSync(asFile)) { - throw `subdirectory in generated sidebar section ${d} has no category page ${asFile}`; - } - sectionIntros.add(asFile); - return; + // Map category pages to the directories they introduce so we can can add a + // sidebar entry for the category page, then traverse the directory. + let sectionIntros = new Map(); + firstLvlDirs.forEach((d: string) => { + sectionIntros.set(categoryPagePathForDir(fs, d), d); }); // Add files with no corresponding directory to the navigation first. Section @@ -103,36 +132,58 @@ export const generateNavPaths = (fs, dirPath) => { // subdirectory containing pages in the section, or have the name // "introduction.mdx". firstLvlFiles.forEach((f) => { + // Handle section intros separately + if (sectionIntros.has(f)) { + return; + } result.push(getEntryForPath(fs, f)); }); - sectionIntros.forEach((si: string) => { - const { slug, title } = getEntryForPath(fs, si); + sectionIntros.forEach((dirPath, categoryPagePath) => { + const { slug, title } = getEntryForPath(fs, categoryPagePath); const section = { title: title, slug: slug, entries: [], }; - const sectionDir = dirname(si); - const secondLvl = fs.readdirSync(sectionDir, "utf8"); - secondLvl.forEach((f2) => { - const { name } = parse(f2); - - // The directory name is the same as the filename, meaning that we have - // already used this as a category page. - if (sectionDir.endsWith(name)) { + const secondLvl = new Set(fs.readdirSync(dirPath, "utf8")); + + // Find all second-level category pages first so we don't + // repeat them in the sidebar. + secondLvl.forEach((f2: string) => { + let fullPath2 = join(dirPath, f2); + const stat = fs.statSync(fullPath2); + + // List category pages on the second level, but not their contents. + if (!stat.isDirectory()) { return; } + const catPath = categoryPagePathForDir(fs, fullPath2); + fullPath2 = catPath; + secondLvl.delete(f2); - const fullPath2 = join(sectionDir, f2); - const stat = fs.statSync(fullPath2); - if (stat.isDirectory()) { + // Delete the category page from the set so we don't add it again + // when we add individual files. + secondLvl.delete(parse(catPath).base); + section.entries.push(getEntryForPath(fs, fullPath2)); + }); + + secondLvl.forEach((f2: string) => { + let fullPath2 = join(dirPath, f2); + + // This is a first-level category page that happens to exist on the second + // level. + if (sectionIntros.has(fullPath2)) { return; } + const stat = fs.statSync(fullPath2); section.entries.push(getEntryForPath(fs, fullPath2)); }); + + section.entries.sort(sortByTitle); result.push(section); }); + result.sort(sortByTitle); return result; }; diff --git a/uvu-tests/config-docs.test.ts b/uvu-tests/config-docs.test.ts index 7db74aa009..bdef7dc9a4 100644 --- a/uvu-tests/config-docs.test.ts +++ b/uvu-tests/config-docs.test.ts @@ -139,10 +139,6 @@ title: Database RBAC Reference }; const expected = [ - { - title: "Protect Databases with Teleport", - slug: "/database-access/introduction/", - }, { title: "Database Access Guides", slug: "/database-access/guides/guides/", @@ -161,16 +157,20 @@ title: Database RBAC Reference title: "Database Access RBAC", slug: "/database-access/rbac/rbac/", entries: [ - { - title: "Get Started with DB RBAC", - slug: "/database-access/rbac/get-started/", - }, { title: "Database RBAC Reference", slug: "/database-access/rbac/reference/", }, + { + title: "Get Started with DB RBAC", + slug: "/database-access/rbac/get-started/", + }, ], }, + { + title: "Protect Databases with Teleport", + slug: "/database-access/introduction/", + }, ]; const vol = Volume.fromJSON(files); @@ -179,6 +179,96 @@ title: Database RBAC Reference assert.equal(actual, expected); }); +Suite( + "generateNavPaths alphabetizes second-level links except 'Introduction'", + () => { + const files = { + "/docs/pages/database-access/mongodb.mdx": `--- +title: MongoDB +---`, + "/docs/pages/database-access/azure-dbs.mdx": `--- +title: Azure +---`, + "/docs/pages/database-access/introduction.mdx": `--- +title: Introduction to Database Access +---`, + }; + + const expected = [ + { + title: "Introduction to Database Access", + slug: "/database-access/introduction/", + }, + { + title: "Azure", + slug: "/database-access/azure-dbs/", + }, + { + title: "MongoDB", + slug: "/database-access/mongodb/", + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + const actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); + } +); + +Suite( + "generateNavPaths alphabetizes third-level links except 'Introduction'", + () => { + const files = { + "/docs/pages/database-access/guides/guides.mdx": `--- +title: Database Access Guides +---`, + "/docs/pages/database-access/guides/postgres.mdx": `--- +title: Postgres Guide +---`, + "/docs/pages/database-access/guides/mysql.mdx": `--- +title: MySQL Guide +---`, + "/docs/pages/database-access/guides/get-started.mdx": `--- +title: Introduction to Database RBAC +---`, + "/docs/pages/database-access/guides/reference.mdx": `--- +title: Database RBAC Reference +---`, + }; + + const expected = [ + { + title: "Database Access Guides", + slug: "/database-access/guides/guides/", + entries: [ + { + title: "Introduction to Database RBAC", + slug: "/database-access/guides/get-started/", + }, + { + title: "Database RBAC Reference", + slug: "/database-access/guides/reference/", + }, + { + title: "MySQL Guide", + slug: "/database-access/guides/mysql/", + }, + { + title: "Postgres Guide", + slug: "/database-access/guides/postgres/", + }, + ], + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + const actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); + } +); + Suite( "generateNavPaths throws if there is no category page in a subdirectory", () => { @@ -199,4 +289,102 @@ title: MySQL Guide } ); +Suite( + "generateNavPaths shows third-level category pages on the sidebar", + () => { + const files = { + "/docs/pages/database-access/guides/guides.mdx": `--- +title: Database Access Guides +---`, + "/docs/pages/database-access/guides/postgres.mdx": `--- +title: Postgres Guide +---`, + "/docs/pages/database-access/guides/mysql.mdx": `--- +title: MySQL Guide +---`, + "/docs/pages/database-access/guides/rbac/rbac.mdx": `--- +title: Database Access RBAC +---`, + "/docs/pages/database-access/guides/rbac/get-started.mdx": `--- +title: Get Started with DB RBAC +---`, + }; + + const expected = [ + { + title: "Database Access Guides", + slug: "/database-access/guides/guides/", + entries: [ + { + title: "Database Access RBAC", + slug: "/database-access/guides/rbac/rbac/", + }, + { + title: "MySQL Guide", + slug: "/database-access/guides/mysql/", + }, + { + title: "Postgres Guide", + slug: "/database-access/guides/postgres/", + }, + ], + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + const actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); + } +); + +Suite( + "allows category pages in the same directory as the associated subdirectory", + () => { + const files = { + "/docs/pages/database-access/guides.mdx": `--- +title: Database Access Guides +---`, + "/docs/pages/database-access/guides/postgres.mdx": `--- +title: Postgres Guide +---`, + "/docs/pages/database-access/guides/mysql.mdx": `--- +title: MySQL Guide +---`, + "/docs/pages/database-access/guides/rbac.mdx": `--- +title: Database Access RBAC +---`, + "/docs/pages/database-access/guides/rbac/get-started.mdx": `--- +title: Get Started with DB RBAC +---`, + }; + + const expected = [ + { + title: "Database Access Guides", + slug: "/database-access/guides/", + entries: [ + { + title: "Database Access RBAC", + slug: "/database-access/guides/rbac/", + }, + { + title: "MySQL Guide", + slug: "/database-access/guides/mysql/", + }, + { + title: "Postgres Guide", + slug: "/database-access/guides/postgres/", + }, + ], + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + let actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); + } +); + Suite.run();