> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/algolia/docsearch/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Integration

> Learn how to build custom DocSearch integrations for any framework or platform

This guide shows you how to build custom DocSearch integrations using the core packages. Whether you're building for a new framework, a custom documentation platform, or need advanced customization, this guide covers the fundamentals.

## Understanding DocSearch Packages

DocSearch is built as a modular system with several packages:

<CardGroup cols={2}>
  <Card title="@docsearch/js" icon="js">
    Vanilla JavaScript wrapper using Preact
  </Card>

  <Card title="@docsearch/react" icon="react">
    React components and hooks
  </Card>

  <Card title="@docsearch/core" icon="gear">
    Framework-agnostic core logic
  </Card>

  <Card title="@docsearch/css" icon="palette">
    Styles and CSS variables
  </Card>
</CardGroup>

## Architecture Overview

DocSearch is built on top of Algolia's Autocomplete library:

```mermaid theme={null}
graph TB
    A[Your Application] --> B[@docsearch/react or @docsearch/js]
    B --> C[@docsearch/core]
    C --> D[@algolia/autocomplete-core]
    D --> E[Algolia API]
    B --> F[@docsearch/css]
```

## Building a Framework Integration

### Approach 1: Wrap the JavaScript Package

The simplest approach is to wrap `@docsearch/js`:

<Steps>
  <Step title="Install dependencies">
    ```bash theme={null}
    npm install @docsearch/js @docsearch/css
    ```
  </Step>

  <Step title="Create a wrapper component">
    Example for Vue 3:

    ```vue title="DocSearch.vue" theme={null}
    <script setup>
    import { onMounted, onUnmounted, ref } from 'vue'
    import docsearch from '@docsearch/js'
    import '@docsearch/css'

    const props = defineProps({
      appId: { type: String, required: true },
      apiKey: { type: String, required: true },
      indexName: { type: String, required: true },
      placeholder: { type: String, default: 'Search docs' },
      searchParameters: { type: Object, default: () => ({}) },
    })

    const instance = ref(null)

    onMounted(() => {
      instance.value = docsearch({
        container: '#docsearch',
        appId: props.appId,
        apiKey: props.apiKey,
        indexName: props.indexName,
        placeholder: props.placeholder,
        searchParameters: props.searchParameters,
      })
    })

    onUnmounted(() => {
      instance.value?.destroy()
    })

    // Expose methods
    defineExpose({
      open: () => instance.value?.open(),
      close: () => instance.value?.close(),
    })
    </script>

    <template>
      <div id="docsearch"></div>
    </template>
    ```
  </Step>

  <Step title="Use the component">
    ```vue title="App.vue" theme={null}
    <script setup>
    import DocSearch from './components/DocSearch.vue'
    </script>

    <template>
      <div>
        <header>
          <h1>My Docs</h1>
          <DocSearch
            app-id="YOUR_APP_ID"
            api-key="YOUR_SEARCH_API_KEY"
            index-name="YOUR_INDEX_NAME"
          />
        </header>
      </div>
    </template>
    ```
  </Step>
</Steps>

### Approach 2: Use React Components Directly

Many frameworks support React components. Example for Solid.js:

```tsx title="DocSearch.tsx" theme={null}
import { createSignal, onMount, onCleanup } from 'solid-js'
import { DocSearch as DocSearchReact } from '@docsearch/react'
import '@docsearch/css'

export function DocSearch(props) {
  return (
    <DocSearchReact
      appId={props.appId}
      apiKey={props.apiKey}
      indexName={props.indexName}
    />
  )
}
```

### Approach 3: Build from Core

For complete control, use `@docsearch/core` directly:

```typescript title="custom-docsearch.ts" theme={null}
import { useDocSearch, DocSearchProvider } from '@docsearch/core'
import type { DocSearchProps } from '@docsearch/core'

// Create your own UI using the core hooks
export function createDocSearch(props: DocSearchProps) {
  // Use core logic
  const {
    query,
    collections,
    isOpen,
    recentSearches,
    favoriteSearches,
    openModal,
    closeModal,
    // ... other state and methods
  } = useDocSearch()

  // Build your custom UI with this data
  return {
    query,
    collections,
    isOpen,
    open: openModal,
    close: closeModal,
    // Expose what you need
  }
}
```

