Skip to content

Commit 7ed25ba

Browse files
committed
add plugin to find floating pages
1 parent e1c33c4 commit 7ed25ba

File tree

2 files changed

+161
-30
lines changed

2 files changed

+161
-30
lines changed

docusaurus.config.en.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import katex from "rehype-katex";
44
import chHeader from "./plugins/header.js";
55
import fixLinks from "./src/hooks/fixLinks.js";
66
const { customParseFrontMatter } = require('./plugins/frontmatter-validation/customParseFrontMatter');
7-
7+
const checkFloatingPages = require('./plugins/checkFloatingPages');
8+
const frontmatterValidator = require('./plugins/frontmatter-validation/frontmatterValidatorPlugin');
89
// Helper function to skip over index.md files.
910
function skipIndex(items) {
1011
return items.filter(({ type, id }) => {
@@ -317,13 +318,25 @@ const config = {
317318
},
318319
],
319320
chHeader,
320-
'./plugins/frontmatter-validation/frontmatterValidatorPlugin'
321+
[
322+
frontmatterValidator,
323+
{
324+
failBuild: true,
325+
},
326+
],
327+
[
328+
checkFloatingPages,
329+
{
330+
failBuild: true,
331+
},
332+
]
321333
],
322334
customFields: {
323335
blogSidebarLink: "/docs/knowledgebase", // Used for KB article page
324336
galaxyApiEndpoint:
325337
process.env.NEXT_PUBLIC_GALAXY_API_ENDPOINT || "http://localhost:3000",
326338
},
339+
327340
};
328341

329342
module.exports = config;

plugins/checkFloatingPages.js

