← All Writing
March 3, 20268 min read

How I Built a Self-Updating Search Bar Using Claude Code

From architecture decision to React portal — why a simple search bar required a codebase refactor, a build script, and fixing a CSS rule I didn’t know existed

YieldA site-wide search bar with dropdown previews, keyboard navigation, and a self-regenerating index at joseandgoose.com — click the magnifying glass in the nav
DifficultyIntermediate (first time thinking about build-time automation and React portals)
Total Cook Time~3 hours across 4 feedback sessions in one evening

Ingredients

The Problem: The Site Was Getting Too Big to Navigate by Eye

When the site launched, there were four pages. By March, there were fourteen — seven writing posts, two interactive features, five static pages. Someone looking for the post about Garmin automations had to scroll through the Writing index and scan titles. Someone looking for Fruit Exchange had to know it existed to find it.

The site needed search. The question wasn’t really "should we add search" — it was "how do we build it in a way that doesn’t fall apart as the site keeps growing?" That’s where the real design work started.

Session 1: The Architecture Decision

Evening, March 3 — ~30 minutes of planning, then build

Pace: Slow start (making the right structural decision up front), fast execution after.

Before writing any code, Claude and I worked through how the search index should be structured. A search index is essentially a master list of everything on the site that search can look through — every page title, every post subtitle, every feature description — stored in a single file.

The first instinct was the simplest option: a hand-maintained JSON file. Create it once, update it when you add content. But I knew this would break down fast. The Writing section alone was growing every week. Forget to update the file after adding a post and the post simply wouldn’t appear in search results — silently, with no warning.

The better option was a build script — a small program that runs automatically every time the site gets built for production, reading the actual list of posts and writing the search index file on the fly. The tradeoff: it required a code refactor first.

Architecture tip

If you’re building something that you’ll maintain for years, the ten-minute upfront investment in automation pays for itself the first time you forget to do the manual version. The static file was simpler to describe. The build script was simpler to live with.

The Refactor: One Source of Truth

The problem: the list of Writing posts was defined inside the Writing page file itself — the code that displayed the posts and the list of posts were in the same place. That works fine for a single page, but a build script in a separate file couldn’t read it.

The fix was a refactor — moving the posts list into its own file (app/lib/posts.ts) that both the Writing page and the build script could import independently. Think of it like moving a recipe from inside a cooking show script into a recipe card that any show can reference. The Writing page still works exactly the same; now the build script can also see the same list.

🔧 Developer section: Refactor and build script

Terminal — generating the index
user@MacBook-Air joseandgoose-site-main % npm run generate:search
> tsx scripts/generate-search-index.ts
✓ search-index.json written (14 entries)

One command generates a fresh index. Every production build runs this automatically — no manual updates needed.

From now on, adding a new post means one thing: add it to app/lib/posts.ts. The Writing page picks it up. The search index picks it up. Nothing else to remember.

Session 2: Building the Search Component

Same evening — ~1 hour

Pace: Fast. The index structure was already decided; this was pure UI execution.

With the index in place, Claude built the search component — the visible piece that visitors interact with. The design: a magnifying glass icon in the navigation bar. Click it, and a panel drops down with a search input. Start typing and results appear instantly, filtered from the index, with the matching word highlighted in yellow.

🔧 Developer section: SearchBar component

The panel drops down right-aligned under the nav, positioned to line up with the right edge of the navigation bar — same max-width and padding as the nav itself, using justify-content: flex-end to push it to that edge.

Session 3: The Mobile Bug

Later that evening — ~30 minutes of debugging

Pace: Confusing at first, then a clean fix once the root cause was found.

The search worked perfectly on desktop. On mobile, the panel opened — but you couldn’t type anything into the input. The letters just wouldn’t appear.

This took a moment to diagnose. The SearchBar component lived inside the site’s navigation bar — specifically inside the list of nav links. On mobile, those nav links are hidden with a CSS rule: display: none. The search icon, being inside that hidden list, was also hidden — that was expected. But the search overlay panel was hidden too, even though it used position: fixed which normally takes an element out of the page flow entirely.

It turns out display: none is absolute. When a parent element is set to display none, every single one of its children is hidden — no exceptions, regardless of whether the child is fixed, absolute, or anything else. The overlay panel existed in the DOM (React had rendered it), but the browser was refusing to paint it because its great-grandparent was invisible.

CSS lesson

position: fixed breaks free from the normal document layout — but not from display: none. A fixed element inside a hidden parent is still hidden. If you need an overlay that’s always visible regardless of where it lives in your component tree, it needs to render outside that tree entirely.

The fix was a React Portal. A portal is a way of rendering a component at a completely different location in the page’s HTML structure — in this case, directly on the <body> tag, outside the navigation entirely. The component still belongs to the nav (React tracks it as part of the same tree for state and events), but it renders in a location where no parent can hide it.

🔧 Developer section: Portal implementation

Session 4: Nav Redesign

Same evening — ~45 minutes of iteration

Pace: Fast visual iteration once the layout direction was decided.

Adding search also revealed a crowding problem. The navigation bar had been accumulating items over time — About, Work, Writing, Fruit Exchange, Contact, a search icon, and a yellow Play Numerator button. A new green Fruit Exchange button was added the same session. Seven items plus two distinct CTA buttons in a single row was too much.

The solution: split the nav into two rows. The first row keeps the text links and search icon. The second row holds only the CTA buttons, right-aligned, with room to add more features in the future without crowding the primary navigation.

🔧 Developer section: Two-row nav layout

Final Output

A site-wide search bar at joseandgoose.com — click the magnifying glass in the nav — with a self-regenerating 14-entry search index (7 posts, 5 static pages, 2 features), debounced input, keyboard navigation, yellow text highlighting, type badges, mobile support via React portal, and a redesigned two-row navigation that has room to grow. Adding a new post now requires one step: add it to app/lib/posts.ts.

What went fast

What needed patience

The decision that made everything else easier

The five minutes spent deciding not to use a static JSON file changed the entire build. It added a refactor (moving the posts array) and a script (the generator), but it removed the permanent maintenance cost of keeping a file in sync by hand. Every session after that — the search component, the portal fix, the nav redesign — was cleaner because the data architecture was right from the start.

When you’re building on a site you plan to keep adding to, the question isn’t just “what works now?” — it’s “what will I still trust in six months?” The build script is what I’ll still trust.

← Back to all writing