1
0
Fork 0

fix: cater for transformItems to use DS with custom indices (#2705)

This commit is contained in:
Dylan Tientcheu 2025-08-08 16:24:24 +02:00 committed by GitHub
parent 95af410f82
commit 3478262b3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 669 additions and 288 deletions

View file

@ -6,11 +6,11 @@
},
{
"path": "packages/docsearch-react/dist/umd/index.js",
"maxSize": "70 kB"
"maxSize": "75 kB"
},
{
"path": "packages/docsearch-js/dist/umd/index.js",
"maxSize": "85 kB"
"maxSize": "90 kB"
}
]
}

View file

@ -1,23 +0,0 @@
{
"name": "@docsearch/askai-example",
"description": "DocSearch v4 AskAI example",
"version": "4.0.0-beta.5",
"private": true,
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@docsearch/css": "workspace:*",
"@docsearch/react": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.7"
}
}

View file

@ -1,4 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}

View file

@ -1,27 +0,0 @@
/* eslint-disable react/react-in-jsx-scope */
import { DocSearch } from '@docsearch/react';
import type { JSX } from 'react';
import './App.css';
import '@docsearch/css/dist/style.css';
function App(): JSX.Element {
return (
<div>
<h1>DocSearch v4 - AskAI</h1>
<DocSearch
indexName="docsearch"
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
askAi={{
assistantId: 'askAIDemo',
searchParameters: {
facetFilters: ['language:en'],
},
}}
insights={true}
/>
</div>
);
}
export default App;

View file

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocSearch v4 - React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,23 +0,0 @@
{
"name": "@docsearch/react-18-example",
"description": "DocSearch v3 React 18 example",
"version": "4.0.0-beta.5",
"private": true,
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@docsearch/css": "workspace:*",
"@docsearch/react": "workspace:*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.7"
}
}

View file

@ -1,4 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}

View file

@ -1,15 +0,0 @@
import { DocSearch } from '@docsearch/react';
import React from 'react';
import './App.css';
import '@docsearch/css/dist/style.css';
function App(): React.JSX.Element {
return (
<div>
<h1>DocSearch v4 - React - 18</h1>
<DocSearch indexName="vuejs" appId="ML0LEBN7FQ" apiKey="21cf9df0734770a2448a9da64a700c22" insights={true} />
</div>
);
}
export default App;

View file

@ -1,11 +0,0 @@
/* eslint-disable react/react-in-jsx-scope */
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View file

@ -1,31 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": [
"src",
"vite.config.ts"
],
}

View file

@ -1,7 +0,0 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
});

View file

@ -11,8 +11,8 @@
"preview": "vite preview"
},
"dependencies": {
"@docsearch/css": "4.0.0-beta.5",
"@docsearch/react": "4.0.0-beta.5",
"@docsearch/css": "workspace:*",
"@docsearch/react": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},

