Search Plugin
Add powerful search functionality to your VitePress documentation site.
Overview
VitePress supports multiple search solutions, from built-in local search to external services like Algolia DocSearch. Choose the solution that best fits your needs.
Local Search
VitePress includes a built-in client-side search that works out of the box.
Configuration
js
// .vitepress/config.js
export default {
themeConfig: {
search: {
provider: 'local'
}
}
}
Advanced Local Search Options
js
export default {
themeConfig: {
search: {
provider: 'local',
options: {
// Exclude specific pages from search
exclude: ['guide/api.md', 'reference/cli.md'],
// Custom search root directory
searchRoot: 'docs',
// Customize search translations
translations: {
button: {
buttonText: 'Search',
buttonAriaLabel: 'Search'
},
modal: {
noResultsText: 'No results for',
resetButtonTitle: 'Clear search',
footer: {
selectText: 'to select',
navigateText: 'to navigate',
closeText: 'to close'
}
}
}
}
}
}
}
Algolia DocSearch
For larger sites with more comprehensive search needs, Algolia DocSearch provides powerful search capabilities.
Setup
- Apply for Algolia DocSearch
- Once approved, you'll receive your API credentials
- Configure VitePress to use Algolia
Configuration
js
// .vitepress/config.js
export default {
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: 'YOUR_APP_ID',
apiKey: 'YOUR_SEARCH_API_KEY',
indexName: 'YOUR_INDEX_NAME',
// Optional: customize search parameters
searchParameters: {
facetFilters: ['tags:guide']
},
// Optional: customize search translations
translations: {
button: {
buttonText: 'Search',
buttonAriaLabel: 'Search'
},
modal: {
searchBox: {
resetButtonTitle: 'Clear search',
resetButtonAriaLabel: 'Clear search',
cancelButtonText: 'Cancel',
cancelButtonAriaLabel: 'Cancel'
},
startScreen: {
recentSearchesTitle: 'Recent',
noRecentSearchesText: 'No recent searches',
saveRecentSearchButtonTitle: 'Save this search',
removeRecentSearchButtonTitle: 'Remove this search from history',
favoriteSearchesTitle: 'Favorite',
removeFavoriteSearchButtonTitle: 'Remove this search from favorites'
},
errorScreen: {
titleText: 'Unable to fetch results',
helpText: 'You might want to check your network connection.'
},
footer: {
selectText: 'to select',
navigateText: 'to navigate',
closeText: 'to close',
searchByText: 'Search by'
},
noResultsScreen: {
noResultsText: 'No results for',
suggestedQueryText: 'Try searching for',
reportMissingResultsText: 'Believe this query should return results?',
reportMissingResultsLinkText: 'Let us know.'
}
}
}
}
}
}
}
Crawler Configuration
If you're managing your own Algolia index, configure the crawler:
json
{
"index_name": "your_index_name",
"start_urls": ["https://your-site.com/"],
"sitemap_urls": ["https://your-site.com/sitemap.xml"],
"selectors": {
"lvl0": {
"selector": ".sidebar-heading.active",
"global": true,
"default_value": "Documentation"
},
"lvl1": "h1",
"lvl2": "h2",
"lvl3": "h3",
"lvl4": "h4",
"lvl5": "h5",
"text": "p, li"
},
"strip_chars": " .,;:#",
"custom_settings": {
"separatorsToIndex": "_",
"attributesForFaceting": ["language", "version"]
}
}
Custom Search Solutions
Flexsearch Integration
For more control over search functionality:
bash
npm install flexsearch
js
// .vitepress/theme/composables/useSearch.js
import { ref, computed } from 'vue'
import FlexSearch from 'flexsearch'
const searchIndex = ref(null)
const searchResults = ref([])
export function useSearch() {
const initSearch = async () => {
const index = new FlexSearch.Index({
tokenize: 'forward',
resolution: 9
})
// Load and index your content
const pages = await import('../data/pages.json')
pages.forEach((page, id) => {
index.add(id, `${page.title} ${page.content}`)
})
searchIndex.value = index
}
const search = (query) => {
if (!searchIndex.value || !query) {
searchResults.value = []
return
}
const results = searchIndex.value.search(query)
searchResults.value = results.map(id => pages[id])
}
return {
initSearch,
search,
searchResults: computed(() => searchResults.value)
}
}
Fuse.js Integration
For fuzzy search capabilities:
bash
npm install fuse.js
js
// .vitepress/theme/composables/useFuzzySearch.js
import { ref, computed } from 'vue'
import Fuse from 'fuse.js'
const fuse = ref(null)
const searchResults = ref([])
export function useFuzzySearch() {
const initSearch = async (pages) => {
const options = {
keys: ['title', 'content', 'tags'],
threshold: 0.3,
includeScore: true,
includeMatches: true
}
fuse.value = new Fuse(pages, options)
}
const search = (query) => {
if (!fuse.value || !query) {
searchResults.value = []
return
}
const results = fuse.value.search(query)
searchResults.value = results.map(result => ({
...result.item,
score: result.score,
matches: result.matches
}))
}
return {
initSearch,
search,
searchResults: computed(() => searchResults.value)
}
}
Search UI Components
Custom Search Modal
vue
<!-- .vitepress/theme/components/SearchModal.vue -->
<template>
<div v-if="isOpen" class="search-modal" @click="close">
<div class="search-container" @click.stop>
<div class="search-input-container">
<input
ref="searchInput"
v-model="query"
type="text"
placeholder="Search documentation..."
class="search-input"
@keydown="handleKeydown"
/>
<button @click="close" class="close-button">×</button>
</div>
<div v-if="results.length" class="search-results">
<div
v-for="(result, index) in results"
:key="result.id"
:class="['search-result', { active: index === activeIndex }]"
@click="selectResult(result)"
>
<h3>{{ result.title }}</h3>
<p>{{ result.excerpt }}</p>
</div>
</div>
<div v-else-if="query" class="no-results">
No results found for "{{ query }}"
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue'
import { useRouter } from 'vitepress'
const props = defineProps({
isOpen: Boolean
})
const emit = defineEmits(['close'])
const router = useRouter()
const searchInput = ref()
const query = ref('')
const results = ref([])
const activeIndex = ref(0)
const close = () => {
emit('close')
query.value = ''
results.value = []
activeIndex.value = 0
}
const search = async (q) => {
if (!q) {
results.value = []
return
}
// Implement your search logic here
// This could call your search service or filter local data
results.value = await performSearch(q)
}
const selectResult = (result) => {
router.go(result.path)
close()
}
const handleKeydown = (e) => {
switch (e.key) {
case 'Escape':
close()
break
case 'ArrowDown':
e.preventDefault()
activeIndex.value = Math.min(activeIndex.value + 1, results.value.length - 1)
break
case 'ArrowUp':
e.preventDefault()
activeIndex.value = Math.max(activeIndex.value - 1, 0)
break
case 'Enter':
e.preventDefault()
if (results.value[activeIndex.value]) {
selectResult(results.value[activeIndex.value])
}
break
}
}
watch(query, (newQuery) => {
search(newQuery)
})
watch(() => props.isOpen, (isOpen) => {
if (isOpen) {
nextTick(() => {
searchInput.value?.focus()
})
}
})
</script>
<style scoped>
.search-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 10vh;
z-index: 1000;
}
.search-container {
background: var(--vp-c-bg);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 600px;
max-height: 70vh;
overflow: hidden;
}
.search-input-container {
display: flex;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.search-input {
flex: 1;
border: none;
outline: none;
font-size: 16px;
background: transparent;
color: var(--vp-c-text-1);
}
.close-button {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--vp-c-text-2);
}
.search-results {
max-height: 400px;
overflow-y: auto;
}
.search-result {
padding: 16px;
border-bottom: 1px solid var(--vp-c-divider);
cursor: pointer;
}
.search-result:hover,
.search-result.active {
background: var(--vp-c-bg-soft);
}
.search-result h3 {
margin: 0 0 8px 0;
font-size: 16px;
color: var(--vp-c-text-1);
}
.search-result p {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.no-results {
padding: 32px;
text-align: center;
color: var(--vp-c-text-2);
}
</style>
Performance Optimization
Lazy Loading
js
// Only load search when needed
const loadSearch = async () => {
const { initSearch } = await import('./composables/useSearch.js')
await initSearch()
}
Debounced Search
js
import { debounce } from 'lodash-es'
const debouncedSearch = debounce((query) => {
performSearch(query)
}, 300)
Search Indexing
Pre-build search indices for better performance:
js
// build-search-index.js
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const buildSearchIndex = () => {
const docsDir = path.join(process.cwd(), 'docs')
const pages = []
// Recursively read all markdown files
const readFiles = (dir) => {
const files = fs.readdirSync(dir)
files.forEach(file => {
const filePath = path.join(dir, file)
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
readFiles(filePath)
} else if (file.endsWith('.md')) {
const content = fs.readFileSync(filePath, 'utf-8')
const { data, content: body } = matter(content)
pages.push({
title: data.title || file.replace('.md', ''),
path: filePath.replace(docsDir, '').replace('.md', '.html'),
content: body,
tags: data.tags || []
})
}
})
}
readFiles(docsDir)
// Write search index
fs.writeFileSync(
path.join(process.cwd(), '.vitepress/theme/data/search-index.json'),
JSON.stringify(pages, null, 2)
)
}
buildSearchIndex()
Best Practices
- Choose the Right Solution: Local search for small sites, Algolia for large sites
- Optimize Performance: Use debouncing and lazy loading
- Improve UX: Provide keyboard navigation and clear visual feedback
- Index Quality: Ensure your content is properly structured for search
- Mobile Friendly: Make sure search works well on mobile devices
Troubleshooting
Common Issues
- Search not working: Check your configuration and API keys
- Poor search results: Review your content structure and indexing
- Performance issues: Implement debouncing and optimize your search index
- Mobile issues: Test search functionality on different screen sizes