Skip to content

Commit d640191

Browse files
committed
Improve 404 page with fuzzy and case-insensitive URL matching
Enhanced the 404 page logic to suggest correct URLs not only for trailing slash errors, but also for case-insensitive mismatches and fuzzy matches where path segments have the same first two characters and the remaining letters are rearranged. Updated the UI to display more specific reasons for near matches and refactored the JavaScript for clarity.
1 parent 94f4754 commit d640191

1 file changed

Lines changed: 124 additions & 32 deletions

File tree

404.html

Lines changed: 124 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,11 @@
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

Comments
 (0)