Skip to content

Conversation

@anushka-codes1
Copy link

PR Title

Added searchable blog functionality in navbar using shadcn/ui Command and fuse.js

Fixes: #3081

Description

This PR introduces a search feature in the website navbar, allowing users to quickly find blog posts by title, tags, or author without manually scrolling through the list.

The search experience uses shadcn/ui’s Command component for a modern, responsive, and accessible UX.
For smaller datasets (<500 blogs), search is performed client-side using Fuse.js for lightweight fuzzy searching.

Changes

Added a search icon + expandable Command input in the navbar.

Created a new component: components/blog-search.tsx.

Integrated Fuse.js for fuzzy search over blog titles, tags, and authors.

Styled according to Keploy.io design guidelines (dark theme, accent highlights).

Implemented keyboard shortcuts (Cmd + K / Ctrl + K) to open search.

Ensured mobile and desktop responsiveness.

Added links in results to corresponding /blog/[slug] pages.

Type of Change

New feature (change that adds functionality)

UI improvement (visual or design changes)

Testing

✅ Verified that search results update dynamically on each keystroke.

✅ Tested that all results correctly navigate to their respective blog pages.

✅ Checked responsiveness across mobile, tablet, and desktop.

✅ Tested fuzzy search (partial matches return expected results).

✅ Verified keyboard accessibility (arrow navigation + enter to open post).

Demo

Before:

No search functionality. Users manually scroll through blog list.

After:

Added global search modal accessible via navbar or keyboard shortcut.

Instant client-side filtering by title, tags, or author.

Environment and Dependencies

New Dependencies:

fuse.js – Lightweight fuzzy search library.

@/components/ui/command – From shadcn/ui for search UI.

Configuration Changes:

None required. Compatible with existing Next.js setup.

Checklist

My code follows the style guidelines of this project

I have performed a self-review of my own code

I have made corresponding changes to the documentation

I have added corresponding tests or manual verifications

I have run the build command to ensure there are no build errors

My changes have been tested across relevant browsers/devices

For UI changes, I've included visual evidence of my changes

Copilot AI and others added 4 commits November 11, 2025 13:10
- Resolved conflicts by accepting upstream changes for author handling improvements
- Kept search functionality additions (SearchCommand component)
- Updated package.json with search dependencies (cmdk, fuse.js)
- Kept header.tsx with SearchCommand integration
…201/

Resolve conflicts for PR related to issue keploy#201
Copilot AI review requested due to automatic review settings November 11, 2025 13:13
Copilot finished reviewing on behalf of anushka-codes1 November 11, 2025 13:17
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a searchable blog functionality in the website navbar using shadcn/ui Command component and Fuse.js for client-side fuzzy searching. Users can now quickly find blog posts by title, tags, or author using a keyboard shortcut (Cmd/Ctrl + K) or clicking the search button in the navbar.

  • Added new API endpoint /api/search-index to fetch and cache all blog post data
  • Integrated shadcn/ui Dialog and Command components for search UI
  • Implemented client-side fuzzy search with Fuse.js for fast search results
  • Added keyboard shortcut support and responsive design for mobile/desktop

