From 0f0b476e1f0607a8b433d14d846a8a03ae61c30f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:04:49 +0000 Subject: [PATCH 1/6] Initial plan From 45e858f63bc29a18c4587b11b1942dea118cfa37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:10:49 +0000 Subject: [PATCH 2/6] Initial exploration of documentation structure Co-authored-by: adamziel <205419+adamziel@users.noreply.github.com> --- packages/docs/site/check-orphan-pages-v2.js | 154 ++++++++++++++++++++ packages/docs/site/check-orphan-pages.js | 139 ++++++++++++++++++ packages/docs/site/check-orphans.js | 34 +++++ 3 files changed, 327 insertions(+) create mode 100644 packages/docs/site/check-orphan-pages-v2.js create mode 100644 packages/docs/site/check-orphan-pages.js create mode 100644 packages/docs/site/check-orphans.js diff --git a/packages/docs/site/check-orphan-pages-v2.js b/packages/docs/site/check-orphan-pages-v2.js new file mode 100644 index 0000000000..406db450a2 --- /dev/null +++ b/packages/docs/site/check-orphan-pages-v2.js @@ -0,0 +1,154 @@ +const fs = require('fs'); +const path = require('path'); + +const docsDir = './docs'; +const sidebars = require('./sidebars.js'); + +// Function to get frontmatter from markdown file +function getFrontmatter(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const match = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (!match) return null; + + const frontmatter = {}; + const lines = match[1].split('\n'); + for (const line of lines) { + const [key, ...valueParts] = line.split(':'); + if (key && valueParts.length) { + frontmatter[key.trim()] = valueParts.join(':').trim(); + } + } + return frontmatter; +} + +// Get all markdown files recursively +function getAllMarkdownFiles(dir, baseDir = dir) { + let files = []; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + // Skip _fragments directory + if (entry.name === '_fragments') continue; + files = files.concat(getAllMarkdownFiles(fullPath, baseDir)); + } else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) { + const relativePath = path.relative(baseDir, fullPath); + files.push(relativePath); + } + } + return files; +} + +// Function to get the doc ID for a file +function getDocId(file) { + const fullPath = path.join(docsDir, file); + const fm = getFrontmatter(fullPath); + + if (fm && fm.id) { + // If there's an explicit ID in frontmatter, use it with the directory prefix + const dir = path.dirname(file); + if (dir !== '.') { + const topLevel = dir.split(path.sep)[0]; + return `${topLevel}/${fm.id}`; + } + return fm.id; + } + + // Otherwise, use the file path as the ID (Docusaurus default behavior) + // Remove the file extension and convert to forward slashes + let id = file + .replace(/\.mdx?$/, '') + .replace(/\\/g, '/'); + + // Remove number prefixes (e.g., "01-", "23-") + id = id.replace(/\/\d+-/g, '/').replace(/^\d+-/, ''); + + // If the file is named "index", the ID is the directory path + // But if it's "intro", it might be referenced differently in the sidebar + + return id; +} + +// Function to extract all doc IDs from sidebar config +function extractDocIds(items) { + let ids = []; + for (const item of items) { + if (typeof item === 'string') { + ids.push(item); + } else if (item.type === 'category') { + if (item.link && item.link.id) { + ids.push(item.link.id); + } + if (item.items) { + ids = ids.concat(extractDocIds(item.items)); + } + } else if (item.type === 'doc' && item.id) { + ids.push(item.id); + } + } + return ids; +} + +// Get all sidebar IDs +let allSidebarIds = []; +for (const sidebarKey in sidebars) { + allSidebarIds = allSidebarIds.concat(extractDocIds(sidebars[sidebarKey])); +} + +// Get all file IDs +const allFiles = getAllMarkdownFiles(docsDir); +const fileIdMap = new Map(); // Map from doc ID to file path + +allFiles.forEach(file => { + const docId = getDocId(file); + fileIdMap.set(docId, file); +}); + +console.log('\n=== ANALYSIS ==='); +console.log(`Total markdown files: ${allFiles.length}`); +console.log(`Total sidebar entries: ${allSidebarIds.length}`); +console.log(`Unique sidebar entries: ${new Set(allSidebarIds).size}`); + +// Check if there are any duplicate sidebar entries +const duplicates = allSidebarIds.filter((id, index) => allSidebarIds.indexOf(id) !== index); +if (duplicates.length > 0) { + console.log(`\nWarning: Found ${duplicates.length} duplicate sidebar entry(ies):`); + new Set(duplicates).forEach(id => console.log(` - ${id}`)); +} + +// Find orphaned pages +const orphans = []; +fileIdMap.forEach((file, id) => { + if (!allSidebarIds.includes(id)) { + orphans.push({ id, file }); + } +}); + +if (orphans.length > 0) { + console.log('\n=== ORPHANED PAGES (NOT IN SIDEBARS) ==='); + console.log(`Found ${orphans.length} orphaned page(s):\n`); + orphans.forEach(({ id, file }) => { + console.log(` ID: ${id}`); + console.log(` File: ${file}\n`); + }); + process.exit(1); +} else { + console.log('\n✓ All pages are linked in sidebars!'); +} + +// Find sidebar entries without files +const missingFiles = []; +allSidebarIds.forEach(id => { + if (!fileIdMap.has(id)) { + missingFiles.push(id); + } +}); + +if (missingFiles.length > 0) { + console.log('\n=== SIDEBAR ENTRIES WITHOUT FILES ==='); + console.log(`Found ${missingFiles.length} sidebar entry(ies) without corresponding files:\n`); + missingFiles.forEach(id => { + console.log(` - ${id}`); + }); +} diff --git a/packages/docs/site/check-orphan-pages.js b/packages/docs/site/check-orphan-pages.js new file mode 100644 index 0000000000..195a606599 --- /dev/null +++ b/packages/docs/site/check-orphan-pages.js @@ -0,0 +1,139 @@ +const fs = require('fs'); +const path = require('path'); + +const docsDir = './docs'; +const sidebars = require('./sidebars.js'); + +// Function to get frontmatter from markdown file +function getFrontmatter(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const match = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (!match) return null; + + const frontmatter = {}; + const lines = match[1].split('\n'); + for (const line of lines) { + const [key, ...valueParts] = line.split(':'); + if (key && valueParts.length) { + frontmatter[key.trim()] = valueParts.join(':').trim(); + } + } + return frontmatter; +} + +// Get all markdown files recursively +function getAllMarkdownFiles(dir, baseDir = dir) { + let files = []; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + // Skip _fragments directory + if (entry.name === '_fragments') continue; + files = files.concat(getAllMarkdownFiles(fullPath, baseDir)); + } else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) { + const relativePath = path.relative(baseDir, fullPath); + files.push(relativePath); + } + } + return files; +} + +// Function to extract all doc IDs from sidebar config +function extractDocIds(items) { + let ids = []; + for (const item of items) { + if (typeof item === 'string') { + ids.push(item); + } else if (item.type === 'category') { + if (item.link && item.link.id) { + ids.push(item.link.id); + } + if (item.items) { + ids = ids.concat(extractDocIds(item.items)); + } + } else if (item.type === 'doc' && item.id) { + ids.push(item.id); + } + } + return ids; +} + +// Get all sidebar IDs +let allSidebarIds = []; +for (const sidebarKey in sidebars) { + allSidebarIds = allSidebarIds.concat(extractDocIds(sidebars[sidebarKey])); +} + +// Get all file IDs +const allFiles = getAllMarkdownFiles(docsDir); +const fileIds = new Map(); + +allFiles.forEach(file => { + const fullPath = path.join(docsDir, file); + const fm = getFrontmatter(fullPath); + let docId; + + if (fm && fm.id) { + docId = fm.id; + // Determine directory prefix + const dir = path.dirname(file); + if (dir !== '.') { + const topLevel = dir.split(path.sep)[0]; + docId = `${topLevel}/${fm.id}`; + } + } else { + // Construct default ID from path + docId = file + .replace(/\.mdx?$/, '') + .replace(/\/index$/, '') + .replace(/\\/g, '/') + // Remove number prefixes like "01-", "02-" etc + .replace(/\/\d+-/g, '/') + .replace(/^\d+-/, ''); + } + + fileIds.set(docId, file); +}); + +console.log('\n=== ANALYSIS ==='); +console.log(`Total markdown files: ${allFiles.length}`); +console.log(`Total sidebar entries: ${allSidebarIds.length}`); +console.log(`Unique sidebar entries: ${new Set(allSidebarIds).size}`); + +// Find orphaned pages +const orphans = []; +fileIds.forEach((file, id) => { + if (!allSidebarIds.includes(id)) { + orphans.push({ id, file }); + } +}); + +if (orphans.length > 0) { + console.log('\n=== ORPHANED PAGES ==='); + console.log(`Found ${orphans.length} orphaned page(s):\n`); + orphans.forEach(({ id, file }) => { + console.log(` - ${id}`); + console.log(` File: ${file}`); + }); + process.exit(1); +} else { + console.log('\n✓ No orphaned pages found!'); +} + +// Find sidebar entries without files +const missingFiles = []; +allSidebarIds.forEach(id => { + if (!fileIds.has(id)) { + missingFiles.push(id); + } +}); + +if (missingFiles.length > 0) { + console.log('\n=== SIDEBAR ENTRIES WITHOUT FILES ==='); + console.log(`Found ${missingFiles.length} sidebar entry(ies) without corresponding files:\n`); + missingFiles.forEach(id => { + console.log(` - ${id}`); + }); +} diff --git a/packages/docs/site/check-orphans.js b/packages/docs/site/check-orphans.js new file mode 100644 index 0000000000..ad4aaaeccb --- /dev/null +++ b/packages/docs/site/check-orphans.js @@ -0,0 +1,34 @@ +const fs = require('fs'); +const path = require('path'); + +// Load the sidebars config +const sidebars = require('./sidebars.js'); + +// Function to extract all doc IDs from sidebar config +function extractDocIds(items) { + let ids = []; + for (const item of items) { + if (typeof item === 'string') { + ids.push(item); + } else if (item.type === 'category') { + if (item.link && item.link.id) { + ids.push(item.link.id); + } + if (item.items) { + ids = ids.concat(extractDocIds(item.items)); + } + } else if (item.type === 'doc' && item.id) { + ids.push(item.id); + } + } + return ids; +} + +// Get all sidebar IDs +let allSidebarIds = []; +for (const sidebarKey in sidebars) { + allSidebarIds = allSidebarIds.concat(extractDocIds(sidebars[sidebarKey])); +} + +console.log('Doc IDs in sidebars:', allSidebarIds.length); +console.log('Sidebar IDs:', JSON.stringify(allSidebarIds, null, 2)); From bbe6db38312197d1d6fa227b8b19b6b4ca044266 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:20:22 +0000 Subject: [PATCH 3/6] Add CI check for orphaned Docusaurus documentation pages - Created check-orphan-pages.js script to detect orphaned docs - Added wp-now page to sidebar (was previously orphaned) - Fixed duplicate xdebug/introduction entry in sidebar - Added check-orphan-pages target to nx project config - Integrated check into CI workflow lint-and-typecheck job Co-authored-by: adamziel <205419+adamziel@users.noreply.github.com> --- .github/workflows/ci.yml | 2 + packages/docs/site/bin/check-orphan-pages.js | 218 +++++++++++++++++++ packages/docs/site/check-orphan-pages-v2.js | 154 ------------- packages/docs/site/check-orphan-pages.js | 139 ------------ packages/docs/site/check-orphans.js | 34 --- packages/docs/site/package.json | 3 +- packages/docs/site/project.json | 7 + packages/docs/site/sidebars.js | 2 +- 8 files changed, 230 insertions(+), 329 deletions(-) create mode 100755 packages/docs/site/bin/check-orphan-pages.js delete mode 100644 packages/docs/site/check-orphan-pages-v2.js delete mode 100644 packages/docs/site/check-orphan-pages.js delete mode 100644 packages/docs/site/check-orphans.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33f7b83703..9971f5fa20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ jobs: # This step: # * Warms up the node_modules cache # * Performs linting and typechecking + # * Checks for orphaned documentation pages # # The linting tasks take ~5s to complete and it doesn't # make sense to separate them into separate steps that would @@ -23,6 +24,7 @@ jobs: - uses: ./.github/actions/prepare-playground - run: npx nx affected --target=lint - run: npx nx affected --target=typecheck + - run: npx nx check-orphan-pages docs-site test-unit-asyncify: runs-on: ubuntu-latest strategy: diff --git a/packages/docs/site/bin/check-orphan-pages.js b/packages/docs/site/bin/check-orphan-pages.js new file mode 100755 index 0000000000..a9dd30c31a --- /dev/null +++ b/packages/docs/site/bin/check-orphan-pages.js @@ -0,0 +1,218 @@ +#!/usr/bin/env node + +/** + * Check for orphaned documentation pages + * + * This script verifies that all Docusaurus documentation pages are linked + * in at least one sidebar menu. Orphaned pages (pages not linked anywhere) + * may be forgotten or unintentionally excluded from the documentation. + * + * Exit codes: + * - 0: Success, no orphaned pages found + * - 1: Failure, orphaned pages were found + */ + +const fs = require('fs'); +const path = require('path'); + +const SCRIPT_DIR = __dirname; +const SITE_DIR = path.join(SCRIPT_DIR, '..'); +const DOCS_DIR = path.join(SITE_DIR, 'docs'); +const SIDEBARS_PATH = path.join(SITE_DIR, 'sidebars.js'); + +/** + * Extract frontmatter from a markdown file + */ +function getFrontmatter(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const match = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (!match) return null; + + const frontmatter = {}; + const lines = match[1].split('\n'); + for (const line of lines) { + const [key, ...valueParts] = line.split(':'); + if (key && valueParts.length) { + frontmatter[key.trim()] = valueParts.join(':').trim(); + } + } + return frontmatter; +} + +/** + * Recursively find all markdown files in a directory + */ +function getAllMarkdownFiles(dir, baseDir = dir) { + let files = []; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + // Skip _fragments directory - these are partial files included elsewhere + if (entry.name === '_fragments') continue; + files = files.concat(getAllMarkdownFiles(fullPath, baseDir)); + } else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) { + const relativePath = path.relative(baseDir, fullPath); + files.push(relativePath); + } + } + return files; +} + +/** + * Get the Docusaurus document ID for a file + * Follows Docusaurus conventions: + * - If frontmatter has an 'id' field, use it (prefixed with parent directory path) + * - Otherwise, derive from file path (without extension, with number prefixes removed) + */ +function getDocId(file) { + const fullPath = path.join(DOCS_DIR, file); + const fm = getFrontmatter(fullPath); + + if (fm && fm.id) { + // If there's an explicit ID in frontmatter, use it with the directory prefix + const dir = path.dirname(file); + if (dir !== '.') { + // Use the full directory path, not just the top level + const dirPath = dir.replace(/\\/g, '/'); + return `${dirPath}/${fm.id}`; + } + return fm.id; + } + + // Otherwise, use the file path as the ID (Docusaurus default behavior) + let id = file + .replace(/\.mdx?$/, '') // Remove file extension + .replace(/\\/g, '/'); // Convert to forward slashes + + // Remove number prefixes (e.g., "01-", "23-") that are used for ordering + id = id.replace(/\/\d+-/g, '/').replace(/^\d+-/, ''); + + return id; +} + +/** + * Recursively extract all document IDs referenced in sidebar configuration + */ +function extractDocIds(items) { + let ids = []; + for (const item of items) { + if (typeof item === 'string') { + // Simple string reference to a doc + ids.push(item); + } else if (item.type === 'category') { + // Category with optional link + if (item.link && item.link.id) { + ids.push(item.link.id); + } + // Recursively process category items + if (item.items) { + ids = ids.concat(extractDocIds(item.items)); + } + } else if (item.type === 'doc' && item.id) { + // Explicit doc reference + ids.push(item.id); + } + } + return ids; +} + +/** + * Main function + */ +function main() { + // Load the sidebars configuration + const sidebars = require(SIDEBARS_PATH); + + // Extract all doc IDs from all sidebars + let allSidebarIds = []; + for (const sidebarKey in sidebars) { + allSidebarIds = allSidebarIds.concat( + extractDocIds(sidebars[sidebarKey]) + ); + } + + // Get all markdown files and their IDs + const allFiles = getAllMarkdownFiles(DOCS_DIR); + const fileIdMap = new Map(); + + allFiles.forEach((file) => { + const docId = getDocId(file); + fileIdMap.set(docId, file); + }); + + console.log('\n=== Documentation Link Check ==='); + console.log(`Total documentation files: ${allFiles.length}`); + console.log(`Total sidebar entries: ${allSidebarIds.length}`); + console.log( + `Unique sidebar entries: ${new Set(allSidebarIds).size}` + ); + + // Check for duplicate sidebar entries + const duplicates = allSidebarIds.filter( + (id, index) => allSidebarIds.indexOf(id) !== index + ); + if (duplicates.length > 0) { + console.log( + `\n⚠️ Warning: Found ${duplicates.length} duplicate sidebar entry(ies):` + ); + new Set(duplicates).forEach((id) => console.log(` - ${id}`)); + } + + // Find orphaned pages (files not referenced in any sidebar) + const orphans = []; + fileIdMap.forEach((file, id) => { + if (!allSidebarIds.includes(id)) { + orphans.push({ id, file }); + } + }); + + if (orphans.length > 0) { + console.log('\n❌ ORPHANED PAGES FOUND'); + console.log( + `Found ${orphans.length} documentation page(s) not linked in any sidebar:\n` + ); + orphans.forEach(({ id, file }) => { + console.log(` ID: ${id}`); + console.log(` File: ${file}\n`); + }); + console.log( + 'Please add these pages to the appropriate sidebar in sidebars.js' + ); + console.log('or remove them if they are no longer needed.\n'); + process.exit(1); + } + + console.log('\n✓ All documentation pages are linked in sidebars!'); + + // Find sidebar entries without corresponding files + const missingFiles = []; + allSidebarIds.forEach((id) => { + if (!fileIdMap.has(id)) { + missingFiles.push(id); + } + }); + + if (missingFiles.length > 0) { + console.log('\n⚠️ Warning: Sidebar entries without files'); + console.log( + `Found ${missingFiles.length} sidebar entry(ies) without corresponding files:\n` + ); + missingFiles.forEach((id) => { + console.log(` - ${id}`); + }); + console.log( + '\nThese entries may cause broken links. Please verify.\n' + ); + } + + process.exit(0); +} + +// Run the script +if (require.main === module) { + main(); +} + +module.exports = { getDocId, extractDocIds, getAllMarkdownFiles }; diff --git a/packages/docs/site/check-orphan-pages-v2.js b/packages/docs/site/check-orphan-pages-v2.js deleted file mode 100644 index 406db450a2..0000000000 --- a/packages/docs/site/check-orphan-pages-v2.js +++ /dev/null @@ -1,154 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const docsDir = './docs'; -const sidebars = require('./sidebars.js'); - -// Function to get frontmatter from markdown file -function getFrontmatter(filePath) { - const content = fs.readFileSync(filePath, 'utf8'); - const match = content.match(/^---\s*\n([\s\S]*?)\n---/); - if (!match) return null; - - const frontmatter = {}; - const lines = match[1].split('\n'); - for (const line of lines) { - const [key, ...valueParts] = line.split(':'); - if (key && valueParts.length) { - frontmatter[key.trim()] = valueParts.join(':').trim(); - } - } - return frontmatter; -} - -// Get all markdown files recursively -function getAllMarkdownFiles(dir, baseDir = dir) { - let files = []; - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - // Skip _fragments directory - if (entry.name === '_fragments') continue; - files = files.concat(getAllMarkdownFiles(fullPath, baseDir)); - } else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) { - const relativePath = path.relative(baseDir, fullPath); - files.push(relativePath); - } - } - return files; -} - -// Function to get the doc ID for a file -function getDocId(file) { - const fullPath = path.join(docsDir, file); - const fm = getFrontmatter(fullPath); - - if (fm && fm.id) { - // If there's an explicit ID in frontmatter, use it with the directory prefix - const dir = path.dirname(file); - if (dir !== '.') { - const topLevel = dir.split(path.sep)[0]; - return `${topLevel}/${fm.id}`; - } - return fm.id; - } - - // Otherwise, use the file path as the ID (Docusaurus default behavior) - // Remove the file extension and convert to forward slashes - let id = file - .replace(/\.mdx?$/, '') - .replace(/\\/g, '/'); - - // Remove number prefixes (e.g., "01-", "23-") - id = id.replace(/\/\d+-/g, '/').replace(/^\d+-/, ''); - - // If the file is named "index", the ID is the directory path - // But if it's "intro", it might be referenced differently in the sidebar - - return id; -} - -// Function to extract all doc IDs from sidebar config -function extractDocIds(items) { - let ids = []; - for (const item of items) { - if (typeof item === 'string') { - ids.push(item); - } else if (item.type === 'category') { - if (item.link && item.link.id) { - ids.push(item.link.id); - } - if (item.items) { - ids = ids.concat(extractDocIds(item.items)); - } - } else if (item.type === 'doc' && item.id) { - ids.push(item.id); - } - } - return ids; -} - -// Get all sidebar IDs -let allSidebarIds = []; -for (const sidebarKey in sidebars) { - allSidebarIds = allSidebarIds.concat(extractDocIds(sidebars[sidebarKey])); -} - -// Get all file IDs -const allFiles = getAllMarkdownFiles(docsDir); -const fileIdMap = new Map(); // Map from doc ID to file path - -allFiles.forEach(file => { - const docId = getDocId(file); - fileIdMap.set(docId, file); -}); - -console.log('\n=== ANALYSIS ==='); -console.log(`Total markdown files: ${allFiles.length}`); -console.log(`Total sidebar entries: ${allSidebarIds.length}`); -console.log(`Unique sidebar entries: ${new Set(allSidebarIds).size}`); - -// Check if there are any duplicate sidebar entries -const duplicates = allSidebarIds.filter((id, index) => allSidebarIds.indexOf(id) !== index); -if (duplicates.length > 0) { - console.log(`\nWarning: Found ${duplicates.length} duplicate sidebar entry(ies):`); - new Set(duplicates).forEach(id => console.log(` - ${id}`)); -} - -// Find orphaned pages -const orphans = []; -fileIdMap.forEach((file, id) => { - if (!allSidebarIds.includes(id)) { - orphans.push({ id, file }); - } -}); - -if (orphans.length > 0) { - console.log('\n=== ORPHANED PAGES (NOT IN SIDEBARS) ==='); - console.log(`Found ${orphans.length} orphaned page(s):\n`); - orphans.forEach(({ id, file }) => { - console.log(` ID: ${id}`); - console.log(` File: ${file}\n`); - }); - process.exit(1); -} else { - console.log('\n✓ All pages are linked in sidebars!'); -} - -// Find sidebar entries without files -const missingFiles = []; -allSidebarIds.forEach(id => { - if (!fileIdMap.has(id)) { - missingFiles.push(id); - } -}); - -if (missingFiles.length > 0) { - console.log('\n=== SIDEBAR ENTRIES WITHOUT FILES ==='); - console.log(`Found ${missingFiles.length} sidebar entry(ies) without corresponding files:\n`); - missingFiles.forEach(id => { - console.log(` - ${id}`); - }); -} diff --git a/packages/docs/site/check-orphan-pages.js b/packages/docs/site/check-orphan-pages.js deleted file mode 100644 index 195a606599..0000000000 --- a/packages/docs/site/check-orphan-pages.js +++ /dev/null @@ -1,139 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const docsDir = './docs'; -const sidebars = require('./sidebars.js'); - -// Function to get frontmatter from markdown file -function getFrontmatter(filePath) { - const content = fs.readFileSync(filePath, 'utf8'); - const match = content.match(/^---\s*\n([\s\S]*?)\n---/); - if (!match) return null; - - const frontmatter = {}; - const lines = match[1].split('\n'); - for (const line of lines) { - const [key, ...valueParts] = line.split(':'); - if (key && valueParts.length) { - frontmatter[key.trim()] = valueParts.join(':').trim(); - } - } - return frontmatter; -} - -// Get all markdown files recursively -function getAllMarkdownFiles(dir, baseDir = dir) { - let files = []; - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - // Skip _fragments directory - if (entry.name === '_fragments') continue; - files = files.concat(getAllMarkdownFiles(fullPath, baseDir)); - } else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) { - const relativePath = path.relative(baseDir, fullPath); - files.push(relativePath); - } - } - return files; -} - -// Function to extract all doc IDs from sidebar config -function extractDocIds(items) { - let ids = []; - for (const item of items) { - if (typeof item === 'string') { - ids.push(item); - } else if (item.type === 'category') { - if (item.link && item.link.id) { - ids.push(item.link.id); - } - if (item.items) { - ids = ids.concat(extractDocIds(item.items)); - } - } else if (item.type === 'doc' && item.id) { - ids.push(item.id); - } - } - return ids; -} - -// Get all sidebar IDs -let allSidebarIds = []; -for (const sidebarKey in sidebars) { - allSidebarIds = allSidebarIds.concat(extractDocIds(sidebars[sidebarKey])); -} - -// Get all file IDs -const allFiles = getAllMarkdownFiles(docsDir); -const fileIds = new Map(); - -allFiles.forEach(file => { - const fullPath = path.join(docsDir, file); - const fm = getFrontmatter(fullPath); - let docId; - - if (fm && fm.id) { - docId = fm.id; - // Determine directory prefix - const dir = path.dirname(file); - if (dir !== '.') { - const topLevel = dir.split(path.sep)[0]; - docId = `${topLevel}/${fm.id}`; - } - } else { - // Construct default ID from path - docId = file - .replace(/\.mdx?$/, '') - .replace(/\/index$/, '') - .replace(/\\/g, '/') - // Remove number prefixes like "01-", "02-" etc - .replace(/\/\d+-/g, '/') - .replace(/^\d+-/, ''); - } - - fileIds.set(docId, file); -}); - -console.log('\n=== ANALYSIS ==='); -console.log(`Total markdown files: ${allFiles.length}`); -console.log(`Total sidebar entries: ${allSidebarIds.length}`); -console.log(`Unique sidebar entries: ${new Set(allSidebarIds).size}`); - -// Find orphaned pages -const orphans = []; -fileIds.forEach((file, id) => { - if (!allSidebarIds.includes(id)) { - orphans.push({ id, file }); - } -}); - -if (orphans.length > 0) { - console.log('\n=== ORPHANED PAGES ==='); - console.log(`Found ${orphans.length} orphaned page(s):\n`); - orphans.forEach(({ id, file }) => { - console.log(` - ${id}`); - console.log(` File: ${file}`); - }); - process.exit(1); -} else { - console.log('\n✓ No orphaned pages found!'); -} - -// Find sidebar entries without files -const missingFiles = []; -allSidebarIds.forEach(id => { - if (!fileIds.has(id)) { - missingFiles.push(id); - } -}); - -if (missingFiles.length > 0) { - console.log('\n=== SIDEBAR ENTRIES WITHOUT FILES ==='); - console.log(`Found ${missingFiles.length} sidebar entry(ies) without corresponding files:\n`); - missingFiles.forEach(id => { - console.log(` - ${id}`); - }); -} diff --git a/packages/docs/site/check-orphans.js b/packages/docs/site/check-orphans.js deleted file mode 100644 index ad4aaaeccb..0000000000 --- a/packages/docs/site/check-orphans.js +++ /dev/null @@ -1,34 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -// Load the sidebars config -const sidebars = require('./sidebars.js'); - -// Function to extract all doc IDs from sidebar config -function extractDocIds(items) { - let ids = []; - for (const item of items) { - if (typeof item === 'string') { - ids.push(item); - } else if (item.type === 'category') { - if (item.link && item.link.id) { - ids.push(item.link.id); - } - if (item.items) { - ids = ids.concat(extractDocIds(item.items)); - } - } else if (item.type === 'doc' && item.id) { - ids.push(item.id); - } - } - return ids; -} - -// Get all sidebar IDs -let allSidebarIds = []; -for (const sidebarKey in sidebars) { - allSidebarIds = allSidebarIds.concat(extractDocIds(sidebars[sidebarKey])); -} - -console.log('Doc IDs in sidebars:', allSidebarIds.length); -console.log('Sidebar IDs:', JSON.stringify(allSidebarIds, null, 2)); diff --git a/packages/docs/site/package.json b/packages/docs/site/package.json index 6b205e8d5f..2e6ddad838 100644 --- a/packages/docs/site/package.json +++ b/packages/docs/site/package.json @@ -13,7 +13,8 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc", - "dev": "docusaurus start" + "dev": "docusaurus start", + "check-orphan-pages": "node bin/check-orphan-pages.js" }, "browserslist": { "production": [ diff --git a/packages/docs/site/project.json b/packages/docs/site/project.json index 7c207be176..c944f9cba4 100644 --- a/packages/docs/site/project.json +++ b/packages/docs/site/project.json @@ -87,6 +87,13 @@ "lintFilePatterns": ["packages/docs/site/**/*.ts"], "maxWarnings": 0 } + }, + "check-orphan-pages": { + "executor": "nx:run-commands", + "options": { + "commands": ["node bin/check-orphan-pages.js"], + "cwd": "packages/docs/site" + } } }, "tags": [] diff --git a/packages/docs/site/sidebars.js b/packages/docs/site/sidebars.js index 04ea0a6bb7..20d9c959ed 100644 --- a/packages/docs/site/sidebars.js +++ b/packages/docs/site/sidebars.js @@ -137,6 +137,7 @@ const sidebars = { id: 'developers/local-development/intro', }, items: [ + 'developers/local-development/wp-now', 'developers/local-development/wp-playground-cli', 'developers/local-development/vscode-extension', 'developers/local-development/php-wasm-node', @@ -150,7 +151,6 @@ const sidebars = { id: 'developers/xdebug/introduction', }, items: [ - 'developers/xdebug/introduction', 'developers/xdebug/getting-started', ], }, From 78525d139f5ce5acfe4d0475367cba0ac96c47ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 22:58:34 +0000 Subject: [PATCH 4/6] Add orphaned test page to demonstrate CI check failure This intentionally orphaned page validates that the check-orphan-pages.js script correctly identifies pages not linked in any sidebar. Co-authored-by: adamziel <205419+adamziel@users.noreply.github.com> --- .../docs/site/docs/main/orphaned-test-page.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/docs/site/docs/main/orphaned-test-page.md diff --git a/packages/docs/site/docs/main/orphaned-test-page.md b/packages/docs/site/docs/main/orphaned-test-page.md new file mode 100644 index 0000000000..ff5cab5d6c --- /dev/null +++ b/packages/docs/site/docs/main/orphaned-test-page.md @@ -0,0 +1,25 @@ +--- +title: Orphaned Test Page +description: This is an intentionally orphaned page to test the CI check +--- + +# Orphaned Test Page + +This page is intentionally not linked in any sidebar menu to demonstrate that the orphan page detection check works correctly. + +When this PR is merged, this page should be removed or linked to a sidebar. + +## Purpose + +This page exists to validate that: +1. The `check-orphan-pages.js` script correctly identifies orphaned pages +2. The CI check fails when orphaned pages are present +3. The error message is clear and actionable + +## Expected Behavior + +Running `npx nx check-orphan-pages docs-site` should: +- Exit with code 1 +- Report this page as orphaned +- Show the file path: `main/orphaned-test-page.md` +- Show the doc ID: `main/orphaned-test-page` From bcd893b9c34c4b1fab520113c1fd68b55f3b08b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 18 Nov 2025 12:05:23 +0100 Subject: [PATCH 5/6] Delete packages/docs/site/docs/main/orphaned-test-page.md --- .../docs/site/docs/main/orphaned-test-page.md | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 packages/docs/site/docs/main/orphaned-test-page.md diff --git a/packages/docs/site/docs/main/orphaned-test-page.md b/packages/docs/site/docs/main/orphaned-test-page.md deleted file mode 100644 index ff5cab5d6c..0000000000 --- a/packages/docs/site/docs/main/orphaned-test-page.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Orphaned Test Page -description: This is an intentionally orphaned page to test the CI check ---- - -# Orphaned Test Page - -This page is intentionally not linked in any sidebar menu to demonstrate that the orphan page detection check works correctly. - -When this PR is merged, this page should be removed or linked to a sidebar. - -## Purpose - -This page exists to validate that: -1. The `check-orphan-pages.js` script correctly identifies orphaned pages -2. The CI check fails when orphaned pages are present -3. The error message is clear and actionable - -## Expected Behavior - -Running `npx nx check-orphan-pages docs-site` should: -- Exit with code 1 -- Report this page as orphaned -- Show the file path: `main/orphaned-test-page.md` -- Show the doc ID: `main/orphaned-test-page` From 6c83ef565a096cc5c04f379d626d7ad33df11ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 18 Nov 2025 12:33:53 +0100 Subject: [PATCH 6/6] Allow intentionally orphaned pages --- packages/docs/site/bin/check-orphan-pages.js | 79 +++++++++++++++---- .../05-local-development/01-wp-now.md | 1 + packages/docs/site/sidebars.js | 2 +- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/packages/docs/site/bin/check-orphan-pages.js b/packages/docs/site/bin/check-orphan-pages.js index a9dd30c31a..9a8b2c27dc 100755 --- a/packages/docs/site/bin/check-orphan-pages.js +++ b/packages/docs/site/bin/check-orphan-pages.js @@ -2,11 +2,11 @@ /** * Check for orphaned documentation pages - * + * * This script verifies that all Docusaurus documentation pages are linked * in at least one sidebar menu. Orphaned pages (pages not linked anywhere) * may be forgotten or unintentionally excluded from the documentation. - * + * * Exit codes: * - 0: Success, no orphaned pages found * - 1: Failure, orphaned pages were found @@ -19,6 +19,7 @@ const SCRIPT_DIR = __dirname; const SITE_DIR = path.join(SCRIPT_DIR, '..'); const DOCS_DIR = path.join(SITE_DIR, 'docs'); const SIDEBARS_PATH = path.join(SITE_DIR, 'sidebars.js'); +const INTENTIONAL_ORPHAN_FLAG = 'orphan'; /** * Extract frontmatter from a markdown file @@ -33,7 +34,14 @@ function getFrontmatter(filePath) { for (const line of lines) { const [key, ...valueParts] = line.split(':'); if (key && valueParts.length) { - frontmatter[key.trim()] = valueParts.join(':').trim(); + const rawValue = valueParts.join(':').trim(); + let parsedValue = rawValue; + if (/^(true|false)$/i.test(rawValue)) { + parsedValue = rawValue.toLowerCase() === 'true'; + } else if (/^['\"].*['\"]$/.test(rawValue)) { + parsedValue = rawValue.slice(1, -1); + } + frontmatter[key.trim()] = parsedValue; } } return frontmatter; @@ -66,9 +74,9 @@ function getAllMarkdownFiles(dir, baseDir = dir) { * - If frontmatter has an 'id' field, use it (prefixed with parent directory path) * - Otherwise, derive from file path (without extension, with number prefixes removed) */ -function getDocId(file) { +function getDocId(file, frontmatter = null) { const fullPath = path.join(DOCS_DIR, file); - const fm = getFrontmatter(fullPath); + const fm = frontmatter ?? getFrontmatter(fullPath); if (fm && fm.id) { // If there's an explicit ID in frontmatter, use it with the directory prefix @@ -92,6 +100,24 @@ function getDocId(file) { return id; } +/** + * Determine if a document has opted-in to being an intentional orphan + */ +function isIntentionallyOrphaned(frontmatter) { + if (!frontmatter || typeof frontmatter !== 'object') { + return false; + } + + const flagValue = frontmatter[INTENTIONAL_ORPHAN_FLAG]; + if (typeof flagValue === 'boolean') { + return flagValue; + } + if (typeof flagValue === 'string') { + return flagValue.toLowerCase() === 'true'; + } + return false; +} + /** * Recursively extract all document IDs referenced in sidebar configuration */ @@ -138,16 +164,18 @@ function main() { const fileIdMap = new Map(); allFiles.forEach((file) => { - const docId = getDocId(file); - fileIdMap.set(docId, file); + const fullPath = path.join(DOCS_DIR, file); + const frontmatter = getFrontmatter(fullPath) || {}; + const docId = getDocId(file, frontmatter); + const allowsOrphan = isIntentionallyOrphaned(frontmatter); + + fileIdMap.set(docId, { file, allowsOrphan }); }); console.log('\n=== Documentation Link Check ==='); console.log(`Total documentation files: ${allFiles.length}`); console.log(`Total sidebar entries: ${allSidebarIds.length}`); - console.log( - `Unique sidebar entries: ${new Set(allSidebarIds).size}` - ); + console.log(`Unique sidebar entries: ${new Set(allSidebarIds).size}`); // Check for duplicate sidebar entries const duplicates = allSidebarIds.filter( @@ -162,12 +190,27 @@ function main() { // Find orphaned pages (files not referenced in any sidebar) const orphans = []; - fileIdMap.forEach((file, id) => { + const intentionallyOrphaned = []; + fileIdMap.forEach(({ file, allowsOrphan }, id) => { if (!allSidebarIds.includes(id)) { - orphans.push({ id, file }); + if (allowsOrphan) { + intentionallyOrphaned.push({ id, file }); + } else { + orphans.push({ id, file }); + } } }); + if (intentionallyOrphaned.length > 0) { + console.log( + `\nℹ️ ${intentionallyOrphaned.length} intentionally orphaned documentation page(s) (marked with 'orphan: true'):` + ); + intentionallyOrphaned.forEach(({ id, file }) => { + console.log(` ID: ${id}`); + console.log(` File: ${file}\n`); + }); + } + if (orphans.length > 0) { console.log('\n❌ ORPHANED PAGES FOUND'); console.log( @@ -180,11 +223,15 @@ function main() { console.log( 'Please add these pages to the appropriate sidebar in sidebars.js' ); - console.log('or remove them if they are no longer needed.\n'); + console.log( + "or remove them if they are no longer needed. If a page should stay unlisted, add 'orphan: true' to its frontmatter.\n" + ); process.exit(1); } - console.log('\n✓ All documentation pages are linked in sidebars!'); + console.log( + '\n✓ All documentation pages are linked in sidebars or intentionally marked as orphaned!' + ); // Find sidebar entries without corresponding files const missingFiles = []; @@ -202,9 +249,7 @@ function main() { missingFiles.forEach((id) => { console.log(` - ${id}`); }); - console.log( - '\nThese entries may cause broken links. Please verify.\n' - ); + console.log('\nThese entries may cause broken links. Please verify.\n'); } process.exit(0); diff --git a/packages/docs/site/docs/developers/05-local-development/01-wp-now.md b/packages/docs/site/docs/developers/05-local-development/01-wp-now.md index 5fae30ec1f..39da7ee495 100644 --- a/packages/docs/site/docs/developers/05-local-development/01-wp-now.md +++ b/packages/docs/site/docs/developers/05-local-development/01-wp-now.md @@ -1,6 +1,7 @@ --- title: wp-now slug: /developers/local-development/wp-now +orphan: true --- :::caution Package deprecated diff --git a/packages/docs/site/sidebars.js b/packages/docs/site/sidebars.js index 20d9c959ed..04ea0a6bb7 100644 --- a/packages/docs/site/sidebars.js +++ b/packages/docs/site/sidebars.js @@ -137,7 +137,6 @@ const sidebars = { id: 'developers/local-development/intro', }, items: [ - 'developers/local-development/wp-now', 'developers/local-development/wp-playground-cli', 'developers/local-development/vscode-extension', 'developers/local-development/php-wasm-node', @@ -151,6 +150,7 @@ const sidebars = { id: 'developers/xdebug/introduction', }, items: [ + 'developers/xdebug/introduction', 'developers/xdebug/getting-started', ], },