Guides / Building Search UI / Going further

Server-Side Rendering with Angular InstantSearch

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.

Did you find this page helpful?