next-translate-routes-multi-domain v1.9.0-6
next-translate-routes-multi-domain
Translated routing and more for Next using Next regular file-base routing system
- Features
- Motivation
- How to
- Basic usage
- Wrap you next config with the next-translate-routes-multi-domain plugin
- Define your routes
- Wrap your
\_appcomponent with thewithTranslateRouteshoc - Use
next-translate-routes-multi-domain/linkinstead ofnext/link - Use
next-translate-routes-multi-domain/router insteadofnext/routerfor singleton router (default export)
- Advanced usage
- Basic usage
- Known issue
- How does it work
Features
- Translated paths segments
Ex:/contact-usand/fr/contactez-nous. - Complex paths segments (path-to-regexp synthax)
Ex:/:id{-:slug}?/ - Constrained dynamic paths segments (path-to-regexp synthax)
Ex:/:id(\\d+)/ - No duplicated content (SEO)
Simple rewrites would make a page accessible with 2 different urls which is bad for SEO. - Auto-redirection to correct translated path
Ex:/fr/english/pathredirects to/fr/french/path - No custom server needed!
Next automatic static optimization remains available.
See it in action: https://codesandbox.io/s/github/hozana/next-translate-routes-multi-domain/tree/master
Note: next-translate-routes-multi-domain does not work with Next html static export, since internationalized routing is among static html export unsupported features.
Motivation
To build a fully internationalized website, one need to translate url segments: it is important for UX and SEO. For now, Next only ship with locale prefixes (see Next.js internationalized routing doc).
The next-routes package allow fully internationalized routing but it is no longer maintained, and it is designed without the latest Next api, such as internationalized routing, new data fetching methods, automatic static optimization.
To be able to use theses, a new approach is needed. Next.js provides it with Redirects and Rewrites, but:
- the main problem is that writing the rewrites and redirects to translate all the routes by hand is long, boring, error-prone, and hard to maintain.
- an other problem is that by default, the file path works with all the locale prefixes: it creates 2 urls for the same content, which is not good for SEO. This problem can be worked around by creating redirects from
/${localePrefix}/$(filePath)to/${localePrefix}/$(localePath), but it creates a lot more complexity to write by hand and maintain.
How to
Basic usage
Check the example folder to see next-translate-routes-multi-domain in action. Some advanced techniques are shown there too: they may seem complicated but those 4 steps should cover most of the cases.
1. Wrap you next config with the next-translate-routes-multi-domain plugin
Import the withTranslateRoutes from next-translate-routes-multi-domain/plugin.
// next.config.js
const withTranslateRoutes = require('next-translate-routes-multi-domain/plugin')
module.exports = withTranslateRoutes({
// Next i18n config (mandatory): https://nextjs.org/docs/advanced-features/i18n-routing
i18n: {
locales: ['en', 'fr', 'es', 'pl'],
defaultLocale: 'pl',
},
// ...Remaining next config
})2. Define your routes
You can add a _routes.json, or a _routes.yaml, file in the pages folder, and in the every subfolder where you want to define routes.
Given a folder structure like so, you can add a _routes.json in /pages/ and in /pages/section/.
/pages/
├ section/
| ├ page1.tsx
| ├ page2.tsx
| └ _routes.json
├ somewhere/
| └ else.json
├ _app.tsx
├ about.tsx
├ contact.tsx
└ _routes.jsonIn /pages/section/, the _routes.json file could look like this.
// `/pages/section/_routes.json`
{
"/": {
"es": "seccion" // Folder path in es
},
"page1": {
"default": "article", // Overwrite the default page path (fallback)
"es": "articulo"
},
"page2": "definition" // Overwrite the page path for all language
}The "/": { ... } part define the folder paths, each other section define the paths of a page file in this folder:
- page1 will be accessible at
/seccion/articuloines, and at/section/articlein other languages, - page2 will be accessible at
/seccion/definitionines, and at/section/definitionin other languages.
You don't need a _routes.json file in folder where you don't customize anything. If it is empty, then delete it.
Here, the /somewhere/ subfolder does not have any _routes.json file.
Then, in /pages/, the _routes.json file could look like this.
// `/pages/_routes.json`
{
"/": {
"pt": "blog" // As we are in the root pages folder, this will add a "blog" path prefix for all pages in pt
},
"contact": {
"es": "contactar",
"pt": "contatar"
}
}In the root pages folder, the "/": { ... } part of the root _routes.json allows to add a different basePath
for each language or only some language, like "blog" in pt here:
- about will be accessible at
/blog/aboutinptand at/aboutin other languages, - contact will be accessible at
/blog/contatarinpt, at/contactarinesand at/contactin other languages, - page1 will in fact be accessible at
/blog/section/articleinpt, - page2 will in fact be accessible at
/blog/section/definitioninpt.
3. Wrap you \_app component with the withTranslateRoutes hoc
// `/pages/_app.js`
import { withTranslateRoutes } from 'next-translate-routes-multi-domain'
import { App } from 'next/app'
export default withTranslateRoutes(App)Or:
// `/pages/_app.js`
import { withTranslateRoutes } from 'next-translate-routes-multi-domain'
const App = ({ Component, pageProps }) => {
// Custom code...
return <Component {...pageProps} />
}
export default withTranslateRoutes(App)4. Use next-translate-routes-multi-domain/link instead of next/link
next-translate-routes-multi-domain extends Next Link to translate routes automatically: import it from 'next-translate-routes-multi-domain/link' instead of 'next/link' and use as you ever did.
import Link from 'next-translate-routes-multi-domain/link'
import React, { useEffect, useState } from 'react'
const MyLinks = (props) => {
const { locales } = useRouter()
return (
<>
<Link href="/file/path/to/page">Current locale</Link>
{locales.map((locale) => (
<Link
href={{ pathname: '/file/path/to/[dynamic]/page', query: { dynamic: props.param, otherQueryParam: 'foo' } }}
locale={locale}
key={locale}
>
{locale}
</Link>
))}
</>
)
}5. Use next-translate-routes-multi-domain/router instead of next/router for singleton router (default export)
You can use next-translate-routes-multi-domain/router everywhere instead of next/router but it is only necessary for the singleton router (which is rarely used).
import singletonRouter from 'next-translate-routes-multi-domain/router'
// Indead of:
import singletonRouter from 'next/router'Advanced usage
Check the example folder to see some advanced techniques in action.
Configuration
next-translate-routes-multi-domain is configurable by adding a translateRoutes key in Next config that accept an object of the following NTRConfig type.
type NTRConfig = {
debug?: boolean
routesDataFileName?: string
routesTree?: TRouteBranch<L>
pagesDirectory?: string
}If
i18n.localesis set to['en', 'fr'], then theTRouteBranchgenericLprop would be'en' | 'fr'. A non-generic equivalent ofTRouteBranch<'en' | 'fr'>would be the following./** Non generic version of the TRouteBranch type for better readability, where the generi L prop is set to `'en' | 'fr'` */ type TRouteBranchEnFr = { name: string en: string fr: string children: TRouteBranchEnFr[] }
translateRoutes: {
debug: true,
routesDataFileName: 'routesData',
}When debug is set to true, you will get some logs, both in the server terminal and in the browser console. By default, you will get some logs for each router.push and router.replace, but not router.prefetch. To enable logs for router.prefetch too, you can set debug to withPrefetch.
If routesDataFileName is defined, to 'routesData' for example, next-translate-routes-multi-domain will look in the pages folder for files named routesData.json or routesData.yaml instead of the default _routes.json or _routes.yaml.
If routesTree is defined, next-translate-routes-multi-domain won't parse the pages folder and will use the given object as the routes tree. If you uses it, beware of building correctly the routes tree to avoid bugs.
Translate/untranslate urls
Two helpers are exposed to translate/untranslate urls:
- fileUrlToUrl transforms a file url into a translated url
- urlToFileUrl transforms a translated url into a file url
Both of them take 2 arguments: an url and a locale.
Alternate pages (SEO)
You will probably want to indicate alternate pages for SEO optimization. Here is how you can do that:
const { pathname, query, locale, locale } = useRouter()
return (
<Head>
{locales.map((l) => l !== locale && <link rel="alternate" hrefLang={l} href={fileUrlToUrl({ pathname, query }, l)} />)}
</Head>
)You can do it in the _app component if you are sure to do that for all your pages.
See this article about alternate and canonical pages
Sitemap
See @JacbSoderblom's suggestion
Constrained dynamic paths segments
// `/pages/blog/[id]/_routes.json`
{
"/": ":id(\\d+)", // Constrain a dynamic folder segment (to be a number here)
"[slug]": ":slug(\\w+)", // Constrain a dynamic page segment (to be letters here)
}For a catch all route: "[...path]": ":path*".
Ignoring a path part
This will ignore the blog path segment:
// `/pages/blog/_routes.json`
{
"/": "."
}It can be done for some lang only and not others.
// `/pages/blog/_routes.json`
{
"/": {
fr: "."
}
}⚠️ Ignoring a path segment can cause troubles with the redirections.
Ex: given the /a/[b]/[c] and /a/[b]/[c]/d file paths where [b] is ignored and the b param is merged with the c param: :b-:c.
/a/:b/:c => /a/:b-:c and /a/:b/:c/d => /a/:b-:c/d
Then /a/bb/11 will be redirected to /a/bb-11 and /a/bb/11/d to /a/bb-11/d and that is fine.
But then /a/bb-11/d will match /a/:b-:c and be redirected to /a/bb-11-d and that is not fine!
To handle this case, one can add a path-to-regex pattern to the default ignore token: .(\\d+), or .(\[\^-\]+), or .(\what|ever\).
This path-to-regex pattern will be added after the segment name in the redirect.
/a/:b(\[\^-\]+)/:c => /a/:b-:c and /a/:b(\[\^-\]+)/:c/d => /a/:b-:c/d
Then /a/bb-11/d will no more match /a/[b]/[c] (/a/:b(\[\^-\]+)/:c).
⚠️ This is only handled in default paths (i.e. "/": ".(\\d+)" or "/": { "default": ".(\\d+)" }), not in lang-specific paths.
Complex paths segments
// `/pages/blog/[id]/_routes.json`
{
"/": "article{-:id}?-view", // Add prefix, optional prefix, suffix
}It is also possible to create a path segment with 2 dynamic parameters. Ex: /articles/:id{-:slug}?.
First, create a path segment for each dynamic parameter: `/articles/id/slug.
Then:
// `/articles/[id]/_routes.json`
{
"/": ".", // Ignore the [id] segment
"[slug]": ":id{-:slug}?" // Give the 2 params to the 2nd segment
}Custom route tree
If you want to avoid seeding _routes.json files in your /pages folder,
you can directly create a routesTree object, and inject it in the next config as follow.
// next.config.js
const withTranslateRoutes = require('next-translate-routes-multi-domain')
const getRoutesTree = require('./getRoutesTree')
const routesTree = getRoutesTree()
module.exports = withTranslateRoutes({
// Next i18n config (mandatory): https://nextjs.org/docs/advanced-features/i18n-routing
i18n: {
locales: ['en', 'fr', 'es', 'pl'],
defaultLocale: 'pl',
},
translateRoutes: {
routesTree,
},
// ...Remaining next config
})routesTree must be of type TRouteBranch:
type TRouteBranch<Locale extends string> = {
name: string
paths: { default: string } & Partial<Record<Locale, string>>
children?: TRouteBranch[]
}Outside Next
You might need to mock next-translate-routes-multi-domain outside Next, for example for testing or in Storybook.
First, you need to create next-translate-routes-multi-domain data. You can do it using the createNtrData helper, but it only works in node environment. It takes the next config as first parameter. The second parameter is optional and allows to use a custom pages folder: if omitted, createNtrData will look for you next pages folder.
import { createNtrData } from 'next-translate-routes-multi-domain/plugin`
import nextConfig from '../next.config.js'
const ntrData = createNtrData(
nextConfig,
path.resolve(process.cwd(), './fixtures/pages'),
)Then, if you want to render you app, you need to inject the router context, then (and only then) inject next-translate-routes-multi-domain. You can do it manually, or using next-tranlate-routes/loader with Webpack.
Manually
You will have to execute createNtrData in a node script and store the result somewhere that can be imported.
// nextRouterMock.ts
import { RouterContext } from 'next/dist/shared/lib/router-context'
import withTranslateRoutes from 'next-translate-routes-multi-domain'
import ntrData from 'path/to/your/ntrData'
//[...]
const RouteTranslationsProvider = withTranslateRoutes(
ntrData,
({ children }) => <>{children}</>,
)
const TranslatedRoutesProvider = ({ children }) => (
<RouterContext.Provider value={routerMock}>
<RouteTranslationsProvider router={routerMock}>
{children}
</RouteTranslationsProvider>
</RouterContext.Provider>
)
// [...]For Storybook, this piece of code can be used to create a decorator function:
export const WithNextRouter: DecoratorFn = (Story, context): JSX.Element => (
<TranslatedRoutesProvider routerMock={createRouterFromContext(context)}>
<Story />
</TranslatedRoutesProvider>
)With next-translate-routes-multi-domain/loader for Webpack
next-translate-routes-multi-domain/loader allows to create next-translate-routes-multi-domain data at build time. So you can do exactly the same as described in the Manually paragraph above, but you don't need to create and add ntrData as an argument to withTranslateRoutes.
// nextRouterMock.ts
import { RouterContext } from 'next/dist/shared/lib/router-context'
import withTranslateRoutes from 'next-translate-routes-multi-domain'
//[...]
const RouteTranslationsProvider = withTranslateRoutes(({ children }) => <>{children}</>)
// TranslatedRoutesProvider is the same as in the manually paragraph above
// [...]Then all you have to do is to add this rule in your webpack config:
// storybook/config/webpack.config.js for exemple
const { createNtrData } = require('next-translate-routes-multi-domain/plugin')
const nextConfig = require('../../next.config') // Your project nextConfig
module.exports = ({ config }) => {
// [...]
config.module.rules.push({
test: /path\/to\/nextRouterMock/, // ⚠️ Warning! This test should only match the file where withTranslateRoutes is used! If you cannot, set the mustMatch option to false.
use: {
loader: 'next-translate-routes-multi-domain/loader',
options: { data: createNtrData(nextConfig) },
},
})
return config
}⚠️ Warning! The rule
testshould only match the file wherewithTranslateRoutesis used! If you cannot, then set themustMatchloader option tofalse.
Known issues
Middleware with watcher (Next >=12.2.0)
Unfortunately, Next new middleware synthax (stable) has a bug when using a "matcher" and rewrites. You can keep track of this issue:
So if you want to use a middleware with Next >= 12.2.0, you need to remove any watcher option and filter from within the middleware using conditional statements.
Domain routing
Domain routing is not supported yet but should be in the future. Any PR to make it work are welcome.
How does it work
- next-translate-routes-multi-domain plugin parses the page folder and builds a routes tree object that contains the path tree and the information in the
_routes.jsonfiles. - The plugin then uses this information to build optimized redirects and rewrites, then add them to the next config object.
- Rewrites take care of displaying the right page for the translates urls, redirects take care of the urls that would give an unwanted access to the pages (and would create duplicated content).
- The plugin adds a webpack loader for the pages/_app file. This loader adds a data object (containing the routes tree object along with other config) as first argument of the
withTranslateRouteshigh order component that wrap the app. withTranslateRoutesmakes this data available as a global variable,__NEXT_TRANSLATE_ROUTES_DATA.- The
translateUrlfunction uses this data to translate routes. - The
next-translate-routes-multi-domain/linkleverages thetranslateUrlfunction to set theasprop ofnext/linkto the translated url so that the link is aware of the true url destination (which is then available on hover, or on right-click - copy link for example). - The
withTranslateRoutesenhance the router by overriding the router context, to give translation skills to the router.push (which is used on click on anext/link), router.replace, and router.prefetch functions, using thetranslateUrlfunction too.