Sometimes we see a useful tip when we search for a command or a page and we find a 404 “Not found” page or a “command not found” if we are from the command line.
This behavior is given by an algorithm named: “Levenshtein distance algorithm”.
This particular algorithm allows us to find the distance of a word from a dictionary of other predetermined words. By distance we mean the similarity of that particular word (which in our case will be an url) with another in our dictionary.
To have a more compact version, we will manage the algorithm directly within our error page.
The ingredients for our recipe are:
- 1 module for routing
- 1 constant exported with the paths
- 1 error page with algorithm implementation
Let’s first deal with having an app with the routing system already created (via command line).
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {CommonModule} from '@angular/common';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
import { PageComponent } from './page.component';
import { ErrorComponent } from './error.component';
import { paths } from './app-paths';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: paths.home, component: HomeComponent },
{ path: paths.about, component: AboutComponent },
{ path: paths.generic, component: PageComponent },
{ path: '**', component: ErrorComponent },
];
@NgModule({
imports: [CommonModule, RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Then we create a typescript script containing only the constant to be exported with the paths necessary for our app:
export const paths = {
home: 'home',
about: 'about',
generic: 'generic',
};
And finally the fulcrum of our system, the error page with the “Levenshtein” algorithm inside it. This algorithm will be used for a sort of the page array, created just before.
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { paths } from './app-paths';
@Component({
selector: 'app-error',
template: `
<p>An error page</p>
<p>Maybe you were looking for <a routerLink="path">{{path}}</a> page.</p>
`
})
export class ErrorComponent implements OnInit {
path: string;
constructor(
private _router: Router) { }
ngOnInit() {
this.path = this.resolve(this._router.url);
}
/**
* Resolve our 'not found' problem.
* @param url 'Get the typo url and resolve it the nearest.'
*/
private resolve(url: string): string | null {
const typoPath = url.replace('/', '');
const threshold = this.getThreshold(typoPath);
const dictionary = Object.values(paths)
.filter(path => Math.abs(path.length - typoPath.length) < threshold);
if (!dictionary.length) {
return null;
}
this.sortByDistances(typoPath, dictionary);
return `/${dictionary[0]}`;
}
/**
* Sorts by an algorithm
* @param typoPath
* @param dictionary
*/
private sortByDistances(typoPath: string, dictionary: string[]) {
const pathsDistance = {} as { [name: string]: number };
dictionary.sort((a, b) => {
if (!(a in pathsDistance)) {
pathsDistance[a] = this.levenshtein(a, typoPath);
}
if (!(b in pathsDistance)) {
pathsDistance[b] = this.levenshtein(b, typoPath);
}
return pathsDistance[a] - pathsDistance[b];
});
}
/**
* Range of needed characters.
* @param path
*/
private getThreshold = (path: string): number => {
if (path.length < 5) {
return 3;
}
return 5;
};
/**
* Levenshtein algorithm for 404 smart pages.
* @param a
* @param b
*/
private levenshtein = (a: string, b: string): number => {
if (a.length === 0) {
return b.length;
}
if (b.length === 0) {
return a.length;
}
const matrix = [];
// increment along the first column of each row
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
// increment each column in the first row
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
// Fill in the rest of the matrix
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1, // substitution
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j] + 1, // deletion
);
}
}
}
return matrix[b.length][a.length];
}
}
For example, let’s try to make a mistake inserting the “about” page, writing “abut” instead, and see what happens.
![](https://blog.sandbay.it/wp-content/uploads/2020/06/immagine.png)
Try it at home!
That’s all for an intelligent 404 page.
See you next time.
Originally published (but modified by me in a more compact version) at vitaliy-bobrov.github.io.