Lines changed: 146 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ async function checkFloatingPages(context, options) {
1313
const docsDir = path.resolve(context.siteDir, 'docs');
1414

1515
let sidebarItems = [];
16+
let autogeneratedDirs = [];
1617
const sidebarsPath = path.join(context.siteDir, 'sidebars.js');
1718
if (fs.existsSync(sidebarsPath)) {
1819
try {
1920
const sidebars = require(sidebarsPath);
20-
sidebarItems = getSidebarItems(sidebars);
21+
const result = getSidebarItemsAndAutogenerated(context, sidebars);
22+
sidebarItems = result.items;
23+
autogeneratedDirs = result.autogeneratedDirs;
2124
} catch (err) {
2225
console.error("Error loading sidebars.js", err);
2326
throw err; // Stop the build
@@ -28,58 +31,173 @@ async function checkFloatingPages(context, options) {
2831
const floatingPages = [];
2932

3033
for (const filePath of markdownFiles) {
34+
// directories to skip
35+
if (filePath.includes('_'))
36+
continue;
37+
3138
const fileContent = fs.readFileSync(filePath, 'utf-8');
3239
const { data } = matter(fileContent);
3340

3441
const relativePath = path.relative(docsDir, filePath).replace(/\\/g, '/').replace(/\.md$/, '');
3542

36-
// Check 1: Explicit sidebar_position or id in frontmatter
37-
if (data.sidebar_position || data.id) {
38-
const idToCheck = data.id || relativePath;
39-
if (!sidebarItems.includes(idToCheck)) {
40-
floatingPages.push(filePath);
41-
}
42-
} else {
43-
//Check 2: Implicit.
44-
if (!sidebarItems.includes(relativePath)) {
45-
floatingPages.push(filePath);
46-
}
43+
// Skip if the document has sidebar: false explicitly set
44+
if (data.sidebar === false) {
45+
continue;
46+
}
47+
48+
const idToCheck = data.id || relativePath;
49+
50+
// Check various ways this document could be included in the sidebar
51+
if (isDocumentIncluded(idToCheck, relativePath, sidebarItems, autogeneratedDirs)) {
52+
continue;
4753
}
54+
55+
floatingPages.push(filePath);
4856
}
4957

5058
if (floatingPages.length > 0) {
51-
console.error('\x1b[31m%s\x1b[0m', 'Floating pages found:');
52-
floatingPages.forEach(page => console.error(` - ${page}`));
5359
if (options && options.failBuild) {
54-
throw new Error('Floating pages found. See above for details.');
60+
console.error('\x1b[31m%s\x1b[0m', `${floatingPages.length} floating pages found:`);
61+
floatingPages.forEach(page => console.error(` - ${page}`));
62+
throw new Error('🚨 Floating pages found. See above for details.');
63+
} else {
64+
console.log('⚠️', 'Found floating pages:');
65+
floatingPages.forEach(page => console.log(` - ${page}`));
5566
}
5667
} else {
57-
console.log('\x1b[32m%s\x1b[0m', 'No floating pages found.');
68+
console.log('', 'No floating pages found.');
5869
}
5970
}
6071
}
6172
}
6273

63-
function getSidebarItems(sidebarConfig) {
64-
let items = []
74+
// Helper function to check if a document is included in sidebar or autogenerated directories
75+
function isDocumentIncluded(idToCheck, relativePath, sidebarItems, autogeneratedDirs) {
76+
// Direct check if ID is in sidebar items
77+
if (sidebarItems.includes(idToCheck)) {
78+
return true;
79+
}
80+
81+
// Check variations for index pages
82+
if (relativePath.endsWith('/index')) {
83+
const parentDir = relativePath.replace(/\/index$/, '');
84+
if (sidebarItems.includes(parentDir)) {
85+
return true;
86+
}
87+
}
88+
89+
// Check if any sidebar item is a string and matches the ID without the /index
90+
for (const item of sidebarItems) {
91+
if (typeof item === 'string') {
92+
// If the item directly matches
93+
if (item === idToCheck) {
94+
return true;
95+
}
96+
97+
// For index pages, check if the parent directory matches
98+
if (relativePath.endsWith('/index') && item === relativePath.replace(/\/index$/, '')) {
99+
return true;
100+
}
101+
} else if (typeof item === 'object' && item !== null) {
102+
// Handle link items
103+
if (item.type === 'link' && item.href) {
104+
const href = item.href;
105+
// Extract the path from the href
106+
let linkPath = href.replace(/^\/docs\//, '').replace(/\/$/, '');
107+
108+
// Check if this link points to the current document
109+
if (linkPath === idToCheck ||
110+
linkPath === relativePath ||
111+
(relativePath.endsWith('/index') && linkPath === relativePath.replace(/\/index$/, ''))) {
112+
return true;
113+
}
114+
}
115+
}
116+
}
117+
118+
// Check if document is in an autogenerated directory
119+
for (const dir of autogeneratedDirs) {
120+
// Direct check
121+
if (relativePath.startsWith(dir + '/') || relativePath === dir) {
122+
return true;
123+
}
124+
125+
// Handle index files specifically
126+
if (relativePath === `${dir}/index`) {
127+
return true;
128+
}
129+
}
130+
131+
return false;
132+
}
133+
134+
function getSidebarItemsAndAutogenerated(context, sidebarConfig) {
135+
let items = [];
136+
let autogeneratedDirs = [];
137+
65138
function traverse(item) {
66139
if (item.type === 'doc') {
67140
items.push(item.id);
68-
} else if (item.type === 'category' || item.type === 'autogenerated') {
141+
} else if (item.type === 'category') {
142+
// Add the category link itself if it has a link
143+
if (item.link && item.link.type === 'doc' && item.link.id) {
144+
items.push(item.link.id);
145+
}
146+
147+
// Process all items in the category
69148
(item.items || []).forEach(traverse);
149+
} else if (item.type === 'autogenerated') {
150+
const dirPath = item.dirName;
151+
let dir;
152+
153+
// Handle both absolute and relative paths
154+
if (path.isAbsolute(dirPath)) {
155+
dir = path.relative(path.resolve(context.siteDir, 'docs'), dirPath);
156+
} else {
157+
dir = dirPath;
158+
}
159+
160+
dir = dir.replace(/\\/g, '/');
161+
162+
// Remove leading 'docs/' if present
163+
dir = dir.replace(/^docs\//, '');
164+
165+
// Remove trailing slash if present
166+
dir = dir.replace(/\/$/, '');
167+
168+
autogeneratedDirs.push(dir);
70169
} else if (item.type === 'link') {
71-
// no need to worry about links
170+
// Add the link item directly to check later
171+
items.push(item);
172+
173+
// Check if the link is to a local doc
174+
const href = item.href || '';
175+
if (href.startsWith('/') && !href.startsWith('//')) {
176+
// Extract doc ID from local path
177+
const docPath = href.replace(/^\/docs\//, '').replace(/\/$/, '');
178+
if (docPath) {
179+
items.push(docPath);
180+
}
181+
}
182+
} else if (typeof item === 'string') {
183+
items.push(item);
72184
}
73185
}
74-
// sidebarConfig can be an array or an object
75-
if (Array.isArray(sidebarConfig)) {
76-
sidebarConfig.forEach(traverse);
77-
} else {
78-
Object.values(sidebarConfig).forEach(sidebar => {
79-
sidebar.forEach(traverse)
80-
})
186+
187+
const cleanedSidebarConfig = { ...sidebarConfig };
188+
// we don't check top level nav
189+
if (cleanedSidebarConfig.dropdownCategories) {
190+
delete cleanedSidebarConfig.dropdownCategories;
81191
}
82-
return items;
192+
193+
Object.values(cleanedSidebarConfig).forEach(sidebar => {
194+
sidebar.forEach(traverse);
195+
});
196+
197+
return {
198+
items,
199+
autogeneratedDirs
200+
};
83201
}
84202

85203
module.exports = checkFloatingPages;

0 commit comments

Comments
 (0)