feat(site): migrate search from Pagefind to Algolia DocSearch v4 (#941)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-03-20 10:25:23 -05:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 9ce94ee2d3
commit 9ef1e61b63
20 changed files with 658 additions and 378 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ const { class: className, dark = false } = Astro.props;
>
<Logo width="100%" />
</a>
<Search class="mr-4 ml-auto sm:self-center" />
<Search client:load className="mr-4 ml-auto sm:self-center" />
{/* Desktop nav links */}
<div class="hidden md:flex">
<GetStartedLink
+1 -1
View File
@@ -42,7 +42,7 @@ const { class: className } = Astro.props;
<LogoDocs width="100%" />
</div>
</a>
<Search class="mr-4 ml-auto sm:self-center" />
<Search client:load className="mr-4 ml-auto sm:self-center" />
{/* Desktop nav links */}
<div class="hidden h-full md:flex">
<GetStartedLink
+36
View File
@@ -0,0 +1,36 @@
import { DocSearch } from '@docsearch/react';
import { useStore } from '@nanostores/react';
import { GITHUB_REPO_URL } from '@/consts';
import { DOCSEARCH_API_KEY, DOCSEARCH_APP_ID, DOCSEARCH_BLOG_INDEX, DOCSEARCH_DOCS_INDEX } from '@/search.config';
import { currentFramework } from '@/stores/preferences';
interface SearchProps {
className?: string;
}
export default function Search({ className }: SearchProps) {
const framework = useStore(currentFramework);
return (
<div className={className}>
<DocSearch
appId={DOCSEARCH_APP_ID}
apiKey={DOCSEARCH_API_KEY}
indices={[
{
name: DOCSEARCH_DOCS_INDEX,
searchParameters: {
facetFilters: framework ? [`framework:${framework}`] : [],
},
},
{
name: DOCSEARCH_BLOG_INDEX,
},
]}
getMissingResultsUrl={({ query }) =>
`${GITHUB_REPO_URL}issues/new?title=${encodeURIComponent(`Search: no results for "${query}"`)}&labels=search`
}
/>
</div>
);
}
-118
View File
@@ -1,118 +0,0 @@
---
import { join } from 'node:path/posix';
import clsx from 'clsx';
/**
* We wrap Search.tsx in an .astro entry point for
* 1. easy CSS support, since we can't use Tailwind on pagefind-ui
* 2. a synchronous script to set the meta key (ctrl or cmd) without FOUC or hydration errors
*/
import SearchClient from './Search';
import SearchIcon from './searchIcon.svg';
import searchIconString from './searchIcon.svg?raw';
const baseUrl = import.meta.env.BASE_URL || '/';
const bundlePath = join(baseUrl, 'pagefind/');
interface Props {
class?: string;
}
const { class: className } = Astro.props;
---
<SearchClient
client:load
className={clsx(
"inline-flex min-h-6 cursor-pointer items-center gap-2 rounded-full border-faded-black px-2 sm:border dark:border-manila-dark intent:border-manila-dark dark:intent:border-warm-gray",
className,
)}
baseUrl={baseUrl}
bundlePath={bundlePath}
searchId="pagefind-ui"
searchStyle={{
"--search-icon-url": `url('data:image/svg+xml;utf8,${encodeURIComponent(searchIconString)}')`,
} as React.CSSProperties}
>
<SearchIcon class="h-4 w-4 sm:h-3 sm:w-3" />
<kbd data-platform-key="default" class="hidden font-display text-h4 sm:inline"
>Ctrl K</kbd
>
<kbd data-platform-key="mac" class="hidden font-display text-h4 sm:inline"
>⌘K</kbd
>
</SearchClient>
{
/**
* Adapted from https://github.com/withastro/starlight/blob/8a72a19e2cfec235941b4e1401b69b44e6695068/packages/starlight/components/Search.astro#L55C1-L75C1
* This is intentionally inlined to avoid briefly showing an invalid shortcut.
* Purposely using the deprecated `navigator.platform` property to detect Apple devices, as the
* user agent is spoofed by some browsers when opening the devtools.
*/
}
<script is:inline>
(() => {
const defaultPlatformKey = document.querySelector(
'kbd[data-platform-key="default"]',
);
const macPlatformKey = document.querySelector(
'kbd[data-platform-key="mac"]',
);
if (/Mac|iPhone|iPod|iPad/i.test(navigator.platform)) {
if (defaultPlatformKey) defaultPlatformKey.style.display = "none";
} else {
if (macPlatformKey) macPlatformKey.style.display = "none";
}
})();
</script>
<style is:global>
@import url("@pagefind/default-ui/css/ui.css");
#pagefind-ui {
--pagefind-ui-scale: 1;
--pagefind-ui-primary: var(--color-gold);
--pagefind-ui-text: var(--color-faded-black);
--pagefind-ui-background: var(--color-manila-50);
--pagefind-ui-border: var(--color-manila-75);
--pagefind-ui-tag: green;
--pagefind-ui-border-width: 1px;
--pagefind-ui-border-radius: var(--radius-xs);
--pagefind-ui-font: inherit;
}
.dark #pagefind-ui {
--pagefind-ui-text: var(--color-manila-light);
--pagefind-ui-background: var(--color-warm-gray);
--pagefind-ui-border: var(--color-soot);
}
#pagefind-ui .pagefind-ui__form::before {
-webkit-mask-image: var(--search-icon-url);
mask-image: var(--search-icon-url);
}
#pagefind-ui .pagefind-ui__search-input {
font-size: var(--text-p2);
line-height: var(--text-p2--line-height);
letter-spacing: var(--text-p2--letter-spacing);
font-weight: var(--text-p2--font-weight);
}
#pagefind-ui .pagefind-ui__filter-block {
border-bottom: solid 1px var(--pagefind-ui-border);
}
#pagefind-ui .pagefind-ui__message {
font-weight: var(--font-weight-normal);
}
#pagefind-ui .pagefind-ui__result-title,
#pagefind-ui .pagefind-ui__filter-name {
font-weight: var(--font-weight-bold);
}
#pagefind-ui .pagefind-ui__result-nested .pagefind-ui__result-link::before {
font-weight: var(--font-weight-normal);
font-size: 0.9em;
}
#pagefind-ui mark {
background-color: var(--color-gold);
color: var(--color-faded-black);
}
</style>
-90
View File
@@ -1,90 +0,0 @@
import { Dialog } from '@base-ui/react/dialog';
import { useStore } from '@nanostores/react';
import { useEffect, useRef, useState } from 'react';
import { currentFramework } from '@/stores/preferences';
interface SearchProps {
children: React.ReactNode;
className?: string;
baseUrl: string;
bundlePath: string;
searchId: string;
searchStyle?: React.CSSProperties;
}
export default function Search({ children, className, baseUrl, bundlePath, searchId, searchStyle }: SearchProps) {
const [open, setOpen] = useState(false);
const framework = useStore(currentFramework);
const searchRef = useRef<HTMLDivElement>(null);
// load Pagefind when opened
useEffect(() => {
if (!open) return;
let search: any;
const init = async () => {
// default-ui is actually an ultra-lightweight svelte app
// ...which we're loading in this react component, in an astro app.
// this isn't relevant to dev. I just found it funny.
const { PagefindUI } = await import('@pagefind/default-ui');
// I'd love if we built our own ui with BaseUI/Autocomplete, but... time, ya know?
search = new PagefindUI({
element: `#${searchId}`,
baseUrl,
bundlePath,
showImages: false,
showSubResults: true,
autofocus: true,
});
search.triggerFilters({
framework: [framework],
});
};
init();
return () => {
search.destroy();
};
}, [baseUrl, bundlePath, framework, open, searchId]);
// open with cmd+k or ctrl+k
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
setOpen((prev) => !prev);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
return (
<Dialog.Root
open={open}
onOpenChange={(newOpen) => {
setOpen(newOpen);
}}
modal
>
<Dialog.Trigger aria-label="Search" className={className}>
{children}
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Backdrop className="fixed inset-0 flex items-start justify-center bg-faded-black/20 backdrop-blur-xs z-50">
<Dialog.Popup
initialFocus={false} // pagefind.autofocus = true
id={searchId}
ref={searchRef}
className="w-full max-w-5xl mx-4 mb-4 p-4 rounded-xs bg-manila-light dark:bg-faded-black dark:border-warm-gray border-manila-75 border overflow-y-scroll overflow-x-hidden"
style={{
minHeight: '50svh',
maxHeight: '75svh',
marginTop: '12.5svh',
...searchStyle,
}}
/>
</Dialog.Backdrop>
</Dialog.Portal>
</Dialog.Root>
);
}
-1
View File
@@ -1 +0,0 @@
export { default } from './Search.astro';
-12
View File
@@ -1,12 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 21l-4.34-4.34" vector-effect="non-scaling-stroke" />
<circle cx="11" cy="11" r="8" vector-effect="non-scaling-stroke" />
</svg>

Before

Width:  |  Height:  |  Size: 319 B

+3 -1
View File
@@ -254,7 +254,9 @@ export function Tab({ value, children, initial, variant = 'compact' }: TabProps)
)}
<span className="relative">
{/* to prevent layout shift on state change, we have an invisible bold version of the text preserving space */}
<span className="font-bold invisible">{children}</span>
<span className="font-bold invisible" data-search-ignore data-llms-ignore>
{children}
</span>
<span className={clsx('absolute top-0 left-0', isActive && 'font-bold')}>{children}</span>
</span>
</button>
+1 -1
View File
@@ -30,7 +30,7 @@ const shouldRender = !frameworks || frameworks.includes(framework as SupportedFr
<div
class="contents"
hidden={!shouldRender}
data-pagefind-ignore={shouldRender ? undefined : "all"}
data-search-ignore={shouldRender ? undefined : "all"}
data-llms-ignore={shouldRender ? undefined : "all"}
>
<slot />