Concepts / Building Search UI / Server-side rendering
Aug. 08, 2019

Server-side Rendering

You are reading the documentation for Angular InstantSearch v3, which is in beta. You can find the v2 documentation here.

Overview

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, starting from Angular 5. We provide an API that’s easy to use with @angular/universal modules.

For simplicity we are going to use the @angular/universal-starter boilerplate which is a minimal Angular starter for Universal JavaScript using TypeScript and Webpack.

How it works?

The server-side rendering uses two concepts from @angular/universal modules:

  • TransferState: Will cache the first request made to Algolia from your server in order to avoid replicating it when the Angular application starts on the client side.
  • preboot: Will avoid the first rendering of your Angular application on the client side and will start it from the HTML markup sent by the server.

In order to assemble all the pieces you will need to write some code in your own application and instantiate Angular InstantSearch. Let’s dive into the code!

Setup

First, clone the @angular/universal-starter boilerplate:

1
2
3
git clone git@github.com:angular/universal-starter.git [your-app-name]
cd [your-app-name]
npm install

Then, add all the necessary dependencies: preboot, Angular InstantSearch, and InstantSearch.js, which Angular InstantSearch depends on:

1
npm install preboot angular-instantsearch@beta instantsearch.js@^3.0.0

Now you have all the requirements to start developing your universal Angular InstantSearch application!

1. Switch to Webpack for compiling (only required for Angular 6+)

Up until Angular 5, the Angular Universal Starter used Webpack to compile the server code. Angular 6+ versions started using TypeScript only. In our example, we still need to use Webpack. If you are running this example with Angular 6+, do the following:

1. Create a file called webpack.server.config.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
// webpack.server.config.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'none',
  entry: {
    // This is our Express server for Dynamic universal
    server: './server.ts',
    // This is an example of static pre-rendering (generative)
    prerender: './prerender.ts'
  },
  target: 'node',
  resolve: { extensions: ['.ts', '.js'] },
  // Make sure we include all `node_modules`, etc.
  externals: [/node_modules/],
  output: {
    // Sets the output at the root of the `dist` folder
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
  },
  plugins: [
    new webpack.ContextReplacementPlugin(
      // fixes error: "WARNING Critical dependency: the request of a dependency is an expression"
      /(.+)?angular(\\|\/)core(.+)?/,
      path.join(__dirname, 'src'), // location of your src
      {} // a map of your routes
    ),
    new webpack.ContextReplacementPlugin(
      // fixes error: 'WARNING Critical dependency: the request of a dependency is an expression'
      /(.+)?express(\\|\/)(.+)?/,
      path.join(__dirname, 'src'),
      {}
    )
  ]
};

2. In your server.ts and prerender.ts files, update the path of your server app:

Replace this line:

1
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./server/main');

with this line:

1
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');

3. Install Webpack

1
npm install -D webpack-cli webpack

4. In your package.json file, change the compile:server command into:

1
"compile:server": "webpack --config webpack.server.config.js --progress --colors",

5. Run yarn build:ssr && yarn serve:ssr to ensure that everything still works.

2. Angular Universal modules

Once you’ve installed the dependencies you will need to add the TransferState, preboot and HttpClient modules into src/app/app.module.ts:

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
// src/app.module.ts

import {
  BrowserModule,
  BrowserTransferStateModule, // Add this line
} from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http'; // Add this line
import { PrebootModule } from 'preboot'; // Add this line
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { TransferHttpCacheModule } from '@nguniversal/common';
import { NgAisModule } from 'angular-instantsearch';

