Bryan Hamilton / writing

bryanhamilton.info / writing / article

Press Cmd+K: A Command Palette That Indexes Itself

A commenter asked the question every self-deleting deploy script has to answer: what state is the site in if the run dies halfway through? The answer is markers, per-file backups, verify-after-write, and a self-delete that always comes last.

  • November 10, 2026
  • 4 min read
  • php · devops · webdev · error-handling
  • More writing

Press Cmd+K on this page right now. (Windows: Ctrl+K.) Type "tour". That little launcher shipped as one worker file, runs zero dependencies, and weighs about six kilobytes of JavaScript - and the reason it needs no maintenance is the best part: it indexes itself.

1. Every app you respect has one

Slack, Linear, GitHub, VS Code, Gmail. The keyboard launcher is the tell that software was built by people who use their own tools. Command palettes started as a power-user feature and became table stakes - and when an engineer lands on my site, muscle memory reaches for Cmd+K before the mouse settles. A portfolio that answers that muscle memory says "engineered" before anyone reads a word. A portfolio that does it with zero dependencies says something else again.

2. The index was already on the page

Most palette tutorials start with a data file: export your pages to JSON, fetch it on boot, filter it. I did none of that, and skipping it turned out to be the whole design - because a palette for a website does not need a search index. It needs the website.

Every internal link on a page is already a curated, human-labeled index entry: article titles in the writing hub, project cards, section anchors. So the entire index build is one pass over querySelectorAll('a[href]'): resolve each href with new URL(), keep same-origin only, dedupe by pathname plus hash, take the link text, trim it, and drop anything empty or longer than 70 characters.

That last filter is doing real work. It quietly removes the "Read on dev.to" style button labels while keeping whole sentence-length article titles. And because the index is built at open time from the live DOM, the launcher learns about new pages the moment they are linked anywhere. Publish an article, and the palette knows. There is no step two, no manifest to update, no index to regenerate.

3. Fuzzy enough in one indexOf

Full fuzzy search - Levenshtein distance, trigram scoring, a prefix trie - is a fun afternoon. It is also overkill for an index of a few dozen entries. Mine is a case-insensitive substring match against both the label and the URL, capped at twelve results. When a row matches, the matched span gets wrapped in a bold tag so the eye lands on the reason it matched. When nothing matches, the empty state stays in the fiction of the page: "nothing on the scope at" whatever you typed.

Matching the URL too is a quiet superpower: typing "mysql" surfaces articles whose titles never say MySQL but whose slugs do. The palette is the site's table of contents with a cursor in it.

4. The keyboard is the product

Opening is metaKey or ctrlKey plus K, with preventDefault so the browser's own launcher does not fight you for the chord. Escape closes. Arrow keys move a selection that is clamped, not wrapped. Enter navigates. And the detail people feel more than they notice: hovering a row moves the selection too, so the mouse and the keyboard never fight over state - whichever you touched last is in charge.

One timing trap worth writing down: the input gets focused inside a setTimeout(0) after the overlay's class flips, because focusing an element before it is displayed is a no-op in some browsers and a flicker in others. And the selection clamps instead of wrapping on purpose - wrapping arrows feel fun for a week and wrong forever after.

5. What I left out on purpose

A feature is also defined by what it does not do:

  1. No ranking or frequency memory. Adaptive palettes are great in tools you live in eight hours a day. Here, every search is someone's first search.
  2. No fetched index. The palette has to open instantly, offline, on a cached page. A fetch dependency breaks all three at once.
  3. No command registry. It is a jump box, not an IDE. Links are the entire vocabulary.
  4. No animation library. One 160-millisecond pop, CSS only.
  5. No analytics. Nobody needs to know what you searched.

6. Try it right here

You do not have to take my word for any of this. Press Cmd+K (or Ctrl+K) and type "tour" - you should land on the tour-prediction work. Type "sitemap". Type "eq". Twelve results maximum, one indexOf, zero dependencies, and the index rebuilt itself from this very page when you opened it.

7. Shipped like everything else: one worker file

My host gives me a file manager, not a shell, so this shipped the way everything on this site does: a token-gated PHP worker that appended one style block and one script block before the closing body tag, made a backup first, lint-checked the result, and self-reports. Before it touched production, a headless browser loaded the patched page, pressed Cmd+K, typed "tour", and counted twelve results. The build output I trust is the DOM, not the file size.

Total footprint: one style block, one script block, one small hint pill in the corner of the screen. The next time someone asks whether a vanilla-JS site can feel like an app, the answer is one keyboard chord away.

More from the workshop

All writingEvery article, on this domain. Open source11 production-extracted projects.