Guides / Building Search UI / Going further

Server-Side Rendering with Vue InstantSearch

You’re reading the documentation for Vue InstantSearch v4. Read the migration guide to learn how to upgrade from v3 to v4. You can still find the v3 documentation for this page.

Introduction

Server-side rendering (SSR) is a technique used to render results of a client-side framework in a server language.

Here are the main steps to implementing server-side rendering when you’re making external requests (here to Algolia):

On the server:

  1. Make a request to Algolia to get search results
  2. Render the Vue app with the results of the request
  3. Store the search results in the page
  4. Return the HTML page as a string

On the client:

  1. Read the search results from the page
  2. Render (or hydrate) the Vue app with the search results

There are different ways of making a server-side rendered app with Vue. We’ll cover how to do it with Vue CLI and with Nuxt.

The following guide is for Vue 2 and Vue InstantSearch v4. The flow is similar to Vue 3, and you can find examples in the Vue InstantSearch repository on GitHub with Vue CLI and with Vite.

With Vue CLI

First, you need to generate a Vue app with Vue CLI, then add the SSR plugin:

1
2
3
4
vue create algolia-ssr-example
cd algolia-ssr-example
vue add router
vue add @akryum/ssr

You can then start the development server by running npm run ssr:serve.

The next step is to install Vue InstantSearch:

1
npm install vue-instantsearch algoliasearch

We now need to build a Vue InstantSearch implementation without fetching data on the back end. For that, we install the plugin in src/main.js:

1
2
3
import VueInstantSearch from 'vue-instantsearch';

Vue.use(VueInstantSearch);

Then, we create a new page (src/views/Search.vue) and build a search interface:

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
<template>
  <ais-instant-search :search-client="searchClient" index-name="instant_search">
    <ais-search-box />
    <ais-stats />
    <ais-refinement-list attribute="brand" />
    <ais-hits>
      <template v-slot:item="{ item }">
        <p>
          <ais-highlight attribute="name" :hit="item" />
        </p>
        <p>
          <ais-highlight attribute="brand" :hit="item" />
        </p>
      </template>
    </ais-hits>
    <ais-pagination />
  </ais-instant-search>
</template>

<script>
import algoliasearch from 'algoliasearch/lite';
const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

export default {
  data() {
    return {
      searchClient,
    };
  },
};
</script>

We then add a route to this page in src/router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import Search from './views/Search.vue';

Vue.use(Router);

export function createRouter() {
  return new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    routes: [
      /* ... */
      {
        path: '/search',
        name: 'search',
        component: Search,
      },
    ],
  });
}

Also, we update the header in src/App.vue:

1
2
3
4
5
6
7
8
9
10
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/search">Search</router-link>
    </div>
    <router-view />
  </div>
</template>

For styling, we use instantsearch.css in public/index.html:

1
2
3
4
5
6
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css"
  integrity="sha256-TehzF/2QvNKhGQrrNpoOb2Ck4iGZ1J/DI4pkd2oUsBc="
  crossorigin="anonymous"
/>

Our InstantSearch code uses ES modules, yet it needs to be executed in Node.js. For that reason, we need to let Vue CLI know that those files should be transpiled for a Node usage. For that, we add the following configuration to vue.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
  pluginOptions: {
    ssr: {
      nodeExternalsWhitelist: [
        /\.css$/,
        /\?vue&type=style/,
        /vue-instantsearch/,
        /instantsearch.js/,
      ],
    },
  },
};

At this point, Vue is rendering the app on the server, but when you go to /search in your browser, you won’t see the search results on the page. That’s because, by default, Vue InstantSearch only starts searching and showing results once the page is rendered for the first time.

To perform searches on the back end as well, we need to create a back end instance in src/main.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
import VueInstantSearch, { createServerRootMixin } from 'vue-instantsearch';
import algoliasearch from 'algoliasearch/lite';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

export async function createApp({
  renderToString,
  beforeApp = () => {},
  afterApp = () => {},
} = {}) {
  const router = createRouter();

  // provide access to all components
  Vue.use(VueInstantSearch);

  await beforeApp({
    router,
  });

  const app = new Vue({
    // provide access to the instance
    mixins: [
      createServerRootMixin({
        searchClient,
        indexName: 'instant_search',
      }),
    ],
    serverPrefetch() {
      return this.instantsearch.findResultsState({
        component: this,
        renderToString,
      });
    },
    beforeMount() {
      if (typeof window === 'object' && window.__ALGOLIA_STATE__) {
        this.instantsearch.hydrate(window.__ALGOLIA_STATE__);
        delete window.__ALGOLIA_STATE__;
      }
    },
    router,
    render: h => h(App),
  });

  const result = {
    app,
    router,
  };

  await afterApp(result);

  return result;
}

The Vue app can now inject the back end instance of Vue InstantSearch.

In the main file, we need to replace ais-instant-search with ais-instant-search-ssr. We can also remove its props since they are now passed to the createServerRootMixin function.

1
2
3
4
5
<template>
  <ais-instant-search-ssr>
    <!-- ... -->
  </ais-instant-search-ssr>
</template>

The next step is necessary to make sure that we save the results on the backend. For this, the InstantSearch state gets added to context using the getState function:

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
import _renderToString from 'vue-server-renderer/basic';
import { createApp } from './main';

function renderToString(app) {
  return new Promise((resolve, reject) => {
    _renderToString(app, (err, res) => {
      if (err) reject(err);
      resolve(res);
    });
  });
}

