diff --git a/gatsby-config.js b/gatsby-config.js index cf11179..d2a3b1e 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -72,6 +72,7 @@ module.exports = { }, `gatsby-transformer-sharp`, `gatsby-plugin-react-svg`, + `gatsby-plugin-remove-trailing-slashes`, `gatsby-plugin-sharp`, { resolve: `gatsby-plugin-manifest`, diff --git a/gatsby-node.js b/gatsby-node.js index f5bed1f..8227128 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -136,10 +136,16 @@ exports.onCreateNode = async ({ node, getNode, actions }) => { } } -exports.onCreatePage = ({ page, actions }) => { - const { createPage } = actions +exports.onCreatePage = async ({ page, actions }) => { + const { createPage, deletePage } = actions - page.context.layout = resolveLayout(page.path) + if (page.path.match(/^\/resources\/404/)) { + const oldPage = { ...page } + page.matchPath = `/resources/*` + deletePage(oldPage) + } else { + page.context.layout = resolveLayout(page.path) + } createPage(page) } diff --git a/package-lock.json b/package-lock.json index 24801c5..81bf4ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9167,6 +9167,26 @@ "svg-react-loader": "^0.4.4" } }, + "gatsby-plugin-remove-trailing-slashes": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/gatsby-plugin-remove-trailing-slashes/-/gatsby-plugin-remove-trailing-slashes-2.1.15.tgz", + "integrity": "sha512-IMuubnIdsgRyEGWYZZD5QhynO3Mw3rf+cM5uBk594I97MR/BcqYVjXVhVKp4zGpip2HQNzPPgHWODS8tn6n30g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.7.4" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.4.tgz", + "integrity": "sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + } + } + }, "gatsby-plugin-sharp": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/gatsby-plugin-sharp/-/gatsby-plugin-sharp-2.3.2.tgz", diff --git a/package.json b/package.json index 029efee..5208243 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@types/styled-components": "^4.4.0", "babel-plugin-styled-components": "^1.10.6", "discord.js": "^11.5.1", + "gatsby-plugin-remove-trailing-slashes": "^2.1.14", "prettier": "^1.19.1", "tslint": "^5.20.1", "tslint-config-prettier": "^1.18.0", diff --git a/src/pages/resources/404.tsx b/src/pages/resources/404.tsx new file mode 100644 index 0000000..cebad1b --- /dev/null +++ b/src/pages/resources/404.tsx @@ -0,0 +1,122 @@ +import { graphql, Link } from "gatsby" +import React, { Component, Fragment } from "react" +import { Location } from "@reach/router" + +import { SEO } from "../../components/SEO" + +// helper function to make matrix generation easier +// credits to https://stackoverflow.com/a/13808461 +function makeMatrix(w, h, val) { + 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, term2) { + /* 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, data) { + // the data prop has the graphql result + // we're abstracting it to `link_array` 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 link_array = 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 display_array = link_array.filter( + (value, index) => + levenshteinDistance( + search.toLowerCase(), + value.node.relativePath.toLowerCase() + ) < 8 + ) + + // helpful message if no matches found + const found = display_array.length == 0 ? "Oops, nothing similar found." : "" + + return ( + +

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

+ + {found} +
+ ) +} + +export default ({ location, data }) => ( + + +

RESOURCE NOT FOUND

+

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

+ + {({ navigate, location }) => getPossibleResources(location, data)} + +
+) +export const query = graphql` + query { + allFile(filter: { sourceInstanceName: { eq: "resources" } }) { + edges { + node { + relativePath + } + } + } + } +` diff --git a/src/pages/resources.tsx b/src/pages/resources/index.tsx similarity index 75% rename from src/pages/resources.tsx rename to src/pages/resources/index.tsx index b242598..f5a4931 100644 --- a/src/pages/resources.tsx +++ b/src/pages/resources/index.tsx @@ -1,9 +1,9 @@ import React, { Fragment } from "react" -import packageJson from "../../package.json" -import { Link } from "../components/Link" -import { ResourcesList } from "../components/ResourcesList" -import { SEO } from "../components/SEO" +import packageJson from "../../../package.json" +import { Link } from "../../components/Link" +import { ResourcesList } from "../../components/ResourcesList" +import { SEO } from "../../components/SEO" function ResourcesPage() { return (