@NgModule({
  declarations: [AppComponent, HomeComponent],
  imports: [
    BrowserModule.withServerTransition({ appId: 'my-app' }),
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },
      { path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule' },
      { path: 'lazy/nested', loadChildren: './lazy/lazy.module#LazyModule' },
    ]),
    TransferHttpCacheModule,
    PrebootModule.withConfig({ appRoot: 'app-root' }), // Add this line
    BrowserTransferStateModule, // Add this line
    HttpClientModule // Add this line
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

And voilà, you have the requirements and your are now ready to plug Angular InstantSearch into your universal Angular application!

3. Transfer the request object to your server-side Angular Application

In order to get the query of the client request into your Angular application you need to provide the original request object you receive into the express server. Open ./server.ts and replace this block:

1
2
3
4
5
6
app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

By this one:

1
2
3
4
5
6
7
8
9
10
app.engine('html', (_, options, callback) => {
  const engine = ngExpressEngine({
    bootstrap: AppServerModuleNgFactory,
    providers: [
      { provide: 'request', useFactory: () => options.req, deps: [] },
      provideModuleMap(LAZY_MODULE_MAP)
    ]
  });
  engine(_, options, callback);
});

Now on server-side rendering we can have access to the request object by using the injector. We will see how to do that in the next chapter.

4. Introducing Angular InstantSearch

First, you need to import the Angular InstantSearch module into your application like you will do in any Angular application. (If you don’t know how to do this, please read the following part in the getting started guide). The only difference is on how you configure <ais-instantsearch> component.

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
// src/app.module.ts

import {
  BrowserModule,
  BrowserTransferStateModule,
} from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { PrebootModule } from 'preboot';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { TransferHttpCacheModule } from '@nguniversal/common';
import { NgAisModule } from 'angular-instantsearch'; // Add this line

@NgModule({
  declarations: [AppComponent, HomeComponent],
  imports: [
    BrowserModule.withServerTransition({ appId: 'my-app' }),
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },
      { path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule' },
      { path: 'lazy/nested', loadChildren: './lazy/lazy.module#LazyModule' },
    ]),
    TransferHttpCacheModule,
    PrebootModule.withConfig({ appRoot: 'app-root' }),
    BrowserTransferStateModule,
    HttpClientModule,
    NgAisModule.forRoot(), // Add this line
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

This will be our starting component. For simplicity you can re-use the Home component from the universal starter boilerplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/home/home.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'home',
  template: `
    <ais-instantsearch [config]="config">
    </ais-instantsearch>
  `
})
export class HomeComponent {
  public config: any;

  constructor() {
    this.config = {
      appId: "latency",
      apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
      indexName: "instant_search"
    }
  }
}

We now need to import the TransferState, HttpClient, Injector and PLATFORM_ID components into our constructor. Let’s update the component’s code.

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
// src/home/home.component.ts

import { Component, Inject, Injector, PLATFORM_ID } from '@angular/core';
import {
  createSSRSearchClient,
  parseServerRequest,
} from 'angular-instantsearch';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformServer } from '@angular/common';

@Component({
  selector: 'app-home',
  template: `
    <ais-instantsearch [config]="config">
      <ais-search-box></ais-search-box>
      <ais-hits>
        <ng-template let-hits="hits">
          <ol class="ais-Hits-list">
            <li *ngFor="let hit of hits; index as i" class="ais-Hits-item">
              <div class="hit-name">
                <ais-highlight attribute="name" [hit]="hit"></ais-highlight>
              </div>
            </li>
          </ol>
        </ng-template>
      </ais-hits>
    </ais-instantsearch>
  `,
})
export class HomeComponent {
  public config: any;
  constructor(
    private httpClient: HttpClient,
    private transferState: TransferState,
    private injector: Injector,
    @Inject(PLATFORM_ID) private platformId: Object
  ) {
    const req = isPlatformServer(this.platformId)
      ? this.injector.get('request')
      : undefined;

    const searchParameters = parseServerRequest(req);

    const searchClient = createSSRSearchClient({
      makeStateKey,
      HttpHeaders,
      transferState: this.transferState,
      httpClient: this.httpClient,
      appId: 'latency',
      apiKey: '6be0576ff61c053d5f9a3225e2a90f76',
    });

    this.config = {
      searchParameters,
      indexName: 'instant_search',
      searchClient,
      routing: true
    };
  }
}

Note that we also updated config with the modules we provided to angular-instantsearch, so we can make Algolia API requests on the server:

5. Wrapping things up

Congratulations! You can now add more Angular InstantSearch widgets on your search page component and run:

1
2
> npm run build:ssr && npm run serve:ssr
> open http://localhost:4000

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?