Last active
May 26, 2023 13:26
-
-
Save aweber1/85e0ce9ca6df74b4374d27cd0523c78a to your computer and use it in GitHub Desktop.
Next.js custom routes with regex + Server support + Client-side routing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Link from './Link'; | |
export function MyComponent(props) { | |
// NOTE: you can use all of the same props used for `next/link` | |
return ( | |
<div> | |
<Link href="/some-rad-page"> | |
<a>My Link Text</a> | |
</Link> | |
<Link href="/en/some-rad-page"> | |
<a>My Other Link Text</a> | |
</Link> | |
</div> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Link from 'next/link'; | |
import { matchRoute } from './routeMatcher'; | |
// This component acts as an abstraction around the `next/link` component to: | |
// 1. Provide the ability to use regex patterns when declaring dynamic routes. | |
// 2. Help reduce refactoring if routing libraries change. | |
// This component is intended to accept the same props interface as the `next/link` component. | |
// Next.js has the concept of dynamic routes / custom routes, but they are not suited for regex | |
// matching on both server _and_ client. | |
export default ({ href, ...otherProps }) => { | |
if (href && typeof href === 'string') { | |
const { matchedRoute, matchedDefinition } = matchRoute(href); | |
if (matchedRoute && matchedDefinition) { | |
return ( | |
<Link | |
{...otherProps} | |
href={{ pathname: matchedDefinition.destination, query: matchedRoute.params }} | |
as={matchedRoute.path} | |
/> | |
); | |
} | |
} | |
return <Link href={href} {...otherProps} />; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { rewrites } = require('./routeMatcher'); | |
const nextConfig = { | |
experimental: { | |
rewrites: () => rewrites, | |
}, | |
}; | |
module.exports = nextConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"dependencies": { | |
"path-to-regexp": "^6.1.0" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const MyPage = () => { | |
return <div></div>; | |
}; | |
MyPage.getInitialProps = (nextContext) => { | |
const { pathname, query } = nextContext; | |
// `pathname` should be `index`, i.e. the `destination` property in our route matcher definitions. | |
console.log('pathname', pathname); | |
// `query.routeName` should be `/some-rad-page`, i.e. the `:routeName` parameter matched in our route matcher definitions. | |
console.log('query.routeName', query.routeName); | |
// `query.lang` may not be defined as it is "optional" in our route matcher definitions. | |
// However, if a route url is, for example, `/en/some-rad-page`, then `query.lang` would be `en`. | |
console.log('query.lang', query.lang); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This file uses CommonJS because it is used by both server and client. | |
const { match: createMatcher } = require('path-to-regexp'); | |
// `rewrites` are using by `next.config.experimental.rewrites` property and must be an | |
// array of objects with _only_ `source` and `destination` properties. | |
const rewrites = [ | |
{ | |
source: '/:lang([a-z]{2}-[A-Z]{2})/:routeName*', | |
destination: '/index', | |
}, | |
{ | |
source: '/:lang([a-z]{2})/:routeName*', | |
destination: '/index', | |
}, | |
{ | |
source: '/:routeName*', | |
destination: '/index', | |
}, | |
]; | |
// We create a new set of definitions mapped from `rewrites` and attach | |
// a `matcher` property that we can use for easier match evaluation in | |
// consuming code. | |
const routeMatcherDefinitions = rewrites.map((rewrite) => { | |
return { ...rewrite, matcher: createMatcher(rewrite.source) }; | |
}); | |
// matchRoute iterates `routeMatcherDefinitions` and attempts to match | |
// and parse the given `path`. | |
function matchRoute(path) { | |
let matchedRoute = null; | |
let matchedDefinition = null; | |
// Using a `for` loop allows us to break on the first match. | |
// Why not `for ... of`? because for...of loops get transpiled to something big and clunky. | |
// Why not Array.forEach or Array.reduce? because we can't break early. | |
for (let i = 0; i < routeMatcherDefinitions.length; i++) { | |
const routeDefinition = routeMatcherDefinitions[i]; | |
// matcher returns false if no match made | |
matchedRoute = routeDefinition.matcher(path); | |
if (matchedRoute) { | |
matchedDefinition = routeDefinition; | |
break; | |
} | |
} | |
return { matchedRoute, matchedDefinition }; | |
} | |
module.exports = { | |
matchRoute, | |
rewrites, | |
routeMatcherDefinitions, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment