Routing URLs
On this page
You are currently reading the documentation for InstantSearch.js V3. Read our migration guide to learn how to upgrade from V2 to V3. You can still find the V2 documentation here.
Overview
InstantSearch.js provides the necessary API entries to let you synchronize the state of your search UI (e.g., refined widgets, current search query) with any kind of storage. This is possible with the routing
option. This guide focuses on storing the UI state in the browser URL.
Synchronizing your UI with the browser URL is a good practice. It allows your users to take one of your results pages, copy the URL, and share it. It also improves the user experience by enabling the use of the back and next browser buttons to keep track of previous searches.
By the end of this guide, you will be able to reproduce these examples:
Basic URLs
InstantSearch.js provides a basic way to activate the browser URL synchronization with the routing
option set to true
. You can find a live example on this sandbox.
1
2
3
4
5
const search = instantsearch({
searchClient,
indexName: 'instant_search',
routing: true
});
Assume the following search UI state:
- Query: “galaxy”
- Menu:
categories
: “Cell Phones”
- Refinement List:
brand
: “Apple”, “Samsung”
- Page: 2
The resulting URL in your browser URL bar will look like this:
1
https://website.com/?menu[categories]=Cell Phones&refinementList[brand][0]=Apple&refinementList[brand][1]=Samsung&page=2&query=galaxy
This URL is accurate, and can be translated back to a search UI state. However, we’ll see in the next section how to make it more SEO-friendly.
SEO-friendly URLs
URLs are more than query parameters. Another important part is the path. Manipulating the URL path is a common e-commerce pattern that allows you to better reference your page results. In this section, you’ll learn how to create this kind of URLs:
1
https://website.com/search/Cell+Phones/?query=galaxy&page=2&brands=Apple&brands=Samsung
Example of implementation
Here’s an example storing the brand in the path name, and the query and page as query parameters. You can find a live example on this sandbox.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// Returns a slug from the category name.
// Spaces are replaced by "+" to make
// the URL easier to read and other
// characters are encoded.
function getCategorySlug(name) {
return name
.split(' ')
.map(encodeURIComponent)
.join('+');
}
// Returns a name from the category slug.
// The "+" are replaced by spaces and other
// characters are decoded.
function getCategoryName(slug) {
return slug
.split('+')
.map(decodeURIComponent)
.join(' ');
}
const search = instantsearch({
searchClient,
indexName: 'instant_search',
routing: {
router: instantsearch.routers.history({
windowTitle({ category, query }) {
const queryTitle = query ? `Results for "${query}"` : 'Search';
if (category) {
return `${category} – ${queryTitle}`;
}
return queryTitle;
},
createURL({ qsModule, routeState, location }) {
const urlParts = location.href.match(/^(.*?)\/search/);
const baseUrl = `${urlParts ? urlParts[1] : ''}/`;
const categoryPath = routeState.category
? `${getCategorySlug(routeState.category)}/`
: '';
const queryParameters = {};
if (routeState.query) {
queryParameters.query = encodeURIComponent(routeState.query);
}
if (routeState.page !== 1) {
queryParameters.page = routeState.page;
}
if (routeState.brands) {
queryParameters.brands = routeState.brands.map(encodeURIComponent);
}
const queryString = qsModule.stringify(queryParameters, {
addQueryPrefix: true,
arrayFormat: 'repeat'
});
return `${baseUrl}search/${categoryPath}${queryString}`;
},
parseURL({ qsModule, location }) {
const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
const category = getCategoryName(
(pathnameMatches && pathnameMatches[1]) || ''
);
const { query = '', page, brands = [] } = qsModule.parse(
location.search.slice(1)
);
// `qs` does not return an array when there's a single value.
const allBrands = Array.isArray(brands)
? brands
: [brands].filter(Boolean);
return {
query: decodeURIComponent(query),
page,
brands: allBrands.map(decodeURIComponent),
category
};
}
}),
stateMapping: {
stateToRoute(uiState) {
return {
query: uiState.query,
page: uiState.page,
brands: uiState.refinementList && uiState.refinementList.brand,
category: uiState.menu && uiState.menu.categories
};
},
routeToState(routeState) {
return {
query: routeState.query,
page: routeState.page,
menu: {
categories: routeState.category
},
refinementList: {
brand: routeState.brands
}
};
}
}
}
});
We are now using the instantsearch.routers.history
to explicitly set options on the default router mechanism used in the previous example. You can notice that we use both the router
and stateMapping
options to map uiState
to routeState
, and vice versa.
Using the routing
option as an object, we can configure:
windowTitle
: a method to map therouteState
object returned fromstateToRoute
to the window title.createURL
: a method called every time we need to create a URL. When:- we want to synchronize the
routeState
to the browser URL, - we want to render
a
tags in themenu
widget, - you call
createURL
in one of your connectors’ rendering methods.
- we want to synchronize the
parseURL
: a method called every time the user loads or reloads the page, or clicks on the back or next buttons of the browser.
Making URLs more discoverable
In real-life applications, you might want to make some categories more easily accessible, with a URL that’s easier to read and to remember.
Given our dataset, we can make some categories more discoverable:
- “Cameras & Camcorders” →
/Cameras
- “Car Electronics & GPS” →
/Cars
- etc.
In this example, anytime the users visits https://website.com/search/Cameras
, it pre-selects the “Cameras & Camcorders” filter.
You can achieve this with a dictionary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Step 1. Add the dictionaries to convert the names and the slugs
const encodedCategories = {
Cameras: 'Cameras & Camcorders',
Cars: 'Car Electronics & GPS',
Phones: 'Cell Phones',
TV: 'TV & Home Theater'
};
const decodedCategories = Object.keys(encodedCategories).reduce((acc, key) => {
const newKey = encodedCategories[key];
const newValue = key;
return {
...acc,
[newKey]: newValue
};
}, {});
// Step 2. Update the getters to use the encoded/decoded values
function getCategorySlug(name) {
const encodedName = decodedCategories[name] || name;
return encodedName
.split(' ')
.map(encodeURIComponent)
.join('+');
}
function getCategoryName(slug) {
const decodedSlug = encodedCategories[slug] || slug;
return decodedSlug
.split('+')
.map(decodeURIComponent)
.join(' ');
}
Note that these dictionaries can come from your Algolia records.
With such a solution, you have full control over what categories are discoverable via the URL.
About SEO
For your search results to be part of search engines results, you have to be selective. Adding too many search results inside search engines could be considered as spam.
To do that, you can create a robots.txt
and host it at https://website.com/robots.txt
.
Here’s an example based on the URL scheme we created.
1
2
3
4
5
User-agent: *
Allow: /search/Audio/
Allow: /search/Phones/
Disallow: /search/
Allow: *
References
instantsearch.routers.history
API
InstantSearch.js provides a default router under instantsearch.routers.history
. You can use it when you want to go futher than just aliasing query string parameters in the URL. For example, if you want to generate URLs like https://website.com/search/q/phone/brands/Sony~Samsung/p/1
.
The signature is the following: history({ windowTitle, createURL, parseURL, writeDelay })
.
windowTitle: function(routeState): string
This function allows you to dynamically customize the window title based on the provided routeState
.
This function is called every time the user refines the UI, after the history timer.
createURL: function({ qsModule, location, routeState }): string
This function allows you to directly change the format of URLs that will be created and rendered to the browser URL bar or widgets.
This function is called every time InstantSearch.js needs to create a URL. The provided options are:
qsModule
(object): a query string that parses and stringifies modules (see documentation). We use it internally, so we provide it to you as a convenience.location
(object): an alias towindow.location
.routeState
(objec): therouteState
created by the providedstateMapping
. When absent, this is an untoucheduiState
.
parseURL: function({qsModule, location}): object
This function is responsible for parsing back the URL string to a routeState
. This function must be customized if you customized the createURL
function.
This function is called every time the user loads or reloads, or clicks on the back or next buttons of the browser. The provided options are:
qsModule
(object): a query string that parses and stringifies modules (see documentation). We use it internally, so we provide it to you as a convenience.location
(function): an alias towindow.location
.
writeDelay: number = 400
This option controls the number of milliseconds to wait before writing the new URL to the browse URL bar. You can think about it this way: “400ms after the last user action, let’s save it to the browser URL bar”. Which helps in reducing:
- The number of different history entries. If you type “phone”, you don’t want to have 5 history entries and thus have to click 5 times on the back button to go back to the previous search state
- The performance overhead of updating the browser URL bar too often. We have seen recurring but hard to track performance issues of updating the browser URL bar too often due to a lot of browser extensions reacting to it
400ms is a good guesstimate from our experience to consider a user action “done” and thus save it to the URL.
uiState
object reference
The routeState
object shape is completely up to you and thus not part of any public API.
But the uiState
object is created by InstantSearch.js internally and thus part of a public API. Every widget inside the library has its own way of updating it. Here’s a complete uiState
of all widgets. So that you can easily see, given the widgets you use, what you will receive:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const uiState = {
query: 'Hill Valley',
refinementList: {
colors: ['white', 'black']
},
menu: {
category: 'Decoration'
},
hierarchicalMenu: {
category: ['Decoration > Clocks']
},
numericMenu: {
price: '100:500'
},
ratingMenu: {
rating: 4
},
range: {
ageInYears: '2:10'
},
toggle: {
freeShipping: true
},
geoSearch: {
boundingBox: '47.3165,4.9665,47.3424,5.0201'
},
sortBy: 'most_popular_index',
page: 2,
hitsPerPage: 20
};