9898</ style >
9999
100100< div class ="error-container ">
101- < img src ="/assets/images/hopeful404.png " alt ="Friendly 404 Error " class ="error-image " onerror ="this.src='/assets/images/404_PageNotFound.jpeg' ">
102-
103101 < div class ="trailing-slash-notice " id ="trailingSlashNotice ">
104102 < h1 class ="error-title "> Almost There!</ h1 >
105103 < p class ="error-message ">
106- It looks like you almost typed the correct address. The URL you tried has a trailing slash,
107- which makes a difference in how our site routes pages.
104+ It looks like you almost typed the correct address.
105+ < span id =" nearMatchReason " > </ span >
108106 </ p >
109107 < p class ="error-message ">
110108 < strong > What you tried:</ strong > < code id ="attemptedUrl "> </ code > < br >
@@ -118,6 +116,8 @@ <h1 class="error-title">Almost There!</h1>
118116 </ div >
119117 </ div >
120118
119+ < img src ="/assets/images/hopeful404.png " alt ="Friendly 404 Error " class ="error-image ">
120+
121121 < div class ="standard-404 " id ="standard404 ">
122122 < h1 class ="error-title "> Page Not Found</ h1 >
123123 < p class ="error-message ">
@@ -168,34 +168,126 @@ <h2>Helpful Links</h2>
168168 '/projects/all' ,
169169 '/public-profile' ,
170170 '/transcribe'
171- ] ;
172-
173- const currentPath = window . location . pathname ;
174- const currentSearch = window . location . search ;
175- const currentHash = window . location . hash ;
176-
177- // Check if current path ends with a trailing slash and has more than just "/"
178- if ( currentPath . endsWith ( '/' ) && currentPath . length > 1 ) {
179- // Remove the trailing slash to check if it matches a known path
180- const pathWithoutSlash = currentPath . slice ( 0 , - 1 ) ;
181-
182- if ( knownPaths . includes ( pathWithoutSlash ) ) {
183- // We found a match! Show the trailing slash notice
184- const trailingSlashNotice = document . getElementById ( 'trailingSlashNotice' ) ;
185- const standard404 = document . getElementById ( 'standard404' ) ;
186- const attemptedUrl = document . getElementById ( 'attemptedUrl' ) ;
187- const suggestedLink = document . getElementById ( 'suggestedLink' ) ;
188-
189- trailingSlashNotice . classList . add ( 'visible' ) ;
190- standard404 . style . display = 'none' ;
191-
192- // Build the correct URL with query strings and hash
193- const correctUrl = pathWithoutSlash + currentSearch + currentHash ;
194-
195- attemptedUrl . textContent = currentPath + currentSearch + currentHash ;
196- suggestedLink . href = correctUrl ;
197- suggestedLink . textContent = correctUrl ;
171+ ]
172+
173+ const el = {
174+ trailingSlashNotice : document . getElementById ( 'trailingSlashNotice' ) ,
175+ standard404 : document . getElementById ( 'standard404' ) ,
176+ attemptedUrl : document . getElementById ( 'attemptedUrl' ) ,
177+ suggestedLink : document . getElementById ( 'suggestedLink' ) ,
178+ nearMatchReason : document . getElementById ( 'nearMatchReason' )
179+ }
180+
181+ const currentPath = window . location . pathname
182+ const currentSearch = window . location . search
183+ const currentHash = window . location . hash
184+
185+ const removeTrailingSlash = path => path ?. length > 1 && path . endsWith ( '/' ) ? path . slice ( 0 , - 1 ) : path
186+
187+ const segmentCharCounts = str => {
188+ const counts = new Map ( )
189+ for ( const ch of str ) counts . set ( ch , ( counts . get ( ch ) ?? 0 ) + 1 )
190+ return counts
191+ }
192+
193+ const countsEqual = ( a , b ) => {
194+ if ( a . size !== b . size ) return false
195+ for ( const [ key , val ] of a ) {
196+ if ( b . get ( key ) !== val ) return false
197+ }
198+ return true
199+ }
200+
201+ const isTransposedSegmentMatch = ( inputSeg , targetSeg ) => {
202+ if ( inputSeg . length !== targetSeg . length ) return false
203+ if ( inputSeg . length < 2 ) return inputSeg === targetSeg
204+ if ( inputSeg [ 0 ] !== targetSeg [ 0 ] || inputSeg [ 1 ] !== targetSeg [ 1 ] ) return false
205+ const a = segmentCharCounts ( inputSeg . slice ( 2 ) )
206+ const b = segmentCharCounts ( targetSeg . slice ( 2 ) )
207+ return countsEqual ( a , b )
208+ }
209+
210+ const splitSegments = path => path . split ( '/' ) . filter ( Boolean )
211+
212+ const isTransposedPathMatch = ( inputPath , knownPath ) => {
213+ const a = splitSegments ( inputPath )
214+ const b = splitSegments ( knownPath )
215+ if ( a . length !== b . length ) return false
216+ for ( let i = 0 ; i < a . length ; i ++ ) {
217+ if ( ! isTransposedSegmentMatch ( a [ i ] , b [ i ] ) ) return false
218+ }
219+ return true
220+ }
221+
222+ const normalizedCurrent = removeTrailingSlash ( currentPath )
223+ const normalizedCurrentLC = normalizedCurrent . toLowerCase ( )
224+ const knownPathsLC = knownPaths . map ( p => p . toLowerCase ( ) )
225+
226+ // Prefer exact path match when only a trailing slash is present
227+ const hasTrailingSlash = currentPath . length > 1 && currentPath . endsWith ( '/' )
228+ if ( hasTrailingSlash ) {
229+ const idx = knownPathsLC . indexOf ( normalizedCurrentLC )
230+ if ( idx !== - 1 ) {
231+ const canonical = knownPaths [ idx ]
232+ const correctUrl = canonical + currentSearch + currentHash
233+ el . trailingSlashNotice ?. classList . add ( 'visible' )
234+ if ( el . standard404 ) el . standard404 . style . display = 'none'
235+ if ( el . attemptedUrl ) el . attemptedUrl . textContent = currentPath + currentSearch + currentHash
236+ if ( el . suggestedLink ) {
237+ el . suggestedLink . href = correctUrl
238+ el . suggestedLink . textContent = correctUrl
239+ }
240+ if ( el . nearMatchReason ) {
241+ el . nearMatchReason . textContent = ' The URL you tried has a trailing slash, which makes a difference in how our site routes pages.'
242+ }
243+ return
244+ }
245+ // No exact trailing-slash match; continue to case-insensitive and fuzzy checks
246+ }
247+
248+ // Case-insensitive exact path match (no trailing slash)
249+ {
250+ const idxCIExact = knownPathsLC . indexOf ( normalizedCurrentLC )
251+ if ( idxCIExact === - 1 ) {
252+ // continue to fuzzy check
253+ } else {
254+ const canonical = knownPaths [ idxCIExact ]
255+ const correctUrl = canonical + currentSearch + currentHash
256+ el . trailingSlashNotice ?. classList . add ( 'visible' )
257+ if ( el . standard404 ) el . standard404 . style . display = 'none'
258+ if ( el . attemptedUrl ) el . attemptedUrl . textContent = currentPath + currentSearch + currentHash
259+ if ( el . suggestedLink ) {
260+ el . suggestedLink . href = correctUrl
261+ el . suggestedLink . textContent = correctUrl
262+ }
263+ if ( el . nearMatchReason ) {
264+ el . nearMatchReason . textContent = ' The URL capitalization differs from our canonical path. Here is the correctly cased link.'
265+ }
266+ return
267+ }
268+ }
269+
270+ // Fuzzy match: segments with same first two chars, remaining letters rearranged
271+ let fuzzyMatch = null
272+ for ( let i = 0 ; i < knownPaths . length ; i ++ ) {
273+ const kp = knownPaths [ i ]
274+ const kpLC = knownPathsLC [ i ]
275+ if ( isTransposedPathMatch ( normalizedCurrentLC , kpLC ) ) { fuzzyMatch = kp ; break }
276+ }
277+
278+ if ( ! fuzzyMatch ) return
279+ {
280+ const correctUrl = fuzzyMatch + currentSearch + currentHash
281+ el . trailingSlashNotice ?. classList . add ( 'visible' )
282+ if ( el . standard404 ) el . standard404 . style . display = 'none'
283+ if ( el . attemptedUrl ) el . attemptedUrl . textContent = currentPath + currentSearch + currentHash
284+ if ( el . suggestedLink ) {
285+ el . suggestedLink . href = correctUrl
286+ el . suggestedLink . textContent = correctUrl
287+ }
288+ if ( el . nearMatchReason ) {
289+ el . nearMatchReason . textContent = ' Some letters in the URL look transposed or capitalization differs. We matched the path segments by the first two letters (case-insensitive) and treated the remaining letters as rearranged to suggest the correct page.'
198290 }
199291 }
200- } ) ( ) ;
292+ } ) ( )
201293</ script >
0 commit comments