Vue instantsearch SEO friendly URL

Hi guys! I’m using Vue instantsearch and I want to make the URL a bit pleasing to the eyes so I followed this guide: https://www.algolia.com/doc/guides/building-search-ui/going-further/routing-urls/vue/#seo-friendly-urls

But the problem is I couldn’t get the category filter to work properly. The route is generated and appended to the address bar when I use the category filter but when I access the same URL again, either by pressing enter on the address bar or refreshing the page, the category filter is stripped off and only the query string remains.

Here’s what I have so far:

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

            const queryParameters = {};
            if (routeState.query) {
                queryParameters.query = encodeURIComponent(routeState.query);
            }
            if (routeState.categories) {
                queryParameters.categories = routeState.categories.map(encodeURIComponent);
            }
            if (routeState.page !== 1) {
                queryParameters.page = routeState.page;
            }

            const queryString = qsModule.stringify(queryParameters, {
                addQueryPrefix: true,
                arrayFormat: "repeat",
            });
            return `${baseUrl}search/${queryString}`;
        },
    }),
    parseURL({ qsModule, location }) {
      
        const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);

        const { query = "", page, categories = [] } = qsModule.parse(location.search.slice(1));

        const all_categories = Array.isArray(categories) ? categories : [categories];

        return {
            query: decodeURIComponent(query),
            page,
            category: all_categories.map(decodeURIComponent),
        };
    },
  
    stateMapping: {
        stateToRoute(uiState) {
           
            const indexUiState = uiState["my_index"] || {};
           
            return {
                query: indexUiState.query,
                categories: indexUiState.refinementList && indexUiState.refinementList.category,
                page: indexUiState.page,
            };
        },
        routeToState(routeState) {
          
            const category_list = routeState.category ? routeState.category.split(" ") : [];
          
            return {
                my_index: {
                    query: routeState.query,
                    page: routeState.page,
                    refinementList: {
                        category: category_list,
                    },
                },
            };
        },
    },
},

The category is being picked up nicely by the routeToState function. But as soon as it gets to the stateToRoute, the category is no longer there. The name of my index is my_index.

Any ideas what’s wrong with my code?

Hi @ellis, would it be possible to provide a codesandbox to demonstrate this issue using our vue starter template? That will help us troubleshoot the issue.

@cindy.cullen here it is: https://codesandbox.io/s/eloquent-mahavira-70nfr?file=/src/App.vue

What I did was to replace the code in the starter template such that it uses the same code I was using in my question above. I just replaced category with brand.

As you can see, it works quite well in that demo. So I tried using that same code, replacing all the relevant details such as the index and the actual name of the field I’m trying to filter. But it still wouldn’t work. I tripled check and I don’t think I missed anything. So I’m at a loss as to what’s wrong. Here’s my code (I basically just replaced brand with category, and brands with categories then replaced the my_index with the actual name of my index):

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

            const queryParameters = {};
            if (routeState.query) {
                queryParameters.query = encodeURIComponent(routeState.query);
            }
            if (routeState.categories) {
                queryParameters.categories = routeState.categories.map(encodeURIComponent);
            }
            if (routeState.page !== 1) {
                queryParameters.page = routeState.page;
            }

            const queryString = qsModule.stringify(queryParameters, {
                addQueryPrefix: true,
                arrayFormat: "repeat",
            });


            return `${baseUrl}search/${queryString}`;
        },
    }),
    parseURL({ qsModule, location }) {
        
        const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
        const { query = "", page, categories = [] } = qsModule.parse(location.search.slice(1));

        const allCategories = Array.isArray(categories) ? categories : [categories].filter(Boolean);

        return {
            query: decodeURIComponent(query),
            page,
            categories: allCategories.map(decodeURIComponent),
        };
    },

    stateMapping: {
        stateToRoute(uiState) {
           
            const indexUiState = uiState["my_index"] || {};

            return {
                query: indexUiState.query,
                categories: indexUiState.refinementList && indexUiState.refinementList.category,
                page: indexUiState.page,
            };
        },
        routeToState(routeState) {
            
            return {
                my_index: {
                    query: routeState.query,
                    page: routeState.page,
                    refinementList: {
                        category: routeState.categories,
                    },
                },
            };
        },
    },
},

Here’s what a document in my index looks like:

Basically, I’m trying to show a refinement list for the category field. This field was previously an array with a single item, I thought this was the one causing the problem but it wasn’t since converting it to a string didn’t do the trick either.

I also tried putting in my own index in the sandbox, and it worked:

So I copied the routing code back to my original code, but it simply refuses to work in there :frowning:

If it helps, I’m using the Laravel framework, and I have a backend route /search. I’m not sure how it affects it, but that’s the only possibility since the demo is purely frontend. I don’t think the query parameters are being reset in the backend side, so the frontend should still be able to capture those and build the state for the search accordingly.

update

I haven’t noticed this previously, but even the built-in routing provided by Algolia is also being reset when I access a URL like https://mywebsite.test/search/?refinementList%5Bcategory%5D%5B0%5D=pubs-and-bars&refinementList%5Bcategory%5D%5B1%5D=restaurants-cafes-and-takeaways its being converted back to https://mywebsite.test when I press enter:

routing: {
    router: historyRouter(),
    stateMapping: singleIndexMapping("my_index"),
},

update 2

I’ve eliminated the possibility that it has something to do with the /search backend route because even if I serve the app on the root (/) I still have the same problem. Here it is in action:

ezgif.com-video-to-gif

It generates the correct route when you check items in the refinement list, but when you refresh the page or access the generated URL directly, it resets itself and the state is lost.

putting a query works though (https://test.com/search?query=hey). The refinement list is the only problematic one.

I can capture the refinement list in the query parameters in the backend, but the frontend is the one that’s resetting it for some reason.

update 3

I figured out what’s causing the URL to reset. I’m using <ais-configure> to sort the results based on the user’s current location:

<ais-configure
    :distinct="true"
    :around-lat-lng.camel="coordinates"
    :around-radius.camel="around_radius"
    :hits-per-page.camel="10"
/>

The coordinates is empty by default:

data() {
  return {
    coordinates: ""
  }
}

But when the component is created, I determine the user’s location and update the coordinates. This will then trigger it to re-render. Thus losing the initial state supplied through the URL:

created() {
    this.$getLocation().then((coordinates) => {
        this.coordinates = `${coordinates.lat},${coordinates.lng}`;
    });
},

How do I make it so that it doesn’t do that? Or atleast to capture the route state on page load and apply those refinements again once the coordinates becomes available. That way the initial state isn’t lost.

Hi Ellis,

I’ve looked at the initial code and the CodeSandbox demo you provided. It seems that your parseURL method is misplaced: it should be a method on the router object, not at the top-level of the routing one.

I’ve forked your demo and adjusted the code, and it seems to be working as expected.

Best,

Sarah.

1 Like