Routing URLs
You are reading the documentation for Angular InstantSearch v3, which is in beta. You can find the v2 documentation here.
Overview
With the routing
prop, InstantSearch provides the necessary API entries to allow you to synchronize the state of your search UI (which widget were refined, the current search query, …) with any kind of storage. And most probably you want that storage to be the browser URL bar.
Synchronizing your UI with the browser URL is a good practice. It allows any of your users to take one of your result pages, copy paste the browser URL and send it to a friend. It also allows your users to use the back and next button of their browser and always end up where they were previously.
Basic URLs
For a quick start, you can activate the default behavior:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import * as algoliasearch from 'algoliasearch/lite';
@Component({
template: `
<ais-instantsearch [config]="config">
<ais-search-box></ais-search-box>
<ais-refinement-list attribute="brand"></ais-refinement-list>
<!-- more widgets -->
</ais-instantsearch>
`,
})
export class AppComponent {
config = {
indexName: 'demo_ecommerce',
searchClient: algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey'),
routing: true,
}
}
The resulting URL in your browser URL bar will look like this:
https://website.com/?query=a&refinementList%5Bbrand%5D%5B0%5D=Apple
While not being pretty it is still very accurate: the query is a
and the brand
attribute, which is a refinementList
, was refined (clicked) to Apple
. But if you want something custom and clean, let’s move on to more user friendly URLs.
User-friendly URLs
In this part, you will be able to make URLs that map more clearly the refinements. At the end, you will get URLs that look like that:
https://website.com/?query=a&brands=Sony~Samsung&page=2
We use the character ~ as it is one that is rarely present in data and renders well in URLs. This way your users will be able to read them more easily when shared via emails, documents, social media…
To do so, the routing
option accepts a simple boolean but also more complex objects to allow customization. The first customization option you want to use is stateMapping
. It allows you to define more precisely how the state of your search will be synchronized to your browser url bar (or any other router storage you might have).
Here’s an example achieving just that (and here’s the live version):
This example assumes that you have added the ais-search-box
, ais-refinement-list
and ais-pagination
widgets to your search UI. Then the ais-refinement-list
is activated on the brands
attribute and that there are no values in the brands
attribute which contain “~”. Please adjust given your own data.
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
config = {
indexName: 'demo_ecommerce',
searchClient: algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey'),
routing: {
stateMapping: {
stateToRoute(uiState) {
return {
q: uiState.query || '',
brands:
(uiState.refinementList &&
uiState.refinementList.brand &&
uiState.refinementList.brand.join('~')) ||
'all',
p: uiState.page || 1
};
},
routeToState(routeState) {
if (routeState.brands === 'all') routeState.brands = undefined;
return {
query: routeState.q,
refinementList: {brand: routeState.brands && routeState.brands.split('~')},
page: routeState.p
};
}
}
},
};
There’s a lifecycle in which when the stateMapping
functions are called:
stateToRoute
is called whenever widgets are refined (clicked). It is also called every time any widget needs to create a URL.routeToState
is called whenever the user loads, reloads the page or click on back/next buttons of the browser.
To build your own mapping easily, just console.log(uiState)
and see what you’re getting. Note that the object you return in stateToRoute
will be the one you’ll receive as an argument in routeToState
.
SEO-friendly URLs
URLs are more than just query parameters (with the question mark). Another important part is the path of the URL. In this part, you will end up with URLs that look like that:
https://website.com/search/q/phone/brands/Sony~Samsung/p/1
This format is better for SEO or aligns your search UI urls with your current sitemap and existing url scheme.
Example of implementation
Here’s an example achieving just that (and here’s the live version): First you will need to setup the routing for you angular app.
IndexComponent
is going to be our home pageSearchComponent
is going to be our search page
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
// app.module.ts
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { NgAisModule } from "angular-instantsearch";
import { RouterModule, Routes } from "@angular/router";
import { SearchComponent } from "./search/search.component";
import { IndexComponent } from "./index/index.component";
const appRoutes: Routes = [
{ path: "search/q/:query/brands/:brands/p/:page", component: SearchComponent },
{ path: "", component: IndexComponent }
];
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent, SearchComponent, IndexComponent],
imports: [
NgAisModule.forRoot(),
BrowserModule,
RouterModule.forRoot(appRoutes, { enableTracing: true })
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// search/search.component.ts
import { Component } from "@angular/core";
import { history as historyRouter } from "instantsearch.js/es/lib/routers";
@Component({
selector: "app-search",
templateUrl: "./search.component.html"
})
export class SearchComponent {
config = {
appId: "YourApplicationID",
apiKey: "YourSearchOnlyAPIKey",
indexName: "demo_ecommerce"
};
}
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
<!-- search/search.component.html -->
<ais-instantsearch [config]="config">
<div class="left-panel">
<ais-current-refinements></ais-current-refinements>
<h2>Brands</h2>
<ais-refinement-list attribute="brand"></ais-refinement-list>
<ais-configure [searchParameters]="{ hitsPerPage: 8 }"></ais-configure>
</div>
<div class="right-panel">
<ais-search-box></ais-search-box>
<ais-hits>
<ng-template let-hits="hits">
<ol class="ais-Hits-list">
<li *ngFor="let hit of hits" class="ais-Hits-item">
<img src="{{hit.image}}" alt="{{hit.name}}" align="left" />
<div class="hit-name">
<ais-highlight attribute="name" [hit]="hit"></ais-highlight>
</div>
<div class="hit-description">
<ais-highlight attribute="description" [hit]="hit"></ais-highlight>
</div>
<div class="hit-price">${{hit.price}}</div>
</li>
</ol>
</ng-template>
</ais-hits>
<ais-pagination></ais-pagination>
</div>
</ais-instantsearch>
Now, if you go under /search/q/phone/brands/apple/p/1
you should have a fully functional search. But we still need to sync the url with InstantSearch routeState.
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
import { Component } from "@angular/core";
import { history as historyRouter } from "instantsearch.js/es/lib/routers";
@Component({
selector: "app-search",
templateUrl: "./search.component.html"
})
export class SearchComponent {
config = {
appId: "YourApplicationID",
apiKey: "YourSearchOnlyAPIKey",
indexName: "demo_ecommerce",
routing: {
router: historyRouter({
windowTitle(routeState) {
return `Website / Find ${routeState.q} in ${
routeState.brands
} brands`;
},
createURL({ routeState, location }) {
let baseUrl = location.href.split(/\/search\/?/)[0];
if (
!routeState.q &&
routeState.brands === "all" &&
routeState.p === 1
)
return baseUrl;
if (baseUrl[baseUrl.length - 1] !== "/") baseUrl += "/";
let routeStateArray = [
"q",
encodeURIComponent(routeState.q),
"brands",
encodeURIComponent(routeState.brands),
"p",
routeState.p
];
return `${baseUrl}search/${routeStateArray.join("/")}`;
},
parseURL({ location }) {
let routeStateString = location.href.split(/\/search\/?/)[1];
if (routeStateString === undefined) return {};
const routeStateValues = routeStateString.match(
/^q\/(.*?)\/brands\/(.*?)\/p\/(.*?)$/
);
return {
q: decodeURIComponent(routeStateValues[1]),
brands: decodeURIComponent(routeStateValues[2]),
p: routeStateValues[3]
};
}
}),
stateMapping: {
stateToRoute(uiState) {
return {
q: uiState.query || "",
brands:
(uiState.refinementList &&
uiState.refinementList.brand &&
uiState.refinementList.brand.join("~")) ||
"all",
p: uiState.page || 1
};
},
routeToState(routeState) {
if (routeState.brands === "all") routeState.brands = undefined;
return {
query: routeState.q,
refinementList: {
brand: routeState.brands && routeState.brands.split("~")
},
page: routeState.p
};
}
}
}
};
}
As you can see, we are now using the historyRouter
so that we can explicitly set options on the default router mechanism used in the previous example. What we see also is that both the router
and stateMapping
options can be used together as a way to easily map uiState
to routeState
and vice versa.
Using that we can configure:
windowTitle
: This method can be used to map the object (routeState
) returned fromstateToRoute
to your window titlecreateURL
: This method is called every time we need to create a url. When we want to synchronize therouteState
to the browser url bar, when we want to render<a href>
tags in themenu
widget, or when you callcreateURL
in one of your connectors’ rendering methodparseURL
: This method is called every time the user loads and reloads the page, or clicks on the back/next buttons of the browser
About SEO
For your search results to be part of search engines results, you will have to selectively choose them. Trying to have all of your search results inside search engines could be considered as spam by them.
To do that, you can create a robots.txt
and host it at https://website.com/robots.txt
.
Here’s an example one based on the url scheme we created.
1
2
3
4
5
User-agent: *
Allow: /search/q/phones/brands/Samsung/p/1
Allow: /search/q/phones/brands/Apple/p/1
Disallow: /search/
Allow: *