View file

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -0,0 +1,160 @@
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=TikTok+Sans:opsz,wght@12..36,300..900&display=swap');
/* reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'TikTok Sans', 'sans-serif';
min-height: 100vh;
background: radial-gradient(125% 125% at 50% 90%, #fff 40%, #7c3aed 100%);
color: #333;
line-height: 1.6;
}
/* container styles */
.app-container {
max-width: 1024px;
margin: 0 auto;
padding: 2rem 1rem;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
}
/* header styles */
.app-header {
text-align: center;
margin-bottom: 3rem;
}
.app-title {
font-size: 2rem;
font-weight: 700;
color: black;
margin-bottom: 0;
}
.app-subtitle {
font-size: 1rem;
color: black;
color: #666;
font-weight: 400;
}
main {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 8px;
}
/* demo section styles */
.demo-section {
background: white;
border-radius: 16px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.section-description {
text-align: center;
color: #666;
font-family: 'Source Code Pro', monospace;
font-size: 0.9rem;
margin-bottom: 1rem;
margin-top: -1rem;
opacity: 0.8;
}
.ai-powered {
text-decoration: underline;
text-decoration-color: #f2e8ff;
text-underline-offset: 4px;
text-decoration-thickness: 2px;
}
/* search component wrapper */
.search-wrapper {
display: flex;
justify-content: center;
align-items: center;
min-height: 60px;
}
/* divider styles */
.divider {
border: none;
height: 1px;
background: linear-gradient(90deg, transparent, #e2e8f0, transparent);
margin: 0;
}
/* responsive design */
@media (max-width: 768px) {
.app-container {
padding: 1rem;
}
.app-title {
font-size: 2.5rem;
}
.demo-section {
padding: 1.5rem;
}
.section-description {
font-size: 0.8rem;
}
}
/* docsearch button customization */
.DocSearch-Button {
margin: 0 !important;
background: #f7fafc !important;
border: 2px solid #e2e8f0 !important;
border-radius: 12px !important;
padding: 12px 16px !important;
font-size: 16px !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important;
}
.DocSearch-Button:hover {
border-color: #667eea !important;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15) !important;
transform: translateY(-1px) !important;
}
.DocSearch-Button-Placeholder {
color: #718096 !important;
}
/* add subtle animations */
.demo-section {
animation: fadeInUp 0.6s ease forwards;
}
.demo-section:nth-child(2) {
animation-delay: 0.1s;
}
.demo-section:nth-child(3) {
animation-delay: 0.2s;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View file

@ -0,0 +1,56 @@
/* eslint-disable react/react-in-jsx-scope */
import { version } from '@docsearch/react';
import type { JSX } from 'react';
import './App.css';
import '@docsearch/css/dist/style.css';
import Basic from './examples/basic';
import BasicAskAI from './examples/basic-askai';
import WHitComponent from './examples/w-hit-component';
import WTransformItems from './examples/w-hit-transformItems';
function App(): JSX.Element {
return (
<div className="body-container">
<div className="app-container">
<header className="app-header">
<h1 className="app-title">DocSearch v{version}</h1>
<p className="app-subtitle">Experience the power of intelligent documentation search</p>
</header>
<main>
<section className="demo-section">
<p className="section-description">basic search functionality</p>
<div className="search-wrapper">
<Basic />
</div>
</section>
<section className="demo-section">
<p className="section-description">search with askai integration</p>
<div className="search-wrapper">
<BasicAskAI />
</div>
</section>
<section className="demo-section">
<p className="section-description">custom hit rendering</p>
<div className="search-wrapper">
<WHitComponent />
</div>
</section>
<section className="demo-section">
<p className="section-description">transform items before rendering</p>
<div className="search-wrapper">
<WTransformItems />
</div>
</section>
</main>
</div>
</div>
);
}
export default App;

View file

@ -0,0 +1,21 @@
/* eslint-disable react/react-in-jsx-scope */
import { DocSearch } from '@docsearch/react';
import type { JSX } from 'react';
export default function BasicAskAI(): JSX.Element {
return (
<DocSearch
indexName="docsearch"
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
askAi={{
assistantId: 'askAIDemo',
searchParameters: {
facetFilters: ['language:en'],
},
}}
insights={true}
translations={{ button: { buttonText: 'Search with AskAI' } }}
/>
);
}

View file

@ -0,0 +1,15 @@
/* eslint-disable react/react-in-jsx-scope */
import { DocSearch } from '@docsearch/react';
import type { JSX } from 'react';
export default function Basic(): JSX.Element {
return (
<DocSearch
indexName="docsearch"
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
translations={{ button: { buttonText: 'Keyword search' } }}
insights={true}
/>
);
}

View file

@ -0,0 +1,125 @@
/* eslint-disable react/react-in-jsx-scope */
import { DocSearch } from '@docsearch/react';
import type { JSX } from 'react';
function CustomHit({ hit }: { hit: any }): JSX.Element {
return (
<a
href={hit.url}
style={{
display: 'block',
padding: '12px 16px',
textDecoration: 'none',
color: 'inherit',
borderBottom: '1px solid #f0f0f0',
transition: 'background-color 0.2s ease',
backgroundColor: 'transparent',
cursor: 'pointer',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
lineHeight: 1.4,
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f8f9fa';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
<div
style={{
width: '40px',
height: '40px',
backgroundColor: '#e3f2fd',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
fontSize: '14px',
fontWeight: '600',
color: '#1976d2',
}}
>
{/* eslint-disable-next-line no-nested-ternary */}
{hit.type === 'lvl1' ? 'H1' : hit.type === 'lvl2' ? 'H2' : hit.type === 'lvl3' ? 'H3' : 'DOC'}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: '14px',
fontWeight: '600',
color: '#1a1a1a',
marginBottom: '2px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{hit.hierarchy.lvl1 || 'Untitled'}
</div>
{hit.hierarchy.lvl2 && (
<div
style={{
fontSize: '13px',
color: '#666',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{hit.hierarchy.lvl2}
</div>
)}
{hit.content && (
<div
style={{
fontSize: '12px',
color: '#888',
marginTop: '4px',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
lineHeight: 1.3,
}}
>
{hit.content}
</div>
)}
<div
style={{
fontSize: '11px',
color: '#999',
marginTop: '4px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<span>{hit.url}</span>
{hit.type && <span style={{ textTransform: 'capitalize' }}>{hit.type}</span>}
</div>
</div>
</div>
</a>
);
}
export default function WHitComponent(): JSX.Element {
return (
<DocSearch
indexName="docsearch"
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
insights={true}
translations={{ button: { buttonText: 'Search with custom hits' } }}
hitComponent={CustomHit}
/>
);
}

View file

@ -0,0 +1,61 @@
/* eslint-disable react/react-in-jsx-scope */
import { DocSearch } from '@docsearch/react';
import type { JSX } from 'react';
// this type matches the structure of the provided example hit
/* type _DocSearchCustomHit = {
path: string;
metaDescription: string;
title: string;
h1: string;
h2: string;
content: string;
breadcrumb: string[];
variation: Record<string, unknown>;
pageDepth: number;
domain: string;
objectID: string;
_snippetResult: any;
_highlightResult: any;
}; */
export default function WTransformItems(): JSX.Element {
return (
<DocSearch
indexName="crawler_doc"
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
askAi={{
assistantId: 'askAIDemo',
}}
insights={true}
searchParameters={{
attributesToRetrieve: ['*'],
attributesToSnippet: ['*'],
hitsPerPage: 20,
}}
transformItems={(items) => {
return items.map((item: any) => ({
objectID: item.objectID,
content: item.content ?? '',
url: new URL(item.domain + item.path).toString(),
hierarchy: {
lvl0: item.breadcrumb.join(' > ') ?? '',
lvl1: item.h1 ?? '',
lvl2: item.h2 ?? '',
lvl3: null,
lvl4: null,
lvl5: null,
lvl6: null,
},
url_without_anchor: new URL(item.domain + item.path).toString(),
type: 'content' as const,
anchor: null,
_highlightResult: item._highlightResult,
_snippetResult: item._snippetResult,
}));
}}
translations={{ button: { buttonText: 'Search with transformItems' } }}
/>
);
}

View file

@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocSearch v4 - React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -1,4 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}

View file

@ -1,15 +0,0 @@
import { DocSearch } from '@docsearch/react';
import type { JSX } from 'react';
import './App.css';
import '@docsearch/css/dist/style.css';
function App(): JSX.Element {
return (
<div>
<h1>DocSearch v4 - React</h1>
<DocSearch indexName="vuejs" appId="ML0LEBN7FQ" apiKey="21cf9df0734770a2448a9da64a700c22" insights={true} />
</div>
);
}
export default App;

View file

@ -1,10 +0,0 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View file

@ -1 +0,0 @@
/// <reference types="vite/client" />

View file

@ -1,31 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": [
"src",
"vite.config.ts"
],
}

View file

@ -1,7 +0,0 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -1 +0,0 @@
/// <reference types="vite/client" />

View file

@ -20,7 +20,6 @@
"lint": "eslint .",
"playground:build": "yarn workspace @docsearch/react-example build",
"playground:start": "yarn workspace @docsearch/react-example dev --host",
"playground-askai:start": "yarn workspace @docsearch/askai-example dev --host",
"playground-js:start": "yarn workspace @docsearch/js-example dev --host",
"release": "shipjs prepare",
"start": "yarn run watch",

View file

@ -1211,7 +1211,7 @@ assistive tech users */
color: var(--docsearch-muted-color);
}
.DocSearch-AskAiScreen-MessageContent-Tool.Tool--Result:first-child {
.DocSearch-AskAiScreen-MessageContent-Tool.Tool--Result {
padding-top: 0;
}

View file

@ -150,7 +150,7 @@ const buildQuerySources = async ({
{
query,
indexName,
attributesToRetrieve: [
attributesToRetrieve: searchParameters?.attributesToRetrieve ?? [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
@ -162,7 +162,7 @@ const buildQuerySources = async ({
'type',
'url',
],
attributesToSnippet: [
attributesToSnippet: searchParameters?.attributesToSnippet ?? [
`hierarchy.lvl1:${snippetLength.current}`,
`hierarchy.lvl2:${snippetLength.current}`,
`hierarchy.lvl3:${snippetLength.current}`,
@ -171,11 +171,11 @@ const buildQuerySources = async ({
`hierarchy.lvl6:${snippetLength.current}`,
`content:${snippetLength.current}`,
],
snippetEllipsisText: '…',
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
hitsPerPage: 20,
clickAnalytics: insightsActive,
snippetEllipsisText: searchParameters?.snippetEllipsisText ?? '…',
highlightPreTag: searchParameters?.highlightPreTag ?? '<mark>',
highlightPostTag: searchParameters?.highlightPostTag ?? '</mark>',
hitsPerPage: searchParameters?.hitsPerPage ?? 20,
clickAnalytics: searchParameters?.clickAnalytics ?? insightsActive,
...searchParameters,
},
],
@ -183,7 +183,8 @@ const buildQuerySources = async ({
const firstResult = results[0] as SearchResponse<DocSearchHit>;
const { hits, nbHits } = firstResult;
const sources = groupBy<DocSearchHit>(hits, (hit) => removeHighlightTags(hit), maxResultsPerGroup);
const transformedHits = transformItems(hits);
const sources = groupBy<DocSearchHit>(transformedHits, (hit) => removeHighlightTags(hit), maxResultsPerGroup);
// We store the `lvl0`s to display them as search suggestions
// in the "no results" screen.
@ -222,7 +223,6 @@ const buildQuerySources = async ({
},
getItems(): InternalDocSearchHit[] {
return Object.values(groupBy(items, (item) => item.hierarchy.lvl1, maxResultsPerGroup))
.map(transformItems)
.map((groupedHits) =>
groupedHits.map((item) => {
let parent: InternalDocSearchHit | null = null;

View file

@ -0,0 +1,212 @@
---
id: examples
title: Examples and extensions
description: live demos showing how to use and extend docsearch beyond documentation-only use cases.
---
import { DocSearch } from '@docsearch/react';
import '@docsearch/css/dist/style.css';
> These examples are interactive. click a button to open the modal and try a query.
## Basic keyword search
Use the default experience with your index credentials. this works great for typical docs, blogs, and any site with a docsearch-compliant index.
```jsx
<DocSearch
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
indexName="docsearch"
insights={true}
translations={{ button: { buttonText: 'keyword search (demo)' } }}
/>
```
<DocSearch
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
indexName="docsearch"
insights={true}
translations={{ button: { buttonText: 'keyword search (demo)' } }}
/>
---
## Ask AI: ai-assisted answers
Add algolia askai to get synthesized answers grounded in your indexed content. you can scope the llm context using `searchParameters`.
```jsx
<DocSearch
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
indexName="docsearch"
askAi={{
assistantId: 'askAIDemo',
searchParameters: {
facetFilters: ['language:en'],
},
}}
insights={true}
translations={{ button: { buttonText: 'search with askai (demo)' } }}
/>
```
<DocSearch
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
indexName="docsearch"
askAi={{
assistantId: 'askAIDemo',
searchParameters: {
facetFilters: ['language:en'],
},
}}
insights={true}
translations={{ button: { buttonText: 'search with askai (demo)' } }}
/>
---
## Custom hit rendering (`hitComponent`)
Replace the default hit markup to match your brand and layout. below is a minimal example of a custom component.
```jsx
function CustomHit({ hit }) {
// render a compact, branded hit card
return (
<a href={hit.url} style={{ display: 'block', padding: '12px 16px', textDecoration: 'none' }}>
<div style={{ display: 'flex', gap: 12 }}>
<div style={{ width: 40, height: 40, backgroundColor: '#e3f2fd', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 600, color: '#1976d2' }}>
{hit.type?.toUpperCase?.() || 'DOC'}
</div>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{hit.hierarchy?.lvl1 || 'untitled'}</div>
{hit.hierarchy?.lvl2 && <div style={{ color: '#666', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{hit.hierarchy.lvl2}</div>}
{hit.content && <div style={{ color: '#888', marginTop: 4 }}>{hit.content}</div>}
</div>
</div>
</a>
);
}
<DocSearch
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
indexName="docsearch"
hitComponent={CustomHit}
insights={true}
translations={{ button: { buttonText: 'custom hits (demo)' } }}
/>
```
<DocSearch
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
indexName="docsearch"
hitComponent={({ hit }) => {
// render a compact, branded hit card
return (
<a href={hit.url} style={{ display: 'block', padding: '12px 16px', textDecoration: 'none' }}>
<div style={{ display: 'flex', gap: 12 }}>
<div style={{ width: 40, height: 40, backgroundColor: '#e3f2fd', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 600, color: '#1976d2' }}>
{hit.type?.toUpperCase?.() || 'DOC'}
</div>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{hit.hierarchy?.lvl1 || 'untitled'}</div>
{hit.hierarchy?.lvl2 && <div style={{ color: '#666', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{hit.hierarchy.lvl2}</div>}
{hit.content && <div style={{ color: '#888', marginTop: 4 }}>{hit.content}</div>}
</div>
</div>
</a>
);
}}
insights={true}
translations={{ button: { buttonText: 'custom hits (demo)' } }}
/>
---
## Bring-your-own-data shape with `transformItems`
Docsearch is not limited to docsearch-like records. use `transformItems` to adapt any record shape into the internal structure docsearch expects. this lets you build search for apps, help centers, changelogs, or any custom content.
the snippet below maps a non-standard record to the internal format. try it live:
```jsx
<DocSearch
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
indexName="crawler_doc"
askAi={{ assistantId: 'askAIDemo' }}
searchParameters={{
attributesToRetrieve: ['*'],
attributesToSnippet: ['*'],
hitsPerPage: 20,
}}
transformItems={(items) =>
items.map((item) => ({
objectID: item.objectID,
content: item.content ?? '',
url: item.domain + item.path,
hierarchy: {
lvl0: (item.breadcrumb || []).join(' > ') ?? '',
lvl1: item.h1 ?? '',
lvl2: item.h2 ?? '',
lvl3: null,
lvl4: null,
lvl5: null,
lvl6: null,
},
url_without_anchor: item.domain + item.path,
type: 'content',
anchor: null,
_highlightResult: item._highlightResult,
_snippetResult: item._snippetResult,
}))
}
insights={true}
translations={{ button: { buttonText: 'transform items (demo)' } }}
/>
```
<DocSearch
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
indexName="crawler_doc"
askAi={{ assistantId: 'askAIDemo' }}
searchParameters={{ attributesToRetrieve: ['*'], attributesToSnippet: ['*'], hitsPerPage: 20 }}
transformItems={(items) =>
items.map((item) => ({
objectID: item.objectID,
content: item.content ?? '',
url: item.domain + item.path,
hierarchy: {
lvl0: (item.breadcrumb || []).join(' > ') ?? '',
lvl1: item.h1 ?? '',
lvl2: item.h2 ?? '',
lvl3: null,
lvl4: null,
lvl5: null,
lvl6: null,
},
url_without_anchor: item.domain + item.path,
type: 'content',
anchor: null,
_highlightResult: item._highlightResult,
_snippetResult: item._snippetResult,
}))
}
insights={true}
translations={{ button: { buttonText: 'transform items (demo)' } }}
/>
---
## Tips
- **Instrumentation**: enable `insights` to send usage analytics and iterate on relevance.
- **AskAI scoping**: use `facetFilters` to keep ai answers strictly within your desired corpus (language, product, version, etc.).
- **Customization**: use `hitComponent`, `transformItems`, and `translations` to make docsearch feel native to any product surface.

View file

@ -19,7 +19,7 @@ export default {
{
type: 'category',
label: 'DocSearch v4',
items: ['docsearch', 'api', 'migrating-from-v3'],
items: ['docsearch', 'api', 'examples', 'migrating-from-v3'],
},
{
type: 'category',

View file

@ -2310,19 +2310,6 @@ __metadata:
languageName: node
linkType: hard
"@docsearch/askai-example@workspace:examples/demo-askai":
version: 0.0.0-use.local
resolution: "@docsearch/askai-example@workspace:examples/demo-askai"
dependencies:
"@docsearch/css": "workspace:*"
"@docsearch/react": "workspace:*"
"@vitejs/plugin-react": "npm:^4.3.4"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
vite: "npm:^6.0.7"
languageName: unknown
linkType: soft
"@docsearch/css@npm:4.0.0-beta.4":
version: 4.0.0-beta.4
resolution: "@docsearch/css@npm:4.0.0-beta.4"
@ -2338,9 +2325,9 @@ __metadata:
languageName: unknown
linkType: soft
"@docsearch/js-example@workspace:examples/js-demo":
"@docsearch/js-example@workspace:examples/demo-js":
version: 0.0.0-use.local
resolution: "@docsearch/js-example@workspace:examples/js-demo"
resolution: "@docsearch/js-example@workspace:examples/demo-js"
dependencies:
"@docsearch/css": "npm:4.0.0-beta.5"
"@docsearch/js": "npm:4.0.0-beta.5"
@ -2418,26 +2405,13 @@ __metadata:
languageName: unknown
linkType: soft
"@docsearch/react-18-example@workspace:examples/demo-react-18":
"@docsearch/react-example@workspace:examples/demo-react":
version: 0.0.0-use.local
resolution: "@docsearch/react-18-example@workspace:examples/demo-react-18"
resolution: "@docsearch/react-example@workspace:examples/demo-react"
dependencies:
"@docsearch/css": "workspace:*"
"@docsearch/react": "workspace:*"
"@vitejs/plugin-react": "npm:^4.3.4"
react: "npm:^18.0.0"
react-dom: "npm:^18.0.0"
vite: "npm:^6.0.7"
languageName: unknown
linkType: soft
"@docsearch/react-example@workspace:examples/demo":
version: 0.0.0-use.local
resolution: "@docsearch/react-example@workspace:examples/demo"
dependencies:
"@docsearch/css": "npm:4.0.0-beta.5"
"@docsearch/react": "npm:4.0.0-beta.5"
"@vitejs/plugin-react": "npm:^4.3.4"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
vite: "npm:^6.0.7"