Routing and Locale Prefix
Terminologies
Before going further, we'll define a few terminologies that we'll use throughout this section:
path— URL segment that containsroutePrefix,pathname, andquerystringroutePrefix— Language and country identifier in the url, e.g.id-id.pathname— Relevant URL segment that defines a unique screen (e.g./activities), not including fragment and querystringroute— Route information as encoded in file structure, this can refer to both static route (whererouteequalspathname, e.g:/flight), or dynamic route, e.g:/discovery/_dlp.params— dynamic segment for specificroute.query— Query string for givenpath, parsed as an object
Note that our terminology is incompatible with the way Next.js defines query where they use it to refer to both query string and params. Here we explicitly define the difference between a query and a params. Those definitions also differs to how browser parses URL: pathname in our case always excludes routePrefix.
Getting the current route
The useCurrentRoute hook
To access current URL that works universally in server and browser we provide useCurrentRoute hooks. It returns UrlWithRoutePrefix.
import { useCurrentRoute } from '@traveloka/core';
import { Anchor } from '@traveloka/web-components/future';
function Component() {
const { pathname, query } = useCurrentRoute();
if (query.new === 'true') {
return <NewLink />;
}
// pathname will have no locale information, so this logic works
// regardless of user prefered locale
const isActive = pathname === '/flight/search';
return (
<Anchor style={{ color: isActive ? 'red' : 'grey' }}>Click me!</Anchor>
);
}
Accessing dynamic route segments
You can also get dynamic route segment using the useCurrentRoute hook. For example, if you have /activities/[country]/product/[nameId] as your route, you can access it inside params:
import { useCurrentRoute } from '@traveloka/core';
import { Text } from '@traveloka/web-components';
function Component() {
const { pathname, params } = useCurrentRoute();
// Will print
// '/activities/indonesia/product/jakarta-aquarium-indonesia-tickets-easy-access-2001453539314'
console.log(pathname);
return (
<>
{/* renders indonesia */}
<Text>{params.country}</Text>
{/* renders jakarta-aquarium-indonesia-tickets-easy-access-2001453539314 */}
<Text>{params.nameId}</Text>
</>
);
}
Usage in data-fetching functions
It's exposed as the rawAppContext argument (the second argument of your data-fetching function).
(1) staticProps example
export const getStaticProps = staticProps({
path: '/flight/trpc-playground',
resources: [],
async getStaticProps(ctx, rawAppContext) {
const { currentRoute } = rawAppContext;
const { pathname, params } = currentRoute;
return {};
},
});
(2) getInitialProps example
Page.getInitialProps = async (ctx, rawAppContext) => {
const { currentRoute } = rawAppContext;
const { pathname, params } = currentRoute;
return {};
};
Generating URLs
There are multiple ways to generate URL, depending on how you want user to navigate. Most of them share common API in their arguments/props.
Client-side navigation
If you want to do client-side navigation, there are 2 API: (1) useNextRouter for imperative API (currently preferred by engineers) and (2) LocalizedLink for declarative API. But first, you need to consider whether your route is a static route or dynamic route.
(1) Static route client-side navigation
The simplest way is when you have static route, you can simply pass route arg to either LocalizedLink or useNextRouter.
// using LocalizedLink
import { LocalizedLink } from '@traveloka/core';
import { Text } from '@traveloka/web-components';
function Component() {
return (
<LocalizedLink route="/sample">
<Text>Navigate</Text>
</LocalizedLink>
);
}
// using useRouter
import Router from 'next/router';
import { useNextRouter } from '@traveloka/core';
import { Button } from '@traveloka/web-components';
function Component() {
const router = useNextRouter();
const handleNavigate = () => {
const [href, asPath] = router({
route: '/sample',
});
Router.push(href, asPath);
};
return <Button onPress={handleNavigate} text="Navigate" />;
}
(2) Dynamic route client-side navigation
Using dynamic route is the same as how next.js uses dynamic route. You can then use params to generate correct URL to be displayed in the browsers.
Let's say you want to navigate to /activities/[country]/detail/[nameId] and /activities/indonesia/detail/xxx as resulting URL.
// using useRouter
import { useRouter } from '@traveloka/core';
import { Button } from '@traveloka/web-components';
function Component() {
const router = useRouter();
const handleNavigate = () => {
const [href, asPath] = router({
route: `/activities/[country]/detail/[nameId]`,
params: {
country: 'indonesia',
nameId: 'xxx',
},
});
Router.push(href, asPath);
};
return <Button onPress={handleNavigate} text="Navigate" />;
}
// using LocalizedLink
import { LocalizedLink } from '@traveloka/core';
import { Text } from '@traveloka/web-components';
function Component() {
const route = `/activities/[country]/detail/[nameId]`;
const params = {
country: 'indonesia',
nameId: 'xxx',
};
return (
<LocalizedLink route={route} params={params}>
<Text>Navigate</Text>
</LocalizedLink>
);
}
If you want to add query string, you can do so by passing query field.
<LocalizedLink route={route} query={{ key: 'value' }} />;
// or
route({ route, query: { key: 'value' } });
This also works for dynamic route as well.
Server-side navigation
Server side navigation means when you click a link or perform an action, it's actually navigating to the URL and asks the server to render the content.
You should only use this if you navigate to pages outside your product domain. This is currently the limitation of our microservice infrastructure where you can't navigate between services in client-side, but maybe in the future.
useLocalizedRouter
It returns a function that accepts (1) path as the 1st parameter, and (2) an optional 2nd parameter to customize URL generation with custom routePrefix, params or query.
Using this method, you can use both Next.js dynamic route and express dynamic route syntax to generate the url because it's a server-side navigation.
import React from 'react';
import { useLocalizedRouter } from '@traveloka/core';
function Links() {
const route = useLocalizedRouter();
const hotelSeoUrl = route('/hotel/search/:searchId', {
params: {
searchId: 'xxx',
},
query: {
utm_source: 'home',
},
});
return (
<>
<a href={route('/')}>Homepage</a>
<a href={route('/flight')}>Flight Homepage</a>
<a href={route('/hotel')}>Hotel Homepage</a>
<a href={hotelSeoUrl}>Hotel SEO</a>
</>
);
}
Parsing URLs from backend
Use parseURL if you want to parse a full URL from your backend API for navigation purposes.
This accepts the full URL string that contain the the host (failing to do so will throw an error), and returns UrlWithRoutePrefix so you can use pathname directly without having to worry about the route prefix.
Note that this differs to how browser URL.parse works, but the end result is similar.
import React from 'react';
import { parseURL, LocalizedLink } from '@traveloka/core';
function Component(props) {
// Assuming `props.url` comes from API
const { pathname, query } = parseURL(props.url);
return (
<LocalizedLink route={pathname} query={query}>
<a>{props.text}</a>
</LocalizedLink>
);
}
If you want to get full path (including route prefix and query string), you can use the path property instead.
import React from 'react';
import { parseURL } from '@traveloka/core';
function Component(props) {
const { path } = parseURL(props.url);
return <a href={path}>{props.text}</a>;
}
You can also use this function for other purposes as you see fit.
Definition of UrlWithRoutePrefix
Here's the full type definition of UrlWithRoutePrefix
interface UrlWithRoutePrefix {
path: string;
pathname: string;
routePrefix?: string;
params: Record<string, string>;
// these only work inside pages with getServerSideProps and getInitialProps
// or in client-side (effect, event handler, navigation)
query: Record<string, string | string[]>;
host: string;
hostname: string;
protocol: string;
port: string;
}
Multi language locale
There are several locales that serve different regions and languages. It also impacts what content is displayed to the user based on their language and currency preferences. For example, our app does not recognize 'zh-hk' as a valid locale. Instead, it maps to 'zh-tw' with 'HKD' currency. But on the backend, they are only recognized as 'zh-hk'. So we need to ensure that the correct locale is used throughout the web.
We can use the resolveLanguageTag function to help with this. This function takes a language tag as input and returns the corresponding multi-language locale. This helps us to translate language tags provided by the backend into the correct version used in the web before it being used or stored.
import { resolveLanguageTag } from '@traveloka/core';
const rawLanguageTag = 'zh-hk';
const resolvedLanguageTag = resolveLanguageTag(rawLanguageTag);