> ## Documentation Index
> Fetch the complete documentation index at: https://www.algolia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Routing and URL syncing in Vue InstantSearch

> How to synchronize your URLs with Vue InstantSearch.

export const Filter = () => <Tooltip tip="A filter is a condition that limits which records Algolia returns. Filters often use one or more facet-value pairs, such as brand:Apple AND color:red. You can also filter by numeric values, dates, tags, booleans, or geographic constraints." cta="Filtering" href="/doc/guides/managing-results/refine-results/faceting">
    filter
  </Tooltip>;

export const SearchQuery = () => <Tooltip tip="The text users enter into a search box. In the Search API, this corresponds to the query parameter. A search query is often used with filters, facets, and other parameters, but these aren't part of the query text itself.">
    search query
  </Tooltip>;

export const Records = () => <Tooltip tip="A record is a searchable object in an Algolia index. Each record consists of named attributes." cta="Algolia records" href="/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records">
    records
  </Tooltip>;

export const FlavorSwitcher = ({current, baseHref = "", options = [], label = "InstantSearch framework"}) => {
  if (options.length === 0) {
    return <div className="not-prose" role="alert" style={{
      margin: "0.25rem 0 1.5rem",
      padding: "0.75rem",
      border: "1px solid #f59e0b",
      borderRadius: "0.625rem",
      color: "inherit",
      fontSize: "0.875rem"
    }}>
        FlavorSwitcher requires at least one option.
      </div>;
  }
  const selected = options.find(option => option.value === current) ?? options[0];
  return <div className="not-prose mint-flavor-switcher">
      <style>{`
        .mint-flavor-switcher {
          --mfs-bg: #ffffff;
          --mfs-bg-hover: #f4f4f5;
          --mfs-bg-current: #eef2ff;
          --mfs-border: #d4d4d8;
          --mfs-fg: #18181b;
          --mfs-muted: #71717a;
          --mfs-accent: #4f46e5;
          position: relative;
          width: min(100%, 19rem);
          margin: 0.25rem 0 1.5rem;
          color: var(--mfs-fg);
          font-size: 0.875rem;
          line-height: 1.25rem;
        }

        .dark .mint-flavor-switcher {
          --mfs-bg: #18181b;
          --mfs-bg-hover: #27272a;
          --mfs-bg-current: #272747;
          --mfs-border: #3f3f46;
          --mfs-fg: #fafafa;
          --mfs-muted: #a1a1aa;
          --mfs-accent: #a5b4fc;
        }

        .mint-flavor-switcher details {
          position: relative;
        }

        .mint-flavor-switcher summary {
          display: flex;
          min-height: 2.75rem;
          box-sizing: border-box;
          align-items: center;
          justify-content: space-between;
          gap: 0.75rem;
          padding: 0.625rem 0.75rem;
          border: 1px solid var(--mfs-border);
          border-radius: 0.625rem;
          background: var(--mfs-bg);
          color: var(--mfs-fg);
          cursor: pointer;
          font-weight: 600;
          list-style: none;
          transition: border-color 150ms ease, box-shadow 150ms ease;
        }

        .mint-flavor-switcher summary::-webkit-details-marker {
          display: none;
        }

        .mint-flavor-switcher summary:hover {
          border-color: var(--mfs-accent);
        }

        .mint-flavor-switcher summary:focus-visible {
          outline: 2px solid var(--mfs-accent);
          outline-offset: 2px;
        }

        .mint-flavor-switcher__label {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }

        .mint-flavor-switcher__chevron {
          flex: none;
          transition: transform 150ms ease;
        }

        .mint-flavor-switcher details[open] .mint-flavor-switcher__chevron {
          transform: rotate(180deg);
        }

        .mint-flavor-switcher__menu {
          position: absolute;
          z-index: 50;
          top: calc(100% + 0.375rem);
          left: 0;
          width: 100%;
          box-sizing: border-box;
          margin: 0;
          padding: 0.375rem;
          border: 1px solid var(--mfs-border);
          border-radius: 0.625rem;
          background: var(--mfs-bg);
          box-shadow: 0 12px 30px rgb(0 0 0 / 16%);
          list-style: none;
        }

        .mint-flavor-switcher__menu li {
          margin: 0;
          padding: 0;
        }

        .mint-flavor-switcher__option {
          display: grid;
          gap: 0.125rem;
          padding: 0.625rem 0.75rem;
          border-radius: 0.4rem;
          color: var(--mfs-fg);
          text-decoration: none;
        }

        .mint-flavor-switcher__option:hover {
          background: var(--mfs-bg-hover);
        }

        .mint-flavor-switcher__option:focus-visible {
          outline: 2px solid var(--mfs-accent);
          outline-offset: -2px;
        }

        .mint-flavor-switcher__option[aria-current="page"] {
          background: var(--mfs-bg-current);
          color: var(--mfs-accent);
        }

        .mint-flavor-switcher__name {
          font-weight: 600;
        }

        .mint-flavor-switcher__description {
          color: var(--mfs-muted);
          font-size: 0.8125rem;
        }

        @media (prefers-reduced-motion: reduce) {
          .mint-flavor-switcher summary,
          .mint-flavor-switcher__chevron {
            transition: none;
          }
        }
      `}</style>

      <details>
        <summary aria-label={`${label}: ${selected.label}`}>
          <span className="mint-flavor-switcher__label">{selected.label}</span>
          <svg className="mint-flavor-switcher__chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="m6 9 6 6 6-6" />
          </svg>
        </summary>

        <ul className="mint-flavor-switcher__menu" aria-label={label}>
          {options.map(option => {
    const isCurrent = option.value === selected.value;
    const href = option.href ?? `${baseHref.replace(/\/$/, "")}/${encodeURIComponent(option.value)}`;
    return <li key={option.value}>
                <a className="mint-flavor-switcher__option" href={href} aria-current={isCurrent ? "page" : undefined}>
                  <span className="mint-flavor-switcher__name">
                    {option.label}
                  </span>
                  {option.description ? <span className="mint-flavor-switcher__description">
                      {option.description}
                    </span> : null}
                </a>
              </li>;
  })}
        </ul>
      </details>
    </div>;
};

