Server-Side Rendering with Angular InstantSearch
On this page
This is an advanced guide. If you’ve never used Angular InstantSearch, you should follow the getting-started first.
You can find the result of this guide on the Angular InstantSearch repository.
Angular InstantSearch is compatible with server-side rendering. This guide illustrates how to implement it with
@angular/universal
.
How server-side rendering works
Server-side rendering uses the TransferState
modules from
@angular/platform-browser
and
@angular/platform-server
.
This module caches the first request your server sends to Algolia to avoid re-triggering it when the Angular application starts on the client.
Set up an empty server-side rendering application
First, you need to generate a clean Angular app and enable server-side rendering.
1
2
ng new server-side-rendering
cd server-side-rendering && ng add @nguniversal/express-engine
Run the following command to ensure everything works. You can also inspect the source code of the page to check if the app was indeed rendered on the server, not just on the client.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
npm build:ssr && npm serve:ssr
# or
yarn build:ssr && yarn serve:ssr
## Install Angular InstantSearch
The goal is to have a working client-side implementation of Angular InstantSearch.
**Install `angular-instantsearch` and its peer dependencies**
<!-- vale Vale.Terms = NO -->
```sh
npm install angular-instantsearch@4 instantsearch.js@4 algoliasearch@4
Import NgAisModule
into the main application module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// server-side-rendering/src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
+import { NgAisModule } from "angular-instantsearch";
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
AppRoutingModule,
+ NgAisModule.forRoot(),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Create and expose the configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Component } from '@angular/core';
+import algoliasearch from 'algoliasearch/lite';
+import { InstantSearchOptions } from "instantsearch.js/es/types";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'server-side-rendering';
+ config: InstantSearchOptions;
+ constructor() {
+ this.config = {
+ searchClient: algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey')
+ indexName: 'YourIndexName',
+ };
+ }
}
Add some minimal markup and styles
1
2
3
4
5
6
<!-- in server-side-rendering/src/app/app.component.html -->
<router-outlet></router-outlet>
<ais-instantsearch [config]="config" >
<ais-search-box></ais-search-box>
<ais-hits></ais-hits>
</ais-instantsearch>
1
2
/* server-side-rendering/src/styles.css */
@import '~angular-instantsearch/bundles/instantsearch-theme-algolia.css';
Enable server-side rendering
Import TransferState
and HTTPClient
modules
These modules help you cache HTTP requests that the server sends to Algolia. This avoids duplicate requests during client-side hydration.
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
// server-side-rendering/src/app/app.module.ts
import { NgModule } from '@angular/core';
-import { BrowserModule } from '@angular/platform-browser';
+import { BrowserModule, BrowserTransferStateModule } from "@angular/platform-browser";
+import { HttpClientModule } from '@angular/common/http';
import { NgAisModule } from "angular-instantsearch";
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
+ BrowserTransferStateModule,
+ HttpClientModule,
AppRoutingModule,
NgAisModule.forRoot(),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// in server-side-rendering/src/app/app.server.module.ts
import { NgModule } from '@angular/core';
-import { ServerModule } from '@angular/platform-server';
+import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
+ ServerTransferStateModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
Create a server-side rendering capable search client
The createSSRSearchClient
function wraps the Algolia API client. It enables caching and state transfer from server to client.
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
// server-side-rendering/src/app/app.component.ts
import { Component } from '@angular/core';
import { InstantSearchConfig } from "instantsearch.js/es/types";
+import { TransferState, makeStateKey } from '@angular/platform-browser';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
-import algoliasearch from 'algoliasearch/lite';
+import { createSSRSearchClient } from 'angular-instantsearch';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
title = 'server-side-rendering';
config: any;
constructor(
+ private httpClient: HttpClient,
+ private transferState: TransferState,
) {
this.config = {
- searchClient: algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey')
+ searchClient: createSSRSearchClient({
+ appId: 'YourApplicationID',
+ apiKey: 'YourSearchOnlyAPIKey',
+ makeStateKey,
+ HttpHeaders,
+ transferState: this.transferState,
+ httpClient: this.httpClient,
+ }),
indexName: 'YourIndexName'
};
}
}
Let the server perform and persist search requests
You need to parse the URL of the request that the server receives so that you can infer the state of your search.
For parsing the URL, you can use
qs
.
1
2
3
npm install qs
# or
yarn add qs
Create a new file server-side-rendering/src/app/ssrRouter.ts
that exposes a custom router function ssrRouter
.
This is roughly a server compatible version of the router that uses the /doc/api-reference/widgets/history-router/js/ function from
InstantSearch.js.
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
// server-side-rendering/src/app/ssrRouter.ts
import * as qs from 'qs';
export function ssrRouter(readUrl: () => string) {
return {
read() {
const url = readUrl();
return qs.parse(url.slice(url.lastIndexOf('?') + 1), {
arrayLimit: 99,
});
},
write(routeState) {
if (typeof window === 'undefined') return;
const url = this.createURL(routeState);
const title = this.windowTitle && this.windowTitle(routeState);
if (this.writeTimer) {
window.clearTimeout(this.writeTimer);
}
this.writeTimer = window.setTimeout(() => {
if (window.location.href !== url) {
window.history.pushState(routeState, title || '', url);
}
this.writeTimer = undefined;
}, this.writeDelay);
},
createURL(routeState) {
const url = new URL(readUrl(), 'https://localhost');
const queryString = qs.stringify(routeState, { arrayLimit: 99 });
url.search = queryString;
return url.pathname + url.search + url.hash;
},
onUpdate(cb) {
if (typeof window === 'undefined') return;
this._onPopState = event => {
const routeState = event.state;
// On initial load, the state is read from the URL without
// update. Therefore, the state object isn't present. In this
// case, we fallback and read the URL.
if (!routeState) {
cb(this.read());
} else {
cb(routeState);
}
};
window.addEventListener('popstate', this._onPopState);
},
dispose() {
if (typeof window === 'undefined') return;
if (this.writeTimer) {
window.clearTimeout(this.writeTimer);
}
window.removeEventListener('popstate', this._onPopState);
},
};
}
Now use this ssrRoute
function to replace the router
.
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
// server-side-rendering/src/app/app.component.ts
-import { Component } from '@angular/core';
+import { Component, Inject, Optional } from '@angular/core';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { createSSRSearchClient } from 'angular-instantsearch';
import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { simple } from 'instantsearch.js/es/lib/stateMappings';
+import { ssrRouter } from './ssrRouter';
+import { REQUEST } from '@nguniversal/express-engine/tokens';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
title = 'server-side-rendering';
config: any;
constructor(
private httpClient: HttpClient,
private transferState: TransferState,
+ @Optional() @Inject(REQUEST) protected request: Request
) {
this.config = {
searchClient: createSSRSearchClient({
makeStateKey,
HttpHeaders,
appId: 'YourApplicationID',
apiKey: 'YourSearchOnlyAPIKey',
transferState: this.transferState,
httpClient: this.httpClient,
}),
indexName: 'YourIndexName',
+ routing: {
+ router: ssrRouter(() => {
+ if (this.request) {
+ // request is only defined on the server side
+ return this.request.url;
+ }
+ return window.location.pathname + window.location.search;
+ }),
+ stateMapping: simple(),
+ },
};
}
}
You’re now ready to build and test server-side rendering.
1
2
> npm run build:ssr && npm run serve:ssr
> open http://localhost:4000
If the app runs without errors, it means server-side rendering is working. You can now add more Angular InstantSearch widgets on your search page component and run:
You have now fully universal Angular InstantSearch application running on your server and browser! If you want to run the application directly we provide a complete example that you can find on the angular-instantsearch GitHub repository.