## Custom Search Client

You can customize the Algolia search client:

```typescript title="custom-client.ts" theme={null}
import docsearch from '@docsearch/js'
import { liteClient as algoliasearch } from 'algoliasearch/lite'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  transformSearchClient: (searchClient) => {
    // Add custom Algolia agent
    searchClient.addAlgoliaAgent('my-integration', '1.0.0')

    // Intercept search requests
    const originalSearch = searchClient.search
    searchClient.search = async (requests) => {
      console.log('Searching:', requests)
      const results = await originalSearch(requests)
      console.log('Results:', results)
      return results
    }

    return searchClient
  },
})
```

## Custom Result Transformations

Transform search results before rendering:

```typescript theme={null}
import docsearch from '@docsearch/js'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  transformItems: (items) => {
    return items.map((item) => {
      // Transform URLs
      if (item.url.startsWith('/')) {
        item.url = `https://docs.example.com${item.url}`
      }

      // Add custom metadata
      item.metadata = {
        lastUpdated: item._highlightResult?.lastModified?.value,
      }

      // Filter out certain results
      if (item.hierarchy.lvl0 === 'Internal') {
        return null
      }

      return item
    }).filter(Boolean)
  },
})
```

## Custom Navigation

Control how DocSearch navigates:

```typescript theme={null}
import docsearch from '@docsearch/js'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  navigator: {
    navigate({ itemUrl }) {
      // Custom navigation logic
      if (itemUrl.startsWith('http')) {
        // External link - open in new tab
        window.open(itemUrl, '_blank')
      } else {
        // Internal link - use your router
        yourRouter.push(itemUrl)
      }
    },
    navigateNewTab({ itemUrl }) {
      window.open(itemUrl, '_blank')
    },
    navigateNewWindow({ itemUrl }) {
      window.open(itemUrl, '_blank', 'noopener,noreferrer')
    },
  },
})
```

## Custom Hit Component

Customize how individual results are rendered:

```typescript theme={null}
import docsearch from '@docsearch/js'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  hitComponent: ({ hit, children }, { html }) => {
    // Get the result type from hierarchy
    const type = hit.type || 'content'
    const icon = {
      content: '📄',
      lvl1: '📂',
      lvl2: '📋',
    }[type] || '📄'

    return html`
      <a href="${hit.url}" class="custom-hit" data-type="${type}">
        <span class="hit-icon">${icon}</span>
        <div class="hit-content">
          <div class="hit-title">
            ${children}
          </div>
          ${hit.hierarchy.lvl0 && html`
            <div class="hit-path">
              ${hit.hierarchy.lvl0}
            </div>
          `}
        </div>
      </a>
    `
  },
})
```

## Custom Results Footer

Add a custom footer to the results:

```typescript theme={null}
import docsearch from '@docsearch/js'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  resultsFooterComponent: ({ state }, { html }) => {
    const nbHits = state.context?.nbHits || 0
    const query = state.query

    if (!query) return null

    return html`
      <div class="custom-footer">
        <div class="results-count">
          Found ${nbHits} results for "${query}"
        </div>
        <div class="footer-actions">
          <a href="/search?q=${encodeURIComponent(query)}" class="view-all">
            View all results
          </a>
          <a href="/feedback?q=${encodeURIComponent(query)}" class="feedback">
            Results not relevant?
          </a>
        </div>
      </div>
    `
  },
})
```

## TypeScript Integration

Create type-safe wrappers:

```typescript title="docsearch-wrapper.ts" theme={null}
import docsearch from '@docsearch/js'
import type { DocSearchProps, DocSearchInstance } from '@docsearch/js'
import '@docsearch/css'

