Skip to content

Commit 76a9fde

Browse files
authored
Merge pull request #8248 from cakephp/feature/toc-link-validation
Add CI validation for TOC sidebar links
2 parents 6ad2baf + 9334a7c commit 76a9fde

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

.github/workflows/docs-validation.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ jobs:
4848
echo "✅ Valid: $file"
4949
done
5050
51+
toc-link-check:
52+
name: Validate TOC Links
53+
runs-on: ubuntu-latest
54+
55+
steps:
56+
- name: Checkout code
57+
uses: actions/checkout@v6
58+
59+
- name: Check TOC links exist
60+
run: node bin/check-toc-links.js
61+
5162
markdown-lint:
5263
name: Lint Markdown
5364
runs-on: ubuntu-latest

bin/check-toc-links.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* TOC Link Validator
5+
*
6+
* Validates that all links in toc_*.json files point to existing markdown files.
7+
*
8+
* Usage:
9+
* node bin/check-toc-links.js
10+
*/
11+
12+
const fs = require("fs");
13+
const path = require("path");
14+
15+
/**
16+
* Recursively extract all links from TOC items
17+
*/
18+
function extractLinks(items, links = []) {
19+
for (const item of items) {
20+
if (item.link && !item.link.startsWith("http")) {
21+
links.push(item.link);
22+
}
23+
if (item.items) {
24+
extractLinks(item.items, links);
25+
}
26+
}
27+
return links;
28+
}
29+
30+
/**
31+
* Check if a TOC link resolves to an existing file
32+
*/
33+
function checkLink(link, docsDir, lang) {
34+
// TOC links may include language prefix: "/ja/quickstart" or just "/quickstart"
35+
// Strip the language prefix if present
36+
let relativePath = link.startsWith("/") ? link.slice(1) : link;
37+
38+
// Remove language prefix if link starts with it (e.g., "ja/quickstart" -> "quickstart")
39+
const langPrefix = lang + "/";
40+
if (relativePath.startsWith(langPrefix)) {
41+
relativePath = relativePath.slice(langPrefix.length);
42+
}
43+
44+
const filePath = path.join(docsDir, relativePath + ".md");
45+
46+
return fs.existsSync(filePath);
47+
}
48+
49+
/**
50+
* Extract language code from TOC filename
51+
* e.g., "toc_en.json" -> "en"
52+
*/
53+
function getLangFromTocFile(tocFile) {
54+
const match = tocFile.match(/^toc_(\w+)\.json$/);
55+
return match ? match[1] : null;
56+
}
57+
58+
/**
59+
* Main validation function
60+
*/
61+
function validateTocFiles() {
62+
const tocFiles = fs.readdirSync(".").filter((f) => f.match(/^toc_.*\.json$/));
63+
64+
if (tocFiles.length === 0) {
65+
console.error("No toc_*.json files found");
66+
process.exit(1);
67+
}
68+
69+
let hasErrors = false;
70+
71+
for (const tocFile of tocFiles) {
72+
const lang = getLangFromTocFile(tocFile);
73+
if (!lang) {
74+
console.error(`Could not extract language from ${tocFile}`);
75+
continue;
76+
}
77+
78+
const docsDir = path.join("docs", lang);
79+
if (!fs.existsSync(docsDir)) {
80+
console.error(`Docs directory not found: ${docsDir}`);
81+
hasErrors = true;
82+
continue;
83+
}
84+
85+
console.log(`Checking ${tocFile} against ${docsDir}/...`);
86+
87+
const content = fs.readFileSync(tocFile, "utf8");
88+
const toc = JSON.parse(content);
89+
90+
// TOC structure has keys like "/" with arrays of items
91+
const allLinks = [];
92+
for (const key of Object.keys(toc)) {
93+
extractLinks(toc[key], allLinks);
94+
}
95+
96+
const missingFiles = [];
97+
for (const link of allLinks) {
98+
if (!checkLink(link, docsDir, lang)) {
99+
missingFiles.push(link);
100+
}
101+
}
102+
103+
if (missingFiles.length > 0) {
104+
hasErrors = true;
105+
console.error(`\n✗ ${tocFile}: ${missingFiles.length} broken link(s)\n`);
106+
for (const link of missingFiles) {
107+
// Calculate expected path (strip lang prefix if present)
108+
let relativePath = link.startsWith("/") ? link.slice(1) : link;
109+
const langPrefix = lang + "/";
110+
if (relativePath.startsWith(langPrefix)) {
111+
relativePath = relativePath.slice(langPrefix.length);
112+
}
113+
const expectedPath = path.join(docsDir, relativePath + ".md");
114+
console.error(` ${link}`);
115+
console.error(` Expected: ${expectedPath}`);
116+
}
117+
console.error("");
118+
} else {
119+
console.log(`✓ ${tocFile}: ${allLinks.length} link(s) valid\n`);
120+
}
121+
}
122+
123+
if (hasErrors) {
124+
console.error("TOC validation failed");
125+
process.exit(1);
126+
}
127+
128+
console.log("✓ All TOC links are valid!");
129+
}
130+
131+
validateTocFiles();

0 commit comments

Comments
 (0)