mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
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:
co-authored by
Claude Opus 4.6
parent
9ce94ee2d3
commit
9ef1e61b63
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 +0,0 @@
|
||||
export { default } from './Search.astro';
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user