Server-side Rendering
On this page
You are reading the documentation for Vue InstantSearch v2. Read our migration guide to learn how to upgrade from v1 to v2. You can still find the v1 documentation here.
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:
- Make a request to Algolia to get search results
- Render the Vue app with the results of the request
- Store the search results in the page
- Return the HTML page as a string
On the client:
- Read the search results from the page
- 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.
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
35
36
37
38
39
40
41
42
43
44
45
46
<template>
<ais-instant-search
:search-client="searchClient"
index-name="instant_search"
>
<ais-search-box />
<ais-stats />
<ais-refinement-list attribute="brand" />
<ais-hits>
<template
slot="item"
slot-scope="{ 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.3.1/themes/algolia-min.css"
integrity="sha256-HB49n/BZjuqiCtQQf49OdZn63XuKFaxcIHWf0HNKte8="
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
import VueInstantSearch, { createInstantSearch } from 'vue-instantsearch';
import algoliasearch from 'algoliasearch/lite';
const searchClient = algoliasearch(
'latency',
'6be0576ff61c053d5f9a3225e2a90f76'
);
export async function createApp({
beforeApp = () => {},
afterApp = () => {}
} = {}) {
const router = createRouter();
// provide access to all components
Vue.use(VueInstantSearch);
// provide access to the SSR functions in your app
const { instantsearch, rootMixin } = createInstantSearch({
searchClient,
indexName: 'instant_search'
});
await beforeApp({
router,
// provide access to the instance from beforeApp
instantsearch
});
const app = new Vue({
// provide access to the instance
mixins: [rootMixin],
router,
render: h => h(App)
});
const result = {
app,
router,
// provide access to the instance from afterApp
instantsearch
};
await afterApp(result);
return result;
}
The Vue app can now inject the back end instance of Vue InstantSearch.
Next, we need to set up the asyncData
hook with the findResultsState
function that InstantSearch provides. In our case, we need to do this in src/views/Search.vue
, which is the root component that needs to prefetch results.
In that same 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 createInstantSearch
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<ais-instant-search-ssr>
<!-- ... -->
</ais-instant-search-ssr>
</template>
<script>
export default {
asyncData({ instantsearch }) {
return instantsearch.findResultsState({
// find out which parameters to use here using ais-state-results
query: 'iphone',
hitsPerPage: 5,
disjunctiveFacets: ['brand'],
disjunctiveFacetsRefinements: { brand: ['Apple'] }
})
}
}
</script>
The next step is necessary to make sure that we execute this query on the back end. In src/entry-server.js
, we add code to find the root component where we call asyncData
. Then 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
import { createApp } from './main';
export default context => {
return new Promise(async (resolve, reject) => {
// read the provided instance
const { app, router, instantsearch } = await createApp();
router.push(context.url);
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// find the root component which handles the rendering
Promise.all(
matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
// provide access to the instance from asyncData
instantsearch,
route: router.currentRoute
});
}
})
)
.then(() => {
// save the state of this search to the context
context.algoliaState = instantSearch.getState();
})
.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>
In src/entry-client.js
we add the necessary code to read from window.__ALGOLIA_STATE__
and pass on the data to Vue InstantSearch:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { loadAsyncComponents } from '@akryum/vue-cli-plugin-ssr/client';
import { createApp } from './main';
createApp({
async beforeApp({ router, instantsearch }) {
// read from the serialized state and pass it to the instance
instantsearch.hydrate(window.__ALGOLIA_STATE__);
delete window.__ALGOLIA_STATE__;
await loadAsyncComponents({ router });
},
afterApp({ app }) {
app.$mount('#app');
}
});
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
19
20
21
22
23
24
25
26
27
<template>
<ais-instant-search :search-client="searchClient" index-name="instant_search">
<ais-search-box />
<ais-stats />
<ais-refinement-list attribute="brand" />
<ais-hits>
<template
slot="item"
slot-scope="{ 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
44
import {
AisInstantSearch,
AisRefinementList,
AisHits,
AisHighlight,
AisSearchBox,
AisStats,
AisPagination,
createInstantSearch
} 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:
'https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/algolia-min.css'
}
]
};
}
};
Then, we add:
createInstantSearch
to create a reusable search instancefindResultsState
inasyncData
to perform a search query in the back end
On the front end, Nuxt merges anything that is returned from asyncData
with data
. This calls the hydrate
method, but only in the client (via beforeMount
). The effect it has is that the search parameters we add for the back end search will also be shown if you navigate to the page regularly. This is something that Vue SSR does not natively do.
Finally, we need to replace ais-instant-search
with ais-instant-search-ssr
, and add the rootMixin
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
<template>
<ais-instant-search-ssr>
<ais-search-box />
<ais-stats />
<ais-refinement-list attribute="brand" />
<ais-hits>
<template
slot="item"
slot-scope="{ 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,
createInstantSearch
} from 'vue-instantsearch';
import algoliasearch from 'algoliasearch/lite';
const searchClient = algoliasearch(
'latency',
'6be0576ff61c053d5f9a3225e2a90f76'
);
const { instantsearch, rootMixin } = createInstantSearch({
searchClient,
indexName: 'instant_search'
});
export default {
asyncData() {
return instantsearch
.findResultsState({
// find out which parameters to use here using ais-state-results
query: 'iphone',
hitsPerPage: 5,
disjunctiveFacets: ['brand'],
disjunctiveFacetsRefinements: { brand: ['Apple'] }
})
.then(() => ({
instantSearchState: instantsearch.getState()
}));
},
beforeMount() {
instantsearch.hydrate(this.instantSearchState);
},
mixins: [rootMixin],
components: {
AisInstantSearchSsr,
AisRefinementList,
AisHits,
AisHighlight,
AisSearchBox,
AisStats,
AisPagination
},
head() {
return {
link: [
{
rel: 'stylesheet',
href:
'https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/algolia-min.css'
}
]
};
}
};
</script>