The terminal-style command palette on this site is more than a decorated search box. It is a client-only React interface that connects site navigation, mini-games, theme controls, local achievements, and blog search behind a small command language. This article follows the implementation as it exists in components/CommandPalette.js and the modules around it.

For the content pipeline that makes the grep command possible, see Building a Static Markdown Blog Pipeline. The complete article index remains available on the blog.

Loading the Palette at the Layout Boundary

components/Layout.js mounts the palette once, outside the page-specific content:

const CommandPalette = dynamic(() => import('./CommandPalette'), { ssr: false });

The ssr: false option fits a component whose behavior depends on window, document, localStorage, and the Clipboard API. Because Layout wraps the site, the same palette instance is available while navigating between pages.

There are two opening paths. A global keydown listener in CommandPalette.js toggles the palette when either Command-K or Control-K is pressed:

if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
  e.preventDefault();
  setOpen(o => !o);
}

Buttons in sections/Navbar.js and sections/Hero.js offer a visible alternative. They dispatch a custom open-command-palette event, and the palette listens for that event with onOpenRequest. Both listeners are removed by the effect cleanup.

Commands as Small Integration Points

runCommand trims the input, records it in the transcript and history, then separates the first word from the remaining argument. The long conditional is intentionally direct: each branch performs one small action and prints terminal-like feedback.

Navigation does not maintain a second hand-written route list. lib/nav.js defines NAV_LINKS, derives navbar and footer links, and builds NAV_COMMANDS:

export const NAV_COMMANDS = {
  home: '/',
  ...Object.fromEntries(NAV_LINKS.map((l) => [l.command, l.href])),
};

The palette imports that map, so commands such as about, blog, and resume route through router.push. GAME_PATHS similarly becomes a lookup for play snake, play typing, and play hack. Successful route commands close the overlay after starting navigation.

Other commands stay in the browser. theme dark|light toggles the root dark class, updates the theme-color meta tag, saves the choice to localStorage, and emits theme-changed. pwd prints router.asPath; date, echo, ls, and neofetch generate local output. Social commands use window.open with noopener,noreferrer, while email prints the address and attempts to copy it.

Achievements Without a Server Round Trip

The first non-empty command calls unlockAchievement('secret-palette'). In lib/achievements.js, the corresponding “Power User” achievement is marked secret and has no stats-based check. unlockAchievement finds it by ID and persists it only if it was not already unlocked.

Achievement state uses JSON arrays in localStorage, wrapped by safeGetSet and safeSetSet. Those helpers return safely during server rendering and catch storage failures. The palette’s achievements command reads the resulting set and prints every achievement with either [x] or [ ]. This is per-browser state rather than account data, matching the mini-games’ client-only model.

Reusing Blog Search for grep

The grep <term> branch fetches /api/posts-index through a module-level cache. pages/api/posts-index.js reads the Markdown index with getSortedPostsData, applies searchPosts, and returns only the fields needed by search surfaces: ID, title, description, date, tags, and reading time. Its response also sets shared-cache and stale-while-revalidate directives.

The palette fetches the full index once per loaded JavaScript module, then searches locally:

const matches = searchPosts(posts, argRaw);

lib/postSearch.js is shared with pages/blog.js. It lowercases and trims values, combines each post’s title, metaDesc, and tags, and requires every whitespace-separated query term to occur in that combined text. Results become buttons that display reading time and tags and navigate to /posts/[id]. Fetch failures become grep: blog index unavailable; empty results get their own message.

This split keeps matching rules consistent between the command palette and the blog’s visible grep blog/*.md field.

Focus, Keyboard Behavior, and Dialog Semantics

When open, the overlay contains an element with role="dialog", aria-modal="true", and aria-label="Command palette". The input has an explicit accessible label. A requestAnimationFrame focuses it after opening, while a focusin listener redirects focus that leaves the dialog. Tab is prevented and returned to the single command input.

Escape closes the palette. Enter executes input. Arrow Up and Arrow Down move through command history, including returning to a blank current entry. Clicking the backdrop closes the overlay, while clicks inside stop propagation. Body scrolling is locked for the open lifetime and restored to its previous inline value during cleanup. Output links are actual buttons, so search results remain keyboard-operable.

One limitation is worth stating precisely: focus is sent to the palette input when it opens, but the implementation does not save and restore the previously focused element when it closes.

Tests That Exercise the Contract

Playwright coverage checks the user-facing behavior rather than internal functions. tests/e2e/command-palette.spec.js verifies that Control-K opens the named dialog, a projects command navigates, Escape closes it, and help prints its navigation section.

tests/e2e/features.spec.js goes further: it runs grep docker, expects the first-command achievement message, finds a matching result button, and follows it to a post. The same file clicks the Hero’s “Open command palette” control and tests the separate Konami achievement managed by Layout. Finally, tests/e2e/a11y.spec.js runs axe-core WCAG A/AA scans across the site’s page list.

Together, those tests describe the palette’s real contract: multiple ways to open it, keyboard operation, working navigation, shared post search, persistent achievements, and accessible dialog semantics.