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.

Server-side rendering (SSR) describes a technique for rendering websites. When you request an SSR page, the HTML is rendered on the server and then sent to your client.

The main steps to implement server-side rendering with Algolia are:

On the server:

  1. Request search results from Algolia
  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

You can build server-side rendered apps with Vue.js in several different ways—for example, using the Vue CLI or Nuxt.js.

With Vue CLI

First, create a Vue app with Vue CLI and add the SSR plugin:

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

Start the development server by running: npm run ssr:serve.

Next, install Vue InstantSearch:

1
npm install vue-instantsearch algoliasearch

Add Vue InstantSearch to your app in the src/main.js file:

1
2
3
4
5
6
import {
  AisInstantSearchSsr,
  createServerRootMixin,
} from 'vue-instantsearch/vue3/es';

Vue.use(VueInstantSearch);

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>

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,
    },
  ],
});
}

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, add instantsearch.css to public/index.html:

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

Vue InstantSearch uses ES modules, but the app runs on the server, with Node.js. That’s why you need to add these modules to the vue.config.js configuration file:

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.js renders 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 starts searching and showing results after the page is rendered for the first time.

To perform searches on the backend as well, you need to create a backend 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
56
57
import VueInstantSearch, {
  createServerRootMixin,
} from 'vue-instantsearch/vue3/es';
import algoliasearch from 'algoliasearch/lite';

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

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

  //Pprovide 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 backend instance of Vue InstantSearch.

In the main file, replace ais-instant-search with ais-instant-search-ssr. You can also remove its props since they’re now passed to the createServerRootMixin function.

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

To save the results on the backend, add the InstantSearch state to the 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 that handles the rendering
      Promise.all(
        matchedComponents.map(Component => {
          if (Component.asyncData) {
            return Component.asyncData({
              route: router.currentRoute,
            });
          }
        })
      ).then(() => resolve(app));
    }, reject);
  });
};

Finally, rehydrate the app with the initial request once you start searching. For this, you need to save the data on the page. Vue CLI provides a way to read the value on the context and 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>

With Nuxt 2

The following section describes how to set up server-side rendering with Vue InstantSearch and Nuxt 2. You can check the community for solutions for Nuxt 3.

First, create a Nuxt app and add vue-instantsearch:

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

Vue InstantSearch uses ES modules, but the app runs on the server, with Node.js. That’s why you need to add these modules to the nuxt.config.js configuration file:

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

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>

Add the component declarations and the style sheet:

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/vue3/es';
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: 'https://cdn.jsdelivr.net/npm/instantsearch.css@8.4.0/themes/satellite-min.css',
        },
      ],
    };
  },
};
  1. Add createServerRootMixin to create a reusable search instance.
  2. Add findResultsState in serverPrefetch to perform a search query in the backend.
  3. Call the hydrate method in beforeMount.
  4. Replace ais-instant-search with ais-instant-search-ssr
  5. 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/vue3/es';
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: 'https://cdn.jsdelivr.net/npm/instantsearch.css@8.4.0/themes/satellite-min.css',
        },
      ],
    };
  },
};
</script>
Did you find this page helpful?