Implementing Article Search in kbar Command Palette - Modern Next.js Blog Series #27

Published on

This article is also published at it 邦幫忙 2022 iThome Ironman Contest

This is the 27th article in the "Modern Blog 30 Days" series.

After adding the Command Palette in the previous article, we continue to expand its functionality, enabling article search within the Command Palette and allowing navigation directly to specific article pages!

The final effect is as follows:

Command palette search post light

Command palette search post dark

The code changes for this article are as follows:

https://github.com/Kamigami55/nextjs-tailwind-contentlayer-blog-starter/compare/day26-command-palette...day27-search-post


Implementing Article Search Feature in kbar Command Palette

Ideally, to expand the options in kbar, you only need to modify the actions array in CommandPalette.tsx, and importing the list of allPosts provided by Contentlayer should suffice.

However, due to various technical limitations, we need to implement it in a more complicated way. If you find a simpler method, feel free to share it with me!

The technical limitations encountered, related to Next.js, Contentlayer, and kbar, are as follows:

  • Contentlayer cannot directly import allPosts in CommandPalette.tsx; it can only be imported in Next.js pages' getStaticProps.
  • Next.js currently does not support writing getStaticProps in _app.tsx for site-wide use, so getStaticProps in each page needs to be modified.
  • Dynamically adding Command Palette options on each page requires using kbar's useRegisterActions.

Thus, our implementation direction is:

  1. Add getCommandPalettePosts in getStaticProps of all pages to retrieve all articles.
  2. Add useCommandPalettePostActions in components of all pages to dynamically add all articles to Command Palette options using useRegisterActions.
  3. Modify the CommandPalette component, adding a "Search Articles" action, gathering all article options from 2. for better usability.

Let's start implementing!

Add src/components/CommandPalette/getCommandPalettePosts.ts:

import { allPostsNewToOld } from "@/lib/contentLayerAdapter"; export type PostForCommandPalette = { slug: string; title: string; path: string; }; export const getCommandPalettePosts = (): PostForCommandPalette[] => { const commandPalettePosts = allPostsNewToOld.map((post) => ({ slug: post.slug, title: post.title, path: post.path, })); return commandPalettePosts; };

Add src/components/CommandPalette/useCommandPalettePostActions.tsx:

import { useRegisterActions } from "kbar"; import { useRouter } from "next/router"; import { PostForCommandPalette } from "./getCommandPalettePosts"; export const useCommandPalettePostActions = ( posts: PostForCommandPalette[] ): void => { const router = useRouter(); useRegisterActions( posts.map((post) => ({ id: post.slug, name: post.title, perform: () => router.push(post.path), section: "Search Articles", parent: "search-posts", })), [] ); };

Next, modify each Next.js page to include the logic of getCommandPalettePosts and useCommandPalettePostActions.

Modify src/pages/index.tsx:

// ... import { getCommandPalettePosts, PostForCommandPalette, } from "@/components/CommandPalette/getCommandPalettePosts"; import { useCommandPalettePostActions } from "@/components/CommandPalette/useCommandPalettePostActions"; // ... type Props = { posts: PostForIndexPage[]; commandPalettePosts: PostForCommandPalette[]; }; export const getStaticProps: GetStaticProps<Props> = () => { const commandPalettePosts = getCommandPalettePosts(); // ... return { props: { posts, commandPalettePosts } }; }; const Home: NextPage<Props> = ({ posts, commandPalettePosts }) => { useCommandPalettePostActions(commandPalettePosts); // ... }; // ...

Modify src/pages/posts/[slug].tsx:

// ... import { getCommandPalettePosts, PostForCommandPalette, } from "@/components/CommandPalette/getCommandPalettePosts"; import { useCommandPalettePostActions } from "@/components/CommandPalette/useCommandPalettePostActions"; // ... type Props = { post: PostForPostPage; prevPost: RelatedPostForPostLayout; nextPost: RelatedPostForPostLayout; commandPalettePosts: PostForCommandPalette[]; }; // ... export const getStaticProps: GetStaticProps<Props> = ({ params }) => { const commandPalettePosts = getCommandPalettePosts(); // ... return { props: { post, prevPost, nextPost, commandPalettePosts, }, }; }; const PostPage: NextPage<Props> = ({ post, prevPost, nextPost, commandPalettePosts, }) => { useCommandPalettePostActions(commandPalettePosts); // ... }; // ...

Finally, modify src/components/CommandPalette/CommandPalette.tsx to add the Search section in the actions array:

import { MagnifyingGlassIcon, // ... } from "@heroicons/react/24/outline"; // ... export default function CommandPalette({ children }: Props) { // ... const actions = [ // Page section // ... // Add this section // Search section // - Search posts { id: "search-posts", name: "Articles", keywords: "search find posts writing words blog articles thoughts 搜尋 尋找 文章 寫作 部落格", icon: <MagnifyingGlassIcon className="h-6 w-6" />, section: "Search", }, // Operation section // - Theme toggle // ... ]; return ( <KBarProvider actions={actions}> <CommandBar /> {children} </KBarProvider> ); } // ...

Results

That's it! Use pnpm dev, enter the website, press Ctrl + K (Windows) or Cmd + K (Mac), or click the Command icon in the top-right corner to open the Command Palette. You'll see the "Search Articles" operation available for execution!

The final effect is as follows:

Command palette search post light

Command palette search post dark

The code changes for this article are as follows:

https://github.com/Kamigami55/nextjs-tailwind-contentlayer-blog-starter/compare/day26-command-palette...day27-search-post

References

Next Article

Congratulations on successfully adding article search functionality to the Command Palette!

In the next article, we finally complete the last key feature of this series: "i18next multilingual support"!