export default context => {
  return new Promise(async (resolve, reject) => {
    // read the provided instance
    const { app, router, instantsearch } = await createApp({ renderToString });

    router.push(context.url);

    router.onReady(() => {
      // save the results once rendered fully.
      context.rendered = () => {
        context.algoliaState = app.instantsearch.getState();
      };

      const matchedComponents = router.getMatchedComponents();

      // find the root component which handles the rendering
      Promise.all(
        matchedComponents.map(Component => {
          if (Component.asyncData) {
            return Component.asyncData({
              route: router.currentRoute,
            });
          }
        })
      ).then(() => resolve(app));
    }, reject);
  });
};

Finally, we rehydrate the app with the initial request once we start searching. For this, we need to make sure we save the data on the page. Vue CLI provides a way to do this, allowing us to read the value on context. We can save it in public/index.html:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
  <body>
    <!--vue-ssr-outlet-->
    {{{ renderState() }}}
    {{{ renderState({ contextKey: 'algoliaState', windowKey: '__ALGOLIA_STATE__' }) }}}
    {{{ renderScripts() }}}
  </body>
</html>

That’s it! You can find the source code on GitHub.

With Nuxt

The process of enabling server-side rendering with Nuxt is mostly the same as with Vue CLI. The main difference is that Nuxt directly handles some parts.

The first step is to generate a Nuxt app and adding vue-instantsearch:

1
2
3
npx create-nuxt-app algolia-nuxt-example
cd algolia-nuxt-example
npm install vue-instantsearch algoliasearch

Our InstantSearch code uses ES modules, yet it needs to be executed in Node.js. For that reason, we need to let Nuxt know that those files should be transpiled for a Node usage. For that, we add the following configuration to nuxt.config.js:

1
2
3
4
5
module.exports = {
  build: {
    transpile: ['vue-instantsearch', 'instantsearch.js/es'],
  },
};

We then create a new page (pages/search.vue) and build a Vue InstantSearch interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
  <ais-instant-search :search-client="searchClient" index-name="instant_search">
    <ais-search-box />
    <ais-stats />
    <ais-refinement-list attribute="brand" />
    <ais-hits>
      <template v-slot:item="{ item }">
        <p>
          <ais-highlight attribute="name" :hit="item" />
        </p>
        <p>
          <ais-highlight attribute="brand" :hit="item" />
        </p>
      </template>
    </ais-hits>
    <ais-pagination />
  </ais-instant-search>
</template>

In the script of this component, we add components declarations and the stylesheet:

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
import {
  AisInstantSearch,
  AisRefinementList,
  AisHits,
  AisHighlight,
  AisSearchBox,
  AisStats,
  AisPagination,
  createServerRootMixin,
} from 'vue-instantsearch';
import algoliasearch from 'algoliasearch/lite';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

export default {
  components: {
    AisInstantSearch,
    AisRefinementList,
    AisHits,
    AisHighlight,
    AisSearchBox,
    AisStats,
    AisPagination,
  },
  data() {
    return {
      searchClient,
    };
  },
  head() {
    return {
      link: [
        {
          rel: 'stylesheet',
          href: '<%= app_data.cdn.instantsearch_css_satellite.url %>',
        },
      ],
    };
  },
};

Then, we add:

  1. createServerRootMixin to create a reusable search instance
  2. findResultsState in serverPrefetch to perform a search query in the back end
  3. call hydrate method in beforeMount.

Finally, we need to replace ais-instant-search with ais-instant-search-ssr, and add the createRootMixin to provide the instance to the 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
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
<template>
  <ais-instant-search-ssr>
    <ais-search-box />
    <ais-stats />
    <ais-refinement-list attribute="brand" />
    <ais-hits>
      <template v-slot:item="{ item }">
        <p>
          <ais-highlight attribute="name" :hit="item" />
        </p>
        <p>
          <ais-highlight attribute="brand" :hit="item" />
        </p>
      </template>
    </ais-hits>
    <ais-pagination />
  </ais-instant-search-ssr>
</template>

<script>
import {
  AisInstantSearchSsr,
  AisRefinementList,
  AisHits,
  AisHighlight,
  AisSearchBox,
  AisStats,
  AisPagination,
  createServerRootMixin,
} from 'vue-instantsearch';
import algoliasearch from 'algoliasearch/lite';
import _renderToString from 'vue-server-renderer/basic';

function renderToString(app) {
  return new Promise((resolve, reject) => {
    _renderToString(app, (err, res) => {
      if (err) reject(err);
      resolve(res);
    });
  });
}

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

export default {
  mixins: [
    createServerRootMixin({
      searchClient,
      indexName: 'instant_search',
    }),
  ],
  serverPrefetch() {
    return this.instantsearch
      .findResultsState({
        component: this,
        renderToString,
      }).then(algoliaState => {
        this.$ssrContext.nuxt.algoliaState = algoliaState;
      });
  },
  beforeMount() {
    const results =
      (this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
      window.__NUXT__.algoliaState;

    this.instantsearch.hydrate(results);

    // Remove the SSR state so it can't be applied again by mistake
    delete this.$nuxt.context.nuxtState.algoliaState;
    delete window.__NUXT__.algoliaState;
  },
  components: {
    AisInstantSearchSsr,
    AisRefinementList,
    AisHits,
    AisHighlight,
    AisSearchBox,
    AisStats,
    AisPagination,
  },
  head() {
    return {
      link: [
        {
          rel: 'stylesheet',
          href: '<%= app_data.cdn.instantsearch_css_satellite.url %>',
        },
      ],
    };
  },
};
</script>
Did you find this page helpful?