Reviewed Changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pages/api/search-index.ts New API endpoint that fetches all blog posts from WordPress GraphQL API with pagination and caching
package.json Added dependencies: cmdk (^1.1.1), fuse.js (^7.1.0), and updated @radix-ui/react-dialog to ^1.1.15
package-lock.json Updated lock file with new dependencies and changed package name to "keploy-blog-website"
components/ui/dialog.tsx New reusable Dialog component wrapper for Radix UI Dialog primitives
components/ui/command.tsx New Command component providing search UI primitives from cmdk library
components/header.tsx Integrated SearchCommand component in both desktop and mobile navigation
components/SearchCommand.tsx Main search component implementing the search logic, keyboard shortcuts, and result rendering

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if (process.env.WORDPRESS_AUTH_REFRESH_TOKEN) {
headers["Authorization"] = `Bearer ${process.env.WORDPRESS_AUTH_REFRESH_TOKEN}`;
}
const res = await fetch(API_URL as string, {
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API endpoint doesn't validate that API_URL is defined before using it. If both environment variables are undefined, this will attempt to fetch from undefined as string, which will fail. Consider adding validation similar to the pattern used in lib/api.ts which checks for undefined API_URL and handles it gracefully.

Copilot uses AI. Check for mistakes.
}
`;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API endpoint doesn't restrict HTTP methods, allowing GET, PUT, DELETE, etc. requests when only POST is needed for internal API consumption. Consider adding a method check at the beginning of the handler (e.g., if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' })) to follow REST API best practices.

Suggested change
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") {
res.setHeader("Allow", "GET");
return res.status(405).json({ error: "Method not allowed" });
}

Copilot uses AI. Check for mistakes.
{results.map(({ item }) => {
const href = `${item.isCommunity ? "/community" : "/technology"}/${item.slug}`;
return (
<CommandItem key={item.slug} className="flex flex-col items-start">
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using item.slug as the key assumes all slugs are unique across both community and technology categories. If a slug appears in both categories, this will cause React key conflicts. Consider using a composite key like key={${item.isCommunity ? 'community' : 'technology'}-${item.slug}} to ensure uniqueness.

Suggested change
<CommandItem key={item.slug} className="flex flex-col items-start">
<CommandItem key={`${item.isCommunity ? 'community' : 'technology'}-${item.slug}`} className="flex flex-col items-start">

Copilot uses AI. Check for mistakes.
Comment on lines 103 to 137
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput
value={query}
onValueChange={setQuery}
placeholder={loading ? "Loading index..." : "Search by title, tag, or author"}
/>
<CommandList>
<CommandEmpty>{loading ? "Loading..." : "No results found."}</CommandEmpty>

{results.length > 0 && (
<>
<CommandGroup heading="Results">
{results.map(({ item }) => {
const href = `${item.isCommunity ? "/community" : "/technology"}/${item.slug}`;
return (
<CommandItem key={item.slug} className="flex flex-col items-start">
<Link href={href} className="w-full">
<div className="flex items-center justify-between w-full">
<span className="font-medium text-black" dangerouslySetInnerHTML={{ __html: item.title }} />
<span className="text-[11px] text-zinc-500">{item.isCommunity ? "community" : "technology"}</span>
</div>
<div className="mt-1 text-xs text-zinc-600">
{item.author}
{item.tags?.length ? ` • ${item.tags.slice(0, 3).join(", ")}` : ""}
</div>
</Link>
</CommandItem>
);
})}
</CommandGroup>
<CommandSeparator />
</>
)}
</CommandList>
</CommandDialog>
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dialog is missing an accessible title. Radix UI Dialog requires either a DialogTitle or aria-label/aria-labelledby for screen readers. Consider adding <DialogTitle className="sr-only">Search Blog Posts</DialogTitle> inside the DialogContent to improve accessibility.

Copilot uses AI. Check for mistakes.
Comment on lines 47 to 64
while (hasNextPage) {
const data = await fetchAPI(QUERY, { after });
const edges = data?.posts?.edges ?? [];
for (const { node } of edges) {
const categories = (node.categories?.edges ?? []).map((e: any) => e.node?.name).filter(Boolean);
const tags = (node.tags?.edges ?? []).map((e: any) => e.node?.name).filter(Boolean);
items.push({
title: node.title,
slug: node.slug,
author: node.ppmaAuthorName || node.author?.node?.name || "Anonymous",
tags,
categories,
isCommunity: categories.includes("community"),
});
}
hasNextPage = data?.posts?.pageInfo?.hasNextPage ?? false;
after = data?.posts?.pageInfo?.endCursor ?? null;
}
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The while loop that fetches paginated data has no upper bound, which could cause excessive API calls or infinite loops if the WordPress API returns inconsistent pagination data. Consider adding a maximum iteration limit (e.g., let iterations = 0; const MAX_ITERATIONS = 100;) to prevent potential runaway loops.

Copilot uses AI. Check for mistakes.
Comment on lines 118 to 129
<CommandItem key={item.slug} className="flex flex-col items-start">
<Link href={href} className="w-full">
<div className="flex items-center justify-between w-full">
<span className="font-medium text-black" dangerouslySetInnerHTML={{ __html: item.title }} />
<span className="text-[11px] text-zinc-500">{item.isCommunity ? "community" : "technology"}</span>
</div>
<div className="mt-1 text-xs text-zinc-600">
{item.author}
{item.tags?.length ? ` • ${item.tags.slice(0, 3).join(", ")}` : ""}
</div>
</Link>
</CommandItem>
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping a Link inside a CommandItem can cause issues with keyboard navigation and clicking. The CommandItem component from cmdk handles its own interactions, and having a Link inside may interfere with the command palette's built-in navigation. Consider using the onSelect prop on CommandItem to handle navigation instead: <CommandItem key={...} onSelect={() => router.push(href)}>

Copilot uses AI. Check for mistakes.
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full max-w-lg gap-4 rounded-lg border bg-white p-6 shadow-lg duration-200 sm:zoom-in-90 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class sm:zoom-in-90 appears to be a custom animation class that may not exist in the project's Tailwind configuration. This could result in no animation effect being applied. Verify that this class is defined in your Tailwind config or consider using standard Tailwind animation classes like sm:animate-in sm:fade-in-0 sm:zoom-in-95.

Suggested change
"fixed z-50 grid w-full max-w-lg gap-4 rounded-lg border bg-white p-6 shadow-lg duration-200 sm:zoom-in-90 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",
"fixed z-50 grid w-full max-w-lg gap-4 rounded-lg border bg-white p-6 shadow-lg duration-200 sm:animate-in sm:fade-in-0 sm:zoom-in-95 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +57
.catch((err) => {
if (err?.name !== "AbortError") {
// eslint-disable-next-line no-console
console.error(err);
}
})
.finally(() => {
if (isMounted) setLoading(false);
});
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the fetch fails (non-AbortError), the error is logged but the user sees no feedback. The loading state remains false and the search shows "No results found" which is misleading. Consider setting an error state and displaying a more informative message like "Failed to load search index. Please try again later."

Copilot uses AI. Check for mistakes.
@amaan-bhati
Copy link
Member

Hey @anushka-codes1 Thanks for raising this pr, can you please take a look at the copilot review once? Also, can you please add screenshots and previews for the same? There are chances that you might be using the same shadcn component being used in one of the other prs updating the navbar: #204

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +49 to +54
.catch((err) => {
if (err?.name !== "AbortError") {
// eslint-disable-next-line no-console
console.error(err);
}
})
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetch error handling silently swallows all non-AbortError exceptions. If the API call fails (network error, 500 response, etc.), users won't see any feedback. Consider setting an error state to display to users:

const [error, setError] = useState<string | null>(null);

// In the fetch catch block:
.catch((err) => {
  if (err?.name !== "AbortError") {
    console.error(err);
    setError("Failed to load search index. Please try again later.");
  }
})

// Then show the error in the UI

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +85
const buttonClasses =
"inline-flex items-center gap-2 h-9 px-3 rounded-md border bg-white text-sm text-zinc-600 hover:bg-zinc-100 transition";
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The buttonClasses variable is defined inside the component body but never changes. This should be moved outside the component or use useMemo to avoid recreating the string on every render:

const buttonClasses = useMemo(() => 
  "inline-flex items-center gap-2 h-9 px-3 rounded-md border bg-white text-sm text-zinc-600 hover:bg-zinc-100 transition",
  []
);

Or simply move it outside the component:

const BUTTON_CLASSES = "inline-flex items-center gap-2 h-9 px-3 rounded-md border bg-white text-sm text-zinc-600 hover:bg-zinc-100 transition";

Copilot uses AI. Check for mistakes.
anushka-codes1 and others added 3 commits November 13, 2025 09:45
Co-authored-by: Copilot <[email protected]>
Signed-off-by: Anushka Mukherjee <[email protected]>
Co-authored-by: Copilot <[email protected]>
Signed-off-by: Anushka Mukherjee <[email protected]>
Co-authored-by: Copilot <[email protected]>
Signed-off-by: Anushka Mukherjee <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants