The project I'm working on is written in Gatsby JS, but the solution itself is vanilla react and will work everywhere.
The project I'm working on is written in Gatsby JS, but the solution itself is vanilla react and will work everywhere.
Today, I spent most of my time updating my blog, and thought to add more features like search, tags, MDX support, and a few design changes, including the sidebar.
I was deciding how I would implement the search function, because the only time I have done it, was using a
Self hosted version of Typesense
But well, that was costly to host server-side, especially for
something as simple as a blog. and their hosted solutions aren't that great price-wise either.
So one thing was sure, there is no need to use any API for this. After a quick google search, I came across this documentation on Gatsby's website which is about adding search to Gatsby
From that guide, under the Client Side
section, here's what they recommend:
It is possible to do all the work in your Gatsby site without needing a third-party solution. This involves writing a bit of code, but using less services. With large amounts of content to index, it can also increase the bundle size significantly.
One way of doing this is to use the js-search library:
There are two Gatsby plugins that support this as well:
gatsby-plugin-elasticlunr-search
gatsby-plugin-local-search
Now these search methods index everything which means higher bundle size. And they are also a hassle to set up.
The solution I went with
Now for my use case, it was probably a good idea to just make something simple by myself, and I can build on it as I keep updating this blog.
The idea is really simple, I just need to make a search box, and on every keystroke, loop through the contents and filter them like that.
const BlogIndex = ({ data, location }) => {
// These posts can be anything,
// I've just used the posts from a gatsby query
const posts = data.allMdx.edges;
// We need to filter the posts by the search query.
// by default, we have all posts
const [filteredPosts, setFilteredPosts] = useState(posts);
// This will be the search query
const [search, setSearch] = useState('');
return (
<div>
{/* Our search bar */}
<input
type="text"
placeholder="Search"
onChange={(e) => {
e.preventDefault();
setSearch(e.target.value)}
}/>
{/* Simply mapping through everything and rendering blogs */}
{filteredPosts.map(({ node }) => {
<BlogPost post={node} key={node.id} />
}}
</div>
)
}
Now, whenever something is typed in the box, the search
state will be updated. Now, let's write a useEffect
hook to update the filteredPosts
state whenever the search
state changes.
const BlogIndex = ({ data, location }) => {
const posts = data.allMdx.edges;
const [filteredPosts, setFilteredPosts] = useState(posts);
const [search, setSearch] = useState('');
//highlight-start
useEffect(() => {
if (search) {
// post filtering here
}
}
// only update the filteredPosts state when the search state changes or the posts state changes
, [search, posts]);
///highlight-end
return (
... // rest of the code
)
And now let's write some very simple code to filter the posts.
...
if (search) {
const filteredPosts = posts.filter(post => {
//highlight-start
const title = post.title.toLowerCase();
const description = post.description.toLowerCase();
return title.match(search.toLowerCase()) || description.match(search.toLowerCase());
//highlight-end
}
}
setFilteredPosts(filteredPosts);
...
Since my blog has tags and stuff like that, I added functionality to search and filter by tags, too
if (search.startsWith("#")) {
return tags.includes(search.replace("#", ""));
}
...
And that's it! But wait, there's more. This works, but you can't really share a search query to someone else, I can share google links - google.com/search?q=github
I think that's kinda important, like, for times when I have to share all my Rust blogs, it's just easier and convenient.
so well, let's update the URL to include the search query, in real time! I hadn't ever done this before, so it was great learning it. I got the inspiration from the IFTTT search engine
I found out about the window.history.pushState()
method, which basically allows you to push a new URL without adding it to the browser history, or reloading the page. Read the documentation for the same over here -
History API | MDN
useEffect(() => {
if (search) {
//highlight-start
if (window.history.pushState) {
window.history.pushState(null, null, `/?q=${search}`);
}
//highlight-end
const filteredPosts = posts.filter(post => {
const title = post.title.toLowerCase();
const description = post.description.toLowerCase();
return title.match(search.toLowerCase()) || description.match(search.toLowerCase());
}
}
setFilteredPosts(filteredPosts);
}, [search]);
And now, we also need to parse the original request, using the window location
object, and make it default for the useState
hook we made for search
// ππ» converts the URL from HTML encoded to a string (%20 to space)
const initialState = decodeURI(location.href? // Use window location
.split('/') // Split the URL into an array
.pop() // Get the last element only
.split("=") // at this point, it's q=search, so we only need the "Search" parth
.pop() );
// ππ» We're using the initialState to set the search query.
const [search, setSearch] = useState(initialState); // Now, only the blogs that match the query will be displayed on first load
That's it!
The full implementation can be found in the source code of this blog on Github
You can try out the search yourself
Top comments (2)
without any search* dependencies, (you don't even need gatsby btw)