Skip to content

Commit 0fd3f42

Browse files
committed
Feature: script to detect invalid resource links
1 parent c00bad7 commit 0fd3f42

4 files changed

Lines changed: 202 additions & 2 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ you're welcome at [https://suchdevblog.com/](https://suchdevblog.com/)
88

99
`pnpm docs:dev`
1010

11+
## Build
12+
13+
`pnpm docs:build`
14+
15+
## Link Validation
16+
17+
`pnpm validate-links` - Checks and removes invalid links from resources folder
18+
1119
## Package manager
1220

1321
pnpm

docs/resources/BeginnersResources.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ A definitive guide.
105105

106106
### Why use Single-Page-Application JavaScript Frameworks ?
107107

108-
[Link](https://itnext.io/reddits-voting-ui-in-vanilla-vs-react-vs-vue-vs-hyperapp-shedding-light-on-the-purpose-of-spa-ee6b6ac9a8cc)
108+
109109

110110
The answer with a very detailed study of the Reddit up/downvote button.
111111

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"main": "index.js",
66
"scripts": {
77
"docs:dev": "vuepress dev docs",
8-
"docs:build": "vuepress build docs"
8+
"docs:build": "vuepress build docs",
9+
"validate-links": "node scripts/validate-links.js"
910
},
1011
"keywords": [],
1112
"author": "",

scripts/validate-links.js

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const https = require('https');
6+
const http = require('http');
7+
const { URL } = require('url');
8+
9+
const RESOURCES_DIR = path.join(__dirname, '../docs/resources');
10+
const TIMEOUT_MS = 10000;
11+
const USER_AGENT = 'Mozilla/5.0 (compatible; LinkChecker/1.0)';
12+
13+
class LinkValidator {
14+
constructor() {
15+
this.validLinks = [];
16+
this.invalidLinks = [];
17+
this.removedLinks = [];
18+
}
19+
20+
async validateUrl(url) {
21+
return new Promise((resolve) => {
22+
try {
23+
const urlObj = new URL(url);
24+
const protocol = urlObj.protocol === 'https:' ? https : http;
25+
26+
const options = {
27+
hostname: urlObj.hostname,
28+
port: urlObj.port,
29+
path: urlObj.pathname + urlObj.search,
30+
method: 'HEAD',
31+
headers: {
32+
'User-Agent': USER_AGENT,
33+
'Accept': '*/*'
34+
},
35+
timeout: TIMEOUT_MS
36+
};
37+
38+
const req = protocol.request(options, (res) => {
39+
const statusCode = res.statusCode;
40+
if (statusCode >= 200 && statusCode < 400) {
41+
resolve({ valid: true, status: statusCode });
42+
} else if (statusCode >= 300 && statusCode < 400 && res.headers.location) {
43+
// Follow redirects manually for HEAD requests
44+
this.validateUrl(res.headers.location).then(resolve);
45+
} else {
46+
resolve({ valid: false, status: statusCode, error: `HTTP ${statusCode}` });
47+
}
48+
});
49+
50+
req.on('error', (error) => {
51+
resolve({ valid: false, error: error.message });
52+
});
53+
54+
req.on('timeout', () => {
55+
req.destroy();
56+
resolve({ valid: false, error: 'Request timeout' });
57+
});
58+
59+
req.setTimeout(TIMEOUT_MS);
60+
req.end();
61+
} catch (error) {
62+
resolve({ valid: false, error: error.message });
63+
}
64+
});
65+
}
66+
67+
extractLinksFromMarkdown(content) {
68+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
69+
const links = [];
70+
let match;
71+
72+
while ((match = linkRegex.exec(content)) !== null) {
73+
const text = match[1];
74+
const url = match[2];
75+
const fullMatch = match[0];
76+
77+
// Only process external URLs (not internal links)
78+
if (url.startsWith('http://') || url.startsWith('https://')) {
79+
links.push({
80+
text,
81+
url,
82+
fullMatch,
83+
index: match.index
84+
});
85+
}
86+
}
87+
return links;
88+
}
89+
90+
async processFile(filePath) {
91+
console.log(`\n📄 Processing: ${path.basename(filePath)}`);
92+
93+
const content = fs.readFileSync(filePath, 'utf8');
94+
const links = this.extractLinksFromMarkdown(content);
95+
96+
if (links.length === 0) {
97+
console.log(' No external links found');
98+
return;
99+
}
100+
101+
let modifiedContent = content;
102+
let hasChanges = false;
103+
104+
// Process links in reverse order to maintain correct indices when removing
105+
for (let i = links.length - 1; i >= 0; i--) {
106+
const link = links[i];
107+
process.stdout.write(` 🔍 Checking: ${link.url}... `);
108+
109+
const result = await this.validateUrl(link.url);
110+
111+
if (result.valid) {
112+
console.log(`✅ Valid (${result.status || 'OK'})`);
113+
this.validLinks.push({ file: filePath, ...link, status: result.status });
114+
} else {
115+
console.log(`❌ Invalid (${result.error})`);
116+
this.invalidLinks.push({ file: filePath, ...link, error: result.error });
117+
118+
// Remove the invalid link from content
119+
const beforeLink = modifiedContent.substring(0, link.index);
120+
const afterLink = modifiedContent.substring(link.index + link.fullMatch.length);
121+
modifiedContent = beforeLink + afterLink;
122+
hasChanges = true;
123+
124+
this.removedLinks.push({
125+
file: path.basename(filePath),
126+
text: link.text,
127+
url: link.url,
128+
error: result.error
129+
});
130+
131+
console.log(` 🗑️ Removed invalid link: "${link.text}"`);
132+
}
133+
}
134+
135+
// Write back the modified content if there were changes
136+
if (hasChanges) {
137+
fs.writeFileSync(filePath, modifiedContent, 'utf8');
138+
console.log(` 💾 File updated with ${this.removedLinks.filter(r => r.file === path.basename(filePath)).length} link(s) removed`);
139+
}
140+
}
141+
142+
async validateAllResources() {
143+
console.log('🔗 Link Validator for Resources Folder');
144+
console.log('=====================================\n');
145+
146+
const files = fs.readdirSync(RESOURCES_DIR)
147+
.filter(file => file.endsWith('.md'))
148+
.map(file => path.join(RESOURCES_DIR, file));
149+
150+
if (files.length === 0) {
151+
console.log('No markdown files found in resources directory');
152+
return;
153+
}
154+
155+
for (const file of files) {
156+
await this.processFile(file);
157+
}
158+
159+
this.printSummary();
160+
}
161+
162+
printSummary() {
163+
console.log('\n📊 SUMMARY');
164+
console.log('===========');
165+
console.log(`✅ Valid links: ${this.validLinks.length}`);
166+
console.log(`❌ Invalid links: ${this.invalidLinks.length}`);
167+
console.log(`🗑️ Links removed: ${this.removedLinks.length}`);
168+
169+
if (this.removedLinks.length > 0) {
170+
console.log('\n🗑️ REMOVED LINKS:');
171+
this.removedLinks.forEach(link => {
172+
console.log(` ${link.file}: "${link.text}" (${link.url})`);
173+
console.log(` Reason: ${link.error}`);
174+
});
175+
}
176+
177+
if (this.invalidLinks.length === 0) {
178+
console.log('\n🎉 All links are valid!');
179+
} else {
180+
console.log('\n⚠️ Invalid links have been removed from the files.');
181+
}
182+
}
183+
}
184+
185+
// Run the validator
186+
if (require.main === module) {
187+
const validator = new LinkValidator();
188+
validator.validateAllResources().catch(console.error);
189+
}
190+
191+
module.exports = LinkValidator;

0 commit comments

Comments
 (0)