diff --git a/gatsby-node.js b/gatsby-node.js index a62e9b8..8874cbd 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -170,9 +170,13 @@ exports.onCreatePage = async ({ page, actions }) => { const oldPage = { ...page } page.matchPath = `/resources/*` deletePage(oldPage) - } else { - page.context.layout = resolveLayout(page.path) + } else if (page.path.match(/^\/archives\/404/)) { + const oldPage = { ...page } + page.matchPath = `/archives/*` + deletePage(oldPage) } + + page.context.layout = resolveLayout(page.path) createPage(page) } diff --git a/src/components/404LinkCorrection/index.tsx b/src/components/404LinkCorrection/index.tsx new file mode 100644 index 0000000..32b06ca --- /dev/null +++ b/src/components/404LinkCorrection/index.tsx @@ -0,0 +1,117 @@ +import { WindowLocation } from "@reach/router" +import React, { FC, Fragment } from "react" +import * as SC from "./styles" + +import { FileConnection } from "../../../generated/graphql" + +// helper function to make matrix generation easier +// credits to https://stackoverflow.com/a/13808461 +function makeMatrix(w: number, h: number, val: any = null) { + return Array(h) + .fill(null) + .map(() => Array(w).fill(val)) +} + +// calculate semantic difference +// based on one character edits +// efficient refactoring inspired by MIT licensed repo: +// https://github.com/trekhleb/javascript-algorithms +function levenshteinDistance(term1: string, term2: string) { + /* base case: empty strings */ + if (term1.length === 0) { + return term2.length + } + if (term2.length === 0) { + return term1.length + } + + // this will be our comparison matrix + const matrix = makeMatrix(term1.length + 1, term2.length + 1, null) + + // make zeroeth row based off first term + for (let i = 0; i <= term1.length; i += 1) { + matrix[0][i] = i + } + + // make zeroeth column based off second term + for (let j = 0; j <= term2.length; j += 1) { + matrix[j][0] = j + } + + // iterative with full matrix implementation + // https://en.wikipedia.org/wiki/Levenshtein_distance + for (let j = 1; j <= term2.length; j += 1) { + for (let i = 1; i <= term1.length; i += 1) { + const substitutionCost = term1[i - 1] === term2[j - 1] ? 0 : 1 + + matrix[j][i] = Math.min( + matrix[j - 1][i] + 1, // deletion + matrix[j][i - 1] + 1, // insertion + matrix[j - 1][i - 1] + substitutionCost + ) // substitution + } + } + + return matrix[term2.length][term1.length] +} + +interface IPossibleCorrections { + basepath: string + location: WindowLocation + data: { allFile: FileConnection } + threshold?: number +} + +export const PossibleCorrections: FC = ({ + basepath, + location, + data, + threshold = 8, +}) => { + // the data prop has the graphql result + // we're abstracting it to `linkArray` to just have array of edges matched + // must use `.node.absolutePath` on each edge to get each node's absolute link + // we will compare this link + // however, the relative links are more user friendly + const linkArray = data.allFile.edges + + // location prop has several attributes + // location.origin is base url + // location.href is the window location + // location.pathname is link after protocol and hostname + // by replacing `"/resources/"`, + // we are left with only the relative path to `resources` + const search = location.pathname.replace(`/${basepath}`, "") + + // filter based on distance + // levenshtein distance of x means how 'off' it was + const displayArray = linkArray.filter(value => { + return ( + levenshteinDistance( + search.toLowerCase(), + value.node!.relativePath!.toLowerCase() + ) < threshold + ) + }) + + // helpful message if no matches found + const found = displayArray.length === 0 ? "Oops, nothing similar found." : "" + + return ( + +

Based off of "{search}" you may have meant:

