feat!: organized search config (#2218)

pull/2224/head
Divyansh Singh 1 year ago committed by GitHub
parent 8059ec390a
commit 713a35cbd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -47,14 +47,15 @@ export default defineConfig({
copyright: 'Copyright © 2019-present Evan You' copyright: 'Copyright © 2019-present Evan You'
}, },
algolia: { search: {
provider: 'algolia',
options: {
appId: '8J64VVRP8K', appId: '8J64VVRP8K',
apiKey: 'a18e2f4cc5665f6602c5631fd868adfd', apiKey: 'a18e2f4cc5665f6602c5631fd868adfd',
indexName: 'vitepress' indexName: 'vitepress'
}
}, },
// localSearch: true,
carbonAds: { carbonAds: {
code: 'CEBDT27Y', code: 'CEBDT27Y',
placement: 'vuejsorg' placement: 'vuejsorg'

@ -9,7 +9,9 @@ import { defineConfig } from 'vitepress'
export default defineConfig({ export default defineConfig({
themeConfig: { themeConfig: {
localSearch: true search: {
provider: 'local'
}
} }
}) })
``` ```
@ -29,7 +31,9 @@ import { defineConfig } from 'vitepress'
export default defineConfig({ export default defineConfig({
themeConfig: { themeConfig: {
localSearch: { search: {
provider: 'local',
options: {
locales: { locales: {
zh: { zh: {
translations: { translations: {
@ -50,6 +54,7 @@ export default defineConfig({
} }
} }
} }
}
}) })
``` ```
@ -62,12 +67,15 @@ import { defineConfig } from 'vitepress'
export default defineConfig({ export default defineConfig({
themeConfig: { themeConfig: {
algolia: { search: {
provider: 'algolia',
options: {
appId: '...', appId: '...',
apiKey: '...', apiKey: '...',
indexName: '...' indexName: '...'
} }
} }
}
}) })
``` ```
@ -79,11 +87,10 @@ You can use a config like this to use multilingual search:
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
export default defineConfig({ export default defineConfig({
// ...
themeConfig: { themeConfig: {
// ... search: {
provider: 'algolia',
algolia: { options: {
appId: '...', appId: '...',
apiKey: '...', apiKey: '...',
indexName: '...', indexName: '...',
@ -132,6 +139,7 @@ export default defineConfig({
} }
} }
} }
}
}) })
``` ```

@ -3,7 +3,7 @@
"version": "1.0.0-alpha.65", "version": "1.0.0-alpha.65",
"description": "Vite & Vue powered static site generator", "description": "Vite & Vue powered static site generator",
"type": "module", "type": "module",
"packageManager": "pnpm@8.1.1", "packageManager": "pnpm@8.2.0",
"main": "dist/node/index.js", "main": "dist/node/index.js",
"types": "types/index.d.ts", "types": "types/index.d.ts",
"exports": { "exports": {
@ -91,7 +91,7 @@
"@docsearch/js": "^3.3.3", "@docsearch/js": "^3.3.3",
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.1.0",
"@vue/devtools-api": "^6.5.0", "@vue/devtools-api": "^6.5.0",
"@vueuse/core": "^9.13.0", "@vueuse/core": "^10.0.2",
"body-scroll-lock": "4.0.0-beta.0", "body-scroll-lock": "4.0.0-beta.0",
"minisearch": "^6.0.1", "minisearch": "^6.0.1",
"shiki": "^0.14.1", "shiki": "^0.14.1",
@ -107,10 +107,10 @@
"@mdit-vue/plugin-title": "^0.12.0", "@mdit-vue/plugin-title": "^0.12.0",
"@mdit-vue/plugin-toc": "^0.12.0", "@mdit-vue/plugin-toc": "^0.12.0",
"@mdit-vue/shared": "^0.12.0", "@mdit-vue/shared": "^0.12.0",
"@rollup/plugin-alias": "^4.0.3", "@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-json": "^6.0.0", "@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@types/body-scroll-lock": "^3.1.0", "@types/body-scroll-lock": "^3.1.0",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
@ -134,16 +134,16 @@
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"debug": "^4.3.4", "debug": "^4.3.4",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"esbuild": "^0.17.15", "esbuild": "^0.17.16",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"execa": "^7.1.1", "execa": "^7.1.1",
"fast-glob": "^3.2.12", "fast-glob": "^3.2.12",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"get-port": "^6.1.2", "get-port": "^6.1.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"lint-staged": "^13.2.0", "lint-staged": "^13.2.1",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"lru-cache": "^8.0.4", "lru-cache": "^8.0.5",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.7", "markdown-it-anchor": "^8.6.7",
"markdown-it-attrs": "^4.1.6", "markdown-it-attrs": "^4.1.6",
@ -157,22 +157,22 @@
"path-to-regexp": "^6.2.1", "path-to-regexp": "^6.2.1",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"pkg-dir": "^7.0.0", "pkg-dir": "^7.0.0",
"playwright-chromium": "^1.32.2", "playwright-chromium": "^1.32.3",
"polka": "1.0.0-next.22", "polka": "1.0.0-next.22",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"punycode": "^2.3.0", "punycode": "^2.3.0",
"rimraf": "^4.4.1", "rimraf": "^5.0.0",
"rollup": "^3.20.2", "rollup": "^3.20.2",
"rollup-plugin-dts": "^5.3.0", "rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0", "rollup-plugin-esbuild": "^5.0.0",
"semver": "^7.3.8", "semver": "^7.4.0",
"shiki-processor": "^0.1.3", "shiki-processor": "^0.1.3",
"simple-git-hooks": "^2.8.1", "simple-git-hooks": "^2.8.1",
"sirv": "^2.0.2", "sirv": "^2.0.2",
"supports-color": "^9.3.1", "supports-color": "^9.3.1",
"typescript": "^5.0.3", "typescript": "^5.0.4",
"vitest": "^0.29.8", "vitest": "^0.30.1",
"vue-tsc": "^1.3.2", "vue-tsc": "^1.3.2",
"wait-on": "^7.0.1" "wait-on": "^7.0.1"
}, },

File diff suppressed because it is too large Load Diff

@ -1,4 +1,5 @@
declare const __VP_HASH_MAP__: Record<string, string> declare const __VP_HASH_MAP__: Record<string, string>
declare const __VP_LOCAL_SEARCH__: boolean
declare const __ALGOLIA__: boolean declare const __ALGOLIA__: boolean
declare const __CARBON__: boolean declare const __CARBON__: boolean
declare const __VUE_PROD_DEVTOOLS__: boolean declare const __VUE_PROD_DEVTOOLS__: boolean

@ -1,13 +1,29 @@
<script lang="ts" setup> <script lang="ts" setup>
import { markRaw, nextTick, onMounted, ref, shallowRef, watch, type Ref, createApp } from 'vue'
import { useRouter } from 'vitepress'
import { onKeyStroke, useSessionStorage, debouncedWatch, useLocalStorage, useEventListener, computedAsync } from '@vueuse/core'
import MiniSearch, { type SearchResult } from 'minisearch'
import localSearchIndex from '@localSearchIndex' import localSearchIndex from '@localSearchIndex'
import {
computedAsync,
debouncedWatch,
onKeyStroke,
useEventListener,
useLocalStorage,
useSessionStorage
} from '@vueuse/core'
import MiniSearch, { type SearchResult } from 'minisearch'
import { useRouter } from 'vitepress'
import {
createApp,
markRaw,
nextTick,
onMounted,
ref,
shallowRef,
watch,
type Ref
} from 'vue'
import type { ModalTranslations } from '../../../../types/local-search'
import { pathToFile, withBase } from '../../app/utils'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import { createTranslate } from '../support/translation' import { createTranslate } from '../support/translation'
import type { ModalTranslations } from '../../../../types/local-search'
import { pathToFile, withBase } from '../../app/utils.js'
defineProps<{ defineProps<{
placeholder: string placeholder: string
@ -40,19 +56,29 @@ interface Result {
const { localeIndex } = useData() const { localeIndex } = useData()
const searchIndex = computedAsync(async () => markRaw(MiniSearch.loadJSON<Result>((await searchIndexData.value[localeIndex.value]?.())?.default, { const searchIndex = computedAsync(async () =>
markRaw(
MiniSearch.loadJSON<Result>(
(await searchIndexData.value[localeIndex.value]?.())?.default,
{
fields: ['title', 'titles', 'text'], fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'], storeFields: ['title', 'titles'],
searchOptions: { searchOptions: {
fuzzy: 0.2, fuzzy: 0.2,
prefix: true, prefix: true,
boost: { title: 4, text: 2, titles: 1 }, boost: { title: 4, text: 2, titles: 1 }
}, }
}))) }
)
)
)
const filterText = useSessionStorage('vitepress:local-search-filter', '') const filterText = useSessionStorage('vitepress:local-search-filter', '')
const showDetailedList = useLocalStorage('vitepress:local-search-detailed-list', false) const showDetailedList = useLocalStorage(
'vitepress:local-search-detailed-list',
false
)
const results: Ref<(SearchResult & Result)[]> = shallowRef([]) const results: Ref<(SearchResult & Result)[]> = shallowRef([])
@ -66,7 +92,9 @@ watch(filterText, () => {
enableNoResults.value = false enableNoResults.value = false
}) })
debouncedWatch(() => [searchIndex.value, filterText.value, showDetailedList.value] as const, async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => { debouncedWatch(
() => [searchIndex.value, filterText.value, showDetailedList.value] as const,
async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
let canceled = false let canceled = false
onCleanup(() => { onCleanup(() => {
canceled = true canceled = true
@ -75,11 +103,15 @@ debouncedWatch(() => [searchIndex.value, filterText.value, showDetailedList.valu
if (!index) return if (!index) return
// Search // Search
results.value = index.search(filterTextValue).slice(0, 16) as (SearchResult & Result)[] results.value = index
.search(filterTextValue)
.slice(0, 16) as (SearchResult & Result)[]
enableNoResults.value = true enableNoResults.value = true
// Highlighting // Highlighting
const mods = showDetailedListValue ? await Promise.all(results.value.map(r => fetchExcerpt(r.id))) : [] const mods = showDetailedListValue
? await Promise.all(results.value.map((r) => fetchExcerpt(r.id)))
: []
if (canceled) return if (canceled) return
const c = new Map<string, Map<string, string>>() const c = new Map<string, Map<string, string>>()
for (const { id, mod } of mods) { for (const { id, mod } of mods) {
@ -107,7 +139,7 @@ debouncedWatch(() => [searchIndex.value, filterText.value, showDetailedList.valu
} }
if (canceled) return if (canceled) return
} }
results.value = results.value.map(r => { results.value = results.value.map((r) => {
let title = r.title let title = r.title
let titles = r.titles let titles = r.titles
let text = '' let text = ''
@ -126,7 +158,7 @@ debouncedWatch(() => [searchIndex.value, filterText.value, showDetailedList.valu
title = title.replace(reg, `<mark>$&</mark>`) title = title.replace(reg, `<mark>$&</mark>`)
} }
if (match.includes('titles')) { if (match.includes('titles')) {
titles = titles.map(t => t.replace(reg, `<mark>$&</mark>`)) titles = titles.map((t) => t.replace(reg, `<mark>$&</mark>`))
} }
if (showDetailedListValue && match.includes('text')) { if (showDetailedListValue && match.includes('text')) {
text = text.replace(reg, `<mark>$&</mark>`) text = text.replace(reg, `<mark>$&</mark>`)
@ -137,7 +169,7 @@ debouncedWatch(() => [searchIndex.value, filterText.value, showDetailedList.valu
...r, ...r,
title, title,
titles, titles,
text, text
} }
}) })
contents.value = c contents.value = c
@ -146,10 +178,12 @@ debouncedWatch(() => [searchIndex.value, filterText.value, showDetailedList.valu
const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? [] const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? []
for (const excerpt of excerpts) { for (const excerpt of excerpts) {
excerpt.querySelector('mark')?.scrollIntoView({ excerpt.querySelector('mark')?.scrollIntoView({
block: 'center', block: 'center'
}) })
} }
}, { debounce: 200, immediate: true }) },
{ debounce: 200, immediate: true }
)
async function fetchExcerpt(id: string) { async function fetchExcerpt(id: string) {
const file = pathToFile(withBase(id.slice(0, id.indexOf('#')))) const file = pathToFile(withBase(id.slice(0, id.indexOf('#'))))
@ -195,7 +229,7 @@ function scrollToSelectedResult () {
const selectedEl = document.querySelector('.result.selected') const selectedEl = document.querySelector('.result.selected')
if (selectedEl) { if (selectedEl) {
selectedEl.scrollIntoView({ selectedEl.scrollIntoView({
block: 'nearest', block: 'nearest'
}) })
} }
}) })
@ -250,12 +284,12 @@ const defaultTranslations: { modal: ModalTranslations } = {
selectKeyAriaLabel: 'enter', selectKeyAriaLabel: 'enter',
navigateText: 'to navigate', navigateText: 'to navigate',
navigateUpKeyAriaLabel: 'up arrow', navigateUpKeyAriaLabel: 'up arrow',
navigateDownKeyAriaLabel: 'down arrow', navigateDownKeyAriaLabel: 'down arrow'
} }
} }
} }
const $t = createTranslate(theme.value.localSearch, defaultTranslations) const $t = createTranslate(theme.value.search?.options, defaultTranslations)
// Back // Back
@ -264,7 +298,7 @@ onMounted(() => {
window.history.pushState(null, '', null) window.history.pushState(null, '', null)
}) })
useEventListener('popstate', event => { useEventListener('popstate', (event) => {
event.preventDefault() event.preventDefault()
emit('close') emit('close')
}) })
@ -277,14 +311,34 @@ useEventListener('popstate', event => {
<div class="shell"> <div class="shell">
<div class="search-bar" @pointerup="onSearchBarClick($event)"> <div class="search-bar" @pointerup="onSearchBarClick($event)">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21l-4.35-4.35"/></g></svg> <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24">
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21l-4.35-4.35" />
</g>
</svg>
<div class="search-actions before"> <div class="search-actions before">
<button <button
class="back-button" class="back-button"
:title="$t('modal.backButtonTitle')" :title="$t('modal.backButtonTitle')"
@click="$emit('close')" @click="$emit('close')"
> >
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 12H5m7 7l-7-7l7-7"/></svg> <svg width="16" height="16" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 12H5m7 7l-7-7l7-7"
/>
</svg>
</button> </button>
</div> </div>
<input <input
@ -292,17 +346,26 @@ useEventListener('popstate', event => {
v-model="filterText" v-model="filterText"
:placeholder="placeholder" :placeholder="placeholder"
class="search-input" class="search-input"
> />
<div class="search-actions"> <div class="search-actions">
<button <button
class="toggle-layout-button" class="toggle-layout-button"
:class="{ :class="{
'detailed-list': showDetailedList, 'detailed-list': showDetailedList
}" }"
:title="$t('modal.displayDetails')" :title="$t('modal.displayDetails')"
@click="showDetailedList = !showDetailedList" @click="showDetailedList = !showDetailedList"
> >
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 14h7v7H3zM3 3h7v7H3zm11 1h7m-7 5h7m-7 6h7m-7 5h7"/></svg> <svg width="16" height="16" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 14h7v7H3zM3 3h7v7H3zm11 1h7m-7 5h7m-7 6h7m-7 5h7"
/>
</svg>
</button> </button>
<button <button
@ -310,22 +373,28 @@ useEventListener('popstate', event => {
:title="$t('modal.resetButtonTitle')" :title="$t('modal.resetButtonTitle')"
@click="filterText = ''" @click="filterText = ''"
> >
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 5H9l-7 7l7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Zm-2 4l-6 6m0-6l6 6"/></svg> <svg width="16" height="16" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20 5H9l-7 7l7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Zm-2 4l-6 6m0-6l6 6"
/>
</svg>
</button> </button>
</div> </div>
</div> </div>
<div <div class="results" @mousemove="disableMouseOver = false">
class="results"
@mousemove="disableMouseOver = false"
>
<a <a
v-for="(p, index) in results" v-for="(p, index) in results"
:key="p.id" :key="p.id"
:href="withBase(p.id)" :href="withBase(p.id)"
class="result" class="result"
:class="{ :class="{
selected: selectedIndex === index, selected: selectedIndex === index
}" }"
:aria-title="[...p.titles, p.title].join(' > ')" :aria-title="[...p.titles, p.title].join(' > ')"
@mouseenter="!disableMouseOver && (selectedIndex = index)" @mouseenter="!disableMouseOver && (selectedIndex = index)"
@ -334,13 +403,18 @@ useEventListener('popstate', event => {
<div> <div>
<div class="titles"> <div class="titles">
<span class="title-icon">#</span> <span class="title-icon">#</span>
<span <span v-for="(t, index) in p.titles" :key="index" class="title">
v-for="(t, index) in p.titles"
:key="index"
class="title"
>
<span class="text" v-html="t" /> <span class="text" v-html="t" />
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 18l6-6l-6-6"/></svg> <svg width="16" height="16" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 18l6-6l-6-6"
/>
</svg>
</span> </span>
<span class="title main"> <span class="title main">
<span class="text" v-html="p.title" /> <span class="text" v-html="p.title" />
@ -357,18 +431,59 @@ useEventListener('popstate', event => {
</div> </div>
</a> </a>
<div v-if="filterText && !results.length && enableNoResults" class="no-results"> <div
{{ $t('modal.noResultsText') }} "<strong>{{ filterText }}</strong>" v-if="filterText && !results.length && enableNoResults"
class="no-results"
>
{{ $t('modal.noResultsText') }} "<strong>{{ filterText }}</strong
>"
</div> </div>
</div> </div>
<div class="search-keyboard-shortcuts"> <div class="search-keyboard-shortcuts">
<span> <span>
<kbd :aria-title="$t('modal.footer.navigateUpKeyAriaLabel')"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19V5m-7 7l7-7l7 7"/></svg></kbd> <kbd :aria-title="$t('modal.footer.navigateDownKeyAriaLabel')"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m7-7l-7 7l-7-7"/></svg></kbd> <kbd :aria-title="$t('modal.footer.navigateUpKeyAriaLabel')">
<svg width="14" height="14" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 19V5m-7 7l7-7l7 7"
/>
</svg>
</kbd>
{{ ' ' }}
<kbd :aria-title="$t('modal.footer.navigateDownKeyAriaLabel')">
<svg width="14" height="14" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m7-7l-7 7l-7-7"
/>
</svg>
</kbd>
{{ $t('modal.footer.navigateText') }} {{ $t('modal.footer.navigateText') }}
</span> </span>
<span> <span>
<kbd :aria-title="$t('modal.footer.selectKeyAriaLabel')"><svg width="14" height="14" viewBox="0 0 24 24"><g fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="m9 10l-5 5l5 5"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/></g></svg></kbd> <kbd :aria-title="$t('modal.footer.selectKeyAriaLabel')"
><svg width="14" height="14" viewBox="0 0 24 24">
<g
fill="none"
stroke="currentcolor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="m9 10l-5 5l5 5" />
<path d="M20 4v7a4 4 0 0 1-4 4H4" />
</g>
</svg>
</kbd>
{{ $t('modal.footer.selectText') }} {{ $t('modal.footer.selectText') }}
</span> </span>
</div> </div>

@ -8,17 +8,18 @@ import {
onUnmounted, onUnmounted,
ref ref
} from 'vue' } from 'vue'
import type { DefaultTheme } from '../../shared'
import { useData } from '../composables/data' import { useData } from '../composables/data'
import VPNavBarSearchButton from './VPNavBarSearchButton.vue' import VPNavBarSearchButton from './VPNavBarSearchButton.vue'
const VPLocalSearchBox = __VP_LOCAL_SEARCH__
? defineAsyncComponent(() => import('./VPLocalSearchBox.vue'))
: () => null
const VPAlgoliaSearchBox = __ALGOLIA__ const VPAlgoliaSearchBox = __ALGOLIA__
? defineAsyncComponent(() => import('./VPAlgoliaSearchBox.vue')) ? defineAsyncComponent(() => import('./VPAlgoliaSearchBox.vue'))
: () => null : () => null
const VPLocalSearchBox = __ALGOLIA__
? () => null
: defineAsyncComponent(() => import('./VPLocalSearchBox.vue'))
const { theme, localeIndex } = useData() const { theme, localeIndex } = useData()
// to avoid loading the docsearch js upfront (which is more than 1/3 of the // to avoid loading the docsearch js upfront (which is more than 1/3 of the
@ -27,17 +28,13 @@ const { theme, localeIndex } = useData()
const loaded = ref(false) const loaded = ref(false)
const buttonText = computed(() => { const buttonText = computed(() => {
if (theme.value.algolia) { const options = theme.value.search?.options ?? theme.value.algolia
return theme.value.algolia.locales?.[localeIndex.value]?.translations?.button
?.buttonText || return (
theme.value.algolia.translations?.button?.buttonText || options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
'Search' options?.translations?.button?.buttonText ||
} else if (typeof theme.value.localSearch === 'object') {
return theme.value.localSearch.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
theme.value.localSearch.translations?.button?.buttonText ||
'Search' 'Search'
} )
return 'Search'
}) })
const preconnect = () => { const preconnect = () => {
@ -48,14 +45,17 @@ const preconnect = () => {
const preconnect = document.createElement('link') const preconnect = document.createElement('link')
preconnect.id = id preconnect.id = id
preconnect.rel = 'preconnect' preconnect.rel = 'preconnect'
preconnect.href = `https://${theme.value.algolia!.appId}-dsn.algolia.net` preconnect.href = `https://${
((theme.value.search?.options as DefaultTheme.AlgoliaSearchOptions) ??
theme.value.algolia)!.appId
}-dsn.algolia.net`
preconnect.crossOrigin = '' preconnect.crossOrigin = ''
document.head.appendChild(preconnect) document.head.appendChild(preconnect)
}) })
} }
onMounted(() => { onMounted(() => {
if (!theme.value.algolia) { if (!__ALGOLIA__) {
return return
} }
@ -105,8 +105,8 @@ function poll() {
const showSearch = ref(false) const showSearch = ref(false)
if (!__ALGOLIA__ && theme.value.localSearch) { if (__VP_LOCAL_SEARCH__) {
onKeyStroke('k', event => { onKeyStroke('k', (event) => {
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
event.preventDefault() event.preventDefault()
showSearch.value = true showSearch.value = true
@ -122,23 +122,35 @@ onMounted(() => {
? `'⌘'` ? `'⌘'`
: `'Ctrl'` : `'Ctrl'`
}) })
const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
</script> </script>
<template> <template>
<div class="VPNavBarSearch" :style="{ '--vp-meta-key': metaKey }"> <div class="VPNavBarSearch" :style="{ '--vp-meta-key': metaKey }">
<template v-if="theme.algolia"> <template v-if="provider === 'local'">
<VPAlgoliaSearchBox v-if="loaded" :algolia="theme.algolia" /> <VPLocalSearchBox
v-if="showSearch"
:placeholder="buttonText"
@close="showSearch = false"
/>
<div v-else id="docsearch"> <div id="local-search">
<VPNavBarSearchButton :placeholder="buttonText" @click="load" /> <VPNavBarSearchButton
:placeholder="buttonText"
@click="showSearch = true"
/>
</div> </div>
</template> </template>
<template v-else-if="theme.localSearch"> <template v-else-if="provider === 'algolia'">
<VPLocalSearchBox v-if="showSearch" :placeholder="buttonText" @close="showSearch = false" /> <VPAlgoliaSearchBox
v-if="loaded"
:algolia="theme.search?.options ?? theme.algolia"
/>
<div id="local-search"> <div v-else id="docsearch">
<VPNavBarSearchButton :placeholder="buttonText" @click="showSearch = true" /> <VPNavBarSearchButton :placeholder="buttonText" @click="load" />
</div> </div>
</template> </template>
</div> </div>

@ -5,11 +5,7 @@ defineProps<{
</script> </script>
<template> <template>
<button <button type="button" class="DocSearch DocSearch-Button" aria-label="Search">
type="button"
class="DocSearch DocSearch-Button"
aria-label="Search"
>
<span class="DocSearch-Button-Container"> <span class="DocSearch-Button-Container">
<svg <svg
class="DocSearch-Search-Icon" class="DocSearch-Search-Icon"

@ -21,7 +21,7 @@ export const failMark = '\x1b[31m✖\x1b[0m'
// A list of default theme components that should only be loaded on demand. // A list of default theme components that should only be loaded on demand.
const lazyDefaultThemeComponentsRE = const lazyDefaultThemeComponentsRE =
/VP(HomeSponsors|DocAsideSponsors|TeamPage|TeamMembers|AlgoliaSearch|CarbonAds|DocAsideCarbonAds)/ /VP(HomeSponsors|DocAsideSponsors|TeamPage|TeamMembers|LocalSearchBox|AlgoliaSearchBox|CarbonAds|DocAsideCarbonAds)/
const clientDir = normalizePath( const clientDir = normalizePath(
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../client') path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../client')

@ -22,7 +22,7 @@ import { staticDataPlugin } from './plugins/staticDataPlugin'
import { webFontsPlugin } from './plugins/webFontsPlugin' import { webFontsPlugin } from './plugins/webFontsPlugin'
import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin' import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin'
import { rewritesPlugin } from './plugins/rewritesPlugin' import { rewritesPlugin } from './plugins/rewritesPlugin'
import { localSearchPlugin } from './plugins/localSearchPlugin.js' import { localSearchPlugin } from './plugins/localSearchPlugin'
import { serializeFunctions, deserializeFunctions } from './utils/fnSerialize' import { serializeFunctions, deserializeFunctions } from './utils/fnSerialize'
declare module 'vite' { declare module 'vite' {
@ -122,8 +122,11 @@ export async function createVitePressPlugin(
alias: resolveAliases(siteConfig, ssr) alias: resolveAliases(siteConfig, ssr)
}, },
define: { define: {
__ALGOLIA__: !!site.themeConfig.algolia, __VP_LOCAL_SEARCH__: site.themeConfig?.search?.provider === 'local',
__CARBON__: !!site.themeConfig.carbonAds __ALGOLIA__:
site.themeConfig?.search?.provider === 'algolia' ||
!!site.themeConfig?.algolia, // legacy
__CARBON__: !!site.themeConfig?.carbonAds
}, },
optimizeDeps: { optimizeDeps: {
// force include vue to avoid duplicated copies when linked + optimized // force include vue to avoid duplicated copies when linked + optimized

@ -4,13 +4,13 @@ import MiniSearch from 'minisearch'
import fs from 'fs-extra' import fs from 'fs-extra'
import _debug from 'debug' import _debug from 'debug'
import type { SiteConfig } from '../config' import type { SiteConfig } from '../config'
import { createMarkdownRenderer } from '../markdown/markdown.js' import { createMarkdownRenderer } from '../markdown/markdown'
import { resolveSiteDataByRoute } from '../shared.js' import { resolveSiteDataByRoute } from '../shared'
const debug = _debug('vitepress:local-search') const debug = _debug('vitepress:local-search')
const OFFLINE_SEARCH_INDEX_ID = '@localSearchIndex' const LOCAL_SEARCH_INDEX_ID = '@localSearchIndex'
const OFFLINE_SEARCH_INDEX_REQUEST_PATH = '/' + OFFLINE_SEARCH_INDEX_ID const LOCAL_SEARCH_INDEX_REQUEST_PATH = '/' + LOCAL_SEARCH_INDEX_ID
interface IndexObject { interface IndexObject {
id: string id: string
@ -22,19 +22,16 @@ interface IndexObject {
export async function localSearchPlugin( export async function localSearchPlugin(
siteConfig: SiteConfig siteConfig: SiteConfig
): Promise<Plugin> { ): Promise<Plugin> {
if ( if (siteConfig.userConfig.themeConfig?.search?.provider !== 'local') {
siteConfig.userConfig.themeConfig?.algolia ||
!siteConfig.userConfig.themeConfig?.localSearch
) {
return { return {
name: 'vitepress:local-search', name: 'vitepress:local-search',
resolveId(id) { resolveId(id) {
if (id.startsWith(OFFLINE_SEARCH_INDEX_ID)) { if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
return `/${id}` return `/${id}`
} }
}, },
load(id) { load(id) {
if (id.startsWith(OFFLINE_SEARCH_INDEX_REQUEST_PATH)) { if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
return `export default '{}'` return `export default '{}'`
} }
} }
@ -77,10 +74,10 @@ export async function localSearchPlugin(
async function onIndexUpdated() { async function onIndexUpdated() {
if (server) { if (server) {
server.moduleGraph.onFileChange(OFFLINE_SEARCH_INDEX_REQUEST_PATH) server.moduleGraph.onFileChange(LOCAL_SEARCH_INDEX_REQUEST_PATH)
// HMR // HMR
const mod = server.moduleGraph.getModuleById( const mod = server.moduleGraph.getModuleById(
OFFLINE_SEARCH_INDEX_REQUEST_PATH LOCAL_SEARCH_INDEX_REQUEST_PATH
) )
if (!mod) return if (!mod) return
server.ws.send({ server.ws.send({
@ -113,7 +110,7 @@ export async function localSearchPlugin(
.map(async (file) => { .map(async (file) => {
const fileId = getDocId(file) const fileId = getDocId(file)
const sections = splitPageIntoSections( const sections = splitPageIntoSections(
await md.render(await fs.readFile(file, 'utf-8')) md.render(await fs.readFile(file, 'utf-8'))
) )
const locale = getLocaleForPath(file) const locale = getLocaleForPath(file)
let documents = documentsByLocale.get(locale) let documents = documentsByLocale.get(locale)
@ -167,13 +164,13 @@ export async function localSearchPlugin(
}, },
resolveId(id) { resolveId(id) {
if (id.startsWith(OFFLINE_SEARCH_INDEX_ID)) { if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
return `/${id}` return `/${id}`
} }
}, },
async load(id) { async load(id) {
if (id === OFFLINE_SEARCH_INDEX_REQUEST_PATH) { if (id === LOCAL_SEARCH_INDEX_REQUEST_PATH) {
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
await scanForBuild() await scanForBuild()
} }
@ -186,11 +183,11 @@ export async function localSearchPlugin(
) )
} }
return `export default {${records.join(',')}}` return `export default {${records.join(',')}}`
} else if (id.startsWith(OFFLINE_SEARCH_INDEX_REQUEST_PATH)) { } else if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
return `export default ${JSON.stringify( return `export default ${JSON.stringify(
JSON.stringify( JSON.stringify(
indexByLocales.get( indexByLocales.get(
id.replace(OFFLINE_SEARCH_INDEX_REQUEST_PATH, '') id.replace(LOCAL_SEARCH_INDEX_REQUEST_PATH, '')
) ?? {} ) ?? {}
) )
)}` )}`

@ -11,7 +11,7 @@ export type {
PageDataPayload, PageDataPayload,
SiteData, SiteData,
SSGContext SSGContext
} from '../../types/shared.js' } from '../../types/shared'
export const EXTERNAL_URL_RE = /^[a-z]+:/i export const EXTERNAL_URL_RE = /^[a-z]+:/i
export const PATHNAME_PROTOCOL_RE = /^pathname:\/\// export const PATHNAME_PROTOCOL_RE = /^pathname:\/\//

@ -101,15 +101,14 @@ export namespace DefaultTheme {
*/ */
langMenuLabel?: string langMenuLabel?: string
/** search?:
* The algolia options. Leave it undefined to disable the search feature. | { provider: 'local'; options?: LocalSearchOptions }
*/ | { provider: 'algolia'; options: AlgoliaSearchOptions }
algolia?: AlgoliaSearchOptions
/** /**
* The local search options. Set to `true` or an object to enable, `false` to disable. * @deprecated Use `search` instead.
*/ */
localSearch?: LocalSearchOptions | boolean algolia?: AlgoliaSearchOptions
/** /**
* The carbon ads options. Leave it undefined to disable the ads feature. * The carbon ads options. Leave it undefined to disable the ads feature.
@ -291,6 +290,13 @@ export namespace DefaultTheme {
label?: string label?: string
} }
// local search --------------------------------------------------------------
export interface LocalSearchOptions {
translations?: LocalSearchTranslations
locales?: Record<string, Partial<Omit<LocalSearchOptions, 'locales'>>>
}
// algolia ------------------------------------------------------------------- // algolia -------------------------------------------------------------------
/** /**
@ -301,13 +307,6 @@ export namespace DefaultTheme {
locales?: Record<string, Partial<DocSearchProps>> locales?: Record<string, Partial<DocSearchProps>>
} }
// local search ------------------------------------------------------------
export interface LocalSearchOptions {
translations?: LocalSearchTranslations
locales?: Record<string, Partial<Omit<LocalSearchOptions, 'locales'>>>
}
// carbon ads ---------------------------------------------------------------- // carbon ads ----------------------------------------------------------------
export interface CarbonAdsOptions { export interface CarbonAdsOptions {

Loading…
Cancel
Save