<div className="mint-flavor-switcher-slot not-prose">
  <FlavorSwitcher
    current="vue"
    baseHref="/doc/guides/building-search-ui/going-further/routing-urls"
    options={[
{ value: "js", label: "JavaScript", description: "InstantSearch.js" },
{ value: "react", label: "React", description: "React InstantSearch" },
{ value: "vue", label: "Vue", description: "Vue InstantSearch" },
]}
  />
</div>

Synchronizing your UI with the browser URL is considered good practice.
It lets your users take one of your results pages, copy the URL, and share it.
It also improves the user experience by enabling the use of the back and next browser buttons to keep track of previous searches.

InstantSearch provides the necessary API entries to let you synchronize the state of your search UI (your refined widgets and current <SearchQuery />) with any kind of storage.

Use the [`routing`](/doc/api-reference/widgets/instantsearch/vue#param-routing) option to synchronize UI state with the browser URL, so users can bookmark, share, and revisit the same search.

<Note>
  Don't configure [`initial-ui-state`](/doc/api-reference/widgets/instantsearch/vue#param-initial-ui-state) and `routing` together.

  * Use `initialUiState` to set the UI state when the search first loads, such as for a default query or filter.
  * Use `routing` to keep the UI state synchronized with the URL, so users can bookmark or share a search.
</Note>

## Routing examples

The examples in this section use Vue 2.
If you use Vue 3, adapt them using the [Vue 3 migration guide](https://v3-migration.vuejs.org/).

<Columns cols={2}>
  <Card title="Basic routing demo" icon="box" href="https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-basic">
    Run and edit the basic routing example in CodeSandbox.
  </Card>

  <Card title="Basic routing source code" icon="github" href="https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-basic">
    Browse the source code for the basic routing example.
  </Card>

  <Card title="SEO-friendly routing demo" icon="box" href="https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-seo-friendly">
    Run and edit the SEO-friendly routing example in CodeSandbox.
  </Card>

  <Card title="SEO-friendly routing source code" icon="github" href="https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-seo-friendly">
    Browse the source code for the SEO-friendly routing example.
  </Card>

  <Card title="Vue Router demo" icon="box" href="https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-vue-router">
    Run and edit the Vue Router example in CodeSandbox.
  </Card>

  <Card title="Vue Router source code" icon="github" href="https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-vue-router">
    Browse the source code for the Vue Router example.
  </Card>
</Columns>

## Default URLs

<Info>
  The examples use the InstantSearch.js router.
  Add `instantsearch.js` to your project dependencies alongside `vue-instantsearch`.
</Info>

Configure the `routing` prop on `<ais-instant-search>` to synchronize UI state with the browser URL.
The default routing setup stores routing-compatible UI state in URL query parameters.

```vue Vue icon=code expandable theme={"system"}
<template>
  <ais-instant-search
    :search-client="searchClient"
    index-name="instant_search"
    :routing="routing"
  >
    <!-- add the other components here -->
  </ais-instant-search>
</template>

<script>
import { history as historyRouter } from 'instantsearch.js/es/lib/routers';
import { singleIndex as singleIndexMapping } from 'instantsearch.js/es/lib/stateMappings';

export default {
  data() {
    return {
      searchClient: algoliasearch(
        'latency',
        '6be0576ff61c053d5f9a3225e2a90f76'
      ),
      routing: {
        router: historyRouter(),
        stateMapping: singleIndexMapping('instant_search'),
      },
    };
  },
};
</script>
```

Assume the following search UI state:

* **Query:** "galaxy"

* **Menu:**

  * `categories`: "Cell Phones"

* **Refinement List:**

  * `brand`: "Apple", "Samsung"

* **Page:** 2

This produces the following URL:

```txt theme={"system"}
https://example.org/?instant_search[query]=galaxy&instant_search[menu][categories]=Cell Phones&instant_search[refinementList][brand][0]=Apple&instant_search[refinementList][brand][0]=Samsung&instant_search[page]=2
```

By default, routing includes state from routing-compatible widgets.
With many widgets, this can create long URLs.
To create shorter or more descriptive URLs, customize the URL routing.

## Customize URL routing

You can customize which values appear and rename the URL parameters.

The `stateMapping` option maps between InstantSearch's `uiState` and the `routeState` used by the router.
Use it to rename parameters or omit values that you don't want to include in the URL.

```js JavaScript icon=code theme={"system"}
export default {
  data() {
    return {
      searchClient: algoliasearch(
        "latency",
        "6be0576ff61c053d5f9a3225e2a90f76",
      ),
      routing: {
        stateMapping: {
          stateToRoute(uiState) {
            // ...
          },
          routeToState(routeState) {
            // ...
          },
        },
      },
    };
  },
};
```

InstantSearch stores widget state in [`uiState`](/doc/api-reference/widgets/ui-state/vue).
The following example maps that state to shorter URL parameters.

The state contains information about the user's search, including the query,
the <Filter /> selection,
the page being viewed,
and the widget hierarchy.
`uiState` only stores modified widget values, not defaults.

To persist this state in the URL,
InstantSearch converts the `uiState` into an object called `routeState`:
this `routeState` then becomes a URL.
Conversely, when InstantSearch reads the URL and applies it to the search,
it converts `routeState` into `uiState`.
This logic lives in two functions:

* `stateToRoute`: converts `uiState` to `routeState`.
* `routeToState`: converts `routeState` to `uiState`.

Assume the following search UI state:

* **Query:** "galaxy"

* **Menu:**

  * `categories`: "Cell Phones"

* **Refinement List:**

  * `brand`: "Apple" and "Samsung"

* **Page:** 2

This translates into the following `uiState`:

```json JSON icon="braces" theme={"system"}
{
  "indexName": {
    "query": "galaxy",
    "menu": {
      "categories": "Cell Phones"
    },
    "refinementList": {
      "brand": ["Apple", "Samsung"]
    },
    "page": 2
  }
}
```

Implement `stateToRoute` to flatten this object into a URL, and `routeToState` to restore the URL into a UI state:

```js JavaScript icon=code expandable theme={"system"}
const indexName = "instant_search";

export default {
  data() {
    return {
      indexName,
      searchClient: algoliasearch(
        "latency",
        "6be0576ff61c053d5f9a3225e2a90f76",
      ),
      routing: {
        stateMapping: {
          stateToRoute(uiState) {
            const indexUiState = uiState[indexName];
            return {
              q: indexUiState.query,
              categories: indexUiState.menu && indexUiState.menu.categories,
              brand:
                indexUiState.refinementList &&
                indexUiState.refinementList.brand,
              page: indexUiState.page,
            };
          },
          routeToState(routeState) {
            return {
              [indexName]: {
                query: routeState.q,
                menu: {
                  categories: routeState.categories,
                },
                refinementList: {
                  brand: routeState.brand,
                },
                page: routeState.page,
              },
            };
          },
        },
      },
    };
  },
};
```

### Keep unrelated URL parameters

By default, routing writes only InstantSearch state to the URL.
To preserve unrelated parameters, include them when implementing `createURL`.

The following example preserves URL parameters that start with `utm_`:

```js JavaScript icon=code expandable theme={"system"}
history({
  // ... other options
  parseURL({ qsModule, location }) {
    return qsModule.parse(location.search.slice(1));
  },
  createURL({ qsModule, location, routeState }) {
    const { origin, pathname, hash } = location;

    const queriesFromUrl = qsModule.parse(location.search.slice(1));

    // Preserve unrelated URL parameters that start with "utm_".
    const utmQueries = Object.fromEntries(
      Object.entries(queriesFromUrl).filter(
        ([key]) =>
          !Object.keys(routeState).includes(key) &&
          key.startsWith("utm_"),
      ),
    );

    // Combine InstantSearch state with the preserved parameters.
    const queryString = qsModule.stringify(
      {
        ...routeState,
        ...utmQueries,
      },
      {
        addQueryPrefix: true,
        arrayFormat: "repeat",
      },
    );

    return `${origin}${pathname}${queryString}${hash}`;
  },
});
```

### Change the name of a key in routing

To rename the `query` route parameter to `q`,
return `q` from `stateToRoute` and
map it back to `query` in `routeToState`.

## SEO-friendly URLs

<Info>
  This guide uses the router from InstantSearch.js.
  Make sure you add `instantsearch.js` to your project's dependencies in addition to `vue-instantsearch`.
</Info>

To create more descriptive URLs, move search state from query parameters into the URL path.
This is a common pattern for ecommerce category and search pages.

```txt theme={"system"}
https://example.org/search/Cell+Phones/?query=galaxy&page=2&brands=Apple&brands=Samsung
```

The category appears in the URL path, while the query, page, and brands remain query parameters.
This simplified routing assumes that only one widget controls each routed attribute.

### Store categories in the URL path

This example stores the category in the path and the query, page, and brands as query parameters.

```js JavaScript icon=code expandable theme={"system"}
import { history as historyRouter } from 'instantsearch.js/es/lib/routers';

// Convert a category name to a URL slug.
// Replace spaces with "+" and encode other characters.
function getCategorySlug(name) {
  return name
    .split(' ')
    .map(encodeURIComponent)
    .join('+');
}

// Convert a category slug to a category name.
// Replace "+" with spaces and decode other characters.
function getCategoryName(slug) {
  return slug
    .split('+')
    .map(decodeURIComponent)
    .join(' ');
}

const routing = {
  router: historyRouter({
    windowTitle({ category, query }) {
      const queryTitle = query ? `Results for "${query}"` : 'Search';

      if (category) {
        return `${category} – ${queryTitle}`;
      }

      return queryTitle;
    },

    createURL({ qsModule, routeState, location }) {
      const urlParts = location.href.match(/^(.*?)\/search/);
      const baseUrl = `${urlParts ? urlParts[1] : ''}/`;

      const categoryPath = routeState.category
        ? `${getCategorySlug(routeState.category)}/`
        : '';
      const queryParameters = {};

      if (routeState.query) {
        queryParameters.query = encodeURIComponent(routeState.query);
      }
      if (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}search/${categoryPath}${queryString}`;
    },

    parseURL({ qsModule, location }) {
      const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
      const category = getCategoryName(
        (pathnameMatches && pathnameMatches[1]) || ''
      );
      const { query = '', page, brands = [] } = qsModule.parse(
        location.search.slice(1)
      );
      // `qs` doesn't return an array if there's a single value.
      const allBrands = Array.isArray(brands)
        ? brands
        : [brands].filter(Boolean);

      return {
        query: decodeURIComponent(query),
        page,
        brands: allBrands.map(decodeURIComponent),
        category,
      };
    },
  }),

  stateMapping: {
      stateToRoute(uiState) {
        const indexUiState = uiState['instant_search'] || {};

        return {
          query: indexUiState.query,
          page: indexUiState.page,
          brands: indexUiState.refinementList && indexUiState.refinementList.brand,
          category: indexUiState.menu && indexUiState.menu.categories
        };
      },

      routeToState(routeState) {
        return {
          instant_search: {
            query: routeState.query,
            page: routeState.page,
            menu: {
              categories: routeState.category
            },
            refinementList: {
              brand: routeState.brands
            },
          },
        };
      },
    },
  },
};
```

The [basic routing](#basic-routing) example uses the [history router](/doc/api-reference/widgets/history-router/vue).
The router reads and writes URLs, while `stateMapping` maps `uiState` to `routeState` and back.

When you configure the history router, you can customize these functions:

* `windowTitle`: returns the browser window title for a `routeState`.
* `createURL`: creates a URL from `routeState`. InstantSearch calls it when synchronizing the browser URL, rendering links in the `menu` widget, or when a connector calls `createURL`.
* `parseURL`: creates `routeState` from the URL when users load or reload the page or use the browser's back or forward navigation.

### Make URLs more discoverable

Shorter category URLs can be more readable and memorable.
Use a mapping object to map category names to shorter URL slugs.

Given the dataset in this guide, you can make some categories more discoverable:

* "Cameras and camcorders" → `/Cameras`
* "Car electronics and GPS" → `/Cars`

When users open `https://example.org/search/Cameras`, InstantSearch selects the "Cameras and camcorders" category.

