Skip to content

Switch search from Pagefind to Algolia#835

Merged
teallarson merged 16 commits intomainfrom
teallarson/algolia-search
Mar 2, 2026
Merged

Switch search from Pagefind to Algolia#835
teallarson merged 16 commits intomainfrom
teallarson/algolia-search

Conversation

@teallarson
Copy link
Contributor

@teallarson teallarson commented Feb 27, 2026

Summary

  • Replaces Pagefind's build-time static search index with Algolia's crawler-based search (no build step needed — Algolia crawls the live site automatically)
  • Adds a custom AlgoliaSearch component using react-instantsearch with a keyboard-accessible modal (⌘K), theme-aware light/dark styling, and brand red hit highlights
  • Removes pagefind, rehype-stringify, remark, remark-rehype deps and the custompagefind build script
  • Fixes a pre-existing bug where /site.webmanifest 404'd because the middleware matcher didn't exclude .webmanifest files

Test plan

  • Search button appears in navbar with correct light/dark theming
  • ⌘K opens the search modal
  • Typing returns results with brand red highlights on matched text
  • Clicking a result navigates to the correct page
  • Escape / clicking backdrop closes the modal
  • pnpm build completes without a Pagefind step

🤖 Generated with Claude Code


Note

Medium Risk
Switches the docs search implementation and dependencies, which can affect navigation/UX and build/release behavior; also tweaks middleware routing for .webmanifest assets. Risk is moderate as changes are mostly client-side but touch global layout and request matching.

Overview
Replaces build-time Pagefind search with Algolia-powered search. Nextra’s built-in search is disabled and the layout now injects a new AlgoliaSearch (⌘/Ctrl+K modal) using react-instantsearch, including URL sanitization for hits and new highlight styling.

Removes Pagefind indexing from the build pipeline. The custompagefind script and scripts/pagefind.ts are deleted, related deps are dropped, and .env.local.example adds required Algolia public env vars.

Fixes an asset routing edge case. Middleware matcher now excludes *.webmanifest so /site.webmanifest is served instead of being intercepted.

Written by Cursor Bugbot for commit 7a18739. This will update automatically on new commits. Configure here.

Replaces Pagefind's build-time static search index with Algolia's
crawler-based search. The index is populated automatically by Algolia's
crawler — no build step needed. Adds a custom InstantSearch modal
component that is theme-aware (light/dark) and uses the brand red for
hit highlights.

Also fixes a pre-existing bug where site.webmanifest 404'd in dev
because the middleware matcher didn't exclude .webmanifest files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Mar 2, 2026 3:06pm

Request Review

`React.ComponentType<{ components?: ... }>` was incompatible with
`MDXContent` returned by @mdx-js/mdx's evaluate(), due to React 19
components returning ReactNode (including undefined) while @types/mdx
expects Element | null. Using React.ElementType resolves the boundary
without type suppressions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related but docs weren't building right on my branch until i made this change.

- Only initialize algoliasearch when all three env vars are present;
  show a setup instructions message in the modal otherwise
- Add safeHref() to reject non-relative/non-https hit URLs (XSS defense)
- Remove unused inputRef, FOCUS_DELAY_MS, and the focus useEffect —
  SearchBox autoFocus already handles this correctly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
teallarson and others added 2 commits February 27, 2026 17:13
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@uidotdev/usehooks is already a dependency. useEventListener handles
add/remove lifecycle internally, removing the boilerplate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces hardcoded hsl(347 ...) values with var(--primary) from
@arcadeai/design-system tokens and color-mix() for transparent tints.
Dark mode text override removed since --primary resolves identically
in both modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
teallarson and others added 2 commits February 27, 2026 17:21
Replace hardcoded neutral/dark: pairs with semantic tokens where
the mapping is clean and visually equivalent:
- text-neutral-900 dark:text-white → text-foreground
- text-neutral-400/500 dark:text-neutral-500/400 → text-muted-foreground
- border-neutral-200/300 dark:border-white/10 → border-border
- bg-white dark:bg-neutral-900 (modal panel) → bg-popover

Hover states on hits and the trigger button bg retain explicit
neutral/white values since bg-muted dark resolves too dark for those.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…idotdev/usehooks@2.4.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines +101 to +113
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setIsOpen((prev) => !prev);
}
if (e.key === "Escape") {
setIsOpen(false);
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be neat to either upgrade @uidotdev/usehooks to a verstion that has useEventListener or use https://tanstack.com/hotkeys/latest (never tried it but tanstack is usually legit)

but for now, a useEffect is the best i've got

"react": "19.2.3",
"react-dom": "19.2.3",
"react-hook-form": "7.65.0",
"react-instantsearch": "^7.26.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hitComponent={({ hit }) => (
<SearchHit hit={hit as unknown as HitRecord} />
)}
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hits component always renders alongside empty query message

Medium Severity

The Hits component renders unconditionally, but Algolia returns results for empty queries by default. When the search modal opens, users see the "Start typing to search the docs…" message from EmptyQuery and a list of search results from Hits at the same time. The Hits component needs to be conditionally hidden when no query has been entered.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

@teallarson teallarson Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine? I like the way this shows top docs pages and also prompts for input when empty. Open to other opinions.

export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|manifest|_pagefind|public|.*.svg|.*.png|.*.jpg|.*.jpeg|.*.gif|.*.webp|.*.ico|.*.css|.*.js|.*.woff|.*.woff2|.*.ttf|.*.eot|.*.otf|.*.pdf|.*.txt|.*.xml|.*.json|.*.py|.*.mp4).*)",
"/((?!api|_next/static|_next/image|favicon.ico|manifest|public|.*.svg|.*.png|.*.jpg|.*.jpeg|.*.gif|.*.webp|.*.ico|.*.webmanifest|.*.css|.*.js|.*.woff|.*.woff2|.*.ttf|.*.eot|.*.otf|.*.pdf|.*.txt|.*.xml|.*.json|.*.py|.*.mp4).*)",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a bunch of 404s locally from this. Idk if I should have just ignored them. This fixed it.

Copy link
Contributor

@evantahler evantahler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

goodbye pagefind!

@Spartee
Copy link
Contributor

Spartee commented Mar 1, 2026

nice

Adds Configure with attributesToSnippet, imports Snippet component,
and updates SearchHit to show a content snippet when present (new
per-section index format) with fallback to description highlight for
existing flat records.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

teallarson and others added 2 commits March 2, 2026 09:42
url.startsWith("/") was passing for //evil.com which browsers
interpret as https://evil.com, enabling open redirect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The PR link was returning 404 and failing the external URL check test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@teallarson teallarson enabled auto-merge (squash) March 2, 2026 15:08
@teallarson teallarson merged commit c74e476 into main Mar 2, 2026
9 checks passed
jottakka added a commit that referenced this pull request Mar 2, 2026
Resolve conflicts after main switched search from Pagefind to Algolia
(PR #835). Remove pagefind scripts/tests/helpers (now dead code),
drop postbuild and generate:markdown scripts, keep Algolia deps.

Made-with: Cursor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants