Hey everyone! 👋 Ever felt like your blog was missing that one crucial feature that makes it super user-friendly? For me, it was a search bar! After tinkering around, I successfully integrated a dynamic search functionality into my Gatsby blog, and I’m thrilled to share the journey with you.
This post will walk you through how I added a powerful search feature, powered by Fuse.js
, right into my blog’s sidebar. Let’s dive in! 🚀
My main objective was to allow visitors to quickly find posts by typing keywords, without having to navigate through endless pagination. I wanted something fast, efficient, and well-integrated into the existing design. The sidebar seemed like the perfect spot!
Here’s a peek at the main files involved and how they played their part:
Search.tsx
: This is the brain of the operation! It handles the search input, debouncing (so we don’t search on every single keystroke), and displaying results. It leverages Fuse.js
for fuzzy searching.Sidebar.tsx
: My blog’s sidebar component. This is where the Search
component would visually live.IndexTemplate.tsx
: The main template for my blog’s home page. This file is crucial for fetching all the necessary post data via GraphQL and passing it down to the Sidebar
(and subsequently, to Search
).cleanUrl.ts
(or .js
): A small but mighty utility file to fix a pesky URL issue I encountered.Search
Component (Search.tsx
) 🧠The Search.tsx
component is a React functional component that manages the search logic.
State Management: I used useState
for the query
(what the user types), debouncedQuery
(a delayed version of the query to prevent excessive searches), and results
(the array of found posts).
Fuse.js Integration: Fuse.js
is fantastic for fuzzy searching. I initialized it within a useMemo
hook to ensure the search index is only rebuilt when the posts
data changes, optimizing performance. I configured it to search frontmatter.title
and excerpt
.
// Search.tsx snippet
const fuse = useMemo(() => {
return new Fuse(posts, {
keys: ["frontmatter.title", "excerpt"],
includeScore: true,
threshold: 0.4,
});
}, [posts]);
Debouncing the Query: A useEffect
hook handles debouncing the search query. This is super important for performance, as it delays the search operation until the user pauses typing.
// Search.tsx snippet
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedQuery(query);
}, 300); // 300ms delay
return () => { clearTimeout(timerId); };
}, [query]);
Performing the Search: Another useEffect
triggers the actual search when debouncedQuery
changes. It filters results to show only after at least 2 characters are typed.
Displaying Results: Search results are rendered as a list of Gatsby Link
components, showing the post title and a snippet of its excerpt.
Sidebar.tsx
) 🤝To make the search bar visible, it needed a home. The Sidebar.tsx
was the natural choice.
Prop Drilling: The Sidebar
component now accepts a posts
prop. This posts
array, containing all blog entries, is then passed directly to the Search
component.
Rendering Search
: I simply imported Search
and placed it within the sidebar’s JSX:
// Sidebar.tsx snippet
import Search from "../components/Search/Search";
// ...
const Sidebar = ({ isIndex, posts }: Props) => {
// ...
return (
<div className={styles.sidebar}>
<div className={styles.inner}>
{/* ... other sidebar components */}
<Search posts={posts} /> {/* Here's our search bar! */}
{/* ... other sidebar components */}
</div>
</div>
);
};
IndexTemplate.tsx
📦This was a critical step to ensure the Search
component had all the data it needed to perform its magic.
GraphQL Query for All Posts: I added a new GraphQL query alias, allPosts
, to fetch all markdown remarks that are posts and not drafts. This query fetches slug
, title
, date
, description
, and excerpt
.
// IndexTemplate.tsx snippet
export const query = graphql`
query IndexTemplate($limit: Int!, $offset: Int!) {
// ... existing allMarkdownRemark query for pagination
allPosts: allMarkdownRemark(
filter: { frontmatter: { template: { eq: "post" }, draft: { ne: true } } }
) {
nodes {
fields { slug }
frontmatter { title date description }
excerpt
}
}
}
`;
Passing Data: The nodes
from data.allPosts
are then extracted and passed as the posts
prop to the Sidebar
.
// IndexTemplate.tsx snippet
const allPosts = data.allPosts.nodes;
return (
<Layout>
<Sidebar isIndex posts={allPosts} /> {/* Passing posts to Sidebar */}
{/* ... */}
</Layout>
);
cleanUrl.ts
) 🧹During testing, I noticed a peculiar bug where clicking a search result would lead to URLs like http://localhost:8000/posts//posts/AI-Journey-2024/
– a duplicated /posts/
segment! 🤦♀️
To fix this, I created a small utility function cleanDuplicatePostsPath
that uses a regular expression to find and replace the redundant "/posts//posts/"
with a single "/posts/"
.
// src/utils/cleanUrl.ts
export function cleanDuplicatePostsPath(urlPath: string): string {
const cleanedPath = urlPath.replace(/\/posts\/\/posts\//g, '/posts/');
return cleanedPath;
}
This function is then imported into Search.tsx
and applied to the post.fields.slug
before it’s passed to the Link
component.
// Search.tsx snippet using cleanUrl.ts
import { cleanDuplicatePostsPath } from '../utils/cleanUrl'; // Adjust path
// ...
const cleanedSlug = cleanDuplicatePostsPath(post.fields.slug);
<Link to={cleanedSlug}>
And just like that, my Gatsby blog now has a fully functional, sleek search bar right in the sidebar! It’s a game-changer for navigation and finding specific content.
If you’re looking to add a search feature to your Gatsby blog, I highly recommend this approach. It’s robust, efficient, and provides a much better user experience.
Happy blogging! 💻✨