Define mappings between category names and URL slugs:

```js JavaScript icon=code expandable theme={"system"}
// Map URL slugs to category names.
const encodedCategories = {
  Cameras: "Cameras and camcorders",
  Cars: "Car electronics and GPS",
  Phones: "Phones",
  TV: "TV and home theater",
};

// Map category names to URL slugs.
const decodedCategories = Object.keys(encodedCategories).reduce((acc, key) => {
  const newKey = encodedCategories[key];
  const newValue = key;

  return {
    ...acc,
    [newKey]: newValue,
  };
}, {});

// Use the mappings when converting category names and slugs.
function getCategorySlug(name) {
  const encodedName = decodedCategories[name] || name;

  return encodedName.split(" ").map(encodeURIComponent).join("+");
}

function getCategoryName(slug) {
  const decodedSlug = encodedCategories[slug] || slug;

  return decodedSlug.split("+").map(decodeURIComponent).join(" ");
}
```

You can build these dictionaries from your Algolia <Records />.

With such a solution, you have full control over what categories are discoverable from the URL.

### About SEO

For your search results to be part of a public search engine's results, you must be selective.
Trying to index too many search results pages could be considered spam.

To do that, create a [`robots.txt`](http://www.robotstxt.org/) and host it at `https://example.org/robots.txt`.

Here's an example based on the URL scheme you created.

```txt robots.txt theme={"system"}
User-agent: *
Allow: /search/Audio/
Allow: /search/Phones/
Disallow: /search/
Allow: *
```

## Combine with Vue Router

The previous examples use the InstantSearch history router.
If your search page reads the URL through Vue Router to render content outside InstantSearch, synchronize InstantSearch with Vue Router instead.
Otherwise, keep the InstantSearch router.

Create a router object instead of using `historyRouter`.
The `router` property expects an object with these functions:

```js JavaScript icon=code theme={"system"}
const routing = {
  router: {
    read() {
      /* Read from the URL and return a routeState */
    },
    write(routeState) {
      /* Write to the URL */
    },
    createURL(routeState) {
      /* Return a URL as a string */
    },
    onUpdate(callback) {
      /* Call this callback whenever the URL changes externally */
    },
    dispose() {
      /* Remove any listeners */
    },
  },
};
```

This example uses the default `stateMapping`.
Configure Vue Router to parse nested query parameters and serialize them into query strings in `main.js` first:

```js JavaScript icon=code theme={"system"}
import qs from "qs";

const router = new Router({
  routes: [
    // ...
  ],
  // set custom query resolver
  parseQuery(query) {
    return qs.parse(query);
  },
  stringifyQuery(query) {
    const result = qs.stringify(query);

    return result ? `?${result}` : "";
  },
});
```

Define the custom `router` in the `routing` object returned by `data`:

```js JavaScript icon=code expandable theme={"system"}
const vueRouter = this.$router; /* Get this from Vue Router */

const routing = {
  router: {
    read() {
      return vueRouter.currentRoute.query;
    },
    write(routeState) {
      vueRouter.push({
        query: routeState,
      });
    },
    createURL(routeState) {
      return vueRouter.resolve({
        query: routeState,
      }).href;
    },
    onUpdate(cb) {
      if (typeof window === "undefined") return;

      this._removeAfterEach = vueRouter.afterEach(() => {
        cb(this.read());
      });

      this._onPopState = () => {
        cb(this.read());
      };
      window.addEventListener("popstate", this._onPopState);
    },
    dispose() {
      if (typeof window === "undefined") {
        return;
      }
      if (this._onPopState) {
        window.removeEventListener("popstate", this._onPopState);
      }
      if (this._removeAfterEach) {
        this._removeAfterEach();
      }
    },
  },
};
```

## Combine with Nuxt

To enable routing in a [Nuxt](https://nuxtjs.org) app,
you can't use the `createServerRootMixin` factory as a mixin as usual,
because you need to access Vue Router which is only available on the component instance.

Here's the workaround:

1. Use `createServerRootMixin` in `data`, so `this.$router` is available.
2. Create an InstantSearch router that wraps Vue Router.
3. Set up `provide` as the root mixin would otherwise do.
4. Set up `findResultsState` in `serverPrefetch`.
5. Call `hydrate` in [`beforeMount`](https://v2.vuejs.org/v2/api/#beforeMount).

Set up a custom `renderToString` function.

<CodeGroup>
  ```vue Vue 3 expandable theme={"system"}
  <template>
    <ais-instant-search-ssr>
      <!-- add the other components here -->
    </ais-instant-search-ssr>
  </template>

  <script>
  import {
    AisInstantSearchSsr,
    createServerRootMixin,
  } from 'vue-instantsearch/vue3/es';
  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(
    'latency',
    '6be0576ff61c053d5f9a3225e2a90f76'
  );
  </script>
  ```

  ```vue Vue 2 expandable theme={"system"}
  <template>
    <ais-instant-search-ssr>
      <!-- add the other components here -->
    </ais-instant-search-ssr>
  </template>

  <script>
  import { AisInstantSearchSsr, 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(
    'latency',
    '6be0576ff61c053d5f9a3225e2a90f76'
  );
  </script>
  ```
</CodeGroup>

Wrap the Vue Router for usage with Vue InstantSearch.

```vue Vue icon=code expandable theme={"system"}
<script>
// ...

function nuxtRouter(vueRouter) {
  return {
    read() {
      return vueRouter.currentRoute.query;
    },
    write(routeState) {
      // Only push a new entry if the URL changed (avoid duplicated entries in the history)
      if (this.createURL(routeState) === this.createURL(this.read())) {
        return;
      }
      vueRouter.push({
        query: routeState,
      });
    },
    createURL(routeState) {
      return vueRouter.resolve({
        query: routeState,
      }).href;
    },
    onUpdate(cb) {
      if (typeof window === 'undefined') return;

      this._removeAfterEach = vueRouter.afterEach(() => {
        cb(this.read());
      });

      this._onPopState = () => {
        cb(this.read());
      };
      window.addEventListener('popstate', this._onPopState);
    },
    dispose() {
      if (typeof window === 'undefined') {
        return;
      }
      if (this._onPopState) {
        window.removeEventListener('popstate', this._onPopState);
      }
      if (this._removeAfterEach) {
        this._removeAfterEach();
      }
    },
  };
}

export default {
  data() {
    // Create it in `data` to access the Vue Router
    const mixin = createServerRootMixin({
      searchClient,
      indexName: 'instant_search',
      routing: {
        router: nuxtRouter(this.$router),
      },
    });
    return {
      ...mixin.data(),
    };
  },
  provide() {
    return {
      // Provide the InstantSearch instance for SSR
      $_ais_ssrInstantSearchInstance: this.instantsearch,
    };
  },
  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,
    // Add your other components here
  },
};
</script>
```

As in Vue Router, you must set up Nuxt to write deep query strings. In Nuxt, you do this in `nuxt.config.js`:

```js JavaScript icon=code theme={"system"}
// nuxt.config.js
module.exports = {
  router: {
    parseQuery(queryString) {
      return require("qs").parse(queryString);
    },
    stringifyQuery(object) {
      var queryString = require("qs").stringify(object);
      return queryString ? "?" + queryString : "";
    },
  },
};
```

## Group facet values

To group facet values such as "turquoise", "ocean", and "sky" under "blue", add the group at indexing time.
Either add a separate grouping attribute or store both the individual value and group value in the same attribute.

For example, with the following dataset:

```json JSON icon=braces theme={"system"}
[
  {
    "objectID": "1",
    "color": "turquoise"
  },
  {
    "objectID": "2",
    "color": "ocean"
  },
  {
    "objectID": "3",
    "color": "sky"
  }
]
```

To facet on a separate grouping attribute, add `colorGroup` to each record:

```json JSON icon=braces theme={"system"}
[
  {
    "objectID": "1",
    "color": "turquoise",
    "colorGroup": "blue"
  },
  {
    "objectID": "2",
    "color": "ocean",
    "colorGroup": "blue"
  },
  {
    "objectID": "3",
    "color": "sky",
    "colorGroup": "blue"
  }
]
```

To facet on both individual colors and their group, store both values in the `color` attribute:

```json JSON icon=braces theme={"system"}
[
  {
    "objectID": "1",
    "color": ["turquoise", "blue"]
  },
  {
    "objectID": "2",
    "color": ["ocean", "blue"]
  },
  {
    "objectID": "3",
    "color": ["sky", "blue"]
  }
]
```
