Skip to content

Commit 8d105c6

Browse files
authored
Merge pull request #9 from hoxxep/trailing-slash
Error if invalid trailing slash
2 parents 48ecc71 + ad1e72c commit 8d105c6

File tree

14 files changed

+1936
-2254
lines changed

14 files changed

+1936
-2254
lines changed

.editorconfig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_size = 2
6+
indent_style = space
7+
end_of_line = lf
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
max_line_length = 100
11+
12+
[*.{yaml,yml,json,tf,tfvars}]
13+
indent_size = 2

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,5 @@ dist
131131
node_modules/
132132
dist/
133133
broken-links.log
134-
tests/broken-links.log
134+
tests/broken-links.log
135+
.idea/

check-links.js

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { parse } from 'node-html-parser';
1+
import {parse} from 'node-html-parser';
22
import fs from 'fs';
33
import fetch from 'node-fetch';
4-
import { URL, fileURLToPath } from 'url';
4+
import {URL, fileURLToPath} from 'url';
55
import path from 'path';
66
import pLimit from 'p-limit';
77

@@ -14,7 +14,8 @@ export async function checkLinksInHtml(
1414
distPath = '',
1515
astroConfigRedirects = {},
1616
logger,
17-
checkExternalLinks = true
17+
checkExternalLinks = true,
18+
trailingSlash = 'ignore',
1819
) {
1920
const root = parse(htmlContent);
2021
const linkElements = root.querySelectorAll('a[href]');
@@ -34,7 +35,6 @@ export async function checkLinksInHtml(
3435

3536
let absoluteLink;
3637
try {
37-
3838
// Differentiate between absolute, domain-relative, and relative links
3939
if (/^https?:\/\//i.test(link) || /^:\/\//i.test(link)) {
4040
// Absolute URL
@@ -73,7 +73,6 @@ export async function checkLinksInHtml(
7373
}
7474

7575
let isBroken = false;
76-
7776

7877
if (fetchLink.startsWith('/') && distPath) {
7978
// Internal link in build mode, check if file exists
@@ -89,35 +88,41 @@ export async function checkLinksInHtml(
8988
if (!possiblePaths.some((p) => fs.existsSync(p))) {
9089
// console.log('Failed paths', possiblePaths);
9190
isBroken = true;
92-
// Fall back to checking a redirect file if it exists.
91+
// Fall back to checking a redirect file if it exists.
92+
}
9393

94+
// check trailing slash is correct on internal links
95+
const re = /\/$|\.[a-z0-9]+$/; // match trailing slash or file extension
96+
if (trailingSlash === 'always' && !fetchLink.match(re)) {
97+
isBroken = true;
98+
} else if (trailingSlash === 'never' && fetchLink !== '/' && fetchLink.endsWith('/')) {
99+
isBroken = true;
94100
}
95-
} else {
101+
} else {
96102
// External link, check via HTTP request. Retry 3 times if ECONNRESET
97103
if (checkExternalLinks) {
98104
let retries = 0;
99105
while (retries < 3) {
100106
try {
101-
const response = await fetch(fetchLink, { method: 'GET' });
107+
const response = await fetch(fetchLink, {method: 'GET'});
102108
isBroken = !response.ok;
103109
if (isBroken) {
104110
logger.error(`${response.status} Error fetching ${fetchLink}`);
105111
}
106112
break;
107113
} catch (error) {
108114
isBroken = true;
109-
let statusCodeNumber = error.errno == 'ENOTFOUND' ? 404 : (error.errno);
115+
let statusCodeNumber = error.errno === 'ENOTFOUND' ? 404 : (error.errno);
110116
logger.error(`${statusCodeNumber} error fetching ${fetchLink}`);
111117
if (error.errno === 'ECONNRESET') {
112118
retries++;
113119
continue;
114120
}
115121
break;
116-
}
117122
}
118123
}
119124
}
120-
125+
}
121126

122127
// Cache the link's validity
123128
checkedLinks.set(fetchLink, !isBroken);
@@ -134,16 +139,13 @@ export async function checkLinksInHtml(
134139

135140
function isValidUrl(url) {
136141
// Skip mailto:, tel:, javascript:, and empty links
137-
if (
142+
return !(
138143
url.startsWith('mailto:') ||
139144
url.startsWith('tel:') ||
140145
url.startsWith('javascript:') ||
141146
url.startsWith('#') ||
142147
url.trim() === ''
143-
) {
144-
return false;
145-
}
146-
return true;
148+
);
147149
}
148150

149151
function normalizePath(p) {
@@ -177,9 +179,8 @@ function addBrokenLink(brokenLinksMap, documentPath, brokenLink, distPath) {
177179
// Normalize broken link for reporting
178180
let normalizedBrokenLink = brokenLink;
179181

180-
181182
if (!brokenLinksMap.has(normalizedBrokenLink)) {
182183
brokenLinksMap.set(normalizedBrokenLink, new Set());
183184
}
184185
brokenLinksMap.get(normalizedBrokenLink).add(documentPath);
185-
}
186+
}

index.js

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { fileURLToPath } from 'url';
2-
import { join } from 'path';
1+
import {fileURLToPath} from 'url';
2+
import {join} from 'path';
33
import fs from 'fs';
4-
import { checkLinksInHtml , normalizeHtmlFilePath } from './check-links.js';
4+
import {checkLinksInHtml, normalizeHtmlFilePath} from './check-links.js';
55
import fastGlob from 'fast-glob';
66

77
export default function astroBrokenLinksChecker(options = {}) {
@@ -10,21 +10,22 @@ export default function astroBrokenLinksChecker(options = {}) {
1010
const checkedLinks = new Map();
1111

1212
return {
13-
1413
name: 'astro-broken-links-checker',
1514
hooks: {
16-
'astro:config:setup': async ({ config }) => {
15+
'astro:config:setup': async ({config}) => {
1716
//console.log('config.redirects', config.redirects);
1817
// save the redirects to the options
1918
options.astroConfigRedirects = config.redirects;
19+
20+
// use astro trailingSlash setting, falling back to astro default of 'ignore'
21+
options.trailingSlash = config.trailingSlash || 'ignore';
2022
},
21-
22-
'astro:build:done': async ({ dir, logger }) => {
2323

24+
'astro:build:done': async ({dir, logger}) => {
2425
const astroConfigRedirects = options.astroConfigRedirects;
2526
//console.log('astroConfigRedirects', astroConfigRedirects);
2627
const distPath = fileURLToPath(dir);
27-
const htmlFiles = await fastGlob('**/*.html', { cwd: distPath });
28+
const htmlFiles = await fastGlob('**/*.html', {cwd: distPath});
2829
logger.info(`Checking ${htmlFiles.length} html pages for broken links`);
2930
// start time
3031
const startTime = Date.now();
@@ -41,14 +42,22 @@ export default function astroBrokenLinksChecker(options = {}) {
4142
distPath,
4243
astroConfigRedirects,
4344
logger,
44-
options.checkExternalLinks
45+
options.checkExternalLinks,
46+
options.trailingSlash,
4547
);
4648
});
49+
4750
await Promise.all(checkHtmlPromises);
4851
logBrokenLinks(brokenLinksMap, logFilePath, logger);
52+
4953
// end time
5054
const endTime = Date.now();
5155
logger.info(`Time to check links: ${endTime - startTime} ms`);
56+
57+
// stop the build if we have broken links and the option is set
58+
if (options.throwError && brokenLinksMap.size > 0) {
59+
throw new Error(`Broken links detected. Check the log file: ${logFilePath}`);
60+
}
5261
},
5362
},
5463
};
@@ -74,5 +83,9 @@ function logBrokenLinks(brokenLinksMap, logFilePath, logger) {
7483
}
7584
} else {
7685
logger.info('No broken links detected.');
86+
if (fs.existsSync(logFilePath)) {
87+
logger.info('Removing old log file:', logFilePath);
88+
fs.rmSync(logFilePath);
89+
}
7790
}
78-
}
91+
}

0 commit comments

Comments
 (0)