全站搜索配置
VitePress 内置了本地搜索功能,也支持 Algolia 专业搜索服务。
本地搜索
基础配置
ts
// .vitepress/config.mts
export default defineConfig({
themeConfig: {
search: {
provider: 'local'
}
}
})中文化
ts
export default defineConfig({
themeConfig: {
search: {
provider: 'local',
options: {
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭'
}
}
}
}
}
}
})中文分词优化
ts
export default defineConfig({
themeConfig: {
search: {
provider: 'local',
options: {
miniSearch: {
options: {
// 中文按字符分割
tokenize: (text: string) =>
text.split(/[\s\u3000\u3001\u3002\uff0c\uff1b\uff1a\uff01\uff1f]+/),
// 搜索结果加分
boostDocument: (id: string, term: string, stored: Record<string, string>) => {
if (stored.title?.includes(term)) return 2
if (stored.titles?.some((t: string) => t.includes(term))) return 1.5
return 1
}
},
searchOptions: {
fuzzy: 0.2,
prefix: true,
boost: { title: 4, text: 2, titles: 1 }
}
}
}
}
}
})排除页面
ts
export default defineConfig({
themeConfig: {
search: {
provider: 'local',
options: {
miniSearch: {
options: {
// 返回 false 排除页面
processDocument: (doc) => {
if (doc.url.includes('/private/')) {
return false
}
return doc
}
}
}
}
}
}
})Algolia DocSearch
申请 DocSearch
- 访问 DocSearch 申请页面
- 填写站点信息
- 等待审核通过(通常 1-2 天)
配置
ts
// .vitepress/config.mts
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: 'YOUR_APP_ID',
apiKey: 'YOUR_API_KEY',
indexName: 'YOUR_INDEX_NAME',
locales: {
'/': {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除查询条件',
resetButtonAriaLabel: '清除查询条件'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查网络连接'
},
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭',
searchByText: '搜索提供者'
},
noResultsScreen: {
noResultsText: '没有找到相关结果',
suggestedQueryText: '你可以尝试查询'
}
}
}
}
}
}
}
}
})自定义搜索参数
ts
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
searchParameters: {
facetFilters: ['version:v1'],
hitsPerPage: 10
}
}
}自定义搜索组件
创建搜索框
vue
<!-- .vitepress/theme/components/CustomSearch.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter, useData } from 'vitepress'
const router = useRouter()
const { site } = useData()
const query = ref('')
const isOpen = ref(false)
const results = ref<any[]>([])
// 本地搜索实现
const pages = ref<{ title: string; path: string; content: string }[]>([])
onMounted(async () => {
// 加载搜索索引
const res = await fetch('/search-index.json')
pages.value = await res.json()
})
const handleInput = () => {
if (!query.value.trim()) {
results.value = []
return
}
const q = query.value.toLowerCase()
results.value = pages.value
.filter(page =>
page.title.toLowerCase().includes(q) ||
page.content.toLowerCase().includes(q)
)
.slice(0, 5)
}
const navigate = (path: string) => {
router.go(path)
isOpen.value = false
query.value = ''
}
</script>
<template>
<div class="custom-search">
<input
v-model="query"
type="text"
placeholder="搜索..."
@input="handleInput"
@focus="isOpen = true"
/>
<div v-if="isOpen && results.length" class="results">
<a
v-for="item in results"
:key="item.path"
class="result-item"
@click="navigate(item.path)"
>
{{ item.title }}
</a>
</div>
</div>
</template>
<style scoped>
.custom-search {
position: relative;
}
input {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-alt);
width: 200px;
}
.results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
margin-top: 4px;
overflow: hidden;
}
.result-item {
display: block;
padding: 0.75rem 1rem;
cursor: pointer;
color: var(--vp-c-text-1);
text-decoration: none;
}
.result-item:hover {
background: var(--vp-c-bg-alt);
}
</style>生成搜索索引
构建搜索索引文件:
ts
// scripts/build-search-index.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const docsDir = 'docs'
const index: { title: string; path: string; content: string }[] = []
function walk(dir: string) {
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
walk(filePath)
} else if (file.endsWith('.md')) {
const content = fs.readFileSync(filePath, 'utf-8')
const { data, content: body } = matter(content)
index.push({
title: data.title || file,
path: filePath.replace('docs', '').replace('.md', ''),
content: body.slice(0, 500)
})
}
}
}
walk(docsDir)
fs.writeFileSync('docs/public/search-index.json', JSON.stringify(index))