+ + {found} +
+ ) +} diff --git a/src/components/404LinkCorrection/styles.tsx b/src/components/404LinkCorrection/styles.tsx new file mode 100644 index 0000000..34496dd --- /dev/null +++ b/src/components/404LinkCorrection/styles.tsx @@ -0,0 +1,16 @@ +import { Link } from "gatsby" +import styled from "styled-components" + +export const StyledLink = styled(Link)` + color: #0090d8; + font-weight: 700; + border-bottom: 2px solid; + text-decoration: none; + transition: color 0.3s; + + &:hover, + &:focus { + color: #5dbbea; + transition: none; + } +` diff --git a/src/pages/archives/404.tsx b/src/pages/archives/404.tsx new file mode 100644 index 0000000..7db240a --- /dev/null +++ b/src/pages/archives/404.tsx @@ -0,0 +1,38 @@ +import { Location } from "@reach/router" +import { graphql } from "gatsby" +import React, { Fragment } from "react" + +import { FileConnection } from "../../../generated/graphql" +import { ComponentQuery } from "../../../typings" +import { SEO } from "../../components/SEO" + +import { PossibleCorrections } from "../../components/404LinkCorrection" + +export default ({ data }: ComponentQuery<{ allFile: FileConnection }>) => ( + + +

RESOURCE NOT FOUND

+

You just hit a route that doesn't exist... the sadness.

+ + {({ location }) => ( + + )} + +
+) + +export const query = graphql` + query { + allFile(filter: { sourceInstanceName: { eq: "what-is-archive" } }) { + edges { + node { + relativePath + } + } + } + } +` diff --git a/src/pages/archives.tsx b/src/pages/archives/index.tsx similarity index 85% rename from src/pages/archives.tsx rename to src/pages/archives/index.tsx index f7c89b4..a419c56 100644 --- a/src/pages/archives.tsx +++ b/src/pages/archives/index.tsx @@ -1,7 +1,7 @@ import React, { Fragment } from "react" -import { Link } from "../components/Link" -import { SEO } from "../components/SEO" +import { Link } from "../../components/Link" +import { SEO } from "../../components/SEO" function ArchivesPage() { return ( diff --git a/src/pages/resources/404.tsx b/src/pages/resources/404.tsx index 607fc9c..b8fbe40 100644 --- a/src/pages/resources/404.tsx +++ b/src/pages/resources/404.tsx @@ -1,120 +1,25 @@ -import { Location, WindowLocation } from "@reach/router" -import { graphql, Link } from "gatsby" +import { Location } from "@reach/router" +import { graphql } from "gatsby" import React, { Fragment } from "react" import { FileConnection } from "../../../generated/graphql" import { ComponentQuery } from "../../../typings" +import { PossibleCorrections } from "../../components/404LinkCorrection" import { SEO } from "../../components/SEO" -// helper function to make matrix generation easier -// credits to https://stackoverflow.com/a/13808461 -function makeMatrix(w: number, h: number, val: any = null) { - return Array(h) - .fill(null) - .map(() => Array(w).fill(val)) -} - -// calculate semantic difference -// based on one character edits -// efficient refactoring inspired by MIT licensed repo: -// https://github.com/trekhleb/javascript-algorithms -function levenshteinDistance(term1: string, term2: string) { - /* base case: empty strings */ - if (term1.length === 0) { - return term2.length - } - if (term2.length === 0) { - return term1.length - } - - // this will be our comparison matrix - const matrix = makeMatrix(term1.length + 1, term2.length + 1, null) - - // make zeroeth row based off first term - for (let i = 0; i <= term1.length; i += 1) { - matrix[0][i] = i - } - - // make zeroeth column based off second term - for (let j = 0; j <= term2.length; j += 1) { - matrix[j][0] = j - } - - // iterative with full matrix implementation - // https://en.wikipedia.org/wiki/Levenshtein_distance - for (let j = 1; j <= term2.length; j += 1) { - for (let i = 1; i <= term1.length; i += 1) { - const substitutionCost = term1[i - 1] === term2[j - 1] ? 0 : 1 - - matrix[j][i] = Math.min( - matrix[j - 1][i] + 1, // deletion - matrix[j][i - 1] + 1, // insertion - matrix[j - 1][i - 1] + substitutionCost - ) // substitution - } - } - - return matrix[term2.length][term1.length] -} - -function getPossibleResources( - location: WindowLocation, - data: { allFile: FileConnection } -) { - // the data prop has the graphql result - // we're abstracting it to `linkArray` to just have array of edges matched - // must use `.node.absolutePath` on each edge to get each node's absolute link - // we will compare this link - // however, the relative links are more user friendly - const linkArray = data.allFile.edges - - // location prop has several attributes - // location.origin is base url - // location.href is the window location - // location.pathname is link after protocol and hostname - // by replacing `"/resources/"`, - // we are left with only the relative path to `resources` - const search = location.pathname.replace("/resources/", "") - - // filter based on distance - // levenshtein distance of x means how 'off' it was - const displayArray = linkArray.filter( - value => - levenshteinDistance( - search.toLowerCase(), - value.node!.relativePath!.toLowerCase() - ) < 8 - ) - - // helpful message if no matches found - const found = displayArray.length === 0 ? "Oops, nothing similar found." : "" - - return ( - -

Based off of "{search}" you may have meant:

- - {found} -
- ) -} - export default ({ data }: ComponentQuery<{ allFile: FileConnection }>) => (

RESOURCE NOT FOUND

You just hit a route that doesn't exist... the sadness.

- {({ location }) => getPossibleResources(location, data)} + {({ location }) => ( + + )}
)