@@ -13,11 +13,14 @@ async function checkFloatingPages(context, options) {
13
13
const docsDir = path . resolve ( context . siteDir , 'docs' ) ;
14
14
15
15
let sidebarItems = [ ] ;
16
+ let autogeneratedDirs = [ ] ;
16
17
const sidebarsPath = path . join ( context . siteDir , 'sidebars.js' ) ;
17
18
if ( fs . existsSync ( sidebarsPath ) ) {
18
19
try {
19
20
const sidebars = require ( sidebarsPath ) ;
20
- sidebarItems = getSidebarItems ( sidebars ) ;
21
+ const result = getSidebarItemsAndAutogenerated ( context , sidebars ) ;
22
+ sidebarItems = result . items ;
23
+ autogeneratedDirs = result . autogeneratedDirs ;
21
24
} catch ( err ) {
22
25
console . error ( "Error loading sidebars.js" , err ) ;
23
26
throw err ; // Stop the build
@@ -28,58 +31,173 @@ async function checkFloatingPages(context, options) {
28
31
const floatingPages = [ ] ;
29
32
30
33
for ( const filePath of markdownFiles ) {
34
+ // directories to skip
35
+ if ( filePath . includes ( '_' ) )
36
+ continue ;
37
+
31
38
const fileContent = fs . readFileSync ( filePath , 'utf-8' ) ;
32
39
const { data } = matter ( fileContent ) ;
33
40
34
41
const relativePath = path . relative ( docsDir , filePath ) . replace ( / \\ / g, '/' ) . replace ( / \. m d $ / , '' ) ;
35
42
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 ;
47
53
}
54
+
55
+ floatingPages . push ( filePath ) ;
48
56
}
49
57
50
58
if ( floatingPages . length > 0 ) {
51
- console . error ( '\x1b[31m%s\x1b[0m' , 'Floating pages found:' ) ;
52
- floatingPages . forEach ( page => console . error ( ` - ${ page } ` ) ) ;
53
59
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 } ` ) ) ;
55
66
}
56
67
} else {
57
- console . log ( '\x1b[32m%s\x1b[0m ' , 'No floating pages found.' ) ;
68
+ console . log ( '✅ ' , 'No floating pages found.' ) ;
58
69
}
59
70
}
60
71
}
61
72
}
62
73
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 ( / \/ i n d e x $ / , '' ) ;
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 ( / \/ i n d e x $ / , '' ) ) {
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 ( / ^ \/ d o c s \/ / , '' ) . 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 ( / \/ i n d e x $ / , '' ) ) ) {
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
+
65
138
function traverse ( item ) {
66
139
if ( item . type === 'doc' ) {
67
140
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
69
148
( 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 ( / ^ d o c s \/ / , '' ) ;
164
+
165
+ // Remove trailing slash if present
166
+ dir = dir . replace ( / \/ $ / , '' ) ;
167
+
168
+ autogeneratedDirs . push ( dir ) ;
70
169
} 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 ( / ^ \/ d o c s \/ / , '' ) . replace ( / \/ $ / , '' ) ;
178
+ if ( docPath ) {
179
+ items . push ( docPath ) ;
180
+ }
181
+ }
182
+ } else if ( typeof item === 'string' ) {
183
+ items . push ( item ) ;
72
184
}
73
185
}
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 ;
81
191
}
82
- return items ;
192
+
193
+ Object . values ( cleanedSidebarConfig ) . forEach ( sidebar => {
194
+ sidebar . forEach ( traverse ) ;
195
+ } ) ;
196
+
197
+ return {
198
+ items,
199
+ autogeneratedDirs
200
+ } ;
83
201
}
84
202
85
203
module . exports = checkFloatingPages ;
0 commit comments