From 8ba758ab5d65a0ac7d07a2b82602a824dac15614 Mon Sep 17 00:00:00 2001 From: Darius Cepulis Date: Wed, 5 Nov 2025 13:04:06 -0600 Subject: [PATCH] feat(site): search (#165) --- pnpm-lock.yaml | 36 +++--- site/TODO.md | 1 - site/astro.config.mjs | 2 + site/integrations/pagefind.ts | 99 ++++++++++++++++ site/package.json | 2 + site/src/components/FormattedDate.astro | 8 +- site/src/components/NavBar/NavBar.astro | 3 + site/src/components/Search/Search.astro | 111 ++++++++++++++++++ site/src/components/Search/Search.tsx | 96 +++++++++++++++ site/src/components/Search/index.ts | 1 + site/src/components/Search/searchIcon.svg | 12 ++ site/src/components/docs/FrameworkCase.astro | 2 +- site/src/components/docs/PreferenceSync.tsx | 39 ++++++ .../src/components/docs/PreferenceUpdater.tsx | 12 +- site/src/components/docs/StyleCase.astro | 2 +- site/src/layouts/Base.astro | 2 + site/src/pages/blog/[...slug].astro | 18 +-- .../[framework]/style/[style]/[...slug].astro | 16 ++- site/src/stores/preferences.ts | 10 ++ site/src/styles/globals.css | 10 ++ site/src/utils/docs/__tests__/sidebar.test.ts | 90 ++++++++++++++ site/src/utils/docs/preferences.ts | 19 +++ site/src/utils/docs/sidebar.ts | 28 +++++ site/src/utils/jsonLd/schemas.ts | 2 + 24 files changed, 581 insertions(+), 40 deletions(-) create mode 100644 site/integrations/pagefind.ts create mode 100644 site/src/components/Search/Search.astro create mode 100644 site/src/components/Search/Search.tsx create mode 100644 site/src/components/Search/index.ts create mode 100644 site/src/components/Search/searchIcon.svg create mode 100644 site/src/components/docs/PreferenceSync.tsx create mode 100644 site/src/stores/preferences.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35e7b2e8..ca2d5a8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,7 +234,7 @@ importers: dependencies: '@floating-ui/react': specifier: ^0.27.16 - version: 0.27.16(react-dom@19.2.0(react@18.3.1))(react@18.3.1) + version: 0.27.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@videojs/core': specifier: workspace:* version: link:../core @@ -317,6 +317,9 @@ importers: '@nanostores/react': specifier: ^1.0.0 version: 1.0.0(nanostores@1.0.1)(react@18.3.1) + '@pagefind/default-ui': + specifier: ^1.4.0 + version: 1.4.0 '@tailwindcss/vite': specifier: ^4.1.14 version: 4.1.14(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) @@ -411,6 +414,9 @@ importers: jsdom: specifier: ^27.0.0 version: 27.0.0(postcss@8.5.6) + sirv: + specifier: ^3.0.2 + version: 3.0.2 vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(yaml@2.8.1) @@ -1377,6 +1383,9 @@ packages: '@oxc-project/types@0.95.0': resolution: {integrity: sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==} + '@pagefind/default-ui@1.4.0': + resolution: {integrity: sha512-wie82VWn3cnGEdIjh4YwNESyS1G6vRHwL6cNjy9CFgNnWW/PGRjsLq300xjVH5sfPFK3iK36UxvIBymtQIEiSQ==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -7199,18 +7208,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@floating-ui/react-dom@2.1.6(react-dom@19.2.0(react@18.3.1))(react@18.3.1)': + '@floating-ui/react@0.27.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.7.4 - react: 18.3.1 - react-dom: 19.2.0(react@18.3.1) - - '@floating-ui/react@0.27.16(react-dom@19.2.0(react@18.3.1))(react@18.3.1)': - dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@18.3.1))(react@18.3.1) + '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@floating-ui/utils': 0.2.10 react: 18.3.1 - react-dom: 19.2.0(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) tabbable: 6.2.0 '@floating-ui/utils@0.2.10': {} @@ -7481,6 +7484,8 @@ snapshots: '@oxc-project/types@0.95.0': {} + '@pagefind/default-ui@1.4.0': {} + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -9500,7 +9505,7 @@ snapshots: eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.37.0(jiti@2.6.1)) @@ -9542,7 +9547,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -9618,7 +9623,7 @@ snapshots: optionalDependencies: typescript: 5.9.2 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11953,11 +11958,6 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-dom@19.2.0(react@18.3.1): - dependencies: - react: 18.3.1 - scheduler: 0.27.0 - react-dom@19.2.0(react@19.2.0): dependencies: react: 19.2.0 diff --git a/site/TODO.md b/site/TODO.md index 2ca7dc2f..82901d6b 100644 --- a/site/TODO.md +++ b/site/TODO.md @@ -8,5 +8,4 @@ not comprehensive. just the things I'm worried about forgetting - animations - docs features - /docs/[slug] redirect ("share this guide") -- search - update READMEs following dev diff --git a/site/astro.config.mjs b/site/astro.config.mjs index 33adc7d4..d9e67134 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -8,6 +8,7 @@ import vercel from '@astrojs/vercel'; import tailwindcss from '@tailwindcss/vite'; import { defineConfig, fontProviders } from 'astro/config'; +import pagefind from './integrations/pagefind'; import rehypePrepareCodeBlocks from './src/utils/rehypePrepareCodeBlocks'; import remarkConditionalHeadings from './src/utils/remarkConditionalHeadings'; import { remarkReadingTime } from './src/utils/remarkReadingTime.mjs'; @@ -21,6 +22,7 @@ export default defineConfig({ integrations: [ mdx({ extendMarkdownConfig: true }), sitemap(), + pagefind(), react({ babel: { plugins: [['babel-plugin-react-compiler', { target: '18' }]], diff --git a/site/integrations/pagefind.ts b/site/integrations/pagefind.ts new file mode 100644 index 00000000..eb26b50b --- /dev/null +++ b/site/integrations/pagefind.ts @@ -0,0 +1,99 @@ +import type { AstroIntegration } from 'astro'; +import { spawn } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import sirv from 'sirv'; + +export interface PagefindOptions { + /** + * The path to the built site to index. + * Defaults to the Astro output directory. + */ + site?: string; +} + +export default function pagefind(options: PagefindOptions = {}): AstroIntegration { + return { + name: 'pagefind', + hooks: { + 'astro:server:setup': ({ server, logger }) => { + // Serve Pagefind index from previous build during development + const rootDir = server.config.root; + const indexDir = join(rootDir, 'dist', 'client'); + const pagefindDir = join(indexDir, 'pagefind'); + + // Warn if index doesn't exist yet + if (!existsSync(pagefindDir)) { + logger.warn( + 'Pagefind index not found. Run `pnpm build` first to generate ' + + 'the search index for development mode.', + ); + } else { + logger.debug(`Serving Pagefind index from ${indexDir}`); + } + + // Create sirv middleware to serve static files + // approach adapted from https://github.com/shishkin/astro-pagefind + const serve = sirv(indexDir, { + dev: true, // No caching in dev mode + etag: true, // Enable cache validation + }); + + // Mount middleware for /pagefind/* routes only + server.middlewares.use((req, res, next) => { + if (req.url?.startsWith('/pagefind/')) { + serve(req, res, next); + } else { + next(); + } + }); + }, + + 'astro:build:done': async ({ dir, logger }) => { + // Determine the site directory to index + // The dir parameter already points to the correct static output directory + const siteDir = options.site || fileURLToPath(dir); + + // Map Astro logger levels to Pagefind CLI flags + const logLevel = logger.options.level; + const logFlags: string[] = []; + + if (logLevel === 'silent' || logLevel === 'error') { + logFlags.push('--silent'); + } else if (logLevel === 'warn') { + logFlags.push('--quiet'); + } else if (logLevel === 'debug') { + logFlags.push('--verbose'); + } + // 'info' level uses no flag (default) + + logger.info('Running Pagefind indexer...'); + + return new Promise((resolve, reject) => { + const pagefindProcess = spawn( + 'npx', + ['-y', 'pagefind', ...logFlags, '--site', siteDir], + { + stdio: 'inherit', + shell: true, + }, + ); + + pagefindProcess.on('close', (code) => { + if (code === 0) { + logger.info('Pagefind indexing complete'); + resolve(); + } else { + reject(new Error(`Pagefind process exited with code ${code}`)); + } + }); + + pagefindProcess.on('error', (error) => { + reject(new Error(`Failed to start Pagefind: ${error.message}`)); + }); + }); + }, + }, + }; +} diff --git a/site/package.json b/site/package.json index ce111937..c3525af0 100644 --- a/site/package.json +++ b/site/package.json @@ -20,6 +20,7 @@ "@astrojs/vercel": "^8.2.9", "@base-ui-components/react": "1.0.0-beta.4", "@nanostores/react": "^1.0.0", + "@pagefind/default-ui": "^1.4.0", "@tailwindcss/vite": "^4.1.14", "@videojs/html": "workspace:*", "@videojs/react": "workspace:*", @@ -53,6 +54,7 @@ "@vitest/ui": "^3.2.4", "babel-plugin-react-compiler": "1.0.0", "jsdom": "^27.0.0", + "sirv": "^3.0.2", "vitest": "^3.2.4" } } diff --git a/site/src/components/FormattedDate.astro b/site/src/components/FormattedDate.astro index 60b112c6..651378ea 100644 --- a/site/src/components/FormattedDate.astro +++ b/site/src/components/FormattedDate.astro @@ -1,12 +1,14 @@ --- -interface Props { +import type { HTMLAttributes } from 'astro/types'; + +interface Props extends Omit, 'datetime'> { date: Date; } -const { date } = Astro.props; +const { date, ...rest } = Astro.props; --- -