Skip to main content
This example demonstrates how to implement URL routing to sync your search state with the browser URL, enabling shareable searches, browser history navigation, and better SEO.

Overview

URL routing provides:
  • Shareable searches - Users can share search URLs
  • Browser navigation - Back/forward buttons work correctly
  • Bookmarkable results - Save specific search states
  • SEO benefits - Search engines can index filtered states
  • Deep linking - Link directly to specific search configurations
URL routing in action

Basic Implementation

JavaScript

Enable routing with a single option:
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import instantsearch from 'instantsearch.js';

const search = instantsearch({
  searchClient: algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76'),
  indexName: 'instant_search',
  routing: true, // Enable default routing
  insights: true,
});

React

import { liteClient as algoliasearch } from 'algoliasearch/lite';
import { InstantSearch } from 'react-instantsearch';

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

function App() {
  return (
    <InstantSearch
      searchClient={searchClient}
      indexName="instant_search"
      routing={true} // Enable default routing
    >
      {/* Your search UI */}
    </InstantSearch>
  );
}

Advanced Routing Configuration

Custom State Mapping

Control how search state maps to URLs:
import { history as historyRouter } from 'instantsearch.js/es/lib/routers';
import type { UiState } from 'instantsearch.js';

type RouteState = {
  query?: string;
  page?: string;
  brands?: string[];
  category?: string;
  rating?: string;
  price?: string;
  free_shipping?: string;
  sortBy?: string;
  hitsPerPage?: string;
};

const router = historyRouter<RouteState>({
  cleanUrlOnDispose: false,
  
  windowTitle({ category, query }) {
    const queryTitle = query ? `Results for "${query}"` : '';
    return [queryTitle, category, 'My Store']
      .filter(Boolean)
      .join(' | ');
  },
  
  createURL({ qsModule, routeState, location }) {
    const { protocol, hostname, port = '', pathname, hash } = location;
    const portWithPrefix = port === '' ? '' : `:${port}`;
    const baseUrl = `${protocol}//${hostname}${portWithPrefix}${pathname}search`;
    
    const categoryPath = routeState.category
      ? `${encodeURIComponent(routeState.category)}/`
      : '';
      
    const queryParameters: Partial<RouteState> = {};
    
    if (routeState.query) {
      queryParameters.query = encodeURIComponent(routeState.query);
    }
    if (routeState.page && routeState.page !== '1') {
      queryParameters.page = routeState.page;
    }
    if (routeState.brands) {
      queryParameters.brands = routeState.brands.map(encodeURIComponent);
    }
    
    const queryString = qsModule.stringify(queryParameters, {
      addQueryPrefix: true,
      arrayFormat: 'repeat',
    });
    
    return `${baseUrl}/${categoryPath}${queryString}${hash}`;
  },
  
  parseURL({ qsModule, location }) {
    const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
    const category = pathnameMatches?.[1] || '';
    const queryParameters = qsModule.parse(location.search.slice(1));
    
    const { query = '', page = 1, brands = [] } = queryParameters;
    const allBrands = Array.isArray(brands)
      ? brands
      : [brands].filter(Boolean);
    
    return {
      query: decodeURIComponent(query as string),
      page: page as string,
      brands: allBrands.map(brand => decodeURIComponent(brand as string)),
      category: decodeURIComponent(category),
    };
  },
});

URL Formats

Query String Format (Default)

Simple format with query parameters:
/search?query=phone&brand=Apple&brand=Samsung&page=2

SEO-Friendly URLs

Clean URLs with path segments:
/search/Cell-Phones/?query=phone&page=2

Hash-Based Routing

For static hosting or SPAs:
/search#query=phone&brand=Apple&page=2

React with Next.js Routing

Next.js Pages Router

import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs';
import singletonRouter from 'next/router';
import { InstantSearch } from 'react-instantsearch';

function SearchPage({ serverState, url }) {
  return (
    <InstantSearch
      searchClient={searchClient}
      indexName="instant_search"
      routing={{
        router: createInstantSearchRouterNext({
          serverUrl: url,
          singletonRouter,
          routerOptions: {
            cleanUrlOnDispose: false,
          },
        }),
      }}
    >
      {/* Your search UI */}
    </InstantSearch>
  );
}

export const getServerSideProps: GetServerSideProps = async ({ req }) => {
  const protocol = req.headers.referer?.split('://')[0] || 'https';
  const url = `${protocol}://${req.headers.host}${req.url}`;
  
  return {
    props: { url },
  };
};

Prefilled Queries

Create links with predefined search states:
import Link from 'next/link';

