1
Files
2025/tools/check_internal_links.js

103 lines
2.7 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const http = require("http");
const readline = require("readline");
const BASE_URL = "http://127.0.0.1:1313";
const CONTENT_DIR = path.join(__dirname, "..", "content");
const SITE_ROOT = path.resolve(__dirname, "..");
const BAD_LINKS = [];
function isInternalLink(link) {
return !link.includes("://") && !link.startsWith("mailto:") && !link.startsWith("tel:");
}
function extractLinksFromLine(line) {
const regex = /\]\(([^)"]+)\)/g;
let match;
const links = [];
while ((match = regex.exec(line)) !== null) {
links.push(match[1]);
}
return links;
}
function getBundleRelativeUrl(mdPath, link) {
const bundleRoot = path.dirname(mdPath);
let urlPath;
if (link.startsWith("/")) {
urlPath = link;
} else {
const fullPath = path.resolve(bundleRoot, link);
const relative = path.relative(CONTENT_DIR, fullPath);
urlPath = "/" + relative.replace(/\\/g, "/");
}
return urlPath;
}
async function checkLink(file, lineNumber, link) {
const relativeUrl = getBundleRelativeUrl(file, link);
const fullUrl = `${BASE_URL}${relativeUrl}`;
return new Promise((resolve) => {
http.get(fullUrl, (res) => {
if (res.statusCode !== 200) {
BAD_LINKS.push([path.relative(SITE_ROOT, file), link, lineNumber]);
}
res.resume();
resolve();
}).on("error", () => {
BAD_LINKS.push([path.relative(SITE_ROOT, file), link, lineNumber]);
resolve();
});
});
}
async function processFile(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
let lineNumber = 0;
for await (const line of rl) {
lineNumber++;
const links = extractLinksFromLine(line);
for (const link of links) {
if (isInternalLink(link)) {
process.stdout.write(".");
await checkLink(filePath, lineNumber, link);
}
}
}
}
function walk(dir) {
let results = [];
const list = fs.readdirSync(dir);
list.forEach((file) => {
file = path.resolve(dir, file);
const stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
results = results.concat(walk(file));
} else if (file.endsWith(".md")) {
results.push(file);
}
});
return results;
}
(async () => {
const files = walk(CONTENT_DIR);
console.log(`Analyzing ${files.length} Markdown files...`);
for (const file of files) {
await processFile(file);
}
console.log("\n\n=== Broken Internal Links Report ===");
if (BAD_LINKS.length === 0) {
console.log("✅ No broken internal links found.");
} else {
console.table(BAD_LINKS.map(([f, u, l]) => ({ File: f + '#' + l, URL: u })));
}
})();