How I added Algolia search: front-end
04 Oct 2021
For a few months now, this site has had an internal search function, and some of you are even using it!
Here’s a new code demo showing how I put an Algolia search box into my web site to actually do something with all that data Algolia had on file.
It’s a great approach for Jekyll, Hugo, 11ty, or other truly statically generated sites. (I’ve tried it on both Jekyll and 11ty so far.)
Getting Algolia to index my site is a story for another day. (Hint: every time I submit new content, my site builds twice now – once for you to read, and once for Algolia to read. Algolia doesn’t actually care that it’s not quite reading from the build process of “the real site.” I have to do things this way because of the tech stack behind my “real site” builds.)
(Note to self: I really should write up the “backend” post, because a handy feature of my “2nd build” is that I can inspect the its logs way better than my “main site build” process lets me do.)
Search page
My “search” web page has 3 placeholder <DIV>
tags in it:
<div id="search-searchbar" class=""></div>
<div id="search-poweredby" class=""></div>
<div id="search-hits" class=""></div>
It then has some pretty massive <SCRIPT>
tags in it, which summon all sorts of JavaScript written by Algolia and hosted on a content delivery network (“CDN”) and then invoke that code, passing it my Algolia index’s application ID, search API key, and index name.
Simple, ugly, URL-unaware version
After reading the front-end half of Salma Alam-Naylor’s “How to add Algolia InstantSearch to your Next.js application,” I’m seeing familiar patterns. I’ll take a crack at explaining how my code works.
Her first working-but-ugly demo looks like this:
My first working-but-ugly demo looks like this:
Despite our different approaches, we’re getting pretty much to the same place, so I think that means we’re on track to have something that can be taken apart and have universal lessons learned from it. Let’s go!
Instantiate from the algoliasearch library
You need to instantiante an algoliasearch
object from Algolia’s “Algoliasearch Lite” library and save off the return value into a variable.
When you do, you need to pass the constructor a string representing your Algolia application ID and another string representing your Algolia public search API key.
- I get this library as
cdn.jsdelivr.net/.../algoliasearch-lite.umd.js
over their CDN. It comes with analgoliasearch(...)
function in it. - Salma installs it as an NPM package
algoliasearch
and uses thealgoliasearch(...)
found within itsalgoliasearch/lite
section.
It’s conventional to put the word client
into that variable’s name.
I presume this object is the one that deals with all the asynchronicity of reaching out to Algolia’s servers, etc. I find it useful to think of it kind of like saving my app ID & key into environment variables & setting up a Postman collection that uses the same authentication for every request in it. Oh, and as a tangent, if you want to try searching Algolia with Postman against a sample index that Algolia leaves sitting around for everyone on the internet to try, I wrote a Postman collection.
Instantiate from the instantsearch library
You also need to instantiate an instantsearch
object from Algolia’s “Instant Search” library.
When you do, you need to pass the constructor the algoliasearch
object you saved into a variable with client
in its name, as well as a string representing your Algolia index name.
- I get this library as
cdn.jsdelivr.net/.../instantsearch.production.min.js
over their CDN. It comes with aninstantsearch(...)
function in it, which I call as:const search = instantsearch({ indexName, searchClient, });
- Salma installs it as an NPM package
react-instantsearch-dom
. It comes with an<InstantSearch>...</InstantSearch>
React component in it, which she calls by including it in her JSX templates.
Attach instantsearch to the DOM
Both Salma and I have an objective of trying to inject one of these thingies into the “DOM” / HTML of our page (this is Salma’s diagram):
React approach
Salma gets to do magic and just type out some JSX:
export default function Search() {
return (
<>
<InstantSearch searchClient={searchClient} indexName="my_awesome_content">
<SearchBox />
<Hits />
</InstantSearch>
</>
);
}
Vanilla HTML + CDN JavaScript approach
I, on the other hand, have to have this snippet in my HTML template:
<div id="search-searchbar" class=""></div>
<div id="search-poweredby" class=""></div>
<div id="search-hits" class=""></div>
And then this code:
search.addWidgets([
instantsearch.widgets.searchBox({...some settings...}),
instantsearch.widgets.poweredBy({...some settings...}),
instantsearch.widgets.hits({...some settings...}),
]);
search.start();
Et voilà! A working instant search box, with really ugly-looking search results.
Note that it’s completely insensitive to putting any sort of search term in the URL bar – I’ll have to work on that.
URL-aware, pretty version
Just like Salma went on to replace her use of <Hits />
with <CustomHits />
, I’m going to define a really fancy function (don’t ask me how it works; I copied & pasted it) called hitTemplate
to and pass it to instantsearch.widgets.hits({...some settings...})
. That cleans up the search result formatting from being a bunch of ugly JSON to actually looking like something people want to read.
- I add Moment.js, for date formatting, to my imports.
- Here’s the updated script code.
The only catch is that my definition of hitTemplate
is customized to properties found on objects in my Algolia index.
And there’s no guarantee that any other index’s objects in the world – heck, even other objects in my own index – have the same properties I’ve trained my hitTemplate
to expect. I wrote about this last year.
So when I swap out my personal Algolia app ID, key, & index name for this sample one in my .env
file, things seem to break.
“Search approach #1” works, but this new “search approach #2” seems to show blank results. The “hits” box seems confused.
The “hits” box reverts to being ugly and filtering by “acer” or “google” works fine, though, if I comment out templates: {item: hitTemplate,},
in my instantsearch.widgets.hits({...some settings...})
.
So I guess it works, but buyer beware that any given hitTemplate
you find in someone else’s sample code might need to be totally rewritten to work with your index.
No wonder the default Algolia Instant Search styling shows you a bunch of stringified JSON in the “hits” box. You’ve gotta take a look at the data before you can write a good template to format it, so they might as well put the data right under your nose.
In this approach to writing a search page, I’ll also go a step further than Salma did and customize my invocation of instantsearch(...)
to make it URL-parameter-sensitive with what Algolia calls “routing.”
const singleIndexStateMapping = instantsearch.stateMappings.singleIndex(indexName);
const withoutPageStateMapping = {
stateToRoute(uiState) {
const { page, ...state } = singleIndexStateMapping.stateToRoute(uiState);
return state;
},
routeToState(routeState) {
const { [indexName]: indexUiState } = singleIndexStateMapping.routeToState(routeState);
const { page, ...state } = indexUiState;
return { [indexName]: state };
},
};
const search = instantsearch({ indexName, searchClient, routing: { stateMapping: withoutPageStateMapping }, });
That’s going to let me send people over to search from a non-JavaScript-based form (covered later in this post).
Nifty – it works! Things are pretty, and typing a keyword makes it appear in the URL bar, and directly re-visiting that URL later makes the keyword show up in the search box and filter the hits.
HTML form for other pages
To avoid injecting unnecessary JavaScript onto every page of my site, the rest of the pages on my site simply include a <FORM>
tag that bump you over to the search page if you submit something to the form.
Enhancing the search page with click tracking
Finally, I got it – on 10/5/21, I managed to add click tracking to my search.
Having an internal search box was useful for content creation, because I could see what my regulars expect me to have written about. (Google Search Console doesn’t tell me much except that most of my traffic comes from one-off visitors who need help installing a certain piece of software and leave after finding that one article.) Those are the readers I’d most like to write for.
Through the internal search, I’ve been able to see a few keywords that people seem to expect me to know about.
The ones I know won’t have results are mostly things I also wish I’d taken the time to figure out.
But for the ones I’ve written a lot about, I’d love to see what’s actually looking promising to visitors – what they’re clicking on.
I added a 3rd search page to my demo and pointed the home page’s <FORM />
at it (instead of at #2, where it had been pointed earlier).
I refactored the “hits template” to work both with my real site and with the “e-commerce” sample index that Algolia provides.
The “widgets script” was getting a little long, so I broke it up into 7 different blocks of <script>...</script>
HTML and included them back-to-back.
The new bits are:
- Code that loads up an object named
aa
to the browser’swindow
context from Algolia’s CDN-delivered “Search Insights” library. - Code that runs right after
const search = instantsearch(...);
and before defining the hits template, forcing thesearch
object to use the “Search Insights” middleware provided by the “Instant Search” and to wire it up towindow.aa
(which comes from the “Search Insights” library). - A modification to the definition of
hitTemplate
(besides the dataset-compatibility ones) so that instead of being afunction (hit) {...}
, it’s afunction (hit, bindEvent) {...}
. Plus, inside the actual string it returns, the<A>
link to click on a search result’s URL got an extra property in it: ` ${bindEvent(‘click’, hit, ‘Search Result Clicked’)}`.
And that’s that. On my own web site, I literally just had to go in and make those three changes. (I didn’t, of course, have to modify the hits template for compatibility w/ a sample dataset because on my web site, I am actually using my own dataset.)
Thanks to Bryan Robinson and mystery-French-accent-Algolia-employee for getting me to give this one last hurrah and finally breaking through.