Localized routes for Next.js using Routex.js

https://images.pexels.com/photos/346885/pexels-photo-346885.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260
Picture by Porapak Apichodilok
Index

One of the main reasons I've built routex.js was to handle url localization in Next.js. So if your not familiar with the library, you can learn more about it in this post.

This article shows you a use case of a real estate website that works in Spain realestate.es and UK realestate.co.uk. The main point of this is to manage a multi country website with different languages and content.

ℹ️ For SEO reasons, the language will change depending on the Top Level Domain (.es, .co.uk), not by the path (.com/es/, .com/uk/). But it can be done by path if you want.

You can play with the complete example here routex.js/examples/with-route-localization, or take a look at this gif to see what I am talking about:

So, how we can achieve this? Let's begin!

The routes manifest

First, we need to create a routes manifest for each country, we'll place it under routes directory:

routes/
|- es.js # routes for Spain country
|- uk.js # routes for UK country

Inside the routes/es.js you'll need something like this:

// routes/es.js

module.exports = [
{
name: 'index',
pattern: '/',
},
{
name: 'property',
pattern: '/venta/:propertySlug',
},
// ...
];

ℹ️ The UK file will have the same routes but with the static parts of the pattern translated in english. IN this case "/venta/..." will be "/sale/...".

Handling server side requests

The server code will live in the server.js file. And it will have two responsabilities, server side routing and pass country routes to the Next.js application:

  • Handle server side requests and route them depending on the domain. This responsability will remain on the routex' getRequestHandler middleware.
  • Set the application routes appRoutes and country code countryCode into the request object in order to be accessible inside the Next.js pages via req property in the page context. We will see how this works later.
// server.js

const express = require('express');
const next = require('next');
const { getRequestHandler } = require('routex.js');
const routes = require('./routes');

const dev = process.env.NODE_ENV !== 'production';
const nextApp = next({ dev });

function getCountryCodeFromHostname(hostname) {
// this will return the domain TLD from a hostname "es" or "uk"
// example: if request comes from 'realestate.es' will return 'es'
return hostname.split('.').pop();
}

function routexHandlerMiddleware(req, res, next) {
// Select specific country routes manifest
const currentDomain = getCountryCodeFromHostname(req.hostname);
const countryRoutes = require(`${process.cwd()}/pages/${countryCode}.js`);

// Load the routes so next will know how to
// handle requests and manage server side routing
const requestHandler = getRequestHandler(nextApp, countryRoutes);

// Set routes into the request
// so we can get it on nextjs pages
req.appRoutes = countryRoutes;

// Set countryCode to the request
// This is just to tell the api client from which country we want to fetch de data
// for example, if countryCode is 'es' we will fetch only data from Spain
req.countryCode = countryCode;

return requestHandler(req, res, next);
}

nextApp.prepare().then(() => {
const app = express();

app
// Load the requestHandler middleware
.use(routexHandlerMiddleware)
.listen(3001);
});

ℹ️ This is a very naive example, there can be other ways to load the server routes more efficiently and testable.

Client side navigation

1. Creating the routes context

We will use a React.Context to handle the current country routes and provide them to the links of our application. Let's create it on a file called routes.js.

This file will have the provider and the consumer of the routes:

  • <RoutesProvider />: this function will recieve the routes manifest to provide them to our application. It will need to be invoked on a higher level of our components hyerarchy. It can be on a simple page component or in the Next's built in _app component.
  • <CustomLink />: this will be our routes enhanced consumer. Once this component is load, it will have access to the current country routes to create the appropiate urls in every case.
// routes.js

import React from 'react';
import NextLink from 'next/link';
import { createRouteLinks } from 'routex.js';

// Create the initial React context
// where all routes will live in client side
const RoutesContext = React.createContext([]);

// Create the routes provider
export function RoutesProvider({ children, routes }) {
return (
<RoutesContext.Provider value={routes}>{children}</RoutesContext.Provider>
);
}

// The routes consumer wrapped in a Custom Link component
export default function CustomLink({ children, title, route, params }) {
return (
<RoutesContext.Consumer>
{(routes) => {
const { link } = createRouteLinks(routes);
const { as, href } = link({ route, params: { ...params } });

return (
<NextLink as={as} href={href}>
<a title={title}>{children}</a>
</NextLink>
);
}}
</RoutesContext.Consumer>
);
}

ℹ️ I've joined the context, provider and consumer on the same file for simplicity sake. But you can split it however you want it!

For additional information, you can check this post to understand better how routex' createRouteLinks() works. But basically it takes a routes manifest and closures them into a link() function. Calling this link() with a route and some params it will return an object to pass into a Next.js Link component. This is an example of usage:

const { link } = createRouteLinks([
{
name: 'route-name',
path: '/route-path/:slug',
page: 'route-page',
},
]);

link('a-route-name', { slug: 'peanut' });

// result:
// {
// href: '/route-path/peanut',
// as: '/route-page?slug=peanut',
// }

2. Provide the routes context to the app

We need to provide the routes context to every component that will use the <CustomLink />. This way we could use the link with localization automatically.

In this example we provide the current country routes appRoutes to the React context in Next's _app.js. Also we pass the current country code countryCode to be accessible in every page (this will be usefull to fetch the corresponding data depending on which country is loaded).

// _app.js

import React from 'react';
import { RoutesProvider } from '../routes';

function App({ Component, pageProps, appRoutes }) {
return (
<RoutesProvider routes={appRoutes}>
<Component {...pageProps} />
</RoutesProvider>
);
}

App.getInitialProps = async ({ Component, ctx }) => {
const { appRoutes, countryCode } = ctx.req || window.__NEXT_DATA__.props;

let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps({ ...ctx, countryCode });
}

return {
appRoutes,
countryCode,
pageProps,
};
};

export default App;

🧙‍♂️ Note: you could build your own decorator that provides the routes context out of the box, something like withRoutes(PageComponent). The library could have this built in, but I'll try to keep things simpler ang give the user more knowledge and autonomy of what he's doing.

3. Use it in any page

At the end of it, we will use the <CustomLink /> in any page we want. This example works in index.js page:

ℹ️ Here we're using a fake API client findAllProperties() that gets the data using the current country code.

// index.js

import React from 'react';
import { CustomLink } from '../routes';
import { findAllProperties } from '../api-client';

function Index({ properties, countryCode }) {
return (
<>
<h1>Realestate.{countryCode}</h1>
<div>
{properties.map(property => (
<CustomLink
route="property-for-rent"
params={{ propertySlug: property.slug }}
>
{property.title}
</Link>
))}
</div>
</>
);
}

Index.getInitialProps = async ({ query, countryCode }) => {
// This is just an example to get all properties of a country from an API,
// it's on you the logic you have you fetch the data.
//
// The `properties` result will be something like this:
// [
// {title: 'Piso en Barcelona', slug: 'piso-en-barcelona'},
// {title: 'Casa en Madrid', slug: 'casa-en-madrid'},
// ]
const properties = await findAllProperties(countryCode);

return {
properties,
countryCode
};
};

export default Index;

The rendered html for this page will be similar to this (note the anchor urls):

realestate.es:

<h1>Realestate.es</h1>
<div>
<a href="/venta/piso-en-barcelona">Piso en Barcelona</a>
<a href="/venta/casa-en-madrid">Casa en Madrid</a>
</div>

realestate.co.uk:

<h1>Realestate.uk</h1>
<div>
<a href="/sale/flat-in-london">Flat in London</a>
<a href="/sale/house-in-bristol">House in Bristol</a>
</div>

Also, the global picture about the project will be something like this:

real-estate-project/
|- pages/ # the Next.js pages dir
| |- _app_.js
| |- property.js
| |- index.js
|- routes/ # the routes manifest dir
| |- es.js
| |- uk.js
|- routes.js # your routes context (provider and consumer)
|- server.js # your custom server

And this is it.