function CategoryLinks() {
  return (
    <nav>
      <Link href="/?instant_search[query]=laptop">
        Laptops
      </Link>
      <Link href="/?instant_search[query]=phone&instant_search[refinementList][brand][0]=Apple">
        Apple Phones
      </Link>
      <Link href="/?instant_search[hierarchicalMenu][categories.lvl0][0]=Electronics">
        Electronics
      </Link>
    </nav>
  );
}

Custom URL Schemes

Clean Category URLs

Encode category names for cleaner URLs:
const encodedCategories = {
  Cameras: 'Cameras & Camcorders',
  Cars: 'Car Electronics & GPS',
  Phones: 'Cell Phones',
  TV: 'TV & Home Theater',
} as const;

function getCategorySlug(name: string): string {
  const encodedName = decodedCategories[name] || name;
  return encodedName.split(' ').map(encodeURIComponent).join('+');
}

function getCategoryName(slug: string): string {
  const decodedSlug = encodedCategories[slug] || slug;
  return decodedSlug.split('+').map(decodeURIComponent).join(' ');
}

// URL: /search/Phones/ instead of /search/Cell+Phones/

Omit Default Values

Keep URLs clean by excluding default values:
const routeStateDefaultValues = {
  query: '',
  page: '1',
  sortBy: 'instant_search',
  hitsPerPage: '20',
};

// Only add to URL if different from default
if (routeState.page && routeState.page !== routeStateDefaultValues.page) {
  queryParameters.page = routeState.page;
}

Window Title Updates

Update the browser tab title based on search state:
const router = historyRouter({
  windowTitle({ category, query }) {
    const parts = [];
    
    if (query) {
      parts.push(`Results for "${query}"`);
    }
    
    if (category) {
      parts.push(category);
    }
    
    parts.push('My Store');
    
    return parts.join(' | ');
  },
});

// Tab title: "Results for "iphone" | Electronics | My Store"

Routing Options

Clean URL on Unmount

Reset URL when search is unmounted:
const router = historyRouter({
  cleanUrlOnDispose: true, // Default: true
});

Debounce URL Updates

Reduce URL updates during typing:
const router = historyRouter({
  writeDelay: 400, // Default: 400ms
});

Custom Query String Format

const router = historyRouter({
  createURL({ qsModule, routeState, location }) {
    const queryString = qsModule.stringify(routeState, {
      addQueryPrefix: true,
      arrayFormat: 'bracket', // ?brands[]=Apple&brands[]=Samsung
    });
    
    return `${location.pathname}${queryString}`;
  },
});

State Persistence

Routing automatically persists state in the URL. For additional persistence:

Session Storage

import { simple } from 'instantsearch.js/es/lib/stateMappings';

const sessionStorageRouter = {
  read() {
    const sessionState = window.sessionStorage.getItem('searchState');
    return sessionState ? JSON.parse(sessionState) : {};
  },
  
  write(routeState) {
    window.sessionStorage.setItem('searchState', JSON.stringify(routeState));
  },
  
  createURL() {
    return '';
  },
  
  onUpdate(cb) {
    this._onUpdate = cb;
  },
};

const search = instantsearch({
  searchClient,
  indexName: 'instant_search',
  routing: {
    router: sessionStorageRouter,
    stateMapping: simple(),
  },
});

Testing Routing

Verify URL Updates

  1. Open your search interface
  2. Type a query - URL should update
  3. Select filters - URL should include refinements
  4. Click pagination - URL should show page number
  5. Copy URL, open in new tab - state should be preserved

Check Browser Navigation

  1. Perform several searches
  2. Click browser back button - should restore previous search
  3. Click browser forward button - should restore next search

Common Issues

Double Encoding

Avoid double-encoding URLs:
// ❌ Wrong
const url = encodeURIComponent(encodeURIComponent(query));

// ✅ Correct
const url = encodeURIComponent(query);

Missing State After Refresh

Ensure your parseURL correctly reads all parameters:
parseURL({ qsModule, location }) {
  const params = qsModule.parse(location.search.slice(1));
  
  // Handle both single values and arrays
  const brands = Array.isArray(params.brands)
    ? params.brands
    : [params.brands].filter(Boolean);
  
  return { brands };
}

Running the Example

cd examples/js/e-commerce
yarn install
yarn start

Customization Tips

Create predefined search configurations as clickable links or buttons.
Track URL changes to measure popular search configurations.
Add validation in parseURL to handle malformed or invalid URLs gracefully.

Source Code

JavaScript

View routing implementation

React

View routing implementation

Next.js

View Next.js routing example