Using routing, but want to be able to navigate directly to URL & have it search

I am using Jekyll & the Jekyll Algolia integration. I’m using Instantsearch.js@2.10.4, but I am willing to upgrade if it will solve this problem.

I am using routing to set my URL (below is a snippet demonstrating). So, I get /?s={query}, which is fantastic.

However, I have a requirement that I can navigate directly to a URL, with the query parameter, and have my search page open and have already searched for the query (our application has a built in search that’s meant to lead toward our site, and we currently can’t change it right now.

Using /?s= is what we need for that link, but currently, the search doesn’t go through.

Is there a way for me to set the search query from the URL parameter and search for it by default? Ideally on page load?

Any assistance is appreciated. :slightly_smiling_face:

const search = instantsearch({
    appId: '{{ site.algolia.application_id }}',
    apiKey: '{{ site.algolia.search_only_api_key }}',
    indexName: '{{ site.algolia.index_name }}',
    searchParameters: {
        restrictSearchableAttributes: [
        'title',
        'content'
        ]
    },
    routing: {
    stateMapping: {
      stateToRoute: function(uiState) {
        console.log(uiState);
        return {
          s: uiState.query
        };
      },
      routeToState: function(routeState) {
        console.log(routeState);
        return {
          s: routeState.query
        };
      },
    },
  },
});

Hi @claire.lundeby,

I have a codesandbox to demonstrate routing that might help. Note that it is using Instantsearch v3 instead of v2, but should help you determine whether you want to upgrade and how to implement. We always suggest upgrading if possible to take advantage of new features.

Yes, I see that you have the routing set up in that sandbox. What I’m looking for - I want to be able to navigate to “myurl.com/search/?s=query” and have my search page open to the search results for that query.

Hi @claire.lundeby,

If you got to the sandbox and enter the url: https://9b9zk.csb.app/search/?query=testing in the url in the url field just above the sandbox, you’ll see that it goes to the search results.

If you use this address in your browser, you will also be taken to the search results for that codesandbox.

Line 84 of the src/search-routing.js file, shows the code for the parseUrl and the code below that shows the stateMapping.

Is that what you are looking for?

That is what I’m looking for, except when I go to that URL, I just get the search page as if no search had been performed. Same thing when I enter the URL in the sandbox.

I need for the search to have already been performed on the page – when you open the URL, you’re seeing search results, not just the default search page.

Hi @claire.lundeby,

My apologies, I sent the wrong link. Please try this one:

Oh, that works - thank you! I’ll try this out.

1 Like

@cindy.cullen Is there a way to get this working with InstantSearch v2? I am using the jekyll-algolia integration and when I attempted to upgrade to v3 things got very broken.

Hi @claire.lundeby, it is indeed do-able in v2! It would look something like this: https://codesandbox.io/s/instantsearchjs-app-1gliy.

(Just needed to change s: routeState.query to s: routeState.s, in your original snippet.)

Is that what you are looking for?

With that we are back to having the query show up in the URL after I search, but I still can’ t navigate directly there.

When I try that with my actual implementation, I don’t get it tracked in the URL either.

const router = instantsearch.routers.history({
  windowTitle({ query }) {
    const queryTitle = query ? `Results for "${query}"` : 'Search';
    return queryTitle;
  },

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

    const queryString = qsModule.stringify(queryParameters, {
      addQueryPrefix: true,
      arrayFormat: 'repeat',
    });
    console.log(routeState);
    console.log(baseUrl + queryString);
    return `${baseUrl}search/${queryString}`;
  },

  parseURL({ qsModule, location }) {
    const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
    const { query = ''} = qsModule.parse(
      location.search.slice(1)
    );
    return {
      query: decodeURIComponent(query),
    };
  },
});

const stateMapping = {
  stateToRoute(uiState) {
    return {
      s: uiState.query,
    };
  },
  routeToState(routeState) {
    return {
      s: routeState.s,
    };
  },
};

const searchRouting = {
  router,
  stateMapping,
};

const search = instantsearch({
  appId: '{{ site.algolia.application_id }}',
  apiKey: '{{ site.algolia.search_only_api_key }}',
  indexName: '{{ site.algolia.index_name }}',
  router: searchRouting,
});

Hi @claire.lundeby, thanks for the snipped of your implementation, it was very helpful.

If you’re using instantsearch.routers.history you to not need the stateMapping function. Also I think you inverted s and query in createURL and parseURL.

Here’s your implementation witht the fixes:

const router = instantsearch.routers.history({
  windowTitle({ query }) {
    const queryTitle = query ? `Results for "${query}"` : 'Search';
    return queryTitle;
  },

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

    const queryString = qsModule.stringify(queryParameters, {
      addQueryPrefix: true,
      arrayFormat: 'repeat',
    });
    console.log(routeState);
    console.log(baseUrl + queryString);
    return `${baseUrl}search/${queryString}`;
  },

  parseURL({ qsModule, location }) {
    const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
    const { s = ''} = qsModule.parse(
      location.search.slice(1)
    );
    return {
      query: decodeURIComponent(s),
    };
  },
});

const searchRouting = {
  router,
};

const search = instantsearch({
  appId: 'latency',
  apiKey: '6be0576ff61c053d5f9a3225e2a90f76',
  indexName: 'instant_search',
  routing: searchRouting,
});

Hi Yannick,

Thank you for jumping on the thread. When I try your suggestion on my site, it doesn’t get tracked in the URL and I still can’t navigate directly to the search results via url.

Here is the entire script for my search page. Do you see anything else that could be causing your suggestion not to work?

My site also includes Bootstrap 4.3 JS + dependencies, and a javascript library for lightbox images. I am using Algolia’s autocomplete elsewhere in my site.

const router = instantsearch.routers.history({
  windowTitle({ query }) {
    const queryTitle = query ? `Results for "${query}"` : 'Search';
    return queryTitle;
  },

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

    const queryString = qsModule.stringify(queryParameters, {
      addQueryPrefix: true,
      arrayFormat: 'repeat',
    });
    console.log(routeState);
    console.log(baseUrl + queryString);
    return `${baseUrl}search/${queryString}`;
  },

  parseURL({ qsModule, location }) {
    const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
    const { s = ''} = qsModule.parse(
      location.search.slice(1)
    );
    return {
      query: decodeURIComponent(s),
    };
  },
});

const searchRouting = {
  router,
};
const search = instantsearch({
  appId: '{{ site.algolia.application_id }}',
  apiKey: '{{ site.algolia.search_only_api_key }}',
  indexName: '{{ site.algolia.index_name }}',
  router: searchRouting,
  attributesForFaceting: ['filterOnly(roles)', 'filterOnly(application)'],
});

const hitTemplate = function(hit) {

  let url = `{{ site.baseurl }}${hit.url}#${hit.anchor}`;
  const title = hit._highlightResult.title.value;
  const content = hit._highlightResult.html.value;

  return `
    <div class="post-item">
      <h2><a class="post-link" href="${url}">${title}</a></h2>
      <div class="post-snippet">${content}</div>
    </div>
  `;
}

search.addWidget(
  instantsearch.widgets.searchBox({
    container: '#search-searchbar',
    placeholder: 'Search',
    wrapInput: true,
    magnifier: false,
    reset: false,
    cssClasses: {
        input: 'form-control',
    },
  })
);


search.addWidget(
  instantsearch.widgets.hits({
    container: '#search-hits',
    templates: {
      item: function(hit) {
        return `
          <div class="post-item">
            <h3><a class="post-link" href="{{ site.baseurl }}${hit.url}">${hit.title}</a></h2>
            <div class="post-snippet">${hit.html}</div>
          </div>
        `;
      },
      empty: '<p>No results found for <em>{{query}}</em>.'
    }
  })
);

search.addWidget(
  instantsearch.widgets.refinementList({
    container: '#roles',
    attributeName: 'roles',
    operator: 'and',
    limit: 10,
    cssClasses: {
      checkbox: 'form-check-input',
      item: 'form-check-label',
      header: 'search-header',
      body: 'form-check',
    },
    templates: {
      header: 'Roles',
    }
  })
);


search.addWidget(
  instantsearch.widgets.menuSelect({
    container: '#application',
    attributeName: 'application',
    limit: 10,
    collapsible: true,
    cssClasses: {
      select: 'form-control',
      header: 'search-header',
      footer: 'menu-footer',
      item: 'menu-item',
    },
    templates: {
      header: 'Applications',
      seeAllOption: 'All',
      footer: 'Select an application to filter your search',
    }
  })
);

search.addWidget(
  instantsearch.widgets.clearAll({
    container: '#clear-all',
    cssClasses: {
      link: ['btn', 'btn-sm', 'btn-primary'],
    },
    templates: {
      link: 'Clear Filters'
    },
    autoHideContainer: false,
    clearsQuery: true,
  })
);

search.addWidget(
  instantsearch.widgets.currentRefinedValues({
    container: '#current-refined-values',
    clearAll: false,
  })
);

search.addWidget(
  instantsearch.widgets.pagination({
    container: '#pagination-container',
    maxPages: 20,
    // default is to scroll to 'body', here we disable this behavior
    scrollTo: false,
    autoHideContainer: true,
  })
);

search.start();

Hi @claire.lundeby,
The code you linked that has to do with routing appears to be copied and pasted almost exactly as it is @yannick.croissant’s sandbox: https://codesandbox.io/s/instantsearchjs-app-9h8g6.

The only thing I noticed is that in the initialization of the instantsearch, you appear to use router: searchRouting rather than routing: searchRouting. Could you please make that change and see if it does it?

If not, would you be able to provide a live environment (such as a fork of the sandbox) where the issue is reproduced? Otherwise I am not sure that we will be able to see why the routing is not working as it does the sandbox.

Wow, yes, @maria.schreiber & @yannick.croissant, switching it to routing: searchRouting works. It’s been a long month. :wink:

Thank you so, so much for your help. You’re all so wonderful.

1 Like

:tada: Glad to hear it!

We’ve all had those long months. :slight_smile: