-
Notifications
You must be signed in to change notification settings - Fork 10
Switch search from Pagefind to Algolia #835
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3d8eace
ed4d41e
5f3c64f
1d913ca
31d25ad
75fe4fe
4d68f9e
79545f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,6 @@ node_modules | |
| .env.local | ||
| public/sitemap*.xml | ||
| .env | ||
| _pagefind/ | ||
|
|
||
| # TypeScript | ||
| *.tsbuildinfo | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| "use client"; | ||
|
|
||
| import { liteClient as algoliasearch } from "algoliasearch/lite"; | ||
| import { Search } from "lucide-react"; | ||
| import { useEffect, useState } from "react"; | ||
| import { | ||
| Highlight, | ||
| Hits, | ||
| InstantSearch, | ||
| SearchBox, | ||
| useInstantSearch, | ||
| } from "react-instantsearch"; | ||
|
|
||
| type HitRecord = { | ||
| objectID: string; | ||
| title?: string; | ||
| description?: string; | ||
| url?: string; | ||
| }; | ||
|
|
||
| const appId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID; | ||
| const searchKey = process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY; | ||
| const indexName = process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME; | ||
|
|
||
| const searchClient = | ||
| appId && searchKey ? algoliasearch(appId, searchKey) : null; | ||
|
|
||
| function safeHref(url: string | undefined): string { | ||
| if (!url) { | ||
| return "/"; | ||
| } | ||
| if (url.startsWith("/") || url.startsWith("https://")) { | ||
| return url; | ||
| } | ||
| return "/"; | ||
| } | ||
|
|
||
| function SearchHit({ hit }: { hit: HitRecord }) { | ||
| return ( | ||
| <a | ||
| className="block rounded-lg px-4 py-3 hover:bg-neutral-100 dark:hover:bg-white/5" | ||
| href={safeHref(hit.url)} | ||
| > | ||
| <div className="truncate text-sm font-medium text-foreground"> | ||
| <Highlight | ||
| attribute="title" | ||
| hit={hit as Parameters<typeof Highlight>[0]["hit"]} | ||
| /> | ||
| </div> | ||
| {hit.description && ( | ||
| <div className="mt-0.5 truncate text-xs text-muted-foreground"> | ||
| <Highlight | ||
| attribute="description" | ||
| hit={hit as Parameters<typeof Highlight>[0]["hit"]} | ||
| /> | ||
| </div> | ||
| )} | ||
| </a> | ||
| ); | ||
| } | ||
|
|
||
| function EmptyQuery() { | ||
| const { indexUiState } = useInstantSearch(); | ||
| if (indexUiState.query) { | ||
| return null; | ||
| } | ||
| return ( | ||
| <p className="px-4 py-8 text-center text-sm text-muted-foreground"> | ||
| Start typing to search the docs… | ||
| </p> | ||
| ); | ||
| } | ||
|
|
||
| function NoResults() { | ||
| const { results } = useInstantSearch(); | ||
| if (!results?.query || results.nbHits > 0) { | ||
| return null; | ||
| } | ||
| return ( | ||
| <p className="px-4 py-8 text-center text-sm text-muted-foreground"> | ||
| No results for{" "} | ||
| <strong className="text-foreground">"{results.query}"</strong> | ||
| </p> | ||
| ); | ||
| } | ||
|
|
||
| function SearchUnavailable() { | ||
| return ( | ||
| <p className="px-4 py-8 text-center text-sm text-muted-foreground"> | ||
| Add <code className="text-xs">NEXT_PUBLIC_ALGOLIA_APP_ID</code>,{" "} | ||
| <code className="text-xs">NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY</code>, and{" "} | ||
| <code className="text-xs">NEXT_PUBLIC_ALGOLIA_INDEX_NAME</code> to your | ||
| environment to enable search. | ||
| </p> | ||
| ); | ||
| } | ||
|
|
||
| export function AlgoliaSearch() { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
|
|
||
| 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); | ||
| }, []); | ||
|
|
||
| return ( | ||
| <> | ||
| <button | ||
| aria-label="Search docs" | ||
| className="algolia-search-button flex w-56 items-center gap-2 rounded-lg border border-neutral-200 bg-neutral-100 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-neutral-200 dark:border-white/10 dark:bg-white/5 dark:hover:bg-white/10" | ||
| onClick={() => setIsOpen(true)} | ||
| type="button" | ||
| > | ||
| <Search className="size-3.5 shrink-0" /> | ||
| <span className="flex-1 text-left">Search</span> | ||
| <kbd className="flex items-center gap-0.5 rounded border border-border px-1 text-xs text-muted-foreground"> | ||
| <span>⌘</span>K | ||
| </kbd> | ||
| </button> | ||
|
|
||
| {isOpen && ( | ||
| <div | ||
| aria-label="Search" | ||
| aria-modal="true" | ||
| className="fixed inset-0 z-50 flex items-start justify-center px-4 pt-[10vh]" | ||
| role="dialog" | ||
| > | ||
| <button | ||
| aria-label="Close search" | ||
| className="fixed inset-0 bg-black/30 backdrop-blur-sm dark:bg-black/50" | ||
| onClick={() => setIsOpen(false)} | ||
| type="button" | ||
| /> | ||
| <div className="relative z-10 w-full max-w-2xl overflow-hidden rounded-xl border border-border bg-popover shadow-2xl"> | ||
| {searchClient && indexName ? ( | ||
| <InstantSearch indexName={indexName} searchClient={searchClient}> | ||
| <div className="flex items-center border-b border-border px-4"> | ||
| <Search className="size-4 shrink-0 text-muted-foreground" /> | ||
| <SearchBox | ||
| autoFocus | ||
| classNames={{ | ||
| form: "flex flex-1", | ||
| input: | ||
| "w-full bg-transparent px-3 py-4 text-sm text-foreground placeholder:text-muted-foreground outline-none", | ||
| loadingIndicator: "hidden", | ||
| reset: "hidden", | ||
| root: "flex-1", | ||
| submit: "hidden", | ||
| }} | ||
| placeholder="Search docs…" | ||
| /> | ||
| </div> | ||
| <div className="max-h-[60vh] overflow-y-auto p-2"> | ||
| <EmptyQuery /> | ||
| <NoResults /> | ||
| <Hits | ||
| classNames={{ item: "", list: "space-y-0.5", root: "" }} | ||
| hitComponent={({ hit }) => ( | ||
| <SearchHit hit={hit as unknown as HitRecord} /> | ||
| )} | ||
| /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hits component always renders alongside empty query messageMedium Severity The Additional Locations (1)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| </div> | ||
| </InstantSearch> | ||
| ) : ( | ||
| <SearchUnavailable /> | ||
| )} | ||
| </div> | ||
| </div> | ||
| )} | ||
| </> | ||
| ); | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -239,6 +239,6 @@ export function middleware(request: NextRequest) { | |
|
|
||
| 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).*)", | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| ], | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,17 +5,16 @@ | |
| "type": "module", | ||
| "scripts": { | ||
| "dev": "next dev --webpack", | ||
| "build": "pnpm run toolkit-markdown && next build --webpack && pnpm run custompagefind", | ||
| "build": "pnpm run toolkit-markdown && next build --webpack", | ||
| "start": "next start", | ||
| "lint": "pnpm exec ultracite check", | ||
| "format": "pnpm exec ultracite fix", | ||
| "prepare": "husky install", | ||
| "toolkit-markdown": "pnpm dlx tsx toolkit-docs-generator/scripts/generate-toolkit-markdown.ts", | ||
| "postbuild": "if [ \"$SKIP_POSTBUILD\" != \"true\" ]; then pnpm run generate:markdown && pnpm run custompagefind; fi", | ||
| "postbuild": "if [ \"$SKIP_POSTBUILD\" != \"true\" ]; then pnpm run generate:markdown; fi", | ||
| "generate:markdown": "pnpm dlx tsx scripts/generate-clean-markdown.ts", | ||
| "translate": "pnpm dlx tsx scripts/i18n-sync/index.ts && pnpm format", | ||
| "llmstxt": "pnpm dlx tsx scripts/generate-llmstxt.ts", | ||
| "custompagefind": "pnpm dlx tsx scripts/pagefind.ts", | ||
| "test": "vitest --run", | ||
| "test:watch": "vitest --watch", | ||
| "vale": "vale", | ||
|
|
@@ -49,6 +48,7 @@ | |
| "@ory/client": "1.22.7", | ||
| "@theguild/remark-mermaid": "0.3.0", | ||
| "@uidotdev/usehooks": "2.4.1", | ||
| "algoliasearch": "^5.49.1", | ||
| "chalk": "^5.6.2", | ||
| "lucide-react": "0.548.0", | ||
| "mdast-util-to-string": "4.0.0", | ||
|
|
@@ -61,6 +61,7 @@ | |
| "react": "19.2.3", | ||
| "react-dom": "19.2.3", | ||
| "react-hook-form": "7.65.0", | ||
| "react-instantsearch": "^7.26.0", | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. algolia's react library https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/react |
||
| "react-markdown": "^10.1.0", | ||
| "react-syntax-highlighter": "16.1.0", | ||
| "remark-gfm": "^4.0.1", | ||
|
|
@@ -93,12 +94,8 @@ | |
| "next-validate-link": "1.6.3", | ||
| "openai": "6.7.0", | ||
| "ora": "9.0.0", | ||
| "pagefind": "1.4.0", | ||
| "picocolors": "1.1.1", | ||
| "postcss": "8.5.6", | ||
| "rehype-stringify": "^10.0.1", | ||
| "remark": "^15.0.1", | ||
| "remark-rehype": "^11.1.2", | ||
| "tailwindcss": "4.1.16", | ||
| "turndown": "^7.2.2", | ||
| "typescript": "5.9.3", | ||
|
|
||


There was a problem hiding this comment.
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/usehooksto a verstion that hasuseEventListeneror 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