1- import { parse } from 'node-html-parser' ;
1+ import { parse } from 'node-html-parser' ;
22import fs from 'fs' ;
33import fetch from 'node-fetch' ;
4- import { URL , fileURLToPath } from 'url' ;
4+ import { URL , fileURLToPath } from 'url' ;
55import path from 'path' ;
66import 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 ( / ^ h t t p s ? : \/ \/ / 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 - z 0 - 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
135140function 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
149151function 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+ }
0 commit comments