Building a Blog #2 - Command Palette Search and View Transitions
- 1.Building a Blog #1 - Project Overview and Tech Stack
- 2.Building a Blog #2 - Command Palette Search and View Transitions
- 3.Building a Blog #3 - Color Theme Presets and Post Series
- 4.Building a Blog #4 - Advanced Search and SEO Optimization
- 5.Building a Blog #5 - Google Analytics Integration and View Counts
- 6.Building a Blog #6 - UX Improvements and Final SEO Polish
Command Palette Search (Cmd+K)
I implemented a Command Palette that lets users search quickly with Cmd+K, similar to VS Code or Raycast.
Libraries Used
cmdk- the core Command Palette library- shadcn/ui's
CommandDialog,CommandInput, andCommandListcomponents
Search Logic
The searchPosts() function searches title, description, tags, and content case-insensitively. Because all post data is already loaded in memory at build time, there is no need for async handling or debouncing.
export function searchPosts(query: string): PostMeta[] {
if (!query.trim()) return []
const q = query.toLowerCase()
return getAllPosts().filter(({ title, description, tags, content }) =>
[title, description, ...tags, content]
.some((field) => field.toLowerCase().includes(q))
)
}
Detour: cmdk's Built-in Filter
At first, no results appeared even after typing a search query. The cause was cmdk's built-in filter. It matches against the value prop of CommandItem, which was the slug.
When using custom search logic, shouldFilter={false} must be set.
<CommandDialog shouldFilter={false}>
I also had to adjust shadcn's CommandDialog component so that it forwarded the shouldFilter prop internally.
Keyboard Shortcut
useEffect listens for Cmd+K / Ctrl+K.
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((prev) => !prev)
}
}
document.addEventListener("keydown", onKeyDown)
return () => document.removeEventListener("keydown", onKeyDown)
}, [])
View Transitions API
I added the View Transitions API to make page changes feel smoother.
Migrating to a Data Router
React Router's <Link viewTransition> prop works only with data routers. The app had to move from the old <BrowserRouter> setup to createBrowserRouter.
Before:
// main.tsx
<BrowserRouter>
<App />
</BrowserRouter>
// App.tsx
<Routes>
<Route path="/" element={<HomePage />} />
</Routes>
After:
// App.tsx
const router = createBrowserRouter([{
element: <RootLayout />,
children: [
{ index: true, element: <HomePage /> },
{ path: "posts/:slug", element: <PostPage /> },
],
}])
export function App() {
return <RouterProvider router={router} />
}
Fixing the Overlap
The default View Transition crossfades the old page and the new page. For a very short moment, both pages appeared on top of each other.
I changed the old page to disappear immediately and only faded in the new page.
::view-transition-old(root) {
animation: none;
}
::view-transition-new(root) {
animation: fade-in 100ms ease-in;
}
@keyframes fade-in {
from { opacity: 0; }
}
At 100ms, the transition was quick but still noticeable.
The next post covers color theme presets and post series support.