Overview
Server-side rendering (SSR) with Vue InstantSearch enables you to:
Improve SEO Search engines can crawl your search results
Faster initial load Users see content before JavaScript loads
Better UX No loading states on initial page load
How it works
SSR with Vue InstantSearch follows this flow:
Server prefetch
The server executes search queries and collects results before rendering
Render to HTML
The server renders the Vue component tree to HTML with search results
Serialize state
Search results are serialized and embedded in the HTML
Hydration
The client reuses the server state without making new requests
Basic SSR setup
1. Create the server root mixin
Use createServerRootMixin to create an InstantSearch instance that works with SSR:
import { createServerRootMixin } from 'vue-instantsearch' ;
import { liteClient as 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 ( 'appId' , 'apiKey' );
export const serverRootMixin = createServerRootMixin ({
searchClient ,
indexName: 'products' ,
insights: true ,
});
2. Use ais-instant-search-ssr
Replace <ais-instant-search> with <ais-instant-search-ssr> in your search component:
< template >
< ais-instant-search-ssr >
< ais-search-box />
< ais-stats />
< ais-refinement-list attribute = "brand" />
< ais-hits >
< template # item = " { item } " >
< article >
< h2 >
< ais-highlight attribute = "name" : hit = " item " />
</ h2 >
< p >
< ais-highlight attribute = "brand" : hit = " item " />
</ p >
</ article >
</ template >
</ ais-hits >
< ais-pagination />
</ ais-instant-search-ssr >
</ template >
< script >
import {
AisInstantSearchSsr ,
AisSearchBox ,
AisStats ,
AisRefinementList ,
AisHits ,
AisHighlight ,
AisPagination ,
} from 'vue-instantsearch' ;
export default {
components: {
AisInstantSearchSsr ,
AisSearchBox ,
AisStats ,
AisRefinementList ,
AisHits ,
AisHighlight ,
AisPagination ,
} ,
} ;
</ script >
<ais-instant-search-ssr> automatically connects to the InstantSearch instance provided by createServerRootMixin.
3. Implement server entry point
Create a server entry that fetches search results:
import { createApp } from './main' ;
export default ( context ) =>
new Promise ( async ( resolve , reject ) => {
const { app , router } = await createApp ({ context });
router . push ( context . url );
router . onReady (() => {
// Called when app is rendered
context . rendered = () => {
// Attach search state to context
context . algoliaState = app . instantsearch . getState ();
};
const matchedComponents = router . getMatchedComponents ();
if ( matchedComponents . length === 0 ) {
return reject ({ code: 404 });
}
resolve ( app );
}, reject );
} ) ;
4. Set up main app with mixin
import Vue from 'vue' ;
import { liteClient as algoliasearch } from 'algoliasearch/lite' ;
import { createServerRootMixin } from 'vue-instantsearch' ;
import _renderToString from 'vue-server-renderer/basic' ;
import App from './App.vue' ;
import { createRouter } from './router' ;
function renderToString ( app ) {
return new Promise (( resolve , reject ) => {
_renderToString ( app , ( err , res ) => {
if ( err ) reject ( err );
resolve ( res );
});
});
}
const searchClient = algoliasearch ( 'appId' , 'apiKey' );
export async function createApp ({ context } = {}) {
const router = createRouter ();
const app = new Vue ({
mixins: [
createServerRootMixin ({
searchClient ,
indexName: 'products' ,
insights: true ,
}),
],
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 ),
});
return { app , router };
}
5. Client-side hydration
import { createApp } from './main' ;
createApp ({
afterApp ({ app }) {
app . $mount ( '#app' );
},
});
Nuxt.js integration
Vue InstantSearch works seamlessly with Nuxt.js:
Add the required transpile configuration:
module . exports = {
build: {
transpile: [ 'vue-instantsearch' , 'instantsearch.js/es' , 'algoliasearch' ],
},
};
2. Create a search page
< template >
< ais-instant-search-ssr >
< ais-search-box />
< ais-stats />
< ais-refinement-list attribute = "brand" />
< ais-hits >
< template # item = " { item } " >
< div >
< ais-highlight attribute = "name" : hit = " item " />
</ div >
</ template >
</ ais-hits >
< ais-pagination />
</ ais-instant-search-ssr >
</ template >
< script >
import { liteClient as algoliasearch } from 'algoliasearch/lite' ;
import {
AisInstantSearchSsr ,
AisSearchBox ,
AisStats ,
AisRefinementList ,
AisHits ,
AisHighlight ,
AisPagination ,
createServerRootMixin ,
} from 'vue-instantsearch' ;
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 ( 'appId' , 'apiKey' );
export default {
components: {
AisInstantSearchSsr ,
AisSearchBox ,
AisStats ,
AisRefinementList ,
AisHits ,
AisHighlight ,
AisPagination ,
} ,
mixins: [
createServerRootMixin ({
searchClient ,
indexName: 'products' ,
insights: true ,
}),
] ,
serverPrefetch () {
return this . instantsearch
. findResultsState ({ component: this , renderToString })
. then (( algoliaState ) => {
this . $ssrContext . nuxt . algoliaState = algoliaState ;
});
} ,
beforeMount () {
const results = window . __NUXT__ . algoliaState ;
this . instantsearch . hydrate ( results );
} ,
} ;
</ script >
In Nuxt, you can access the state via window.__NUXT__.algoliaState which is automatically serialized by Nuxt.
Advanced SSR configuration
Initial UI state
Set the initial search state on the server:
createServerRootMixin ({
searchClient ,
indexName: 'products' ,
insights: true ,
initialUiState: {
products: {
query: 'phone' ,
page: 1 ,
refinementList: {
brand: [ 'Apple' ],
},
},
},
});
Custom routing
Implement URL-based search state:
import qs from 'qs' ;
createServerRootMixin ({
searchClient ,
indexName: 'products' ,
routing: {
router: {
read () {
const url = context
? context . url
: typeof window . location === 'object'
? window . location . href
: '' ;
const search = url . slice ( url . indexOf ( '?' ));
return qs . parse ( search , {
ignoreQueryPrefix: true ,
});
},
write ( routeState ) {
const query = qs . stringify ( routeState , {
addQueryPrefix: true ,
});
if ( typeof history === 'object' ) {
history . pushState ( routeState , null , query );
}
},
createURL ( routeState ) {
return qs . stringify ( routeState , {
addQueryPrefix: true ,
});
},
onUpdate ( callback ) {
if ( typeof window !== 'object' ) return ;
this . _onPopState = () => {
callback ( this . read ());
};
window . addEventListener ( 'popstate' , this . _onPopState );
},
dispose () {
if ( this . _onPopState && typeof window === 'object' ) {
window . removeEventListener ( 'popstate' , this . _onPopState );
}
},
},
},
});
Multi-index SSR
Search across multiple indices:
< template >
< ais-instant-search-ssr >
<!-- Main products index -->
< ais-search-box />
< ais-hits >
< template # item = " { item } " >
< div > {{ item . name }} </ div >
</ template >
</ ais-hits >
<!-- Query suggestions index -->
< ais-index
index-name = "query_suggestions"
index-id = "suggestions"
>
< ais-configure : hits-per-page = " 5 " />
< ais-hits >
< template # item = " { item } " >
< ais-highlight attribute = "query" : hit = " item " />
</ template >
</ ais-hits >
</ ais-index >
<!-- Refinement from another index -->
< ais-index
index-name = "products"
index-id = "refinements"
>
< ais-refinement-list attribute = "category" />
</ ais-index >
</ ais-instant-search-ssr >
</ template >
< script >
import { liteClient as algoliasearch } from 'algoliasearch/lite' ;
import { createServerRootMixin } from 'vue-instantsearch' ;
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 ( 'appId' , 'apiKey' );
export default {
mixins: [
createServerRootMixin ({
searchClient ,
indexName: 'products' ,
insights: true ,
initialUiState: {
products: {
query: 'phone' ,
},
suggestions: {
query: 'p' ,
},
refinements: {
refinementList: {
category: [ 'Electronics' ],
},
},
},
}),
] ,
serverPrefetch () {
return this . instantsearch . findResultsState ({
component: this ,
renderToString ,
});
} ,
beforeMount () {
if ( window . __ALGOLIA_STATE__ ) {
this . instantsearch . hydrate ( window . __ALGOLIA_STATE__ );
}
} ,
} ;
</ script >
The lite client reduces bundle size and is optimized for search-only operations: import { liteClient as algoliasearch } from 'algoliasearch/lite' ;
Limit attributes to retrieve
Only fetch the attributes you need: < ais-configure
: attributes-to-retrieve = " [ 'name' , 'price' , 'image' ] "
/>
Create the search client once and reuse it: // searchClient.js
import { liteClient as algoliasearch } from 'algoliasearch/lite' ;
export const searchClient = algoliasearch ( 'appId' , 'apiKey' );
Use responsive images and lazy loading: < template # item = " { item } " >
< img
: src = " item . image "
: alt = " item . name "
loading = "lazy"
width = "200"
height = "200"
/>
</ template >
Troubleshooting
If you see hydration warnings, ensure:
The server and client render the same content
window.__ALGOLIA_STATE__ is properly serialized
No dynamic content changes between server and client
// Check state is properly passed
beforeMount () {
console . log ( 'Algolia state:' , window . __ALGOLIA_STATE__ );
if ( window . __ALGOLIA_STATE__ ) {
this . instantsearch . hydrate ( window . __ALGOLIA_STATE__ );
}
}
Verify that:
serverPrefetch is called on the server
Search results are serialized in the HTML
The hydration happens before mounting
serverPrefetch () {
console . log ( 'Server prefetch started' );
return this . instantsearch
. findResultsState ({ component: this , renderToString })
. then (( state ) => {
console . log ( 'Results found:' , state );
return state ;
});
}
Build errors with vue-instantsearch
Add the required transpile configuration: // Webpack
module . exports = {
module: {
rules: [
{
test: / \. js $ / ,
include: [
/node_modules \/ vue-instantsearch/ ,
/node_modules \/ instantsearch \. js/ ,
],
use: 'babel-loader' ,
},
],
},
};
// Nuxt
export default {
build: {
transpile: [ 'vue-instantsearch' , 'instantsearch.js/es' , 'algoliasearch' ],
} ,
} ;
Best practices
Always use ais-instant-search-ssr
Don’t mix <ais-instant-search> and <ais-instant-search-ssr> in the same app
Serialize state properly
Ensure the state is embedded in the HTML and accessible on the client
Clean up hydrated state
Remove the state from window after hydration to prevent memory leaks: beforeMount () {
if ( window . __ALGOLIA_STATE__ ) {
this . instantsearch . hydrate ( window . __ALGOLIA_STATE__ );
delete window . __ALGOLIA_STATE__ ;
}
}
Test both server and client rendering
Verify that the initial server-rendered HTML matches the client-hydrated output
Next steps
Component reference Explore all available InstantSearch components
Routing Learn about URL synchronization and routing