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

# Sync your URLs with 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>;

<Note>
  You're reading the documentation for Vue InstantSearch v4.
  Read the migration guide to learn [how to upgrade from v3 to v4](/doc/guides/building-search-ui/upgrade-guides/vue#upgrade-from-v3-to-v4).
  You can still find the [v3 documentation for this page](https://algolia.com/old-docs/deprecated/instantsearch/vue/v3/guides/routing-urls/).
</Note>

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.

This is possible with the [`routing`](/doc/api-reference/widgets/instantsearch/vue#param-routing) option.
This guide focuses on storing the UI state in the browser URL.

When you are using routing, you can't use [`initial-ui-state`](/doc/api-reference/widgets/instantsearch/vue#param-initial-ui-state)
because the two options override each other.
Simple and static use cases can be more straightforward using [`initial-ui-state`](/doc/api-reference/widgets/instantsearch/vue#param-initial-ui-state), but anything dynamic or complex should use `routing`.

## Working examples

This code has been specifically created for Vue 2.
Some [modifications](https://v3-migration.vuejs.org/) may be required for it to work correctly in Vue 3.

### Basic routing

* [Demo](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-basic)
* [Source code](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-basic)

### SEO-friendly routing

* [Demo](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-seo-friendly)
* [Source code](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-seo-friendly)

### Vue router

* [Demo](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-vue-router)
* [Source code](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/routing-vue-router)

## Basic 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>

Vue InstantSearch provides a basic way to activate the browser URL synchronization with the `routing` option.

```vue Vue 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

The resulting URL in your browser's URL bar will look like this:

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

This URL is accurate and can be translated back to a search UI state. However, this isn't the most human-readable, or optimized for search engines. The next section shows how to make it more SEO-friendly.

## Rewriting URLs manually

The default URLs that InstantSearch generates are comprehensive, but if you have many widgets,
this can also generate noise.
You may want to decide what goes in the URL and what doesn't,
or even rename the query parameters to something that makes more sense to you.

The `stateMapping` defines how to go from InstantSearch's internal state to a URL, and vice versa.
You can override it to rename query parameters and choose what to include in the URL.

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

InstantSearch manages [`uiState`](/doc/api-reference/widgets/ui-state/vue).

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 Vue icon=code 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,
              },
            };
          },
        },
      },
    };
  },
};
```

### Change the name of a key in routing

**Applies to Vue InstantSearch v2 and later.**

If you want to change, for example, "query into "q" in routing, use the `stateMapping` functions to:

1. In `stateToRoute`, return an object containing "q" for the query
2. In `routeToState`, replace that "q" with "query".

## 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>

URLs are more than [query parameters](https://wikipedia.org/wiki/Query_string).
Another important part is the path.
Manipulating the URL path is a common ecommerce pattern that better references your page results.
In this section, you'll learn how to create this kind of URLs:

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

This URL is composed of the path which now includes `/search`, then `/Cell+Phones` for the category. The query parameters are also simplified by only using the attribute name as the key. This requires you to have only one widget per attribute.

### Implementation example

In the following SEO-friendly example the brand is stored in the path, and the query and page as query parameters.

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

// Returns a slug from the category name.
// Spaces are replaced by "+" to make
// the URL easier to read and other
// characters are encoded.
function getCategorySlug(name) {
  return name
    .split(' ')
    .map(encodeURIComponent)
    .join('+');
}

// Returns a name from the category slug.
// The "+" are replaced by spaces and other
// characters are decoded.
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` does not return an array when 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) with no extra customization.
Here you're also using the history router but explicitly setting configuration options.
The `router` and `stateMapping` options are used to map `uiState` to `routeState`, and vice versa.

Using the `routing` option as an object, configure:

* `windowTitle`: a method to map the `routeState` object returned from `stateToRoute` to the window title.

* `createURL`: a method called every time you need to create a URL. This should be done when:

  * You want to synchronize the `routeState` to the browser URL,
  * You want to render `a` tags in the `menu` widget,
  * You call `createURL` in one of your connectors' rendering methods.

* `parseURL`: a method called every time users load or reload the page, or click the browser's back or next buttons.

### Make URLs more discoverable

In real-life apps, you might want to make specific categories easier to access by associating them with readable and memorable URLs.

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

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

In this example, whenever users visit `https://example.org/search/Cameras`, it pre-selects the "Cameras and camcorders" filter.
This is achieved with a dictionary:

```js JavaScript icon=code theme={"system"}
// Step 1. Add the dictionaries to convert the names and the slugs
const encodedCategories = {
  Cameras: "Cameras and camcorders",
  Cars: "Car electronics and GPS",
  Phones: "Phones",
  TV: "TV and home theater",
};

const decodedCategories = Object.keys(encodedCategories).reduce((acc, key) => {
  const newKey = encodedCategories[key];
  const newValue = key;

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

// Step 2. Update the getters to use the encoded/decoded values
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 were using the InstantSearch router. This is fine in almost all of the use cases, but if you plan on using Vue Router too, and also plan on reading the URL with Vue Router in the search page to show something outside of the InstantSearch life cycle, you can choose to synchronize with Vue Router. This isn't necessary if you are using Vue Router and aren't planning to read from the URL.

Instead of using `historyRouter`, a new one is written from scratch. The `router` key expects an object with the following keys as values:

```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 */
    },
  },
};
```

For simplicity, in this example, a `stateMapping` won't be completed. The default configuration of Vue Router doesn't allow deeply nested URLs, so that must be implemented first in `main.js`:

```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}` : "";
  },
});
```

Fill in the `router` key on the `routing` object in the `data` function:

```js JavaScript icon=code 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();
      }
    },
  },
};
```

### Remove unrelated URL parameters from the URL

**Applies to Vue InstantSearch v2 and later.**

If you enable InstantSearch routing, only the parameters coming from widgets are included in the URL.
To keep other parameters unrelated to InstantSearch, add them when implementing `createURL`.

For example, to keep all URL parameters that start with `utm_`,
use the following code:

```js JavaScript icon=code 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));

    // Get all parameters from the URL that are not in the InstantSearch state and that start with "utm_"
    const utmQueries = Object.fromEntries(
      Object.entries(queriesFromUrl).filter(
        ([key]) =>
          !Object.keys(routeState).includes(key) &&
          // Add here a condition to keep the parameters you want, for example starting with "utm_"
          key.startsWith("utm_"),
      ),
    );

    // Create query string with InstantSearch state and other parameters
    const queryString = qsModule.stringify(
      {
        ...routeState,
        ...utmQueries,
      },
      {
        addQueryPrefix: true,
        arrayFormat: "repeat",
      },
    );

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

## Combine with Nuxt.js

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 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 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 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 : "";
    },
  },
};
```

For more information, see [`routing`](/doc/api-reference/widgets/instantsearch/vue#param-routing).

## Group facet values

**Applies to Vue InstantSearch v1 and later.**

If you want to group, for example, "turquoise", "ocean" and "sky" under "blue", the recommended solution is to group them at indexing time.
You can either add the group name as a separate attribute to globally filter on, or add both values in an array to make both the group and the individual value show up in the list.

For example, with the following dataset:

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

You could create an additional attribute and use it for faceting:

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

Or you could list the individual colors and their groups so you can use them both for faceting:

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