export interface CustomDocSearchProps extends Omit<DocSearchProps, 'container'> {
  // Add custom props
  onSearch?: (query: string) => void
  onSelect?: (item: any) => void
  analytics?: {
    trackSearch: (query: string) => void
    trackClick: (item: any) => void
  }
}

export function createDocSearch(
  container: string | HTMLElement,
  props: CustomDocSearchProps
): DocSearchInstance {
  const { onSearch, onSelect, analytics, ...docSearchProps } = props

  // Create custom transformSearchClient
  const originalTransform = props.transformSearchClient
  const transformSearchClient = (searchClient: any) => {
    let client = searchClient
    
    // Apply original transform first
    if (originalTransform) {
      client = originalTransform(client)
    }

    // Add analytics
    if (analytics) {
      const originalSearch = client.search
      client.search = async (requests: any[]) => {
        const query = requests[0]?.params?.query
        if (query) {
          analytics.trackSearch(query)
        }
        return originalSearch(requests)
      }
    }

    return client
  }

  // Create instance with enhanced props
  const instance = docsearch({
    container,
    ...docSearchProps,
    transformSearchClient,
    transformItems: (items) => {
      // Call original transform if provided
      const transformed = props.transformItems?.(items) ?? items
      
      // Add click tracking
      if (analytics) {
        return transformed.map((item) => ({
          ...item,
          onClick: () => {
            analytics.trackClick(item)
            onSelect?.(item)
          },
        }))
      }
      
      return transformed
    },
  })

  return instance
}
```

Usage:

```typescript theme={null}
const instance = createDocSearch('#docsearch', {
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  analytics: {
    trackSearch: (query) => {
      console.log('Search:', query)
    },
    trackClick: (item) => {
      console.log('Click:', item)
    },
  },
})
```

## Server-Side Rendering

Handle SSR in your custom integration:

```typescript title="docsearch-ssr.ts" theme={null}
import { useEffect, useRef } from 'react'
import type { DocSearchProps } from '@docsearch/js'

export function DocSearchSSR(props: Omit<DocSearchProps, 'container'>) {
  const containerRef = useRef<HTMLDivElement>(null)
  const instanceRef = useRef<any>(null)

  useEffect(() => {
    // Only run on client
    if (typeof window === 'undefined') return

    // Dynamically import to avoid SSR issues
    import('@docsearch/js').then(({ default: docsearch }) => {
      if (containerRef.current && !instanceRef.current) {
        instanceRef.current = docsearch({
          container: containerRef.current,
          ...props,
        })
      }
    })

    // Cleanup
    return () => {
      instanceRef.current?.destroy()
      instanceRef.current = null
    }
  }, [props])

  return <div ref={containerRef} />
}
```

## Analytics Integration

Track search usage:

```typescript theme={null}
import docsearch from '@docsearch/js'
import { track } from './analytics'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  onOpen: () => {
    track('search_opened')
  },
  onClose: () => {
    track('search_closed')
  },
  transformSearchClient: (searchClient) => {
    const originalSearch = searchClient.search
    
    searchClient.search = async (requests) => {
      const query = requests[0]?.params?.query
      if (query) {
        track('search_query', { query, timestamp: Date.now() })
      }
      
      const results = await originalSearch(requests)
      const hits = results.results[0]?.hits?.length || 0
      
      track('search_results', { query, hits })
      
      return results
    }
    
    return searchClient
  },
  navigator: {
    navigate({ itemUrl, item }) {
      track('search_result_clicked', {
        url: itemUrl,
        title: item.hierarchy?.lvl1 || item.hierarchy?.lvl0,
      })
      window.location.href = itemUrl
    },
  },
})
```

## A/B Testing

Implement A/B testing for search:

```typescript theme={null}
import docsearch from '@docsearch/js'

