“Maybe you were looking for…” 404 page

Maybe you were looking for 404 page
Difficulty

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.


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.

0
Be the first one to like this.
Please wait...

Leave a Reply

Thanks for choosing to leave a comment.
Please keep in mind that all comments are moderated according to our comment policy, and your email address will NOT be published.
Please do NOT use keywords in the name field. Let's have a personal and meaningful conversation.