What is a Dictionary without a search engine or ummm the search feature!?
During the implementation of the base dictionary, I had created these static Search forms (one on the homepage and the other on the Navbar used on the word layout) in preparation for this particular feature.
I just needed to pick-up from right there and get it working, easy work — if only that was true.
Something from the past
It is important to re-iterate that my initial plan was to build jargons.dev with Nextra as I admitted in the initial commit that:
...Nextra (this was infact my knight in shiny armor, I was looking to build with Nextra).
I am a React ⚛️ fan boy, big on Next.js; Nextra is a content-focus web framework that is built on Next.js. So I guess you can just understand why Nextra sounded like a that knight. During my initial exploration of Nextra, one feature stood out to me; the full-text search — I drooled 🤤over this one (I must confess).
The feature was powered by flexsearch — a zero-deps full-text search library; ooh boy I'm a big fan of lightweight and no/low dependencies. I dug into how Nextra uses this to index content at build-time for search; it was interesting.
So!?
I found myself hacking with flexsearch during my early encounter with Astro; as I followed the astro doc's build a blog tutorial, I went a notch further to implement a search feature very easily.
So, the experience from this implementation; I passed down to the search engine for jargons.dev.
The Search Engine
The task was pretty simple, I needed to..
- Get access or call it reference to all files inside of the dictionary directory of words - at this point it was the
src/pages/word
directory - Get these files content indexed with flexsearch
- Plug in the search form and boom 😉
Looks very easy! Maybe for the search indexing and actual searching, it was; but there was some lot of stuffs that went into getting there.
Integrating the first "Island" in jargons.dev
Astro by default takes a server-first approach, meaning it builds your site's HTML/CSS on server removing all client-side javascript (JS) automatically (unless you state other wise). Removing all JS assures performance improvement, but No JS means no interactivity; But if you want interactivity, Astro Island is one of the ways to go. I need the interactivity for the Search Engine so Island it is!
What's an "Island" though!?
I will put simply that an Island is an isolated interactive piece of component on a web page, whose HTML/CSS are rendered on the server-side and/but it's client-side javaScript are (hydrated) also bundled with it - NOT removed.
I gave a talk about Island at TILConf'24, Check it out to learn more.
Astro's offering
Astro offered support for integrating Islands out the box with my favorite UI library (yea, you guessed it, React) of many others. This allowed me build my static Search forms into a functional stuff.
Stuffs I did
- I started by adding the integration module (@astrojs/react) for the Island I needed to integrate; done pretty easily with the
npx astro add react
command - I transferred all the static search forms into single React component (these are two differently sized forms); configured the component to render these at required size based on given props.
- I also implemented some sub-component within that are only consumed locally in the-same search component, these are...
- The SearchDialog - main component where the search operation is carried out
- The SearchResult component, etc...
- I implemented some custom keyboard shortcuts and keybinds that enables interactions with the search component (I'd like to call this "Search Island" from now on), these are...
-
CTRL+K
or⌘K
to start search -
ESC
to close search - ...and base required navigation buttons to navigate within search results
-
- I also added a few custom hooks to allow smooth sailing in the working of the search island, these are...
-
useLockBody
- a hook that disables scrolling once the search dialog is opened -
useRouter
- a hook that I made as wrapper around somewindow.location
methods, making them to feel like the known router libs in React, this is a hook I particularly consumed on theENTER
button click handler in the navigation button keybind on search results component in the search island. - and
useIsMacOS
- which checks whether a machine is MacOS in order to determine the appropriate description text to render on the search form trigger; i.e. CTRL+K or ⌘K
-
- I added the imperative module - flexsearch;
- I retrieved access to files the directory of words using the
Astro.glob()
function super easily (too bad I couldn't talk about how powerful this function is; how glad I am it existed out-the-box in Astro and how much ease it brought into the flow of getting this search engine up and running) and plugged the returned array of word objects into a$dictionary
state (maybe I should call this a store) powered by nanostore (another beautiful stuff right there) - This
$dictionary
are then indexed with flexsearch, prepping them for later search.
Another Imperative feature: The Recent Searches
This is another imperative feature that I must talk about; This feature keeps track of searched items and stores them in localstorage
to persist them on page reload; these store searched items are then render in a list on the homepage of the dictionary.
It also took integration as an Island, couple with a holding the value in a nanostore powered $recentSearches
state.
My implementation of this feature isn't exactly perfect, and here are a list of some Issues that needed fix (at time of writing) to take it a step further down that route (even though we can never reach perfection, YEA for sure)
- Add Loading Component to Recent Searches Island - https://github.com/devjargons/jargons.dev/issues/31
- Bug: Search Operation Performed with Search Form in Navbar Overwrites LocalStorage - https://github.com/devjargons/jargons.dev/issues/10
- Enhancement: Word Editor - Second Iteration Features - https://github.com/devjargons/jargons.dev/issues/9
The PR
This is some long read now, I wish to keep this reads short... Here's the PR
feat: implement dictionary search engine #5
This Pull Request implement the search functionality to the dictionary project. It uses @astro/react
integration to power the Islands coupled with nanostore
for state-management and flexsearch
as text search library.
- Added the following astrojs integration and lib required to text search
- @astrojs/react
- @nanostores/react
- flexsearch
- Implemented the
Search
Island (a react component) within where other sub components are implemented for internal usage- Implemented the
SearchTrigger
component which render a search field of two different sizes and used in two different places on the web page...- size
md
- used on the main page of the web app - size
sm
- used on the dictionaryword
layout navigation section
- size
- Implemented the
SearchDialog
component, which renders only when theSearchTrigger
is clicked - Implemented the
SearchInfo
component, renders as default placeholder when no search term has been inputted in form field - Implemented the
SearchResult
component, renders either search results or a message for a search results not found - Implemented keybinds within
Search
island to allow the following operation with the stated keyboard shortcuts-
CTRL+K
or⌘K
to open the search dialog without clicking on the search tigger -
ArrowUp
,ArrowDown
andEnter
to allow navigation on the Search results list -
ESC
to allow closure of search dialog
-
- Added the custom hooks for consumption on the
Search
island-
useIsMacOS
- check whether current user is browsing the web app with a MacOS machine; this is used to determine the appropriate short to render on the search trigger; i.e.CTRL+K
or⌘K
-
useLockBody
- used to disable current viewport from scrolling when search dialog is opened -
useRouter
- (instead of addingreact-router
to deps) this hook wraps aroundwindow.location
and uses theassign
object aspush
; mainly used in theSearchResult
component to route to selected/clicked result page
-
- Implemented
searchIndex
ing onSearch
island withflexsearch
'sDocument
method as preferred option- Added a new
search
store for managing the search-related states withnanostores
and@nanostores/react
integration - Added the following store values and actions
-
$isSearchOpen
- global state for managing the state ofSearchDialog
-
$recentSearches
- state for keeping track of recently searched words; it works in collab with thelocalStorage
to persist its value even after tab reloads -
$addToRecentSearchesFn
- a store action that adds new item to$recentSearches
store value
-
- Added a new
- Implemented the
- Added a
$dictionary
store for managing the entiredictionary
entries; keeping it accesible to the client and used as value forsearchIndex
in theSearch
island- Computed value for the
dictionary
store as early as possible from thelayout/base
with theAstro.glob()
method indexing the entire dictionary directory
- Computed value for the
- Added the
RecentSearches
island which reads the value from the$recentSearches
store and renders it on the homepage
Full Demo
screencast-bpconcjcammlapcogcnnelfmaeghhagj-2024.03.25-13_32_30.webm
📖
Top comments (0)