From 3d8eace6c5e8e7b8f8e1d3fc64b3b6767ef2a233 Mon Sep 17 00:00:00 2001 From: Teal Larson Date: Fri, 27 Feb 2026 16:35:11 -0500 Subject: [PATCH 1/8] Switch search from Pagefind to Algolia MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.local.example | 5 + .gitignore | 2 - app/_components/algolia-search.tsx | 175 +++++++++++ app/globals.css | 18 +- app/layout.tsx | 2 + middleware.ts | 2 +- next.config.ts | 1 + package.json | 11 +- pnpm-lock.yaml | 480 ++++++++++++++++++++++++----- scripts/pagefind.ts | 187 ----------- 10 files changed, 603 insertions(+), 280 deletions(-) create mode 100644 app/_components/algolia-search.tsx delete mode 100644 scripts/pagefind.ts diff --git a/.env.local.example b/.env.local.example index b75174ae5..2fa613e3e 100644 --- a/.env.local.example +++ b/.env.local.example @@ -9,3 +9,8 @@ OPENAI_API_KEY= # Get a key from https://console.anthropic.com/ # Falls back to OPENAI_API_KEY if not set ANTHROPIC_API_KEY= + +# Algolia search (search API key is public/read-only) +NEXT_PUBLIC_ALGOLIA_APP_ID= +NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY= +NEXT_PUBLIC_ALGOLIA_INDEX_NAME= diff --git a/.gitignore b/.gitignore index 8e9f00c4e..da4d2840a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,6 @@ node_modules .env.local public/sitemap*.xml .env -_pagefind/ - # TypeScript *.tsbuildinfo diff --git a/app/_components/algolia-search.tsx b/app/_components/algolia-search.tsx new file mode 100644 index 000000000..942063d0a --- /dev/null +++ b/app/_components/algolia-search.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { liteClient as algoliasearch } from "algoliasearch/lite"; +import { Search } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { + Highlight, + Hits, + InstantSearch, + SearchBox, + useInstantSearch, +} from "react-instantsearch"; + +type HitRecord = { + objectID: string; + title?: string; + description?: string; + url?: string; +}; + +const FOCUS_DELAY_MS = 50; + +const searchClient = algoliasearch( + process.env.NEXT_PUBLIC_ALGOLIA_APP_ID ?? "", + process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY ?? "" +); + +function SearchHit({ hit }: { hit: HitRecord }) { + return ( + +
+ [0]["hit"]} + /> +
+ {hit.description && ( +
+ [0]["hit"]} + /> +
+ )} +
+ ); +} + +function EmptyQuery() { + const { indexUiState } = useInstantSearch(); + if (indexUiState.query) { + return null; + } + return ( +

+ Start typing to search the docs… +

+ ); +} + +function NoResults() { + const { results } = useInstantSearch(); + if (!results?.query || results.nbHits > 0) { + return null; + } + return ( +

+ No results for{" "} + + "{results.query}" + +

+ ); +} + +export function AlgoliaSearch() { + const [isOpen, setIsOpen] = useState(false); + const inputRef = useRef(null); + + 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); + }, []); + + useEffect(() => { + if (isOpen) { + setTimeout(() => inputRef.current?.focus(), FOCUS_DELAY_MS); + } + }, [isOpen]); + + return ( + <> + + + {isOpen && ( +
+ {/* Backdrop */} +
+ )} + + ); +} diff --git a/app/globals.css b/app/globals.css index eb7ac1ba2..53d8993ab 100644 --- a/app/globals.css +++ b/app/globals.css @@ -145,7 +145,7 @@ nav > a[aria-label="Home page"] { margin-inline-end: 3.5rem !important; } -nav > div:has(.nextra-search) { +nav > div:has(.algolia-search-button) { order: -1; margin-inline-start: 1rem; margin-inline-end: auto; @@ -157,6 +157,22 @@ nav > div:has(.nextra-search) { } } +/* Algolia search hit highlight — brand red */ +.ais-Highlight-highlighted, +.ais-Snippet-highlighted { + background: hsl(347 100% 50% / 0.15); + color: hsl(347 100% 45%); + border-radius: 2px; + font-style: normal; + font-weight: 600; +} + +.dark .ais-Highlight-highlighted, +.dark .ais-Snippet-highlighted { + background: hsl(347 100% 50% / 0.2); + color: hsl(347 100% 65%); +} + /* Override Nextra code highlight colors to use green instead of red */ [data-highlighted-line] { background-color: color-mix(in oklab, #22c55e 20%, transparent) !important; diff --git a/app/layout.tsx b/app/layout.tsx index 2d8aa10e4..dcb44963a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,5 @@ import { getDictionary } from "@/_dictionaries/get-dictionary"; +import { AlgoliaSearch } from "@/app/_components/algolia-search"; import { SignupLink } from "@/app/_components/analytics"; import CustomLayout from "@/app/_components/custom-layout"; import { getDashboardUrl } from "@/app/_components/dashboard-link"; @@ -171,6 +172,7 @@ export default async function RootLayout({ } nextThemes={{ defaultTheme: "dark" }} pageMap={pageMap} + search={} sidebar={{ defaultMenuCollapseLevel: 2, autoCollapse: true, diff --git a/middleware.ts b/middleware.ts index 7cd936828..26d0f14c3 100644 --- a/middleware.ts +++ b/middleware.ts @@ -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).*)", ], }; diff --git a/next.config.ts b/next.config.ts index ca7f49680..a8dbde6a3 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,6 +7,7 @@ import { remarkGlossary } from "./lib/remark-glossary"; const withNextra = nextra({ defaultShowCopyCode: true, codeHighlight: true, + search: false, mdxOptions: { remarkPlugins: [ [ diff --git a/package.json b/package.json index f796fcd9b..de66c501b 100644 --- a/package.json +++ b/package.json @@ -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", "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 212a48a6d..36ab95e17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@uidotdev/usehooks': specifier: 2.4.1 version: 2.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + algoliasearch: + specifier: ^5.49.1 + version: 5.49.1 chalk: specifier: ^5.6.2 version: 5.6.2 @@ -68,6 +71,9 @@ importers: react-hook-form: specifier: 7.65.0 version: 7.65.0(react@19.2.3) + react-instantsearch: + specifier: ^7.26.0 + version: 7.26.0(algoliasearch@5.49.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.7)(react@19.2.3) @@ -159,24 +165,12 @@ importers: ora: specifier: 9.0.0 version: 9.0.0 - pagefind: - specifier: 1.4.0 - version: 1.4.0 picocolors: specifier: 1.1.1 version: 1.1.1 postcss: specifier: 8.5.6 version: 8.5.6 - rehype-stringify: - specifier: ^10.0.1 - version: 10.0.1 - remark: - specifier: ^15.0.1 - version: 15.0.1 - remark-rehype: - specifier: ^11.1.2 - version: 11.1.2 tailwindcss: specifier: 4.1.16 version: 4.1.16 @@ -198,6 +192,65 @@ importers: packages: + '@algolia/abtesting@1.15.1': + resolution: {integrity: sha512-2yuIC48rUuHGhU1U5qJ9kJHaxYpJ0jpDHJVI5ekOxSMYXlH4+HP+pA31G820lsAznfmu2nzDV7n5RO44zIY1zw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-abtesting@5.49.1': + resolution: {integrity: sha512-h6M7HzPin+45/l09q0r2dYmocSSt2MMGOOk5c4O5K/bBBlEwf1BKfN6z+iX4b8WXcQQhf7rgQwC52kBZJt/ZZw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.49.1': + resolution: {integrity: sha512-048T9/Z8OeLmTk8h76QUqaNFp7Rq2VgS2Zm6Y2tNMYGQ1uNuzePY/udB5l5krlXll7ZGflyCjFvRiOtlPZpE9g==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.49.1': + resolution: {integrity: sha512-vp5/a9ikqvf3mn9QvHN8PRekn8hW34aV9eX+O0J5mKPZXeA6Pd5OQEh2ZWf7gJY6yyfTlLp5LMFzQUAU+Fpqpg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.49.1': + resolution: {integrity: sha512-B6N7PgkvYrul3bntTz/l6uXnhQ2bvP+M7NqTcayh681tSqPaA5cJCUBp/vrP7vpPRpej4Eeyx2qz5p0tE/2N2g==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.49.1': + resolution: {integrity: sha512-v+4DN+lkYfBd01Hbnb9ZrCHe7l+mvihyx218INRX/kaCXROIWUDIT1cs3urQxfE7kXBFnLsqYeOflQALv/gA5w==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.49.1': + resolution: {integrity: sha512-Un11cab6ZCv0W+Jiak8UktGIqoa4+gSNgEZNfG8m8eTsXGqwIEr370H3Rqwj87zeNSlFpH2BslMXJ/cLNS1qtg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.49.1': + resolution: {integrity: sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA==} + engines: {node: '>= 14.0.0'} + + '@algolia/events@4.0.1': + resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==} + + '@algolia/ingestion@1.49.1': + resolution: {integrity: sha512-b5hUXwDqje0Y4CpU6VL481DXgPgxpTD5sYMnfQTHKgUispGnaCLCm2/T9WbJo1YNUbX3iHtYDArp804eD6CmRQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.49.1': + resolution: {integrity: sha512-bvrXwZ0WsL3rN6Q4m4QqxsXFCo6WAew7sAdrpMQMK4Efn4/W920r9ptOuckejOSSvyLr9pAWgC5rsHhR2FYuYw==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.49.1': + resolution: {integrity: sha512-h2yz3AGeGkQwNgbLmoe3bxYs8fac4An1CprKTypYyTU/k3Q+9FbIvJ8aS1DoBKaTjSRZVoyQS7SZQio6GaHbZw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.49.1': + resolution: {integrity: sha512-2UPyRuUR/qpqSqH8mxFV5uBZWEpxhGPHLlx9Xf6OVxr79XO2ctzZQAhsmTZ6X22x+N8MBWpB9UEky7YU2HGFgA==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.49.1': + resolution: {integrity: sha512-N+xlE4lN+wpuT+4vhNEwPVlrfN+DWAZmSX9SYhbz986Oq8AMsqdntOqUyiOXVxYsQtfLwmiej24vbvJGYv1Qtw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.49.1': + resolution: {integrity: sha512-zA5bkUOB5PPtTr182DJmajCiizHp0rCJQ0Chf96zNFvkdESKYlDeYA3tQ7r2oyHbu/8DiohAQ5PZ85edctzbXA==} + engines: {node: '>= 14.0.0'} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -981,36 +1034,6 @@ packages: '@ory/client@1.22.7': resolution: {integrity: sha512-DOiZOXFAqG5d1AVgGhqkP3Hhvq9cf9b3EHJDXea6nSHGCg6isKbE/DFIYuAy220ymZ/J3IYYHG8k8CLje9VipA==} - '@pagefind/darwin-arm64@1.4.0': - resolution: {integrity: sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ==} - cpu: [arm64] - os: [darwin] - - '@pagefind/darwin-x64@1.4.0': - resolution: {integrity: sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A==} - cpu: [x64] - os: [darwin] - - '@pagefind/freebsd-x64@1.4.0': - resolution: {integrity: sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q==} - cpu: [x64] - os: [freebsd] - - '@pagefind/linux-arm64@1.4.0': - resolution: {integrity: sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw==} - cpu: [arm64] - os: [linux] - - '@pagefind/linux-x64@1.4.0': - resolution: {integrity: sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg==} - cpu: [x64] - os: [linux] - - '@pagefind/windows-x64@1.4.0': - resolution: {integrity: sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g==} - cpu: [x64] - os: [win32] - '@posthog/core@1.22.0': resolution: {integrity: sha512-WkmOnq95aAOu6yk6r5LWr5cfXsQdpVbWDCwOxQwxSne8YV6GuZET1ziO5toSQXgrgbdcjrSz2/GopAfiL6iiAA==} @@ -2068,6 +2091,9 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@swc/helpers@0.5.18': + resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} + '@tailwindcss/node@4.1.16': resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==} @@ -2299,6 +2325,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/dom-speech-recognition@0.0.1': + resolution: {integrity: sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -2308,9 +2337,15 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/google.maps@3.58.1': + resolution: {integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hogan.js@3.0.5': + resolution: {integrity: sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA==} + '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} @@ -2332,6 +2367,9 @@ packages: '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + '@types/ramda@0.30.2': resolution: {integrity: sha512-PyzHvjCalm2BRYjAU6nIB3TprYwMNOUY/7P/N8bSzp9W/yM2YrtGtAnnVtaCNSeOZ8DzKyFDvaqQs7LnWwwmBA==} @@ -2412,6 +2450,9 @@ packages: '@zod/core@0.9.0': resolution: {integrity: sha512-bVfPiV2kDUkAJ4ArvV4MHcPZA8y3xOX6/SjzSy2kX2ACopbaaAP4wk6hd/byRmfi9MLNai+4SFJMmcATdOyclg==} + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2422,6 +2463,15 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + algoliasearch-helper@3.28.0: + resolution: {integrity: sha512-GBN0xsxGggaCPElZq24QzMdfphrjIiV2xA+hRXE4/UMpN3nsF2WrM8q+x80OGvGpJWtB7F+4Hq5eSfWwuejXrg==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + + algoliasearch@5.49.1: + resolution: {integrity: sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==} + engines: {node: '>= 14.0.0'} + ansi-escapes@7.2.0: resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} engines: {node: '>=18'} @@ -3192,6 +3242,13 @@ packages: highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + hogan.js@3.0.2: + resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} + hasBin: true + + htm@3.1.1: + resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -3230,6 +3287,14 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + instantsearch-ui-components@0.20.0: + resolution: {integrity: sha512-wZ+GT8lb10m3r6+9TEVLDQeB1YNRgXFSFSsfhzh4jUFMVDiovY+NE+cj7oNFk7GzFWvxQvXd7aPjTLDD7RkhnA==} + + instantsearch.js@4.90.0: + resolution: {integrity: sha512-HfMMvQCsmrLikx93skFNXsoX+zkPEqzszYuFbh1nKMI7qy18QfL+yG7BKX/mt7HPC/SbP+k4vZQzXgIT2+SXPw==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} @@ -3490,6 +3555,15 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + markdown-to-jsx@7.7.17: + resolution: {integrity: sha512-7mG/1feQ0TX5I7YyMZVDgCC/y2I3CiEhIRQIhyov9nGBP5eoVrOXXHuL5ZP8GRfxVZKRiXWJgwXkb9It+nQZfQ==} + engines: {node: '>= 10'} + peerDependencies: + react: '>= 0.14.0' + peerDependenciesMeta: + react: + optional: true + marked@16.4.2: resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} engines: {node: '>= 20'} @@ -3716,6 +3790,10 @@ packages: mj-context-menu@0.6.1: resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + mkdirp@0.3.0: + resolution: {integrity: sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==} + deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -3828,6 +3906,10 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true + nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3845,6 +3927,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -3886,10 +3972,6 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} - pagefind@1.4.0: - resolution: {integrity: sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g==} - hasBin: true - parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -3994,6 +4076,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} @@ -4081,6 +4167,19 @@ packages: peerDependencies: react: ^16.8.4 || ^17.0.0 || ^18.0.0 + react-instantsearch-core@7.26.0: + resolution: {integrity: sha512-Pi+sUkOLQhWG0rv550YowJHk0wLVGiLiz6SK3s0Fbz4V2tJqd2t692jZ29zNWJPooFLHzMjTDQ4Wf2CDLXfHDw==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + react: '>= 16.8.0 < 20' + + react-instantsearch@7.26.0: + resolution: {integrity: sha512-XzkNdH5ZVuwEyl7IUQabyi5OEqKWsEo6ny72saLdNCIfXL0O6wkcQvfhSzursoqw1zrwq3r0s+3dKRY4tVh6Cw==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + react: '>= 16.8.0 < 20' + react-dom: '>= 16.8.0 < 20' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4222,9 +4321,6 @@ packages: rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} - rehype-stringify@10.0.1: - resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} - remark-frontmatter@5.0.0: resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} @@ -4327,6 +4423,9 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -4371,6 +4470,22 @@ packages: resolution: {integrity: sha512-KRT/hufMSxXKEDSQujfVE0Faa/kZ51ihUcZQAcmP04t00DvPj7Ox5anHke1sJYUtzSuiT/Y5uyzg/W7bBEGhCg==} hasBin: true + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -4881,6 +4996,11 @@ packages: zenscroll@4.0.2: resolution: {integrity: sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==} + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + zod@4.0.0-beta.20250424T163858: resolution: {integrity: sha512-fKhW+lEJnfUGo0fvQjmam39zUytARR2UdCEh7/OXJSBbKScIhD343K74nW+UUHu/r6dkzN6Uc/GqwogFjzpCXg==} @@ -4910,6 +5030,92 @@ packages: snapshots: + '@algolia/abtesting@1.15.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-abtesting@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-analytics@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-common@5.49.1': {} + + '@algolia/client-insights@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-personalization@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-query-suggestions@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-search@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/events@4.0.1': {} + + '@algolia/ingestion@1.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/monitoring@1.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/recommend@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/requester-browser-xhr@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + + '@algolia/requester-fetch@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + + '@algolia/requester-node-http@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@alloc/quick-lru@5.2.0': {} '@antfu/install-pkg@1.1.0': @@ -5567,24 +5773,6 @@ snapshots: transitivePeerDependencies: - debug - '@pagefind/darwin-arm64@1.4.0': - optional: true - - '@pagefind/darwin-x64@1.4.0': - optional: true - - '@pagefind/freebsd-x64@1.4.0': - optional: true - - '@pagefind/linux-arm64@1.4.0': - optional: true - - '@pagefind/linux-x64@1.4.0': - optional: true - - '@pagefind/windows-x64@1.4.0': - optional: true - '@posthog/core@1.22.0': dependencies: cross-spawn: 7.0.6 @@ -6958,6 +7146,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@swc/helpers@0.5.18': + dependencies: + tslib: 2.8.1 + '@tailwindcss/node@4.1.16': dependencies: '@jridgewell/remapping': 2.3.5 @@ -7201,6 +7393,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/dom-speech-recognition@0.0.1': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -7209,10 +7403,14 @@ snapshots: '@types/geojson@7946.0.16': {} + '@types/google.maps@3.58.1': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 + '@types/hogan.js@3.0.5': {} + '@types/katex@0.16.7': {} '@types/mdast@4.0.4': @@ -7233,6 +7431,8 @@ snapshots: '@types/prismjs@1.26.5': {} + '@types/qs@6.14.0': {} + '@types/ramda@0.30.2': dependencies: types-ramda: 0.30.1 @@ -7317,12 +7517,36 @@ snapshots: '@zod/core@0.9.0': {} + abbrev@1.1.1: {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 acorn@8.15.0: {} + algoliasearch-helper@3.28.0(algoliasearch@5.49.1): + dependencies: + '@algolia/events': 4.0.1 + algoliasearch: 5.49.1 + + algoliasearch@5.49.1: + dependencies: + '@algolia/abtesting': 1.15.1 + '@algolia/client-abtesting': 5.49.1 + '@algolia/client-analytics': 5.49.1 + '@algolia/client-common': 5.49.1 + '@algolia/client-insights': 5.49.1 + '@algolia/client-personalization': 5.49.1 + '@algolia/client-query-suggestions': 5.49.1 + '@algolia/client-search': 5.49.1 + '@algolia/ingestion': 1.49.1 + '@algolia/monitoring': 1.49.1 + '@algolia/recommend': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + ansi-escapes@7.2.0: dependencies: environment: 1.1.0 @@ -8201,6 +8425,13 @@ snapshots: highlightjs-vue@1.0.0: {} + hogan.js@3.0.2: + dependencies: + mkdirp: 0.3.0 + nopt: 1.0.10 + + htm@3.1.1: {} + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} @@ -8225,6 +8456,35 @@ snapshots: inline-style-parser@0.2.7: {} + instantsearch-ui-components@0.20.0(react@19.2.3): + dependencies: + '@swc/helpers': 0.5.18 + markdown-to-jsx: 7.7.17(react@19.2.3) + zod: 4.1.12 + zod-to-json-schema: 3.24.6(zod@4.1.12) + transitivePeerDependencies: + - react + + instantsearch.js@4.90.0(algoliasearch@5.49.1): + dependencies: + '@algolia/events': 4.0.1 + '@swc/helpers': 0.5.18 + '@types/dom-speech-recognition': 0.0.1 + '@types/google.maps': 3.58.1 + '@types/hogan.js': 3.0.5 + '@types/qs': 6.14.0 + algoliasearch: 5.49.1 + algoliasearch-helper: 3.28.0(algoliasearch@5.49.1) + hogan.js: 3.0.2 + htm: 3.1.1 + instantsearch-ui-components: 0.20.0(react@19.2.3) + preact: 10.28.0 + qs: 6.15.0 + react: 19.2.3 + search-insights: 2.17.3 + zod: 4.1.12 + zod-to-json-schema: 3.24.6(zod@4.1.12) + internmap@1.0.1: {} internmap@2.0.3: {} @@ -8444,6 +8704,10 @@ snapshots: markdown-table@3.0.4: {} + markdown-to-jsx@7.7.17(react@19.2.3): + optionalDependencies: + react: 19.2.3 + marked@16.4.2: {} math-intrinsics@1.1.0: {} @@ -8980,6 +9244,8 @@ snapshots: mj-context-menu@0.6.1: {} + mkdirp@0.3.0: {} + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -9138,6 +9404,10 @@ snapshots: node-gyp-build@4.8.4: optional: true + nopt@1.0.10: + dependencies: + abbrev: 1.1.1 + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -9154,6 +9424,8 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -9196,15 +9468,6 @@ snapshots: package-manager-detector@1.6.0: {} - pagefind@1.4.0: - optionalDependencies: - '@pagefind/darwin-arm64': 1.4.0 - '@pagefind/darwin-x64': 1.4.0 - '@pagefind/freebsd-x64': 1.4.0 - '@pagefind/linux-arm64': 1.4.0 - '@pagefind/linux-x64': 1.4.0 - '@pagefind/windows-x64': 1.4.0 - parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -9337,6 +9600,10 @@ snapshots: proxy-from-env@1.1.0: {} + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + query-selector-shadow-dom@1.0.1: {} querystringify@2.2.0: {} @@ -9468,6 +9735,27 @@ snapshots: dependencies: react: 19.2.3 + react-instantsearch-core@7.26.0(algoliasearch@5.49.1)(react@19.2.3): + dependencies: + '@swc/helpers': 0.5.18 + algoliasearch: 5.49.1 + algoliasearch-helper: 3.28.0(algoliasearch@5.49.1) + instantsearch.js: 4.90.0(algoliasearch@5.49.1) + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + zod: 4.1.12 + zod-to-json-schema: 3.24.6(zod@4.1.12) + + react-instantsearch@7.26.0(algoliasearch@5.49.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@swc/helpers': 0.5.18 + algoliasearch: 5.49.1 + instantsearch-ui-components: 0.20.0(react@19.2.3) + instantsearch.js: 4.90.0(algoliasearch@5.49.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-instantsearch-core: 7.26.0(algoliasearch@5.49.1)(react@19.2.3) + react-is@16.13.1: {} react-markdown@10.1.0(@types/react@19.2.7)(react@19.2.3): @@ -9664,12 +9952,6 @@ snapshots: transitivePeerDependencies: - supports-color - rehype-stringify@10.0.1: - dependencies: - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - unified: 11.0.5 - remark-frontmatter@5.0.0: dependencies: '@types/mdast': 4.0.4 @@ -9852,6 +10134,8 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 + search-insights@2.17.3: {} + section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 @@ -9932,6 +10216,34 @@ snapshots: short-unique-id@5.3.2: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -10474,6 +10786,10 @@ snapshots: zenscroll@4.0.2: {} + zod-to-json-schema@3.24.6(zod@4.1.12): + dependencies: + zod: 4.1.12 + zod@4.0.0-beta.20250424T163858: dependencies: '@zod/core': 0.9.0 diff --git a/scripts/pagefind.ts b/scripts/pagefind.ts deleted file mode 100644 index 5e8e912be..000000000 --- a/scripts/pagefind.ts +++ /dev/null @@ -1,187 +0,0 @@ -import fs from "node:fs/promises"; -import path, { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import glob from "fast-glob"; -import { createIndex } from "pagefind"; -import rehypeStringify from "rehype-stringify"; -import { remark } from "remark"; -import remarkRehype from "remark-rehype"; -import { readToolkitData } from "@/app/_lib/toolkit-data"; -import { listToolkitRoutes } from "@/app/_lib/toolkit-static-params"; -import { toolkitDataToSearchMarkdown } from "../toolkit-docs-generator/scripts/pagefind-toolkit-content"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Directory containing pre-generated clean markdown files -const CLEAN_MARKDOWN_DIR = path.join(__dirname, "..", "public", "_markdown"); - -/** - * Converts clean markdown to HTML for Pagefind indexing. - * This function expects pre-cleaned markdown (no MDX syntax). - */ -async function markdownToHtml(markdownContent: string): Promise { - try { - const result = await remark() - .use(remarkRehype) - .use(rehypeStringify) - .process(markdownContent); - - return String(result); - } catch (error) { - console.warn( - `Warning: Failed to convert markdown to HTML, using plain text: ${error}` - ); - return markdownContent; - } -} - -/** - * Checks if clean markdown files exist and returns the appropriate source directory - */ -async function getMarkdownSource(language: string): Promise<{ - dir: string; - pattern: string; - isClean: boolean; -}> { - const cleanDir = path.join(CLEAN_MARKDOWN_DIR, language); - - try { - await fs.access(cleanDir); - const files = await fs.readdir(cleanDir); - if (files.length > 0) { - return { dir: cleanDir, pattern: "**/*.md", isClean: true }; - } - } catch { - // Clean markdown directory doesn't exist - } - - // Fallback to raw MDX (with warning) - console.warn( - `⚠️ Clean markdown not found for ${language}, falling back to raw MDX` - ); - console.warn( - ` Run "pnpm run generate:markdown" first to generate clean files` - ); - return { - dir: path.join(__dirname, "..", "app", language), - pattern: "**/*.mdx", - isClean: false, - }; -} - -const { index } = await createIndex(); -if (!index) { - throw new Error("Failed to create index"); -} - -console.log("\r\n🔍 BUILDING SEARCH INDEX\r\n"); - -// valid languages are those in the app directory that do not start with an underscore and are not "api" -const appDir = path.join(__dirname, "..", "app"); -const entries = await fs.readdir(appDir); -const languages = await Promise.all( - entries.map(async (dir: string) => { - if (dir.startsWith("_") || dir === "api") { - return null; - } - const entryPath = path.join(appDir, dir); - const stats = await fs.stat(entryPath); - return stats.isDirectory() ? dir : null; - }) -).then((results) => results.filter((dir): dir is string => dir !== null)); - -let page_count = 0; - -console.log("Building search index for languages: ", languages.join(", ")); - -for (const language of languages) { - const source = await getMarkdownSource(language); - - console.log( - `Adding directory: ${source.dir} (${source.isClean ? "clean markdown" : "raw MDX"})` - ); - - for (const entry of glob.sync(source.pattern, { cwd: source.dir })) { - // Skip dynamic templates (we index concrete toolkit pages separately). - if ( - !source.isClean && - entry.includes("resources/integrations") && - entry.includes("[toolkitId]/page.mdx") - ) { - continue; - } - - const filePath = path.join(source.dir, entry); - - // Build URL from file path - // Clean markdown: "home/quickstart.md" -> "/en/home/quickstart" - // Raw MDX: "home/quickstart/page.mdx" -> "/en/home/quickstart" - let urlPath: string; - if (source.isClean) { - urlPath = entry.replace(/\.md$/, ""); - } else { - urlPath = entry.split("/page.mdx")[0]; - } - const url = `/${language}/${urlPath}`; - - const markdownContent = await fs.readFile(filePath, "utf-8"); - const htmlContent = await markdownToHtml(markdownContent); - - const { errors, file } = await index.addHTMLFile({ - url, - content: `${htmlContent}`, - }); - - const fileInfo = file - ? ` (${file.uniqueWords} words${file.meta?.title ? `, title: ${file.meta.title}` : ""})` - : ""; - console.log(`Adding page: ${url}${fileInfo}`); - - if (errors.length > 0) { - console.error(`Error adding page: ${url}`); - console.error(errors); - } - - page_count += 1; - } - - // Index toolkit docs pages (rendered from JSON), so search can find tools. - // These pages live under /en/resources/integrations//. - if (language === "en") { - const toolkitRoutes = await listToolkitRoutes(); - for (const route of toolkitRoutes) { - const toolkitData = await readToolkitData(route.toolkitId); - if (!toolkitData) { - continue; - } - - const url = `/en/resources/integrations/${route.category}/${route.toolkitId}`; - const markdown = toolkitDataToSearchMarkdown(toolkitData); - const htmlContent = await markdownToHtml(markdown); - - const { errors, file } = await index.addHTMLFile({ - url, - content: `${htmlContent}`, - }); - - const fileInfo = file - ? ` (${file.uniqueWords} words${file.meta?.title ? `, title: ${file.meta.title}` : ""})` - : ""; - console.log(`Adding page: ${url}${fileInfo}`); - - if (errors.length > 0) { - console.error(`Error adding page: ${url}`); - console.error(errors); - } - - page_count += 1; - } - } -} - -console.log(`Added ${page_count} pages`); - -await index.writeFiles({ - outputPath: path.join(__dirname, "..", "public", "_pagefind"), -}); From ed4d41e9996188ac6f24f5c525a49a7f03ad56e4 Mon Sep 17 00:00:00 2001 From: Teal Larson Date: Fri, 27 Feb 2026 16:46:43 -0500 Subject: [PATCH 2/8] Fix TypeScript error: use React.ElementType for MDX component cache `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 --- .../components/documentation-chunk-renderer.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/_components/toolkit-docs/components/documentation-chunk-renderer.tsx b/app/_components/toolkit-docs/components/documentation-chunk-renderer.tsx index 70ccf3740..8ed9808ca 100644 --- a/app/_components/toolkit-docs/components/documentation-chunk-renderer.tsx +++ b/app/_components/toolkit-docs/components/documentation-chunk-renderer.tsx @@ -112,19 +112,14 @@ const MDX_COMPONENTS = { // Maximum number of MDX components to cache to prevent unbounded memory growth const MDX_CACHE_MAX_SIZE = 100; -const mdxCache = - createMdxCache>( - MDX_CACHE_MAX_SIZE - ); +const mdxCache = createMdxCache(MDX_CACHE_MAX_SIZE); /** * Renders MDX content from a string with custom components. */ function MdxContent({ content }: { content: string }) { const source = useMemo(() => stripMdxImports(content), [content]); - const [Component, setComponent] = useState | null>(null); + const [Component, setComponent] = useState(null); const [error, setError] = useState(null); useEffect(() => { From 5f3c64f288c498ce212096853218ecabd32e5651 Mon Sep 17 00:00:00 2001 From: Teal Larson Date: Fri, 27 Feb 2026 17:11:22 -0500 Subject: [PATCH 3/8] Refactor AlgoliaSearch: guard config, sanitize URLs, remove dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/_components/algolia-search.tsx | 117 ++++++++++++++++------------- 1 file changed, 65 insertions(+), 52 deletions(-) diff --git a/app/_components/algolia-search.tsx b/app/_components/algolia-search.tsx index 942063d0a..d1639ae5d 100644 --- a/app/_components/algolia-search.tsx +++ b/app/_components/algolia-search.tsx @@ -2,7 +2,7 @@ import { liteClient as algoliasearch } from "algoliasearch/lite"; import { Search } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { Highlight, Hits, @@ -18,18 +18,28 @@ type HitRecord = { url?: string; }; -const FOCUS_DELAY_MS = 50; +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 = algoliasearch( - process.env.NEXT_PUBLIC_ALGOLIA_APP_ID ?? "", - process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY ?? "" -); +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 (
+ Add NEXT_PUBLIC_ALGOLIA_APP_ID,{" "} + NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY, and{" "} + NEXT_PUBLIC_ALGOLIA_INDEX_NAME to your + environment to enable search. +

+ ); +} + export function AlgoliaSearch() { const [isOpen, setIsOpen] = useState(false); - const inputRef = useRef(null); useEffect(() => { const handler = (e: KeyboardEvent) => { @@ -94,12 +114,6 @@ export function AlgoliaSearch() { return () => window.removeEventListener("keydown", handler); }, []); - useEffect(() => { - if (isOpen) { - setTimeout(() => inputRef.current?.focus(), FOCUS_DELAY_MS); - } - }, [isOpen]); - return ( <>
)} From 1d913ca20cb4b5391e298b13a269b42d64cf551a Mon Sep 17 00:00:00 2001 From: Teal Larson Date: Fri, 27 Feb 2026 17:13:38 -0500 Subject: [PATCH 4/8] Remove "Search by Algolia" footer from search modal Co-Authored-By: Claude Opus 4.6 --- app/_components/algolia-search.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/_components/algolia-search.tsx b/app/_components/algolia-search.tsx index d1639ae5d..0e85eafaf 100644 --- a/app/_components/algolia-search.tsx +++ b/app/_components/algolia-search.tsx @@ -171,11 +171,6 @@ export function AlgoliaSearch() { )} /> -
- - Search by Algolia - -
) : ( From 31d25ad0d286c8784ca87d2d233ba9dc1d73e698 Mon Sep 17 00:00:00 2001 From: Teal Larson Date: Fri, 27 Feb 2026 17:15:35 -0500 Subject: [PATCH 5/8] Replace useEffect keyboard handler with useEventListener @uidotdev/usehooks is already a dependency. useEventListener handles add/remove lifecycle internally, removing the boilerplate. Co-Authored-By: Claude Opus 4.6 --- app/_components/algolia-search.tsx | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/app/_components/algolia-search.tsx b/app/_components/algolia-search.tsx index 0e85eafaf..651cefda7 100644 --- a/app/_components/algolia-search.tsx +++ b/app/_components/algolia-search.tsx @@ -1,8 +1,9 @@ "use client"; +import { useEventListener } from "@uidotdev/usehooks"; import { liteClient as algoliasearch } from "algoliasearch/lite"; import { Search } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Highlight, Hits, @@ -100,19 +101,15 @@ function SearchUnavailable() { 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); - }, []); + useEventListener("keydown", (e) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + setIsOpen((prev) => !prev); + } + if (e.key === "Escape") { + setIsOpen(false); + } + }); return ( <> From 75fe4fe0cde6020b70b292518f4cfacd6ebc4d24 Mon Sep 17 00:00:00 2001 From: Teal Larson Date: Fri, 27 Feb 2026 17:19:12 -0500 Subject: [PATCH 6/8] Use design system --primary var for Algolia highlight colors 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 --- app/globals.css | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/globals.css b/app/globals.css index 53d8993ab..4896714a7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -160,8 +160,8 @@ nav > div:has(.algolia-search-button) { /* Algolia search hit highlight — brand red */ .ais-Highlight-highlighted, .ais-Snippet-highlighted { - background: hsl(347 100% 50% / 0.15); - color: hsl(347 100% 45%); + background: color-mix(in oklch, var(--primary) 15%, transparent); + color: var(--primary); border-radius: 2px; font-style: normal; font-weight: 600; @@ -169,8 +169,7 @@ nav > div:has(.algolia-search-button) { .dark .ais-Highlight-highlighted, .dark .ais-Snippet-highlighted { - background: hsl(347 100% 50% / 0.2); - color: hsl(347 100% 65%); + background: color-mix(in oklch, var(--primary) 20%, transparent); } /* Override Nextra code highlight colors to use green instead of red */ From 4d68f9e18cb3c8995e34a253cc18ccd30a7ded55 Mon Sep 17 00:00:00 2001 From: Teal Larson Date: Fri, 27 Feb 2026 17:21:43 -0500 Subject: [PATCH 7/8] Use design system semantic tokens in AlgoliaSearch component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/_components/algolia-search.tsx | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/_components/algolia-search.tsx b/app/_components/algolia-search.tsx index 651cefda7..bb5da96fa 100644 --- a/app/_components/algolia-search.tsx +++ b/app/_components/algolia-search.tsx @@ -42,14 +42,14 @@ function SearchHit({ hit }: { hit: HitRecord }) { className="block rounded-lg px-4 py-3 hover:bg-neutral-100 dark:hover:bg-white/5" href={safeHref(hit.url)} > -
+
[0]["hit"]} />
{hit.description && ( -
+
[0]["hit"]} @@ -66,7 +66,7 @@ function EmptyQuery() { return null; } return ( -

+

Start typing to search the docs…

); @@ -78,18 +78,16 @@ function NoResults() { return null; } return ( -

+

No results for{" "} - - "{results.query}" - + "{results.query}"

); } function SearchUnavailable() { return ( -

+

Add NEXT_PUBLIC_ALGOLIA_APP_ID,{" "} NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY, and{" "} NEXT_PUBLIC_ALGOLIA_INDEX_NAME to your @@ -115,13 +113,13 @@ export function AlgoliaSearch() { <> @@ -139,17 +137,17 @@ export function AlgoliaSearch() { onClick={() => setIsOpen(false)} type="button" /> -

+
{searchClient && indexName ? ( -
- +
+ Date: Fri, 27 Feb 2026 17:22:48 -0500 Subject: [PATCH 8/8] =?UTF-8?q?Revert=20to=20useEffect=20for=20keyboard=20?= =?UTF-8?q?handler=20=E2=80=94=20useEventListener=20not=20in=20@uidotdev/u?= =?UTF-8?q?sehooks@2.4.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- app/_components/algolia-search.tsx | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/app/_components/algolia-search.tsx b/app/_components/algolia-search.tsx index bb5da96fa..cf0dbdec4 100644 --- a/app/_components/algolia-search.tsx +++ b/app/_components/algolia-search.tsx @@ -1,9 +1,8 @@ "use client"; -import { useEventListener } from "@uidotdev/usehooks"; import { liteClient as algoliasearch } from "algoliasearch/lite"; import { Search } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Highlight, Hits, @@ -99,15 +98,19 @@ function SearchUnavailable() { export function AlgoliaSearch() { const [isOpen, setIsOpen] = useState(false); - useEventListener("keydown", (e) => { - if ((e.metaKey || e.ctrlKey) && e.key === "k") { - e.preventDefault(); - setIsOpen((prev) => !prev); - } - if (e.key === "Escape") { - setIsOpen(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 ( <>