// Determine variant
const variant = Math.random() < 0.5 ? 'A' : 'B'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  searchParameters: {
    // Variant A: default behavior
    // Variant B: boost recent content
    ...(variant === 'B' && {
      optionalFilters: ['recentContent:true<score=2>'],
    }),
  },
  transformSearchClient: (searchClient) => {
    // Add variant to user agent
    searchClient.addAlgoliaAgent('ab-test', variant)
    return searchClient
  },
})

// Track which variant performed better
window.addEventListener('beforeunload', () => {
  analytics.track('ab_variant', { variant, searches: searchCount })
})
```

## Testing Your Integration

Write tests for your custom integration:

```typescript title="docsearch.test.ts" theme={null}
import { describe, it, expect, vi } from 'vitest'
import { createDocSearch } from './docsearch-wrapper'

describe('DocSearch Custom Integration', () => {
  it('should initialize with correct props', () => {
    const container = document.createElement('div')
    container.id = 'docsearch'
    document.body.appendChild(container)

    const instance = createDocSearch('#docsearch', {
      appId: 'test-app-id',
      apiKey: 'test-api-key',
      indexName: 'test-index',
    })

    expect(instance).toBeDefined()
    expect(instance.isReady).toBe(true)
  })

  it('should call analytics on search', async () => {
    const trackSearch = vi.fn()
    
    const instance = createDocSearch('#docsearch', {
      appId: 'test-app-id',
      apiKey: 'test-api-key',
      indexName: 'test-index',
      analytics: {
        trackSearch,
        trackClick: vi.fn(),
      },
    })

    // Simulate search
    instance.open()
    // ... trigger search

    expect(trackSearch).toHaveBeenCalled()
  })
})
```

## Best Practices

<AccordionGroup>
  <Accordion title="1. Handle Lifecycle Properly">
    Always clean up DocSearch instances when components unmount:

    ```typescript theme={null}
    onUnmounted(() => {
      instance.value?.destroy()
    })
    ```
  </Accordion>

  <Accordion title="2. Optimize for Performance">
    * Lazy load DocSearch only when needed
    * Use the `onTouchStart`, `onFocus`, and `onMouseOver` callbacks to preload
    * Implement debouncing for search queries
  </Accordion>

  <Accordion title="3. Type Safety">
    Use TypeScript for better developer experience:

    ```typescript theme={null}
    import type { DocSearchProps, DocSearchHit } from '@docsearch/js'
    ```
  </Accordion>

  <Accordion title="4. Error Handling">
    Handle errors gracefully:

    ```typescript theme={null}
    transformSearchClient: (searchClient) => {
      const originalSearch = searchClient.search
      searchClient.search = async (requests) => {
        try {
          return await originalSearch(requests)
        } catch (error) {
          console.error('Search failed:', error)
          // Show user-friendly error
          return { results: [] }
        }
      }
      return searchClient
    }
    ```
  </Accordion>

  <Accordion title="5. Accessibility">
    Ensure your custom integration maintains accessibility:

    * Keep keyboard navigation working
    * Preserve ARIA attributes
    * Test with screen readers
  </Accordion>
</AccordionGroup>

## Examples by Framework

<CardGroup cols={2}>
  <Card title="Vue 3" icon="vuejs">
    See the Vue 3 example above for wrapping `@docsearch/js`
  </Card>

  <Card title="Svelte" icon="code">
    Similar to Vue, use `onMount` and `onDestroy` lifecycle hooks
  </Card>

  <Card title="Angular" icon="angular">
    Create a directive or component that initializes DocSearch in `ngAfterViewInit`
  </Card>

  <Card title="Next.js" icon="react">
    Use dynamic imports to avoid SSR issues
  </Card>
</CardGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="API Reference" icon="code" href="/api/docsearch-react">
    Complete API documentation
  </Card>

  <Card title="Styling" icon="palette" href="/concepts/styling">
    Customize appearance
  </Card>

  <Card title="Core Package" icon="gear" href="/api/docsearch-react">
    Use @docsearch/core directly
  </Card>

  <Card title="Contributing" icon="github" href="https://github.com/algolia/docsearch">
    Contribute your integration
  </Card>
</CardGroup>
