Merge branch 'main' into main

pull/4893/head
Evorp 2 weeks ago committed by GitHub
commit f4d448fc61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,3 +1,19 @@
## [2.0.0-alpha.16](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.15...v2.0.0-alpha.16) (2026-01-31)
### Bug Fixes
- always log error when failed to fetch page ([66cf64e](https://github.com/vuejs/vitepress/commit/66cf64e6d127dd8473e582d11e1133acda6c3bc8))
- **theme:** add fallback for `heroImageSlotExists` ([#5076](https://github.com/vuejs/vitepress/issues/5076)) ([f119b18](https://github.com/vuejs/vitepress/commit/f119b18e39b545f39e29358913fe9ed1fd69bc55))
- **theme:** align badges in h1 and h2 ([#5087](https://github.com/vuejs/vitepress/issues/5087)) (closes [#5063](https://github.com/vuejs/vitepress/issues/5063)) ([b200865](https://github.com/vuejs/vitepress/commit/b2008654ffaf0d8f8c425e88592e9ed2f8791506))
- **theme:** highlight active link in mobile nav menu ([#5086](https://github.com/vuejs/vitepress/issues/5086)) (closes [#5068](https://github.com/vuejs/vitepress/issues/5068), closes [#5074](https://github.com/vuejs/vitepress/issues/5074)) ([923aa90](https://github.com/vuejs/vitepress/commit/923aa902523739bfb9d77aed376ebc73c32eeb33))
- **theme:** overflow clip is buggy on safari ([8ed6ea0](https://github.com/vuejs/vitepress/commit/8ed6ea048cb49256e3302de2de0edfbe635afd32)), closes [#5050](https://github.com/vuejs/vitepress/issues/5050) [#5039](https://github.com/vuejs/vitepress/issues/5039) [#5027](https://github.com/vuejs/vitepress/issues/5027)
- **theme:** remove margin between code groups and blocks in markdown containers ([a28e171](https://github.com/vuejs/vitepress/commit/a28e171604605713a221d0eb2bbdce211930d94f)), closes [#5099](https://github.com/vuejs/vitepress/issues/5099)
### Features
- **theme:** upgrade DocSearch to 4.5 with sidepanel ([#5092](https://github.com/vuejs/vitepress/issues/5092)) ([0d646a6](https://github.com/vuejs/vitepress/commit/0d646a66cd44e97adef516a6a36e03365f179906))
- **theme:** use `@layer __vitepress_base` to wrap the styles in base.css ([#4905](https://github.com/vuejs/vitepress/issues/4905)) ([f8d8c0d](https://github.com/vuejs/vitepress/commit/f8d8c0d712fba4728c750e1f44c5ba9596979ba1))
## [2.0.0-alpha.15](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.14...v2.0.0-alpha.15) (2025-11-22)
### Bug Fixes

@ -4,7 +4,7 @@ describe('local search', () => {
})
test('exclude content from search results', async () => {
await page.locator('#local-search button').click()
await page.locator('.VPNavBarSearchButton').click()
const input = await page.waitForSelector('input#localsearch-input')
await input.type('local')

@ -0,0 +1,196 @@
import {
buildAskAiConfig,
hasAskAi,
hasKeywordSearch,
mergeLangFacetFilters,
validateCredentials
} from 'client/theme-default/support/docsearch'
describe('client/theme-default/support/docsearch', () => {
describe('mergeLangFacetFilters', () => {
test('adds a lang facet filter when none is provided', () => {
expect(mergeLangFacetFilters(undefined, 'en')).toEqual(['lang:en'])
})
test('replaces existing lang facet filters', () => {
expect(mergeLangFacetFilters('lang:fr', 'en')).toEqual(['lang:en'])
expect(mergeLangFacetFilters(['foo', 'lang:fr'], 'en')).toEqual([
'foo',
'lang:en'
])
})
test('handles nested facet filters (OR conditions)', () => {
expect(
mergeLangFacetFilters([['tag:foo', 'tag:bar'], 'lang:fr'], 'en')
).toEqual([['tag:foo', 'tag:bar'], 'lang:en'])
})
test('removes empty nested arrays', () => {
expect(mergeLangFacetFilters([['lang:fr'], 'other'], 'en')).toEqual([
'other',
'lang:en'
])
})
test('handles multiple lang filters in nested arrays', () => {
expect(
mergeLangFacetFilters([['lang:fr', 'tag:foo'], 'bar'], 'en')
).toEqual([['tag:foo'], 'bar', 'lang:en'])
})
})
describe('hasKeywordSearch', () => {
test('returns true when all credentials are provided', () => {
expect(
hasKeywordSearch({
appId: 'app',
apiKey: 'key',
indexName: 'index'
})
).toBe(true)
})
test('returns false when any credential is missing', () => {
expect(
hasKeywordSearch({
appId: undefined,
apiKey: 'key',
indexName: 'index'
})
).toBe(false)
expect(
hasKeywordSearch({
appId: 'app',
apiKey: undefined,
indexName: 'index'
})
).toBe(false)
expect(
hasKeywordSearch({
appId: 'app',
apiKey: 'key',
indexName: undefined
})
).toBe(false)
})
})
describe('hasAskAi', () => {
test('returns true for valid string assistantId', () => {
expect(hasAskAi('assistant123')).toBe(true)
})
test('returns false for empty string assistantId', () => {
expect(hasAskAi('')).toBe(false)
})
test('returns true for object with assistantId', () => {
expect(hasAskAi({ assistantId: 'assistant123' } as any)).toBe(true)
})
test('returns false for object without assistantId', () => {
expect(hasAskAi({ assistantId: null } as any)).toBe(false)
expect(hasAskAi({} as any)).toBe(false)
})
test('returns false for undefined', () => {
expect(hasAskAi(undefined)).toBe(false)
})
})
describe('validateCredentials', () => {
test('validates complete credentials', () => {
const result = validateCredentials({
appId: 'app',
apiKey: 'key',
indexName: 'index'
})
expect(result.valid).toBe(true)
expect(result.appId).toBe('app')
expect(result.apiKey).toBe('key')
expect(result.indexName).toBe('index')
})
test('invalidates incomplete credentials', () => {
expect(
validateCredentials({
appId: undefined,
apiKey: 'key',
indexName: 'index'
}).valid
).toBe(false)
})
})
describe('buildAskAiConfig', () => {
test('builds config from string assistantId', () => {
const result = buildAskAiConfig(
'assistant123',
{
appId: 'app',
apiKey: 'key',
indexName: 'index'
} as any,
'en'
)
expect(result.assistantId).toBe('assistant123')
expect(result.appId).toBe('app')
expect(result.apiKey).toBe('key')
expect(result.indexName).toBe('index')
})
test('builds config from object with overrides', () => {
const result = buildAskAiConfig(
{
assistantId: 'assistant123',
appId: 'custom-app',
apiKey: 'custom-key',
indexName: 'custom-index'
} as any,
{
appId: 'default-app',
apiKey: 'default-key',
indexName: 'default-index'
} as any,
'en'
)
expect(result.assistantId).toBe('assistant123')
expect(result.appId).toBe('custom-app')
expect(result.apiKey).toBe('custom-key')
expect(result.indexName).toBe('custom-index')
})
test('merges facet filters with lang', () => {
const result = buildAskAiConfig(
{
assistantId: 'assistant123',
searchParameters: {
facetFilters: ['tag:docs']
}
} as any,
{
appId: 'app',
apiKey: 'key',
indexName: 'index'
} as any,
'en'
)
expect(result.searchParameters?.facetFilters).toContain('tag:docs')
expect(result.searchParameters?.facetFilters).toContain('lang:en')
})
test('always adds lang facet filter to searchParameters', () => {
const result = buildAskAiConfig(
'assistant123',
{
appId: 'app',
apiKey: 'key',
indexName: 'index'
} as any,
'en'
)
expect(result.searchParameters?.facetFilters).toEqual(['lang:en'])
})
})
})

@ -26,10 +26,13 @@ export default defineConfig({
markdown: {
math: true,
codeTransformers: [
// We use `[!!code` in demo to prevent transformation, here we revert it back.
// We use `[!!code` and `@@include` in demo to prevent transformation,
// here we revert it back.
{
postprocess(code) {
return code.replace(/\[\!\!code/g, '[!code')
return code
.replaceAll('[!!code', '[!code')
.replaceAll('@@include', '@include')
}
}
],
@ -118,7 +121,10 @@ export default defineConfig({
appId: '8J64VVRP8K',
apiKey: '52f578a92b88ad6abde815aae2b0ad7c',
indexName: 'vitepress',
askAi: 'YaVSonfX5bS8'
askAi: {
assistantId: 'YaVSonfX5bS8',
sidePanel: true
}
}
},

@ -105,7 +105,7 @@ Note: the `vercel.json` file should be placed at the root of your **repository**
## Platform Guides
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#generic}
Set up a new project and change these settings using your dashboard:
@ -221,7 +221,9 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f
- main
```
### Azure Static Web Apps
<!-- keep headings sorted alphabetically, leave nginx at the end -->
### Azure
1. Follow the [official documentation](https://docs.microsoft.com/en-us/azure/static-web-apps/build-configuration).
@ -231,6 +233,10 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f
- **`output_location`**: `docs/.vitepress/dist`
- **`app_build_command`**: `npm run docs:build`
### CloudRay
You can deploy your VitePress project with [CloudRay](https://cloudray.io/) by following these [instructions](https://cloudray.io/articles/how-to-deploy-vitepress-site).
### Firebase
1. Create `firebase.json` and `.firebaserc` at the root of your project:
@ -262,14 +268,6 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f
firebase deploy
```
### Surge
1. After running `npm run docs:build`, run this command to deploy:
```sh
npx surge docs/.vitepress/dist
```
### Heroku
1. Follow documentation and guide given in [`heroku-buildpack-static`](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-static).
@ -282,11 +280,11 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f
}
```
### Edgio
### Hostinger
Refer [Creating and Deploying a VitePress App To Edgio](https://docs.edg.io/guides/vitepress).
You can deploy your VitePress project with [Hostinger](https://www.hostinger.com/web-apps-hosting) by following these [instructions](https://www.hostinger.com/support/how-to-deploy-a-nodejs-website-in-hostinger/). While configuring build settings, choose VitePress as the framework and adjust the root directory to `./docs`.
### Kinsta Static Site Hosting
### Kinsta
You can deploy your VitePress website on [Kinsta](https://kinsta.com/static-site-hosting/) by following these [instructions](https://kinsta.com/docs/vitepress-static-site-example/).
@ -294,9 +292,13 @@ You can deploy your VitePress website on [Kinsta](https://kinsta.com/static-site
You can deploy your VitePress project to [Stormkit](https://www.stormkit.io) by following these [instructions](https://stormkit.io/blog/how-to-deploy-vitepress).
### CloudRay
### Surge
You can deploy your VitePress project with [CloudRay](https://cloudray.io/) by following these [instructions](https://cloudray.io/articles/how-to-deploy-vitepress-site).
1. After running `npm run docs:build`, run this command to deploy:
```sh
npx surge docs/.vitepress/dist
```
### Nginx

@ -793,7 +793,7 @@ For example, you can include a relative markdown file using this:
## Basics
<!--@include: ./parts/basics.md-->
<!--@@include: ./parts/basics.md-->
```
**Part file** (`parts/basics.md`)
@ -829,7 +829,7 @@ It also supports selecting a line range:
## Basics
<!--@include: ./parts/basics.md{3,}-->
<!--@@include: ./parts/basics.md{3,}-->
```
**Part file** (`parts/basics.md`)
@ -865,8 +865,8 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
## Basics
<!--@include: ./parts/basics.md#basic-usage{,2}-->
<!--@include: ./parts/basics.md#basic-usage{5,}-->
<!--@@include: ./parts/basics.md#basic-usage{,2}-->
<!--@@include: ./parts/basics.md#basic-usage{5,}-->
```
**Part file** (`parts/basics.md`)
@ -917,7 +917,7 @@ You can include the `My Base Section` section like this:
```md
## My Extended Section
<!--@include: ./parts/basics.md#my-base-section-->
<!--@@include: ./parts/basics.md#my-base-section-->
```
**Equivalent code**
@ -941,7 +941,7 @@ Here, `my-base-section` is the generated id of the heading element. In case it's
and include it like this:
```md
<!--@include: ./parts/basics.md#custom-id-->
<!--@@include: ./parts/basics.md#custom-id-->
```
## Math Equations

@ -4,7 +4,7 @@ layout: home
hero:
name: VitePress
text: Vite & Vue Powered Static Site Generator
tagline: Markdown to Beautiful Docs in Minutes
tagline: Markdown to beautiful docs in minutes
actions:
- theme: brand
text: What is VitePress?
@ -21,7 +21,7 @@ hero:
features:
- icon: 📝
title: Focus on Your Content
title: Focus on your content
details: Effortlessly create beautiful documentation sites with just markdown.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 256.32"><defs><linearGradient id="a" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"/><stop offset="100%" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"/><stop offset="8.333%" stop-color="#FFDD35"/><stop offset="100%" stop-color="#FFA800"/></linearGradient></defs><path fill="url(#a)" d="M255.153 37.938 134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"/><path fill="url(#b)" d="M185.432.063 96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028 72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"/></svg>
title: Enjoy the Vite DX
@ -30,6 +30,6 @@ features:
title: Customize with Vue
details: Use Vue syntax and components directly in markdown, or build custom themes with Vue.
- icon: 🚀
title: Ship Fast Sites
title: Ship fast sites
details: Fast initial load with static HTML, fast post-load navigation with client-side routing.
---

@ -179,7 +179,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.title)
return await md.renderAsync(`# ${env.frontmatter.title}`) + html
return (await md.renderAsync(`# ${env.frontmatter.title}`)) + html
return html
}
}
@ -213,6 +213,19 @@ export default defineConfig({
You can use a configuration like this to enable multilingual search:
<details>
<summary>View full example</summary>
<<< @/snippets/algolia-i18n.ts
</details>
Refer [official Algolia docs](https://docsearch.algolia.com/docs/api#translations) to learn more about them. To quickly get started, you can also copy the translations used by this site from [our GitHub repo](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code).
### Algolia Ask AI Support {#ask-ai}
If you would like to include **Ask AI**, pass the `askAi` option (or any of the partial fields) inside `options`:
```ts
import { defineConfig } from 'vitepress'
@ -224,79 +237,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
clearButtonTitle: '清除查询条件',
clearButtonAriaLabel: '清除查询条件',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档',
placeholderTextAskAi: '向 AI 提问:',
placeholderTextAskAiStreaming: '回答中...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键字搜索',
backToKeywordSearchButtonAriaLabel: '返回关键字搜索'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除',
recentConversationsTitle: '最近的对话',
removeRecentConversationButtonTitle: '从历史记录中删除对话'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
},
resultsScreen: {
askAiPlaceholder: '向 AI 提问: '
},
askAiScreen: {
disclaimerText: '答案由 AI 生成,可能不准确,请自行验证。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '赞',
dislikeButtonTitle: '踩',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索 ',
afterToolCallText: '已搜索'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: 'Enter 键',
navigateText: '切换',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '搜索提供者'
}
}
}
}
// askAi: "YOUR-ASSISTANT-ID"
// OR
askAi: {
// at minimum you must provide the assistantId you received from Algolia
assistantId: 'XXXYYY',
// optional overrides if omitted, the top-level appId/apiKey/indexName values are reused
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -304,11 +253,13 @@ export default defineConfig({
})
```
[These options](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) can be overridden. Refer to the official Algolia docs to learn more about them.
::: warning Note
If you want to default to keyword search and do not want to use Ask AI, omit the `askAi` property.
:::
### Algolia Ask AI Support {#ask-ai}
### Ask AI Side Panel {#ask-ai-side-panel}
If you would like to include **Ask AI**, pass the `askAi` option (or any of the partial fields) inside `options`:
DocSearch v4.5+ supports an optional **Ask AI side panel**. When enabled, it can be opened with **Ctrl/Cmd+I** by default. The [Sidepanel API Reference](https://docsearch.algolia.com/docs/sidepanel/api-reference) contains the full list of options.
```ts
import { defineConfig } from 'vitepress'
@ -321,15 +272,17 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
// askAi: "YOUR-ASSISTANT-ID"
// OR
askAi: {
// at minimum you must provide the assistantId you received from Algolia
assistantId: 'XXXYYY',
// optional overrides if omitted, the top-level appId/apiKey/indexName values are reused
// apiKey: '...',
// appId: '...',
// indexName: '...'
sidePanel: {
panel: {
variant: 'floating', // or 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
@ -337,116 +290,70 @@ export default defineConfig({
})
```
::: warning Note
If you want to default to keyword search and not use Ask AI, just omit the `askAi` property
:::
If you need to disable the keyboard shortcut, use the `keyboardShortcuts` option at the sidepanel root level:
The translations for the Ask AI UI live under `options.translations.modal.askAiScreen` and `options.translations.resultsScreen` — see the [type definitions](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) for all keys.
```ts
import { defineConfig } from 'vitepress'
### Crawler Config
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
Here is an example config based on what this site uses:
#### Mode (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
You can optionally control how VitePress integrates keyword search and Ask AI:
- `mode: 'auto'` (default): infer `hybrid` when keyword search is configured, otherwise `sidePanel` when Ask AI side panel is configured.
- `mode: 'sidePanel'`: force side panel only (hides the keyword search button).
- `mode: 'hybrid'`: enable keyword search modal + Ask AI side panel (requires keyword search configuration).
- `mode: 'modal'`: keep Ask AI inside the DocSearch modal (even if you configured the side panel).
#### Ask AI only (no keyword search) {#ask-ai-only}
If you want to use **Ask AI side panel only**, you can omit top-level keyword search config and provide credentials under `askAi`:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
### Crawler Config
Here is an example config based on what this site uses:
<<< @/snippets/algolia-crawler.js

@ -49,7 +49,7 @@ interface PageData {
titleTemplate?: string | boolean
description: string
relativePath: string
filePath: string,
filePath: string
headers: Header[]
frontmatter: Record<string, any>
params?: Record<string, any>

@ -181,7 +181,6 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'Buscar documentos',
translations: {
button: {
buttonText: 'Buscar',
@ -189,46 +188,72 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
},
modal: {
searchBox: {
clearButtonTitle: 'Limpiar búsqueda',
clearButtonAriaLabel: 'Limpiar búsqueda',
clearButtonTitle: 'Limpiar',
clearButtonAriaLabel: 'Borrar la consulta',
closeButtonText: 'Cerrar',
closeButtonAriaLabel: 'Cerrar',
placeholderText: undefined,
placeholderTextAskAi: undefined,
placeholderText: 'Buscar en la documentación o preguntar a Ask AI',
placeholderTextAskAi: 'Haz otra pregunta...',
placeholderTextAskAiStreaming: 'Respondiendo...',
searchInputLabel: 'Buscar',
backToKeywordSearchButtonText:
'Volver a la búsqueda por palabras clave',
backToKeywordSearchButtonAriaLabel:
'Volver a la búsqueda por palabras clave'
'Volver a la búsqueda por palabras clave',
newConversationPlaceholder: 'Haz una pregunta',
conversationHistoryTitle: 'Mi historial de conversaciones',
startNewConversationText: 'Iniciar una nueva conversación',
viewConversationHistoryText: 'Historial de conversaciones',
threadDepthErrorPlaceholder: 'Se alcanzó el límite de conversación'
},
newConversation: {
newConversationTitle: '¿Cómo puedo ayudarte hoy?',
newConversationDescription:
'Busco en tu documentación para ayudarte a encontrar guías de configuración, detalles de funciones y consejos de solución de problemas rápidamente.'
},
footer: {
selectText: 'Seleccionar',
submitQuestionText: 'Enviar pregunta',
selectKeyAriaLabel: 'Tecla Enter',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Flecha arriba',
navigateDownKeyAriaLabel: 'Flecha abajo',
closeText: 'Cerrar',
backToSearchText: 'Volver a la búsqueda',
closeKeyAriaLabel: 'Tecla Escape',
poweredByText: 'Con la tecnología de'
},
errorScreen: {
titleText: 'No se pueden obtener resultados',
helpText: 'Puede que quieras comprobar tu conexión de red.'
},
startScreen: {
recentSearchesTitle: 'Historial de búsqueda',
noRecentSearchesText: 'Ninguna búsqueda reciente',
saveRecentSearchButtonTitle: 'Guardar en el historial de búsqueda',
removeRecentSearchButtonTitle: 'Borrar del historial de búsqueda',
recentSearchesTitle: 'Recientes',
noRecentSearchesText: 'No hay búsquedas recientes',
saveRecentSearchButtonTitle: 'Guardar esta búsqueda',
removeRecentSearchButtonTitle: 'Eliminar esta búsqueda del historial',
favoriteSearchesTitle: 'Favoritos',
removeFavoriteSearchButtonTitle: 'Borrar de favoritos',
removeFavoriteSearchButtonTitle:
'Eliminar esta búsqueda de favoritos',
recentConversationsTitle: 'Conversaciones recientes',
removeRecentConversationButtonTitle:
'Eliminar esta conversación del historial'
},
errorScreen: {
titleText: 'No fue posible obtener resultados',
helpText: 'Verifique su conexión de red'
},
noResultsScreen: {
noResultsText: 'No fue posible encontrar resultados',
suggestedQueryText: 'Puede intentar una nueva búsqueda',
noResultsText: 'No se encontraron resultados para',
suggestedQueryText: 'Intenta buscar',
reportMissingResultsText:
'¿Deberían haber resultados para esta consulta?',
reportMissingResultsLinkText: 'Click para enviar feedback'
'¿Crees que esta consulta debería devolver resultados?',
reportMissingResultsLinkText: 'Avísanos.'
},
resultsScreen: {
askAiPlaceholder: 'Preguntar a la IA: '
askAiPlaceholder: 'Preguntar a la IA: ',
noResultsAskAiPlaceholder:
'¿No lo encontraste en la documentación? Pide ayuda a Ask AI: '
},
askAiScreen: {
disclaimerText:
'Las respuestas son generadas por IA y pueden contener errores. Verifica las respuestas.',
'Las respuestas se generan con IA y pueden contener errores. Verifícalas.',
relatedSourcesText: 'Fuentes relacionadas',
thinkingText: 'Pensando...',
copyButtonText: 'Copiar',
@ -236,23 +261,70 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
copyButtonTitle: 'Copiar',
likeButtonTitle: 'Me gusta',
dislikeButtonTitle: 'No me gusta',
thanksForFeedbackText: '¡Gracias por tu opinión!',
thanksForFeedbackText: '¡Gracias por tu comentario!',
preToolCallText: 'Buscando...',
duringToolCallText: 'Buscando ',
afterToolCallText: 'Búsqueda de',
aggregatedToolCallText: 'Búsqueda de'
duringToolCallText: 'Buscando...',
afterToolCallText: 'Buscado',
stoppedStreamingText: 'Has detenido esta respuesta',
errorTitleText: 'Error de chat',
threadDepthExceededMessage:
'Esta conversación se ha cerrado para mantener respuestas precisas.',
startNewConversationButtonText: 'Iniciar una nueva conversación'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'Preguntar a la IA',
buttonAriaLabel: 'Preguntar a la IA'
}
},
footer: {
selectText: 'Seleccionar',
submitQuestionText: 'Enviar pregunta',
selectKeyAriaLabel: 'Tecla Enter',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Flecha arriba',
navigateDownKeyAriaLabel: 'Flecha abajo',
closeText: 'Cerrar',
backToSearchText: 'Volver a la búsqueda',
closeKeyAriaLabel: 'Tecla Escape',
poweredByText: 'Búsqueda por'
panel: {
translations: {
header: {
title: 'Preguntar a la IA',
conversationHistoryTitle: 'Mi historial de conversaciones',
newConversationText: 'Iniciar una nueva conversación',
viewConversationHistoryText: 'Historial de conversaciones'
},
promptForm: {
promptPlaceholderText: 'Haz una pregunta',
promptAnsweringText: 'Respondiendo...',
promptAskAnotherQuestionText: 'Haz otra pregunta',
promptDisclaimerText:
'Las respuestas se generan con IA y pueden contener errores.',
promptLabelText:
'Pulsa Enter para enviar, o Shift+Enter para una nueva línea.',
promptAriaLabelText: 'Entrada de prompt'
},
conversationScreen: {
preToolCallText: 'Buscando...',
searchingText: 'Buscando...',
toolCallResultText: 'Buscado',
conversationDisclaimer:
'Las respuestas se generan con IA y pueden contener errores. Verifícalas.',
reasoningText: 'Razonando...',
thinkingText: 'Pensando...',
relatedSourcesText: 'Fuentes relacionadas',
stoppedStreamingText: 'Has detenido esta respuesta',
copyButtonText: 'Copiar',
copyButtonCopiedText: '¡Copiado!',
likeButtonTitle: 'Me gusta',
dislikeButtonTitle: 'No me gusta',
thanksForFeedbackText: '¡Gracias por tu comentario!',
errorTitleText: 'Error de chat'
},
newConversationScreen: {
titleText: '¿Cómo puedo ayudarte hoy?',
introductionText:
'Busco en tu documentación para ayudarte a encontrar guías de configuración, detalles de funciones y consejos de solución de problemas rápidamente.'
},
logo: {
poweredByText: 'Con la tecnología de'
}
}
}
}
}

@ -105,13 +105,13 @@ Nota: el archivo `vercel.json` debe ser colocado en la raiz de su **repositório
## Guias de Plataforma {#platform-guides}
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#generic}
Configure un nuevo proyecto y altere estas configuraciones usando su panel:
- **Comando de Compilación:** `npm run docs:build`
- **directorio de Salida:** `docs/.vitepress/dist`
- **Versión de Node:** `18` (o superior)
- **Versión de Node:** `20` (o superior)
::: warning
No active opciones como _Auto Minify_ para código HTML. Eso removera comentarios de salida que tiene significado para Vue. Habrán errores de incompatibilidad de hidratación se fueran removidos.
@ -169,10 +169,8 @@ No active opciones como _Auto Minify_ para código HTML. Eso removera comentario
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci # o pnpm install / yarn install / bun install
- name: Build with VitePress
run: |
npm run docs:build # o pnpm docs:build / yarn docs:build / bun run docs:build
touch docs/.vitepress/dist/.nojekyll
- name: Build with VitePress
run: npm run docs:build # o pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
@ -192,6 +190,7 @@ No active opciones como _Auto Minify_ para código HTML. Eso removera comentario
uses: actions/deploy-pages@v4
```
::: warning
Asegurese de que la opción `base` en su VitePress esté configurada correctamentse. Vea [Configuranco un Path base Público](#setting-a-public-base-path) para más detalles.
:::
@ -201,7 +200,7 @@ No active opciones como _Auto Minify_ para código HTML. Eso removera comentario
### GitLab Pages
1. Defina `outDir` en la configuración VitePress como `../public`. Configure la opción `base` para `'/<repository>/'` se desea implantar en `https://<username>.gitlab.io/<repository>/`.
1. Defina `outDir` en la configuración VitePress como `../public`. Configure la opción `base` para `'/<repository>/'` se desea implantar en `https://<username>.gitlab.io/<repository>/`. No necesita `base` si está implementando en un dominio personalizado, páginas de usuario o grupo, o si la configuración "Use unique domain" está habilitada en GitLab.
2. Cree un archivo llamado `.gitlab-ci.yml` en la raiz del proyecto con el contenido abajo. Esto construirá e implantará su sitio siempre que haga alteraciones en el contenido.
@ -222,7 +221,7 @@ No active opciones como _Auto Minify_ para código HTML. Eso removera comentario
- main
```
### Azure Static Web Apps {#azure-static-web-apps}
### Azure
1. Siga la [documentación oficial](https://docs.microsoft.com/en-us/azure/static-web-apps/build-configuration).
@ -232,7 +231,11 @@ No active opciones como _Auto Minify_ para código HTML. Eso removera comentario
- **`output_location`**: `docs/.vitepress/dist`
- **`app_build_command`**: `npm run docs:build`
### Firebase {#firebase}
### CloudRay
Puedes desplegar tu proyecto VitePress con [CloudRay](https://cloudray.io/) siguiendo estas [instrucciones](https://cloudray.io/articles/how-to-deploy-vitepress-site).
### Firebase
1. Cree `firebase.json` y `.firebaserc` en la raiz de su proyecto:
@ -263,14 +266,6 @@ No active opciones como _Auto Minify_ para código HTML. Eso removera comentario
firebase deploy
```
### Surge
1. Después de ejecutar `npm run docs:build`, ejecute este comando para implantar:
```sh
npx surge docs/.vitepress/dist
```
### Heroku
1. Siga la documentación y el guia proporcionados por [`heroku-buildpack-static`](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-static).
@ -283,10 +278,66 @@ No active opciones como _Auto Minify_ para código HTML. Eso removera comentario
}
```
### Edgio
### Hostinger
Consulte [Crear e Implantar una Aplicación VitePress en Edgio](https://docs.edg.io/guides/vitepress).
Puedes desplegar tu proyecto VitePress con [Hostinger](https://www.hostinger.com/web-apps-hosting) siguiendo estas [instrucciones](https://www.hostinger.com/support/how-to-deploy-a-nodejs-website-in-hostinger/). Al configurar los ajustes de compilación, elige VitePress como framework y ajusta el directorio raíz a `./docs`.
### Kinsta Static Site Hosting {#kinsta-static-site-hosting}
### Kinsta
Puede implantar su sitio VitePress em [Kinsta](https://kinsta.com/static-site-hosting/) siguiendo estas [instrucciones](https://kinsta.com/docs/vitepress-static-site-example/).
### Stormkit
Puedes desplegar tu proyecto VitePress en [Stormkit](https://www.stormkit.io) siguiendo estas [instrucciones](https://stormkit.io/blog/how-to-deploy-vitepress).
### Surge
1. Después de ejecutar `npm run docs:build`, ejecute este comando para implantar:
```sh
npx surge docs/.vitepress/dist
```
### Nginx
Aquí hay un ejemplo de configuración de bloque de servidor Nginx. Esta configuración incluye compresión gzip para recursos comunes basados en texto, reglas para servir los archivos estáticos de su sitio VitePress con encabezados de caché adecuados, así como el manejo de `cleanUrls: true`.
```nginx
server {
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
listen 80;
server_name _;
index index.html;
location / {
# content location
root /app;
# exact matches -> reverse clean urls -> folders -> not found
try_files $uri $uri.html $uri/ =404;
# non existent pages
error_page 404 /404.html;
# a folder without index.html raises 403 in this setup
error_page 403 /404.html;
# adjust caching headers
# files in the assets folder have hashes filenames
location ~* ^/assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
}
```
Esta configuración asume que su sitio VitePress compilado está ubicado en el directorio `/app` de su servidor. Ajuste la directiva `root` según corresponda si los archivos de su sitio se encuentran en otro lugar.
::: warning No predeterminar index.html
La resolución de try_files no debe predeterminar index.html como en otras aplicaciones Vue. Esto resultará en un estado de página inválido.
:::
Se puede encontrar más información en la [documentación oficial de nginx](https://nginx.org/en/docs/), en estos issues [#2837](https://github.com/vuejs/vitepress/discussions/2837), [#3235](https://github.com/vuejs/vitepress/issues/3235) así como en este [post del blog](https://blog.mehdi.cc/articles/vitepress-cleanurls-on-nginx-environment#readings) de Mehdi Merah.

@ -773,7 +773,7 @@ Por ejemplo, puede incluir un archivo markdown relativo usando esto:
## Conceptos Básicos
<!--@include: ./parts/basics.md-->
<!--@@include: ./parts/basics.md-->
```
**Archivo de Parte** (`parts/basics.md`)
@ -809,7 +809,7 @@ También soporta la selección de un intervalo de lineas:
## Conceptos Básicos
<!--@include: ./parts/basics.md{3,}-->
<!--@@include: ./parts/basics.md{3,}-->
```
**Archivo de Parte** (`parts/basics.md`)

@ -39,18 +39,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
zh: {
es: { // usa `root` si quieres traducir la configuración regional predeterminada
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
buttonText: 'Buscar',
buttonAriaLabel: 'Buscar'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
displayDetails: 'Mostrar lista detallada',
resetButtonTitle: 'Restablecer búsqueda',
backButtonTitle: 'Cerrar búsqueda',
noResultsText: 'No hay resultados',
footer: {
selectText: '选择',
navigateText: '切换'
selectText: 'Seleccionar',
selectKeyAriaLabel: 'Intro',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Flecha arriba',
navigateDownKeyAriaLabel: 'Flecha abajo',
closeText: 'Cerrar',
closeKeyAriaLabel: 'Esc'
}
}
}
@ -62,7 +69,7 @@ export default defineConfig({
})
```
### Opciones MiniSearch {#mini-search-options}
### Opciones MiniSearch {#minisearch-options}
Puedes configurar MiniSearch de esta manera:
@ -116,7 +123,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// retorne un string HTML
// devuelve una cadena HTML
}
}
}
@ -141,7 +148,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.search === false) return ''
if (env.relativePath.startsWith('algum/caminho')) return ''
if (env.relativePath.startsWith('some/path')) return ''
return html
}
}
@ -167,7 +174,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.title)
return await md.renderAsync(`# ${env.frontmatter.title}`) + html
return (await md.renderAsync(`# ${env.frontmatter.title}`)) + html
return html
}
}
@ -197,10 +204,23 @@ export default defineConfig({
})
```
### i18n {#algolia-search-i18n} {#algolia-search-i18n}
### i18n {#algolia-search-i18n}
Puedes utilizar una configuración como esta para utilizar la búsqueda multilingüe:
<details>
<summary>Haz clic para expandir</summary>
<<< @/snippets/algolia-i18n.ts
</details>
Consulta la [documentación oficial de Algolia](https://docsearch.algolia.com/docs/api#translations) para conocer más detalles. Para empezar rápidamente, también puedes copiar las traducciones usadas por este sitio desde [nuestro repositorio de GitHub](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code).
### Algolia Ask AI Support {#ask-ai}
Si deseas incluir **Ask AI**, pasa la opción `askAi` (o alguno de sus campos parciales) dentro de `options`:
```ts
import { defineConfig } from 'vitepress'
@ -212,72 +232,51 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: { buttonText: '搜索文档', buttonAriaLabel: '搜索文档' },
modal: {
searchBox: {
clearButtonTitle: '清除查询条件',
clearButtonAriaLabel: '清除查询条件',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档',
placeholderTextAskAi: '向 AI 提问:',
placeholderTextAskAiStreaming: '回答中...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键字搜索',
backToKeywordSearchButtonAriaLabel: '返回关键字搜索'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除',
recentConversationsTitle: '最近的对话',
removeRecentConversationButtonTitle: '从历史记录中删除对话'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
},
resultsScreen: { askAiPlaceholder: '向 AI 提问: ' },
askAiScreen: {
disclaimerText: '答案由 AI 生成,可能不准确,请自行验证。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '赞',
dislikeButtonTitle: '踩',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索 ',
afterToolCallText: '已搜索'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: 'Enter 键',
navigateText: '切换',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '搜索提供者'
}
}
// askAi: "TU-ID-DE-ASISTENTE"
// O
askAi: {
// como mínimo debes proporcionar el assistantId que recibiste de Algolia
assistantId: 'XXXYYY',
// anulaciones opcionales — si se omiten, se reutilizan los valores appId/apiKey/indexName de nivel superior
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
}
})
```
::: warning Nota
Si prefieres solo la búsqueda por palabra clave y no la Ask AI, simplemente omite `askAi`.
:::
### Panel lateral de Ask AI {#ask-ai-side-panel}
DocSearch v4.5+ admite un **panel lateral de Ask AI** opcional. Cuando está habilitado, se puede abrir con **Ctrl/Cmd+I** por defecto. La [Referencia de API del Panel Lateral](https://docsearch.algolia.com/docs/sidepanel/api-reference) contiene la lista completa de opciones.
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// Refleja la API de @docsearch/sidepanel-js SidepanelProps
panel: {
variant: 'floating', // o 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
@ -287,132 +286,70 @@ export default defineConfig({
})
```
### Algolia Ask AI Support {#ask-ai}
Si deseas incluir **Ask AI**, pasa la opción `askAi` (o alguno de sus campos parciales) dentro de `options`:
Si necesitas deshabilitar el atajo de teclado, usa la opción `keyboardShortcuts` del panel lateral:
```ts
options: {
appId: '...',
apiKey: '...',
indexName: '...',
// askAi: 'TU-ASSISTANT-ID'
askAi: {
assistantId: 'XXXYYY'
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
}
})
```
::: warning Nota
Si prefieres solo la búsqueda por palabra clave y no la Ask AI, simplemente omite `askAi`.
:::
#### Modo (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
[Estas opciones](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) se pueden superponer. Consulte la documentación oficial de Algolia para obtener más información sobre ellos.
Puedes controlar opcionalmente cómo VitePress integra la búsqueda por palabra clave y Ask AI:
### Configuración _Crawler_ {#crawler-config}
- `mode: 'auto'` (por defecto): infiere `hybrid` cuando la búsqueda por palabra clave está configurada, de lo contrario `sidePanel` cuando el panel lateral de Ask AI está configurado.
- `mode: 'sidePanel'`: fuerza solo el panel lateral (oculta el botón de búsqueda por palabra clave).
- `mode: 'hybrid'`: habilita el modal de búsqueda por palabra clave + panel lateral de Ask AI (requiere configuración de búsqueda por palabra clave).
- `mode: 'modal'`: mantiene Ask AI dentro del modal de DocSearch (incluso si configuraste el panel lateral).
A continuación se muestra un ejemplo de la configuración que utiliza este sitio:
#### Solo Ask AI (sin búsqueda por palabra clave) {#ask-ai-only}
Si quieres usar **solo el panel lateral de Ask AI**, puedes omitir la configuración de búsqueda por palabra clave de nivel superior y proporcionar las credenciales bajo `askAi`:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
### Configuración _Crawler_ {#crawler-config}
A continuación se muestra un ejemplo de la configuración que utiliza este sitio:
<<< @/snippets/algolia-crawler.js

@ -45,7 +45,7 @@ interface PageData {
titleTemplate?: string | boolean
description: string
relativePath: string
filePath: string,
filePath: string
headers: Header[]
frontmatter: Record<string, any>
params?: Record<string, any>

@ -182,7 +182,6 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'جستجوی مستندات',
translations: {
button: {
buttonText: 'جستجو',
@ -190,67 +189,139 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
},
modal: {
searchBox: {
clearButtonTitle: 'پاک کردن جستجو',
clearButtonAriaLabel: 'پاک کردن جستجو',
clearButtonTitle: 'پاک کردن',
clearButtonAriaLabel: 'پاک کردن عبارت جستجو',
closeButtonText: 'بستن',
closeButtonAriaLabel: 'بستن',
placeholderText: 'جستجوی مستندات',
placeholderTextAskAi: 'از هوش مصنوعی بپرسید: ',
placeholderTextAskAiStreaming: 'در حال پاسخ...',
placeholderText: 'در مستندات جستجو کنید یا از Ask AI بپرسید',
placeholderTextAskAi: 'سؤال دیگری بپرسید...',
placeholderTextAskAiStreaming: 'در حال پاسخ گویی...',
searchInputLabel: 'جستجو',
backToKeywordSearchButtonText: 'بازگشت به جستجوی کلیدواژه',
backToKeywordSearchButtonAriaLabel: 'بازگشت به جستجوی کلیدواژه'
backToKeywordSearchButtonAriaLabel: 'بازگشت به جستجوی کلیدواژه',
newConversationPlaceholder: 'یک سؤال بپرسید',
conversationHistoryTitle: 'تاریخچه گفت وگوی من',
startNewConversationText: 'شروع گفت وگوی جدید',
viewConversationHistoryText: 'تاریخچه گفت وگو',
threadDepthErrorPlaceholder: 'محدودیت گفت وگو رسید'
},
startScreen: {
recentSearchesTitle: 'جستجوهای اخیر',
noRecentSearchesText: 'هیچ جستجوی اخیر',
saveRecentSearchButtonTitle: 'ذخیره در تاریخچه جستجو',
removeRecentSearchButtonTitle: 'حذف از تاریخچه جستجو',
favoriteSearchesTitle: 'علاقه‌مندی‌ها',
removeFavoriteSearchButtonTitle: 'حذف از علاقه‌مندی‌ها',
recentConversationsTitle: 'گفتگوهای اخیر',
removeRecentConversationButtonTitle: 'حذف این گفتگو از تاریخچه'
newConversation: {
newConversationTitle: 'امروز چگونه می توانم کمک کنم؟',
newConversationDescription:
'در مستندات شما جستجو می کنم تا سریع راهنماهای راه اندازی، جزئیات ویژگی ها و نکات رفع اشکال را پیدا کنم.'
},
footer: {
selectText: 'انتخاب',
submitQuestionText: 'ارسال سؤال',
selectKeyAriaLabel: 'کلید Enter',
navigateText: 'پیمایش',
navigateUpKeyAriaLabel: 'پیکان بالا',
navigateDownKeyAriaLabel: 'پیکان پایین',
closeText: 'بستن',
backToSearchText: 'بازگشت به جستجو',
closeKeyAriaLabel: 'کلید Escape',
poweredByText: 'قدرت گرفته از'
},
errorScreen: {
titleText: 'عدم امکان دریافت نتایج',
helpText: 'اتصال شبکه خود را بررسی کنید'
titleText: 'امکان دریافت نتایج وجود ندارد',
helpText: 'ممکن است لازم باشد اتصال شبکه را بررسی کنید.'
},
startScreen: {
recentSearchesTitle: 'اخیر',
noRecentSearchesText: 'جستجوی اخیر وجود ندارد',
saveRecentSearchButtonTitle: 'ذخیره این جستجو',
removeRecentSearchButtonTitle: 'حذف این جستجو از تاریخچه',
favoriteSearchesTitle: 'علاقه مندی ها',
removeFavoriteSearchButtonTitle: 'حذف این جستجو از علاقه مندی ها',
recentConversationsTitle: 'گفت وگوهای اخیر',
removeRecentConversationButtonTitle: 'حذف این گفت وگو از تاریخچه'
},
noResultsScreen: {
noResultsText: 'هیچ نتیجه‌ای یافت نشد',
suggestedQueryText: 'می‌توانید جستجوی دیگری امتحان کنید',
reportMissingResultsText: 'فکر می‌کنید باید نتیجه‌ای نمایش داده شود؟',
reportMissingResultsLinkText: 'برای ارسال بازخورد کلیک کنید'
noResultsText: 'هیچ نتیجه ای برای',
suggestedQueryText: 'سعی کنید جستجو کنید',
reportMissingResultsText:
'فکر می کنید این جستجو باید نتیجه داشته باشد؟',
reportMissingResultsLinkText: 'به ما اطلاع دهید.'
},
resultsScreen: {
askAiPlaceholder: 'از هوش مصنوعی بپرسید: '
askAiPlaceholder: 'از هوش مصنوعی بپرسید: ',
noResultsAskAiPlaceholder:
'در مستندات پیدا نکردید؟ از Ask AI کمک بگیرید: '
},
askAiScreen: {
disclaimerText:
'پاسخها توسط هوش مصنوعی تولید می‌شوند و ممکن است خطا داشته باشند. لطفاً بررسی کنید.',
'پاسخ ها توسط هوش مصنوعی تولید می شوند و ممکن است اشتباه باشند. بررسی کنید.',
relatedSourcesText: 'منابع مرتبط',
thinkingText: 'در حال پردازش...',
thinkingText: 'در حال فکر کردن...',
copyButtonText: 'کپی',
copyButtonCopiedText: 'کپی شد!',
copyButtonTitle: 'کپی',
likeButtonTitle: 'پسندیدم',
dislikeButtonTitle: 'نپسندیدم',
thanksForFeedbackText: 'از بازخورد شما سپاسگزاریم!',
thanksForFeedbackText: 'از بازخورد شما متشکریم!',
preToolCallText: 'در حال جستجو...',
duringToolCallText: 'در حال جستجو برای ',
afterToolCallText: 'جستجو انجام شد',
aggregatedToolCallText: 'جستجو انجام شد'
duringToolCallText: 'در حال جستجو...',
afterToolCallText: 'جستجو برای',
stoppedStreamingText: 'شما این پاسخ را متوقف کردید',
errorTitleText: 'خطای گفتگو',
threadDepthExceededMessage:
'برای حفظ دقت پاسخ ها، این گفت وگو بسته شد.',
startNewConversationButtonText: 'شروع گفت وگوی جدید'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'از هوش مصنوعی بپرسید',
buttonAriaLabel: 'از هوش مصنوعی بپرسید'
}
},
footer: {
selectText: 'انتخاب',
submitQuestionText: 'ارسال پرسش',
selectKeyAriaLabel: 'کلید Enter',
navigateText: 'حرکت',
navigateUpKeyAriaLabel: 'کلید جهت بالا',
navigateDownKeyAriaLabel: 'کلید جهت پایین',
closeText: 'بستن',
backToSearchText: 'بازگشت به جستجو',
closeKeyAriaLabel: 'کلید Escape',
poweredByText: 'جستجو توسط'
panel: {
translations: {
header: {
title: 'از هوش مصنوعی بپرسید',
conversationHistoryTitle: 'تاریخچه گفت وگوی من',
newConversationText: 'شروع گفت وگوی جدید',
viewConversationHistoryText: 'تاریخچه گفت وگو'
},
promptForm: {
promptPlaceholderText: 'یک سؤال بپرسید',
promptAnsweringText: 'در حال پاسخ گویی...',
promptAskAnotherQuestionText: 'سؤال دیگری بپرسید',
promptDisclaimerText:
'پاسخ ها توسط هوش مصنوعی تولید می شوند و ممکن است اشتباه باشند.',
promptLabelText:
'برای ارسال Enter را بزنید، یا برای خط جدید Shift+Enter.',
promptAriaLabelText: 'ورودی پرسش'
},
conversationScreen: {
preToolCallText: 'در حال جستجو...',
searchingText: 'در حال جستجو...',
toolCallResultText: 'جستجو برای',
conversationDisclaimer:
'پاسخ ها توسط هوش مصنوعی تولید می شوند و ممکن است اشتباه باشند. بررسی کنید.',
reasoningText: 'در حال استدلال...',
thinkingText: 'در حال فکر کردن...',
relatedSourcesText: 'منابع مرتبط',
stoppedStreamingText: 'شما این پاسخ را متوقف کردید',
copyButtonText: 'کپی',
copyButtonCopiedText: 'کپی شد!',
likeButtonTitle: 'پسندیدم',
dislikeButtonTitle: 'نپسندیدم',
thanksForFeedbackText: 'از بازخورد شما متشکریم!',
errorTitleText: 'خطای گفتگو'
},
newConversationScreen: {
titleText: 'امروز چگونه می توانم کمک کنم؟',
introductionText:
'در مستندات شما جستجو می کنم تا سریع راهنماهای راه اندازی، جزئیات ویژگی ها و نکات رفع اشکال را پیدا کنم.'
},
logo: {
poweredByText: 'قدرت گرفته از'
}
}
}
}
}

@ -105,13 +105,13 @@ Cache-Control: max-age=31536000,immutable
## راهنمای‌های پلتفرم {#platform-guides}
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#netlify-vercel-cloudflare-pages-aws-amplify-render}
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#generic}
یک پروژه جدید راه‌اندازی کرده و این تنظیمات را با استفاده از داشبورد خود تغییر دهید:
- **دستور ساخت:** `npm run docs:build`
- **دایرکتوری خروجی:** `docs/.vitepress/dist`
- **نسخه Node:** `18` (یا بالاتر)
- **نسخه Node:** `20` (یا بالاتر)
::: warning هشدار
گزینه‌هایی مانند _Auto Minify_ را برای کد HTML فعال نکنید. این گزینه‌ها ممکن است توضیحاتی را که به Vue معنا دارد، از خروجی حذف کنند. ممکن است خطاهای ناسازگاری را در اجرا ببینید اگر حذف شوند.
@ -198,7 +198,7 @@ Cache-Control: max-age=31536000,immutable
### صفحات GitLab {#gitlab-pages}
1. `outDir` را در پیکربندی ویت‌پرس به `../public` تنظیم کنید. گزینه `base` را به `'/<repository>/'` تنظیم کنید اگر می‌خواهید در `https://<username>.gitlab.io/<repository>/` انتشار دهید.
1. `outDir` را در پیکربندی ویت‌پرس به `../public` تنظیم کنید. گزینه `base` را به `'/<repository>/'` تنظیم کنید اگر می‌خواهید در `https://<username>.gitlab.io/<repository>/` انتشار دهید. اگر روی دامنه سفارشی، صفحات کاربر یا گروه منتشر می‌کنید یا تنظیمات "Use unique domain" در GitLab فعال است، نیازی به `base` ندارید.
2. یک فایل به نام `.gitlab-ci.yml` در ریشه پروژه خود با محتوای زیر ایجاد کنید. این کار به ساخت و انتشار وب‌سایت شما هر زمانی که تغییری در محتوا ایجاد می‌کنید، می‌پردازد:
@ -219,7 +219,7 @@ Cache-Control: max-age=31536000,immutable
- main
```
### Azure Static Web Apps {#azure-static-web-apps}
### Azure
1. دستورالعمل [رسمی](https://docs.microsoft.com/en-us/azure/static-web-apps/build-configuration) را دنبال کنید.
@ -229,7 +229,11 @@ Cache-Control: max-age=31536000,immutable
- **`output_location`**: `docs/.vitepress/dist`
- **`app_build_command`**: `npm run docs:build`
### Firebase {#firebase}
### CloudRay
می‌توانید پروژه ویت‌پرس خود را با [CloudRay](https://cloudray.io/) با دنبال کردن این [دستورالعمل‌ها](https://cloudray.io/articles/how-to-deploy-vitepress-site) منتشر کنید.
### Firebase
1. فایل‌های `firebase.json` و `.firebaserc` را در ریشه پروژه خود ایجاد کنید:
@ -260,15 +264,7 @@ Cache-Control: max-age=31536000,immutable
firebase deploy
```
### Surge {#surge}
1. بعد از اجرای `npm run docs:build`، دستور زیر را برای انتشار اجرا کنید:
```sh
npx surge docs/.vitepress/dist
```
### Heroku {#heroku}
### Heroku
1. دستورالعمل و راهنماها را در [`heroku-buildpack-static`](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-static) دنبال کنید.
@ -280,11 +276,11 @@ Cache-Control: max-age=31536000,immutable
}
```
### Edgio {#edgio}
### Hostinger
به [ایجاد و انتشار یک برنامه ویت‌پرس در Edgio](https://docs.edg.io/guides/vitepress) مراجعه کنید.
می‌توانید پروژه ویت‌پرس خود را با [Hostinger](https://www.hostinger.com/web-apps-hosting) با دنبال کردن این [دستورالعمل‌ها](https://www.hostinger.com/support/how-to-deploy-a-nodejs-website-in-hostinger/) منتشر کنید. هنگام پیکربندی تنظیمات ساخت، VitePress را به عنوان فریم‌ورک انتخاب کنید و ریشه دایرکتوری را به `./docs` تنظیم کنید.
### Kinsta Static Site Hosting {#kinsta-static-site-hosting}
### Kinsta
شما می‌توانید وب‌سایت ویت‌پرس خود را بر روی [Kinsta](https://kinsta.com/static-site-hosting/) با دنبال کردن این [دستورالعمل‌ها](https://kinsta.com/docs/vitepress-static-site-example/) انتشار دهید.
@ -292,6 +288,14 @@ Cache-Control: max-age=31536000,immutable
شما می‌توانید پروژه ویت‌پرس خود را به [Stormkit](https://www.stormkit.io) با دنبال کردن این [دستورالعمل‌ها](https://stormkit.io/blog/how-to-deploy-vitepress) انتشار دهید.
### Surge
1. بعد از اجرای `npm run docs:build`، دستور زیر را برای انتشار اجرا کنید:
```sh
npx surge docs/.vitepress/dist
```
### Nginx
اینجا یک مثال از پیکربندی بلوک سرور Nginx است. این تنظیم شامل فشرده‌سازی gzip برای فایل‌های متن معمولی، قوانین برای سرویس فایل‌های ایستا سایت ویت‌پرس شما با هدرهای مناسب برای حافظه‌نگهداری مناسب است و همچنین مدیریت `cleanUrls: true` می‌کند.

@ -724,7 +724,7 @@ export default config
## مبانی
<!--@include: ./parts/basics.md-->
<!--@@include: ./parts/basics.md-->
```
**قسمت فایل** (`parts/basics.md`)
@ -760,7 +760,7 @@ export default config
## مبانی
<!--@include: ./parts/basics.md{3,}-->
<!--@@include: ./parts/basics.md{3,}-->
```
**قسمت فایل** (`parts/basics.md`)
@ -796,8 +796,8 @@ export default config
## مبانی
<!--@include: ./parts/basics.md#basic-usage{,2}-->
<!--@include: ./parts/basics.md#basic-usage{5,}-->
<!--@@include: ./parts/basics.md#basic-usage{,2}-->
<!--@@include: ./parts/basics.md#basic-usage{5,}-->
```
**قسمت فایل** (`parts/basics.md`)

@ -39,25 +39,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
zh: { // اگر می‌خواهید زبان پیش‌فرض را ترجمه کنید، این را به `root` تغییر دهید
fa: { // اگر می خواهید زبان پیش فرض را ترجمه کنید، این را `root` قرار دهید
translations: {
button: {
buttonText: 'جستجو',
buttonAriaLabel: 'جستجو'
},
modal: {
displayDetails: 'نمایش جزئیات',
displayDetails: 'نمایش فهرست کامل',
resetButtonTitle: 'بازنشانی جستجو',
backButtonTitle: 'بستن جستجو',
noResultsText: 'نتیجهای یافت نشد',
noResultsText: 'نتیجه ای یافت نشد',
footer: {
selectText: 'انتخاب',
selectKeyAriaLabel: 'ورود',
selectKeyAriaLabel: 'Enter',
navigateText: 'پیمایش',
navigateUpKeyAriaLabel: 'کلید بالا',
navigateDownKeyAriaLabel: 'کلید پایین',
navigateUpKeyAriaLabel: 'فلش بالا',
navigateDownKeyAriaLabel: 'فلش پایین',
closeText: 'بستن',
closeKeyAriaLabel: 'esc'
closeKeyAriaLabel: 'Esc'
}
}
}
@ -123,7 +123,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// بازگشت رشته HTML
// رشته HTML را برمی گرداند
}
}
}
@ -174,7 +174,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.title)
return await md.renderAsync(`# ${env.frontmatter.title}`) + html
return (await md.renderAsync(`# ${env.frontmatter.title}`)) + html
return html
}
}
@ -208,6 +208,19 @@ export default defineConfig({
می‌توانید با استفاده از تنظیماتی مانند این برای جستجوی چندزبانه استفاده کنید:
<details>
<summary>برای باز کردن کلیک کنید</summary>
<<< @/snippets/algolia-i18n.ts
</details>
برای اطلاعات بیشتر به [مستندات رسمی Algolia](https://docsearch.algolia.com/docs/api#translations) مراجعه کنید. برای شروع سریع‌تر، می‌توانید ترجمه‌های استفاده‌شده در این سایت را از [مخزن GitHub ما](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code) کپی کنید.
### پشتیبانی Algolia Ask AI {#ask-ai}
برای فعال‌سازی **Ask AI** کافی است گزینه `askAi` را اضافه کنید:
```ts
import { defineConfig } from 'vitepress'
@ -219,46 +232,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除搜索条件',
resetButtonAriaLabel: '清除搜索条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '最近搜索',
noRecentSearchesText: '没有最近搜索',
saveRecentSearchButtonTitle: '保存到最近搜索',
removeRecentSearchButtonTitle: '从最近搜索中删除'
},
errorScreen: {
titleText: '无法显示结果',
helpText: '您可能需要检查您的互联网连接'
},
footer: {
selectText: '选择',
navigateText: '导航',
closeText: '关闭',
searchByText: '搜索由'
},
noResultsScreen: {
noResultsText: '没有找到结果',
suggestedQueryText: '您可以尝试',
reportMissingResultsText: '您认为应该有结果吗?',
reportMissingResultsLinkText: '点击这里报告'
}
}
}
}
// askAi: "شناسه-دستیار-شما"
// یا
askAi: {
// حداقل باید assistantId دریافت شده از Algolia را ارائه کنید
assistantId: 'XXXYYY',
// بازنویسی های اختیاری — اگر حذف شوند، مقادیر appId/apiKey/indexName سطح بالا دوباره استفاده می شوند
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -266,131 +248,108 @@ export default defineConfig({
})
```
این [گزینه‌ها](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) می‌توانند بازنویسی شوند. برای یادگیری بیشتر درباره آن‌ها به اسناد رسمی Algolia مراجعه کنید.
::: warning نکته
اگر فقط به جستجوی کلمات کلیدی نیاز دارید، `askAi` را اضافه نکنید.
:::
### پیکربندی Crawler {#crawler-config}
### پنل کناری Ask AI {#ask-ai-side-panel}
در اینجا یک پیکربندی نمونه بر اساس آنچه که این سایت استفاده می‌کند آمده است:
DocSearch v4.5+ از **پنل کناری Ask AI** اختیاری پشتیبانی می‌کند. وقتی فعال باشد، به طور پیش‌فرض می‌توان آن را با **Ctrl/Cmd+I** باز کرد. [مرجع API پنل کناری](https://docsearch.algolia.com/docs/sidepanel/api-reference) شامل لیست کامل گزینه‌ها است.
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: '',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// آینه API @docsearch/sidepanel-js SidepanelProps
panel: {
variant: 'floating', // یا 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
})
```
اگر نیاز به غیرفعال کردن میانبر صفحه‌کلید دارید، از گزینه `keyboardShortcuts` پنل کناری استفاده کنید:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
### پشتیبانی Algolia Ask AI {#ask-ai}
#### حالت (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
برای فعال‌سازی **Ask AI** کافی است گزینه `askAi` را اضافه کنید:
می‌توانید به صورت اختیاری نحوه ادغام جستجوی کلمات کلیدی و Ask AI در VitePress را کنترل کنید:
- `mode: 'auto'` (پیش‌فرض): وقتی جستجوی کلمات کلیدی پیکربندی شده باشد `hybrid` را استنباط می‌کند، در غیر این صورت وقتی پنل کناری Ask AI پیکربندی شده باشد `sidePanel` را استنباط می‌کند.
- `mode: 'sidePanel'`: فقط پنل کناری را اعمال می‌کند (دکمه جستجوی کلمات کلیدی را پنهان می‌کند).
- `mode: 'hybrid'`: مودال جستجوی کلمات کلیدی + پنل کناری Ask AI را فعال می‌کند (نیاز به پیکربندی جستجوی کلمات کلیدی دارد).
- `mode: 'modal'`: Ask AI را درون مودال DocSearch نگه می‌دارد (حتی اگر پنل کناری را پیکربندی کرده باشید).
#### فقط Ask AI (بدون جستجوی کلمات کلیدی) {#ask-ai-only}
اگر می‌خواهید **فقط پنل کناری Ask AI** را استفاده کنید، می‌توانید پیکربندی جستجوی کلمات کلیدی سطح بالا را حذف کرده و اعتبارنامه‌ها را در `askAi` ارائه دهید:
```ts
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY'
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
}
}
})
```
::: warning نکته
اگر فقط به جستجوی کلمات کلیدی نیاز دارید، `askAi` را اضافه نکنید.
:::
### پیکربندی Crawler {#crawler-config}
در اینجا یک پیکربندی نمونه بر اساس آنچه که این سایت استفاده می‌کند آمده است:
<<< @/snippets/algolia-crawler.js

@ -49,7 +49,7 @@ interface PageData {
titleTemplate?: string | boolean
description: string
relativePath: string
filePath: string,
filePath: string
headers: Header[]
frontmatter: Record<string, any>
params?: Record<string, any>

@ -149,7 +149,6 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'ドキュメントを検索',
translations: {
button: {
buttonText: '検索',
@ -157,67 +156,138 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
},
modal: {
searchBox: {
clearButtonTitle: '検索をクリア',
clearButtonAriaLabel: '検索をクリア',
clearButtonTitle: 'クリア',
clearButtonAriaLabel: 'クエリをクリア',
closeButtonText: '閉じる',
closeButtonAriaLabel: '閉じる',
placeholderText: 'ドキュメントを検索',
placeholderTextAskAi: 'AI に質問: ',
placeholderTextAskAiStreaming: '回答を作成中...',
placeholderText: 'ドキュメントを検索するか Ask AI に質問',
placeholderTextAskAi: '別の質問をする...',
placeholderTextAskAiStreaming: '回答中...',
searchInputLabel: '検索',
backToKeywordSearchButtonText: 'キーワード検索に戻る',
backToKeywordSearchButtonAriaLabel: 'キーワード検索に戻る'
backToKeywordSearchButtonAriaLabel: 'キーワード検索に戻る',
newConversationPlaceholder: '質問する',
conversationHistoryTitle: '自分の会話履歴',
startNewConversationText: '新しい会話を開始',
viewConversationHistoryText: '会話履歴',
threadDepthErrorPlaceholder: '会話上限に達しました'
},
newConversation: {
newConversationTitle: '今日はどのようにお手伝いできますか?',
newConversationDescription:
'ドキュメントを検索して、設定ガイド、機能の詳細、トラブルシューティングのヒントをすばやく見つけるお手伝いをします。'
},
footer: {
selectText: '選択',
submitQuestionText: '質問を送信',
selectKeyAriaLabel: 'Enter キー',
navigateText: '移動',
navigateUpKeyAriaLabel: '上矢印',
navigateDownKeyAriaLabel: '下矢印',
closeText: '閉じる',
backToSearchText: '検索に戻る',
closeKeyAriaLabel: 'Escape キー',
poweredByText: '提供'
},
errorScreen: {
titleText: '結果を取得できませんでした',
helpText: 'ネットワーク接続を確認してください。'
},
startScreen: {
recentSearchesTitle: '検索履歴',
recentSearchesTitle: '最近',
noRecentSearchesText: '最近の検索はありません',
saveRecentSearchButtonTitle: '検索履歴に保存',
removeRecentSearchButtonTitle: '検索履歴から削除',
saveRecentSearchButtonTitle: 'この検索を保存',
removeRecentSearchButtonTitle: '履歴からこの検索を削除',
favoriteSearchesTitle: 'お気に入り',
removeFavoriteSearchButtonTitle: 'お気に入りから削除',
removeFavoriteSearchButtonTitle: 'お気に入りからこの検索を削除',
recentConversationsTitle: '最近の会話',
removeRecentConversationButtonTitle: '会話履歴から削除'
},
errorScreen: {
titleText: '結果を取得できません',
helpText: 'ネットワーク接続を確認してください'
removeRecentConversationButtonTitle: '履歴からこの会話を削除'
},
noResultsScreen: {
noResultsText: '結果が見つかりません',
suggestedQueryText: '別の検索語を試してください',
reportMissingResultsText: '結果があるはずだと思いますか?',
reportMissingResultsLinkText: 'フィードバックを送る'
noResultsText: '次の検索結果はありません',
suggestedQueryText: '次を検索してみてください',
reportMissingResultsText:
'この検索には結果があるべきだと思いますか?',
reportMissingResultsLinkText: 'お知らせください。'
},
resultsScreen: {
askAiPlaceholder: 'AI に質問: '
askAiPlaceholder: 'AI に質問:',
noResultsAskAiPlaceholder:
'ドキュメントに見つかりませんでしたか? Ask AI に相談:'
},
askAiScreen: {
disclaimerText:
'AI が生成した回答には誤りが含まれる可能性があります。必ずご確認ください。',
'回答は AI により生成され、誤りが含まれる場合があります。内容をご確認ください。',
relatedSourcesText: '関連ソース',
thinkingText: '考え中...',
copyButtonText: 'コピー',
copyButtonCopiedText: 'コピーしました!',
copyButtonTitle: 'コピー',
likeButtonTitle: 'いいね',
dislikeButtonTitle: 'よくない',
dislikeButtonTitle: 'よくない',
thanksForFeedbackText: 'フィードバックありがとうございます!',
preToolCallText: '検索中...',
duringToolCallText: '検索中 ',
afterToolCallText: '検索完了',
aggregatedToolCallText: '検索完了'
duringToolCallText: '検索中...',
afterToolCallText: '検索しました',
stoppedStreamingText: 'この応答を停止しました',
errorTitleText: 'チャットエラー',
threadDepthExceededMessage:
'回答の正確性を保つため、この会話は終了しました。',
startNewConversationButtonText: '新しい会話を開始'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'AI に質問',
buttonAriaLabel: 'AI に質問'
}
},
footer: {
selectText: '選択',
submitQuestionText: '質問を送信',
selectKeyAriaLabel: 'Enter キー',
navigateText: '移動',
navigateUpKeyAriaLabel: '上矢印キー',
navigateDownKeyAriaLabel: '下矢印キー',
closeText: '閉じる',
backToSearchText: '検索に戻る',
closeKeyAriaLabel: 'Esc キー',
poweredByText: '提供: '
panel: {
translations: {
header: {
title: 'AI に質問',
conversationHistoryTitle: '自分の会話履歴',
newConversationText: '新しい会話を開始',
viewConversationHistoryText: '会話履歴'
},
promptForm: {
promptPlaceholderText: '質問する',
promptAnsweringText: '回答中...',
promptAskAnotherQuestionText: '別の質問をする',
promptDisclaimerText:
'回答は AI により生成され、誤りが含まれる場合があります。',
promptLabelText: 'Enterで送信、Shift+Enterで改行。',
promptAriaLabelText: 'プロンプト入力'
},
conversationScreen: {
preToolCallText: '検索中...',
searchingText: '検索中...',
toolCallResultText: '検索しました',
conversationDisclaimer:
'回答は AI により生成され、誤りが含まれる場合があります。内容をご確認ください。',
reasoningText: '推論中...',
thinkingText: '考え中...',
relatedSourcesText: '関連ソース',
stoppedStreamingText: 'この応答を停止しました',
copyButtonText: 'コピー',
copyButtonCopiedText: 'コピーしました!',
likeButtonTitle: 'いいね',
dislikeButtonTitle: 'よくないね',
thanksForFeedbackText: 'フィードバックありがとうございます!',
errorTitleText: 'チャットエラー'
},
newConversationScreen: {
titleText: '今日はどのようにお手伝いできますか?',
introductionText:
'ドキュメントを検索して、設定ガイド、機能の詳細、トラブルシューティングのヒントをすばやく見つけるお手伝いをします。'
},
logo: {
poweredByText: '提供'
}
}
}
}
}

@ -12,44 +12,44 @@ CMS ごとに動作が異なるため、ここでは各自の環境に合わせ
1. CMS が認証を必要とする場合は、API トークンを格納するための `.env` を作成し、次のように読み込みます。
```js
// posts/[id].paths.js
import { loadEnv } from 'vitepress'
```js
// posts/[id].paths.js
import { loadEnv } from 'vitepress'
const env = loadEnv('', process.cwd())
```
const env = loadEnv('', process.cwd())
```
2. CMS から必要なデータを取得し、適切なパスデータの形式に整形します。
```js
export default {
async paths() {
// 必要に応じて各 CMS のクライアントライブラリを使用
const data = await (await fetch('https://my-cms-api', {
headers: {
// 必要ならトークン
}
})).json()
return data.map(entry => {
return {
params: { id: entry.id, /* title, authors, date など */ },
content: entry.content
}
})
}
}
```
```js
export default {
async paths() {
// 必要に応じて各 CMS のクライアントライブラリを使用
const data = await (await fetch('https://my-cms-api', {
headers: {
// 必要ならトークン
}
})).json()
return data.map((entry) => {
return {
params: { id: entry.id, /* title, authors, date など */ },
content: entry.content
}
})
}
}
```
3. ページ内でコンテンツをレンダリングします。
```md
# {{ $params.title }}
```md
# {{ $params.title }}
- {{ $params.date }} に {{ $params.author }} が作成
- {{ $params.date }} に {{ $params.author }} が作成
<!-- @content -->
```
<!-- @content -->
```
## 連携ガイドの募集 {#integration-guides}

@ -23,43 +23,42 @@ VitePress のカスタムテーマは次のインターフェースを持つオ
```ts
interface Theme {
/**
* すべてのページに適用されるルートレイアウトコンポーネント
* @required
*/
Layout: Component
/**
* Vue アプリインスタンスを拡張
* @optional
*/
enhanceApp?: (ctx: EnhanceAppContext) => Awaitable<void>
/**
* 別のテーマを拡張し、そのテーマの `enhanceApp` を先に実行
* @optional
*/
extends?: Theme
/**
* すべてのページに適用されるルートレイアウトコンポーネント
* @required
*/
Layout: Component
/**
* Vue アプリインスタンスを拡張
* @optional
*/
enhanceApp?: (ctx: EnhanceAppContext) => Awaitable<void>
/**
* 別のテーマを拡張し、そのテーマの `enhanceApp` を先に実行
* @optional
*/
extends?: Theme
}
interface EnhanceAppContext {
app: App // Vue アプリインスタンス
router: Router // VitePress のルーターインスタンス
siteData: Ref<SiteData> // サイト全体のメタデータ
app: App // Vue アプリインスタンス
router: Router // VitePress のルーターインスタンス
siteData: Ref<SiteData> // サイト全体のメタデータ
}
```
テーマエントリファイルでは、このテーマをデフォルトエクスポートとして公開します。
```js [.vitepress/theme/index.js]
// テーマエントリでは Vue ファイルを直接インポートできます
// VitePress は @vitejs/plugin-vue をあらかじめ設定済みです
import Layout from './Layout.vue'
export default {
Layout,
enhanceApp({ app, router, siteData }) {
// ...
}
Layout,
enhanceApp({ app, router, siteData }) {
// ...
}
}
```
@ -73,10 +72,10 @@ enhanceApp({ app, router, siteData }) {
```vue [.vitepress/theme/Layout.vue]
<template>
<h1>Custom Layout!</h1>
<h1>Custom Layout!</h1>
<!-- この部分に markdown コンテンツが描画されます -->
<Content />
<!-- この部分に markdown コンテンツが描画されます -->
<Content />
</template>
```
@ -100,11 +99,11 @@ const { page } = useData()
[`useData()`](../reference/runtime-api#usedata) ヘルパーを使うと、条件によってレイアウトを切り替えるために必要なすべてのランタイムデータを取得できます。アクセスできるデータのひとつにフロントマターがあります。これを利用すると、ページごとにレイアウトを制御できます。例えば、ユーザーが特別なホームページレイアウトを使いたい場合は以下のように記述します。
```md
```md
---
layout: home
---
```
```
テーマ側を次のように調整します。
@ -164,7 +163,6 @@ npm パッケージとして配布する場合は、次の手順を踏みます
## カスタムテーマの利用 {#consuming-a-custom-theme}
外部テーマを利用するには、カスタムテーマエントリからインポートして再エクスポートします。
```js [.vitepress/theme/index.js]
@ -179,10 +177,10 @@ export default Theme
import Theme from 'awesome-vitepress-theme'
export default {
extends: Theme,
enhanceApp(ctx) {
// ...
}
extends: Theme,
enhanceApp(ctx) {
// ...
}
}
```
@ -192,8 +190,8 @@ enhanceApp(ctx) {
import baseConfig from 'awesome-vitepress-theme/config'
export default {
// 必要に応じてテーマの基本設定を拡張
extends: baseConfig
// 必要に応じてテーマの基本設定を拡張
extends: baseConfig
}
```
@ -205,9 +203,9 @@ import { defineConfigWithTheme } from 'vitepress'
import type { ThemeConfig } from 'awesome-vitepress-theme'
export default defineConfigWithTheme<ThemeConfig>({
extends: baseConfig,
themeConfig: {
// 型は `ThemeConfig`
}
extends: baseConfig,
themeConfig: {
// 型は `ThemeConfig`
}
})
```

@ -10,12 +10,12 @@ VitePress には **データローダー (data loaders)** という機能があ
```js [example.data.js]
export default {
load() {
return {
hello: 'world'
load() {
return {
hello: 'world'
}
}
}
}
```
ローダーモジュールは Node.js 上でのみ評価されるため、Node API や npm 依存関係を自由に利用できます。
@ -179,7 +179,6 @@ interface ContentOptions<T = ContentData[]> {
## 型付きデータローダー {#typed-data-loaders}
TypeScript を使用する場合は、ローダーと `data` エクスポートを型付けできます。
```ts
@ -202,7 +201,6 @@ export default defineLoader({
## 設定情報の取得 {#configuration}
ローダー内で設定情報を取得するには次のようにします。
```ts

@ -4,7 +4,6 @@ outline: deep
# VitePress サイトをデプロイする {#deploy-your-vitepress-site}
以下のガイドは、次の前提に基づいています。
- VitePress のサイトはプロジェクトの `docs` ディレクトリ内にある。
@ -12,17 +11,16 @@ outline: deep
- VitePress はプロジェクトのローカル依存としてインストールされており、`package.json` に次のスクリプトが設定されている。
```json [package.json]
{
"scripts": {
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
}
}
{
"scripts": {
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
}
}
```
## ローカルでビルドしてテストする {#build-and-test-locally}
1. 次のコマンドでドキュメントをビルドします。
```sh
@ -35,30 +33,28 @@ outline: deep
$ npm run docs:preview
```
`preview` コマンドはローカルの静的 Web サーバーを起動し、出力ディレクトリ `.vitepress/dist``http://localhost:4173` で配信します。プロダクションへプッシュする前に見た目を確認できます。
`preview` コマンドはローカルの静的 Web サーバーを起動し、出力ディレクトリ `.vitepress/dist``http://localhost:4173` で配信します。プロダクションへプッシュする前に見た目を確認できます。
3. `--port` 引数でサーバーのポートを設定できます。
```json
{
"scripts": {
"docs:preview": "vitepress preview docs --port 8080"
}
}
{
"scripts": {
"docs:preview": "vitepress preview docs --port 8080"
}
}
```
これで `docs:preview``http://localhost:8080` でサーバーを起動します。
これで `docs:preview``http://localhost:8080` でサーバーを起動します。
## 公開ベースパスの設定 {#setting-a-public-base-path}
デフォルトでは、サイトはドメインのルートパス(`/`)にデプロイされることを想定しています。サイトをサブパス、例:`https://mywebsite.com/blog/` で配信する場合は、VitePress の設定で [`base`](../reference/site-config#base) オプションを `'/blog/'` に設定してください。
**例:** GitHubまたは GitLabPages に `user.github.io/repo/` としてデプロイするなら、`base` を `/repo/` に設定します。
## HTTP キャッシュヘッダー {#http-cache-headers}
本番サーバーの HTTP ヘッダーを制御できる場合は、`cache-control` ヘッダーを設定して、再訪時のパフォーマンスを向上させましょう。
本番ビルドでは静的アセットJavaScript、CSS、`public` 以外のインポートアセット)にハッシュ付きファイル名が使用されます。ブラウザの開発者ツールのネットワークタブで本番プレビューを確認すると、`app.4f283b18.js` のようなファイルが見られます。
@ -86,19 +82,19 @@ Cache-Control: max-age=31536000,immutable
::: details `vercel.json` による Vercel 設定例
```json
{
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=31536000, immutable"
}
]
}
]
}
{
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=31536000, immutable"
}
]
}
]
}
```
注:`vercel.json` は **リポジトリのルート** に配置してください。
@ -109,8 +105,7 @@ Cache-Control: max-age=31536000,immutable
## プラットフォーム別ガイド {#platform-guides}
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#netlify-vercel-cloudflare-pages-aws-amplify-render}
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#generic}
新しいプロジェクトを作成し、ダッシュボードで次の設定に変更します。
@ -122,143 +117,146 @@ Cache-Control: max-age=31536000,immutable
HTML の _Auto Minify_ のようなオプションを有効にしないでください。Vue にとって意味のあるコメントが出力から削除され、削除されるとハイドレーションの不整合エラーが発生する可能性があります。
:::
### GitHub Pages {#github-pages}
### GitHub Pages
1. プロジェクトの `.github/workflows` ディレクトリに `deploy.yml` を作成し、以下の内容を記述します。
```yaml [.github/workflows/deploy.yml]
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages
on:
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
branches: [main]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
# - uses: pnpm/action-setup@v4 # Uncomment this block if you're using pnpm
# with:
# version: 9 # Not needed if you've set "packageManager" in package.json
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci # or pnpm install / yarn install / bun install
- name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages
on:
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
branches: [main]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
# - uses: pnpm/action-setup@v4 # Uncomment this block if you're using pnpm
# with:
# version: 9 # Not needed if you've set "packageManager" in package.json
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci # or pnpm install / yarn install / bun install
- name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
```
::: warning
VitePress の `base` オプションが正しく設定されていることを確認してください。詳細は [公開ベースパスの設定](#公開ベースパスの設定) を参照してください。
:::
::: warning
VitePress の `base` オプションが正しく設定されていることを確認してください。詳細は [公開ベースパスの設定](#公開ベースパスの設定) を参照してください。
:::
2. リポジトリ設定の「Pages」メニューで、「Build and deployment > Source」を「GitHub Actions」に設定します。
3. 変更を `main` ブランチにプッシュし、GitHub Actions の完了を待ちます。設定に応じて、サイトは `https://<username>.github.io/[repository]/` または `https://<custom-domain>/` にデプロイされます。以後、`main` へのプッシュごとに自動デプロイされます。
### GitLab Pages {#gitlab-pages}
### GitLab Pages
1. VitePress の設定で `outDir``../public` に設定します。`https://<username>.gitlab.io/<repository>/` にデプロイする場合は `base``'/<repository>/'` に設定します。カスタムドメイン、ユーザー/グループページ、または GitLab の「Use unique domain」を有効にしている場合は `base` は不要です。
2. プロジェクトのルートに `.gitlab-ci.yml` を作成して、以下を追加します。これにより、コンテンツを更新するたびにサイトがビルド・デプロイされます。
```yaml [.gitlab-ci.yml]
image: node:18
pages:
cache:
paths:
- node_modules/
script:
# - apk add git # Uncomment this if you're using small docker images like alpine and have lastUpdated enabled
- npm install
- npm run docs:build
artifacts:
paths:
- public
only:
- main
image: node:18
pages:
cache:
paths:
- node_modules/
script:
# - apk add git # Uncomment this if you're using small docker images like alpine and have lastUpdated enabled
- npm install
- npm run docs:build
artifacts:
paths:
- public
only:
- main
```
### Azure Static Web Apps {#azure-static-web-apps}
### Azure
1. [公式ドキュメント](https://docs.microsoft.com/en-us/azure/static-web-apps/build-configuration) に従います。
2. 設定ファイルで次の値を指定します(`api_location` のように不要なものは削除)。
- **`app_location`**: `/`
- **`output_location`**: `docs/.vitepress/dist`
- **`app_build_command`**: `npm run docs:build`
### Firebase {#firebase}
### CloudRay
[CloudRay](https://cloudray.io/) でのデプロイ方法は [こちらの手順](https://cloudray.io/articles/how-to-deploy-vitepress-site) を参照してください。
### Firebase
1. プロジェクトのルートに `firebase.json``.firebaserc` を作成します。
`firebase.json`:
```json [firebase.json]
{
"hosting": {
"public": "docs/.vitepress/dist",
"ignore": []
}
}
{
"hosting": {
"public": "docs/.vitepress/dist",
"ignore": []
}
}
```
`.firebaserc`:
```json [.firebaserc]
{
"projects": {
"default": "<YOUR_FIREBASE_ID>"
}
}
{
"projects": {
"default": "<YOUR_FIREBASE_ID>"
}
}
```
2. `npm run docs:build` の後、次のコマンドでデプロイします。
@ -267,43 +265,39 @@ HTML の _Auto Minify_ のようなオプションを有効にしないでくだ
firebase deploy
```
### Surge {#surge}
1. `npm run docs:build` の後、次のコマンドでデプロイします。
```sh
npx surge docs/.vitepress/dist
```
### Heroku {#heroku}
### Heroku
1. [`heroku-buildpack-static`](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-static) のドキュメントとガイドに従います。
2. プロジェクトのルートに `static.json` を作成し、以下を記述します。
```json [static.json]
{
"root": "docs/.vitepress/dist"
}
{
"root": "docs/.vitepress/dist"
}
```
### Edgio {#edgio}
### Hostinger
[Creating and Deploying a VitePress App To Edgio](https://docs.edg.io/guides/vitepress) を参照してください。
[Hostinger](https://www.hostinger.com/web-apps-hosting) を使用して VitePress プロジェクトをデプロイするには、こちらの [手順](https://www.hostinger.com/support/how-to-deploy-a-nodejs-website-in-hostinger/) に従ってください。ビルド設定の構成では、フレームワークに VitePress を選択し、ルートディレクトリを `./docs` に調整してください。
### Kinsta Static Site Hosting {#kinsta-static-site-hosting}
### Kinsta
[VitePress](https://kinsta.com/static-site-hosting/) を [こちらの手順](https://kinsta.com/docs/vitepress-static-site-example/) に従ってデプロイできます。
### Stormkit {#stormkit}
### Stormkit
[VitePress プロジェクトを Stormkit にデプロイ](https://stormkit.io/blog/how-to-deploy-vitepress) する手順を参照してください。
### CloudRay {#cloudray}
### Surge
[CloudRay](https://cloudray.io/) でのデプロイ方法は [こちらの手順](https://cloudray.io/articles/how-to-deploy-vitepress-site) を参照してください。
1. `npm run docs:build` の後、次のコマンドでデプロイします。
```sh
npx surge docs/.vitepress/dist
```
### Nginx {#nginx}
### Nginx
以下は Nginx サーバーブロックの設定例です。一般的なテキスト系アセットの gzip 圧縮、VitePress サイトの静的ファイル配信における適切なキャッシュヘッダー、そして `cleanUrls: true` のハンドリングを含みます。

@ -70,7 +70,9 @@ export default DefaultTheme
export default {
transformHead({ assets }) {
// 使うフォントに合わせて正規表現を調整
const myFontFile = assets.find(file => /font-name\.[\w-]+\.woff2/.test(file))
const myFontFile = assets.find((file) =>
/font-name\.[\w-]+\.woff2/.test(file)
)
if (myFontFile) {
return [
[

@ -5,10 +5,10 @@
VitePress はすべての Markdown ファイルで YAML フロントマターをサポートしており、[gray-matter](https://github.com/jonschlinkert/gray-matter) で解析します。フロントマターは Markdown ファイルの先頭(`<script>` タグを含むあらゆる要素より前)に配置し、三本のハイフンで囲まれた **有効な YAML** 形式で記述します。例:
```md
---
title: Docs with VitePress
editLink: true
---
---
title: Docs with VitePress
editLink: true
---
```
サイトやデフォルトテーマの多くの設定オプションには、フロントマター上で対応するオプションがあります。フロントマターを使うことで、**そのページに限って** 特定の振る舞いを上書きできます。詳細は [Frontmatter Config Reference](../reference/frontmatter-config) を参照してください。
@ -22,14 +22,14 @@ VitePress はすべての Markdown ファイルで YAML フロントマターを
Markdown ファイル内での使用例:
```md
---
title: Docs with VitePress
editLink: true
---
---
title: Docs with VitePress
editLink: true
---
# {{ $frontmatter.title }}
# {{ $frontmatter.title }}
Guide content
Guide content
```
[`useData()`](../reference/runtime-api#usedata) ヘルパーを使えば、`<script setup>` 内からも現在のページのフロントマターデータにアクセスできます。
@ -38,11 +38,11 @@ Markdown ファイル内での使用例:
VitePress は JSON フロントマター構文もサポートしています。中括弧で開始・終了する形式です。
```json
---
{
"title": "Blogging Like a Hacker",
"editLink": true
}
---
```md
---
{
"title": "Blogging Like a Hacker",
"editLink": true
}
---
```

@ -10,7 +10,7 @@
- [Node.js](https://nodejs.org/) バージョン 18 以上
- VitePress をコマンドラインインターフェース (CLI) で操作するためのターミナル
- [Markdown](https://en.wikipedia.org/wiki/Markdown) 構文に対応したテキストエディタ
- [Markdown](https://en.wikipedia.org/wiki/Markdown) 構文に対応したテキストエディタ
- 推奨: [VSCode](https://code.visualstudio.com/) と [公式 Vue 拡張](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
VitePress は単独でも利用できますし、既存プロジェクトに組み込むことも可能です。いずれの場合も以下でインストールできます。

File diff suppressed because it is too large Load Diff

@ -8,15 +8,15 @@ MPA モードでは、既定で **あらゆるページが JavaScript を含ま
また、「既定で JS なし」ということは、実質的に Vue をサーバーサイドのテンプレート言語としてのみ使うことを意味します。ブラウザ側ではイベントハンドラがアタッチされないため、インタラクティブ性はありません。クライアントサイドの JavaScript を読み込むには、特別な `<script client>` タグを使用します:
```html
<script client>
document.querySelector('h1').addEventListener('click', () => {
console.log('client side JavaScript!')
})
</script>
# Hello
```
```md
<script client>
document.querySelector('h1').addEventListener('click', () => {
console.log('client side JavaScript!')
})
</script>
# Hello
```
`<script client>` は VitePress 固有の機能であり、Vue の機能ではありません。`.md` と `.vue` の両方で動作しますが、**MPA モード時のみ** 有効です。テーマコンポーネント内のクライアントスクリプトはひとつにバンドルされ、特定ページ専用のクライアントスクリプトはそのページごとに分割されます。

@ -8,23 +8,23 @@ outline: deep
VitePress はファイルベースのルーティングを採用しており、生成される HTML はソースの Markdown ファイルのディレクトリ構造に対応します。例えば、次のディレクトリ構造があるとします:
```
.
├─ guide
│ ├─ getting-started.md
│ └─ index.md
├─ index.md
└─ prologue.md
```
```
.
├─ guide
│ ├─ getting-started.md
│ └─ index.md
├─ index.md
└─ prologue.md
```
生成される HTML は次のとおりです:
```
index.md --> /index.html / でアクセス可能)
prologue.md --> /prologue.html
guide/index.md --> /guide/index.html /guide/ でアクセス可能)
guide/getting-started.md --> /guide/getting-started.html
```
```
index.md --> /index.html / でアクセス可能)
prologue.md --> /prologue.html
guide/index.md --> /guide/index.html /guide/ でアクセス可能)
guide/getting-started.md --> /guide/getting-started.html
```
生成された HTML は、静的ファイルを配信できる任意の Web サーバーでホストできます。
@ -38,25 +38,25 @@ VitePress プロジェクトのファイル構成には重要な概念が 2 つ
コマンドラインから `vitepress dev``vitepress build` を実行すると、VitePress は現在の作業ディレクトリをプロジェクトルートとして使用します。サブディレクトリをルートとして指定したい場合は、コマンドに相対パスを渡します。例えば、VitePress プロジェクトが `./docs` にある場合、`vitepress dev docs` を実行します:
```
.
├─ docs # プロジェクトルート
│ ├─ .vitepress # 設定ディレクトリ
│ ├─ getting-started.md
│ └─ index.md
└─ ...
```
```
.
├─ docs # プロジェクトルート
│ ├─ .vitepress # 設定ディレクトリ
│ ├─ getting-started.md
│ └─ index.md
└─ ...
```
```sh
vitepress dev docs
```
```sh
vitepress dev docs
```
これにより、ソースから HTML へのマッピングは次のようになります:
```
docs/index.md --> /index.html / でアクセス可能)
docs/getting-started.md --> /getting-started.html
```
```
docs/index.md --> /index.html / でアクセス可能)
docs/getting-started.md --> /getting-started.html
```
### ソースディレクトリ {#source-directory}
@ -64,34 +64,34 @@ VitePress プロジェクトのファイル構成には重要な概念が 2 つ
`srcDir` はプロジェクトルートからの相対パスで解決されます。例えば `srcDir: 'src'` の場合、ファイル構成は次のようになります:
```
. # プロジェクトルート
├─ .vitepress # 設定ディレクトリ
└─ src # ソースディレクトリ
├─ getting-started.md
└─ index.md
```
```
. # プロジェクトルート
├─ .vitepress # 設定ディレクトリ
└─ src # ソースディレクトリ
├─ getting-started.md
└─ index.md
```
ソースから HTML へのマッピングは次のとおりです:
```
src/index.md --> /index.html / でアクセス可能)
src/getting-started.md --> /getting-started.html
```
```
src/index.md --> /index.html / でアクセス可能)
src/getting-started.md --> /getting-started.html
```
## ページ間リンク {#linking-between-pages}
ページ間のリンクには、絶対パスと相対パスのどちらも使用できます。`.md` と `.html` の拡張子はどちらも機能しますが、最終的な URL を設定に応じて VitePress が生成できるよう、**拡張子は省略する** のがベストプラクティスです。
```md
<!-- 良い例 -->
[はじめに](./getting-started)
[はじめに](../guide/getting-started)
```md
<!-- 良い例 -->
[はじめに](./getting-started)
[はじめに](../guide/getting-started)
<!-- 悪い例 -->
[はじめに](./getting-started.md)
[はじめに](./getting-started.html)
```
<!-- 悪い例 -->
[はじめに](./getting-started.md)
[はじめに](./getting-started.html)
```
画像などのアセットへのリンクについては、[アセットの取り扱い](./asset-handling) を参照してください。
@ -101,13 +101,13 @@ VitePress プロジェクトのファイル構成には重要な概念が 2 つ
**入力**
```md
[pure.html へのリンク](/pure.html){target="_self"}
```
```md
[pure.html へのリンク](/pure.html){target="_self"}
```
**出力**
[pure.html へのリンク](/pure.html){target="_self"}
[pure.html へのリンク](/pure.html){target="_self"}
::: tip 注意
@ -115,9 +115,9 @@ Markdown のリンクでは、`base` が自動的に URL の先頭に付与さ
あるいは、アンカータグの構文を直接使うこともできます:
```md
<a href="/pure.html" target="_self">pure.html へのリンク</a>
```
```md
<a href="/pure.html" target="_self">pure.html へのリンク</a>
```
:::
## クリーン URL の生成 {#generating-clean-urls}
@ -140,83 +140,83 @@ VitePress でクリーン URL を提供するには、サーバー側のサポ
もしサーバーをそのように設定できない場合は、次のようなディレクトリ構造に手動でする必要があります:
```
.
├─ getting-started
│ └─ index.md
├─ installation
│ └─ index.md
└─ index.md
```
```
.
├─ getting-started
│ └─ index.md
├─ installation
│ └─ index.md
└─ index.md
```
## ルートのリライト {#route-rewrites}
ソースディレクトリ構造と生成ページのマッピングをカスタマイズできます。これは複雑なプロジェクト構成で有用です。例えば、複数パッケージを持つモノレポで、ソースファイルと並べてドキュメントを配置したい場合:
```
.
└─ packages
├─ pkg-a
│ └─ src
│ ├─ foo.md
│ └─ index.md
└─ pkg-b
└─ src
├─ bar.md
└─ index.md
```
```
.
└─ packages
├─ pkg-a
│ └─ src
│ ├─ foo.md
│ └─ index.md
└─ pkg-b
└─ src
├─ bar.md
└─ index.md
```
生成したいページが次のような場合:
```
packages/pkg-a/src/index.md --> /pkg-a/index.html
packages/pkg-a/src/foo.md --> /pkg-a/foo.html
packages/pkg-b/src/index.md --> /pkg-b/index.html
packages/pkg-b/src/bar.md --> /pkg-b/bar.html
```
```
packages/pkg-a/src/index.md --> /pkg-a/index.html
packages/pkg-a/src/foo.md --> /pkg-a/foo.html
packages/pkg-b/src/index.md --> /pkg-b/index.html
packages/pkg-b/src/bar.md --> /pkg-b/bar.html
```
[`rewrites`](../reference/site-config#rewrites) オプションを次のように設定します:
```ts [.vitepress/config.js]
export default {
rewrites: {
'packages/pkg-a/src/index.md': 'pkg-a/index.md',
'packages/pkg-a/src/foo.md': 'pkg-a/foo.md',
'packages/pkg-b/src/index.md': 'pkg-b/index.md',
'packages/pkg-b/src/bar.md': 'pkg-b/bar.md'
}
}
```
```ts [.vitepress/config.js]
export default {
rewrites: {
'packages/pkg-a/src/index.md': 'pkg-a/index.md',
'packages/pkg-a/src/foo.md': 'pkg-a/foo.md',
'packages/pkg-b/src/index.md': 'pkg-b/index.md',
'packages/pkg-b/src/bar.md': 'pkg-b/bar.md'
}
}
```
`rewrites` は動的なルートパラメータにも対応しています。上記の例で多くのパッケージがある場合、同じ構造なら次のように簡略化できます:
```ts
export default {
rewrites: {
'packages/:pkg/src/:slug*': ':pkg/:slug*'
}
}
```
```ts
export default {
rewrites: {
'packages/:pkg/src/:slug*': ':pkg/:slug*'
}
}
```
リライトのパスは `path-to-regexp` パッケージでコンパイルされます。高度な構文は[ドキュメント](https://github.com/pillarjs/path-to-regexp/tree/6.x#parameters)を参照してください。
`rewrites` は、元のパスを受け取って新しいパスを返す **関数** として定義することもできます:
```ts
export default {
rewrites(id) {
return id.replace(/^packages\/([^/]+)\/src\//, '$1/')
}
}
```
```ts
export default {
rewrites(id) {
return id.replace(/^packages\/([^/]+)\/src\//, '$1/')
}
}
```
::: warning リライト時の相対リンク
リライトを有効にした場合、**相対リンクはリライト後のパスに基づいて** 記述してください。例えば、`packages/pkg-a/src/pkg-a-code.md` から `packages/pkg-b/src/pkg-b-code.md` への相対リンクを作るには、次のように書きます:
```md
[PKG B へのリンク](../pkg-b/pkg-b-code)
```
```md
[PKG B へのリンク](../pkg-b/pkg-b-code)
```
:::
## 動的ルート {#dynamic-routes}
@ -227,37 +227,37 @@ VitePress でクリーン URL を提供するには、サーバー側のサポ
VitePress は静的サイトジェネレーターなので、生成可能なページパスはビルド時に確定している必要があります。したがって、動的ルートページには **パスローダーファイル** が **必須** です。`packages/[pkg].md` に対しては `packages/[pkg].paths.js``.ts` も可)が必要です:
```
.
└─ packages
├─ [pkg].md # ルートテンプレート
└─ [pkg].paths.js # ルートのパスローダー
```
```
.
└─ packages
├─ [pkg].md # ルートテンプレート
└─ [pkg].paths.js # ルートのパスローダー
```
パスローダーは、`paths` メソッドを持つオブジェクトをデフォルトエクスポートします。`paths` は `params` プロパティを持つオブジェクトの配列を返します。各オブジェクトが 1 ページに対応します。
例えば次の `paths` 配列を返すと:
```js
// packages/[pkg].paths.js
export default {
paths() {
return [
{ params: { pkg: 'foo' }},
{ params: { pkg: 'bar' }}
]
}
}
```
```js
// packages/[pkg].paths.js
export default {
paths() {
return [
{ params: { pkg: 'foo' } },
{ params: { pkg: 'bar' } }
]
}
}
```
生成される HTML は次のようになります:
```
.
└─ packages
├─ foo.html
└─ bar.html
```
```
.
└─ packages
├─ foo.html
└─ bar.html
```
### 複数パラメータ {#multiple-params}
@ -265,36 +265,36 @@ VitePress は静的サイトジェネレーターなので、生成可能なペ
**ファイル構成**
```
.
└─ packages
├─ [pkg]-[version].md
└─ [pkg]-[version].paths.js
```
```
.
└─ packages
├─ [pkg]-[version].md
└─ [pkg]-[version].paths.js
```
**パスローダー**
```js
export default {
paths: () => [
{ params: { pkg: 'foo', version: '1.0.0' }},
{ params: { pkg: 'foo', version: '2.0.0' }},
{ params: { pkg: 'bar', version: '1.0.0' }},
{ params: { pkg: 'bar', version: '2.0.0' }}
]
}
```
```js
export default {
paths: () => [
{ params: { pkg: 'foo', version: '1.0.0' } },
{ params: { pkg: 'foo', version: '2.0.0' } },
{ params: { pkg: 'bar', version: '1.0.0' } },
{ params: { pkg: 'bar', version: '2.0.0' } }
]
}
```
**出力**
```
.
└─ packages
├─ foo-1.0.0.html
├─ foo-2.0.0.html
├─ bar-1.0.0.html
└─ bar-2.0.0.html
```
```
.
└─ packages
├─ foo-1.0.0.html
├─ foo-2.0.0.html
├─ bar-1.0.0.html
└─ bar-2.0.0.html
```
### パスを動的に生成する {#dynamically-generating-paths}
@ -302,60 +302,60 @@ VitePress は静的サイトジェネレーターなので、生成可能なペ
ローカルファイルから生成する例:
```js
import fs from 'fs'
```js
import fs from 'fs'
export default {
paths() {
return fs
.readdirSync('packages')
.map((pkg) => {
return { params: { pkg } }
})
}
}
```
export default {
paths() {
return fs
.readdirSync('packages')
.map((pkg) => {
return { params: { pkg } }
})
}
}
```
リモートデータから生成する例:
```js
export default {
async paths() {
const pkgs = await (await fetch('https://my-api.com/packages')).json()
return pkgs.map((pkg) => {
return {
params: {
pkg: pkg.name,
version: pkg.version
}
}
})
}
}
```
```js
export default {
async paths() {
const pkgs = await (await fetch('https://my-api.com/packages')).json()
return pkgs.map((pkg) => {
return {
params: {
pkg: pkg.name,
version: pkg.version
}
}
})
}
}
```
### ページ内でパラメータにアクセスする {#accessing-params-in-page}
各ページへ追加データを渡すために、パラメータを利用できます。Markdown のルートファイルでは、Vue 式内で `$params` グローバルプロパティから現在ページのパラメータにアクセスできます:
```md
- パッケージ名: {{ $params.pkg }}
- バージョン: {{ $params.version }}
```
```md
- パッケージ名: {{ $params.pkg }}
- バージョン: {{ $params.version }}
```
[`useData`](../reference/runtime-api#usedata) ランタイム API からも、現在ページのパラメータにアクセスできますMarkdown と Vue コンポーネントの両方で利用可能):
```vue
<script setup>
import { useData } from 'vitepress'
```vue
<script setup>
import { useData } from 'vitepress'
// params は Vue の ref
const { params } = useData()
// params は Vue の ref
const { params } = useData()
console.log(params.value)
</script>
```
console.log(params.value)
</script>
```
### 生コンテンツのレンダリング {#rendering-raw-content}
@ -363,23 +363,23 @@ VitePress は静的サイトジェネレーターなので、生成可能なペ
代わりに、各パスオブジェクトの `content` プロパティでコンテンツを渡せます:
```js
export default {
async paths() {
const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
return posts.map((post) => {
return {
params: { id: post.id },
content: post.content // 生の Markdown または HTML
}
})
}
}
```
```js
export default {
async paths() {
const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
return posts.map((post) => {
return {
params: { id: post.id },
content: post.content // 生の Markdown または HTML
}
})
}
}
```
そのうえで、Markdown ファイル内で次の特別な構文を使って、そのコンテンツを埋め込みます:
```md
<!-- @content -->
```
```md
<!-- @content -->
```

@ -2,13 +2,13 @@
VitePress には、サイト用の `sitemap.xml` を生成する機能が標準で用意されています。有効化するには、`.vitepress/config.js` に次を追加します。
```ts
export default {
sitemap: {
hostname: 'https://example.com'
}
}
```
```ts
export default {
sitemap: {
hostname: 'https://example.com'
}
}
```
`siteamp.xml``<lastmod>` タグを含めるには、[`lastUpdated`](../reference/default-theme-last-updated) オプションを有効にします。
@ -16,43 +16,43 @@ VitePress には、サイト用の `sitemap.xml` を生成する機能が標準
サイトマップ生成は [`sitemap`](https://www.npmjs.com/package/sitemap) モジュールで行われます。設定ファイルの `sitemap` に、このモジュールがサポートする任意のオプションを渡せます。指定した値はそのまま `SitemapStream` コンストラクタに渡されます。詳しくは [`sitemap` のドキュメント](https://www.npmjs.com/package/sitemap#options-you-can-pass) を参照してください。例:
```ts
export default {
sitemap: {
hostname: 'https://example.com',
lastmodDateOnly: false
}
}
```
```ts
export default {
sitemap: {
hostname: 'https://example.com',
lastmodDateOnly: false
}
}
```
設定で `base` を使っている場合は、`hostname` にもそれを付与してください:
```ts
export default {
base: '/my-site/',
sitemap: {
hostname: 'https://example.com/my-site/'
}
}
```
```ts
export default {
base: '/my-site/',
sitemap: {
hostname: 'https://example.com/my-site/'
}
}
```
## `transformItems` フック {#transformitems-hook}
`siteamp.xml` に書き出す直前にサイトマップ項目を加工するには、`sitemap.transformItems` フックを使います。このフックはサイトマップ項目の配列を受け取り、配列を返す必要があります。例:
```ts
export default {
sitemap: {
hostname: 'https://example.com',
transformItems: (items) => {
// 既存項目の追加・変更・フィルタリングが可能
items.push({
url: '/extra-page',
changefreq: 'monthly',
priority: 0.8
})
return items
}
}
}
```
```ts
export default {
sitemap: {
hostname: 'https://example.com',
transformItems: (items) => {
// 既存項目の追加・変更・フィルタリングが可能
items.push({
url: '/extra-page',
changefreq: 'monthly',
priority: 0.8
})
return items
}
}
}
```

@ -12,11 +12,11 @@ VitePress は本番ビルド時に、Node.js 上で Vue のサーバーサイド
SSR に適さないコンポーネント(例:カスタムディレクティブを含むなど)を使用・デモする場合は、組み込みの `<ClientOnly>` コンポーネントでラップできます。
```md
<ClientOnly>
<NonSSRFriendlyComponent />
</ClientOnly>
```
```md
<ClientOnly>
<NonSSRFriendlyComponent />
</ClientOnly>
```
## インポート時に Browser API にアクセスするライブラリ {#libraries-that-access-browser-api-on-import}
@ -24,112 +24,112 @@ SSR に適さないコンポーネント(例:カスタムディレクティ
### mounted フック内でのインポート {#importing-in-mounted-hook}
```vue
<script setup>
import { onMounted } from 'vue'
```vue
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
import('./lib-that-access-window-on-import').then((module) => {
// ここでコードを利用
})
})
</script>
```
onMounted(() => {
import('./lib-that-access-window-on-import').then((module) => {
// ここでコードを利用
})
})
</script>
```
### 条件付きインポート {#conditional-import}
[`import.meta.env.SSR`](https://vitejs.dev/guide/env-and-mode.html#env-variables) フラグVite の環境変数の一部)を使って、依存関係を条件付きでインポートすることもできます。
```js
if (!import.meta.env.SSR) {
import('./lib-that-access-window-on-import').then((module) => {
// ここでコードを利用
})
}
```
```js
if (!import.meta.env.SSR) {
import('./lib-that-access-window-on-import').then((module) => {
// ここでコードを利用
})
}
```
[`Theme.enhanceApp`](./custom-theme#theme-interface) は非同期にできるため、**インポート時に Browser API に触れる Vue プラグイン** を条件付きでインポート・登録できます。
```js [.vitepress/theme/index.js]
/** @type {import('vitepress').Theme} */
export default {
// ...
async enhanceApp({ app }) {
if (!import.meta.env.SSR) {
const plugin = await import('plugin-that-access-window-on-import')
app.use(plugin.default)
}
}
}
```
```js [.vitepress/theme/index.js]
/** @type {import('vitepress').Theme} */
export default {
// ...
async enhanceApp({ app }) {
if (!import.meta.env.SSR) {
const plugin = await import('plugin-that-access-window-on-import')
app.use(plugin.default)
}
}
}
```
TypeScript を使う場合:
```ts [.vitepress/theme/index.ts]
import type { Theme } from 'vitepress'
```ts [.vitepress/theme/index.ts]
import type { Theme } from 'vitepress'
export default {
// ...
async enhanceApp({ app }) {
if (!import.meta.env.SSR) {
const plugin = await import('plugin-that-access-window-on-import')
app.use(plugin.default)
}
}
} satisfies Theme
```
export default {
// ...
async enhanceApp({ app }) {
if (!import.meta.env.SSR) {
const plugin = await import('plugin-that-access-window-on-import')
app.use(plugin.default)
}
}
} satisfies Theme
```
### `defineClientComponent`
VitePress は、**インポート時に Browser API にアクセスする Vue コンポーネント** を読み込むためのユーティリティを提供します。
```vue
<script setup>
import { defineClientComponent } from 'vitepress'
```vue
<script setup>
import { defineClientComponent } from 'vitepress'
const ClientComp = defineClientComponent(() => {
return import('component-that-access-window-on-import')
})
</script>
const ClientComp = defineClientComponent(() => {
return import('component-that-access-window-on-import')
})
</script>
<template>
<ClientComp />
</template>
```
<template>
<ClientComp />
</template>
```
ターゲットコンポーネントに props / children / slots を渡すこともできます。
```vue
<script setup>
import { ref } from 'vue'
import { defineClientComponent } from 'vitepress'
const clientCompRef = ref(null)
const ClientComp = defineClientComponent(
() => import('component-that-access-window-on-import'),
// 引数は h() に渡されます - https://vuejs.org/api/render-function.html#h
[
{
ref: clientCompRef
},
{
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
}
],
// コンポーネント読み込み後のコールバック(非同期可)
() => {
console.log(clientCompRef.value)
}
)
</script>
<template>
<ClientComp />
</template>
```
```vue
<script setup>
import { ref } from 'vue'
import { defineClientComponent } from 'vitepress'
const clientCompRef = ref(null)
const ClientComp = defineClientComponent(
() => import('component-that-access-window-on-import'),
// 引数は h() に渡されます - https://vuejs.org/api/render-function.html#h
[
{
ref: clientCompRef
},
{
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
}
],
// コンポーネント読み込み後のコールバック(非同期可)
() => {
console.log(clientCompRef.value)
}
)
</script>
<template>
<ClientComp />
</template>
```
ターゲットコンポーネントは、ラッパーコンポーネントの mounted フックで初めてインポートされます。

@ -16,9 +16,9 @@ Vue の使用は SSR 互換である必要があります。詳細と一般的
**入力**
```md
{{ 1 + 1 }}
```
```md
{{ 1 + 1 }}
```
**出力**
@ -30,9 +30,9 @@ Vue の使用は SSR 互換である必要があります。詳細と一般的
**入力**
```html
<span v-for="i in 3">{{ i }}</span>
```
```html
<span v-for="i in 3">{{ i }}</span>
```
**出力**
@ -42,30 +42,30 @@ Vue の使用は SSR 互換である必要があります。詳細と一般的
Markdown ファイルのルート直下に置く `<script>``<style>` タグは、Vue の SFC と同様に動作します(`<script setup>` や `<style module>` などを含む)。大きな違いは `<template>` タグが無い点で、その他のルート直下のコンテンツは Markdown になることです。すべてのタグはフロントマターの**後**に配置してください。
```html
---
hello: world
---
```md
---
hello: world
---
<script setup>
import { ref } from 'vue'
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
const count = ref(0)
</script>
## Markdown コンテンツ
## Markdown コンテンツ
現在の値: {{ count }}
現在の値: {{ count }}
<button :class="$style.button" @click="count++">Increment</button>
<button :class="$style.button" @click="count++">Increment</button>
<style module>
.button {
color: red;
font-weight: bold;
}
</style>
```
<style module>
.button {
color: red;
font-weight: bold;
}
</style>
```
::: warning Markdown での `<style scoped>` は避ける
Markdown で `<style scoped>` を使うと、そのページ内のすべての要素に特殊な属性を付与する必要があり、ページサイズが大きく膨らみます。ページ単位でローカルスコープが必要な場合は `<style module>` を推奨します。
@ -75,26 +75,26 @@ VitePress のランタイム API現在ページのメタデータにア
**入力**
```html
<script setup>
import { useData } from 'vitepress'
```md
<script setup>
import { useData } from 'vitepress'
const { page } = useData()
</script>
const { page } = useData()
</script>
<pre>{{ page }}</pre>
```
<pre>{{ page }}</pre>
```
**出力**
```json
{
"path": "/using-vue.html",
"title": "Using Vue in Markdown",
"frontmatter": {},
...
}
```
```json
{
"path": "/using-vue.html",
"title": "Using Vue in Markdown",
"frontmatter": {},
...
}
```
## コンポーネントの利用 {#using-components}
@ -104,21 +104,21 @@ Markdown ファイルで、Vue コンポーネントを直接インポートし
特定のページでしか使わないコンポーネントは、そのページで明示的にインポートするのがおすすめです。これにより適切にコード分割され、該当ページでのみ読み込まれます。
```md
<script setup>
import CustomComponent from '../components/CustomComponent.vue'
</script>
```md
<script setup>
import CustomComponent from '../components/CustomComponent.vue'
</script>
# ドキュメント
# ドキュメント
これはカスタムコンポーネントを使う .md です
これはカスタムコンポーネントを使う .md です
<CustomComponent />
<CustomComponent />
## 続き
## 続き
...
```
...
```
### グローバル登録 {#registering-components-globally}
@ -132,10 +132,10 @@ Markdown ファイルで、Vue コンポーネントを直接インポートし
見出し内で Vue コンポーネントを使うこともできますが、次の書き方の違いに注意してください。
| Markdown | 出力 HTML | 解析される見出し |
| ------------------------------------------------------- | ------------------------------------------- | --------------- |
| <pre v-pre><code> # text &lt;Tag/&gt; </code></pre> | `<h1>text <Tag/></h1>` | `text` |
| <pre v-pre><code> # text \`&lt;Tag/&gt;\` </code></pre> | `<h1>text <code>&lt;Tag/&gt;</code></h1>` | `text <Tag/>` |
| Markdown | 出力 HTML | 解析される見出し |
| ------------------------------------------------------- | ----------------------------------------- | ---------------- |
| <pre v-pre><code> # text &lt;Tag/&gt; </code></pre> | `<h1>text <Tag/></h1>` | `text` |
| <pre v-pre><code> # text \`&lt;Tag/&gt;\` </code></pre> | `<h1>text <code>&lt;Tag/&gt;</code></h1>` | `text <Tag/>` |
`<code>` に包まれた HTML はそのまま表示されます。包まれて**いない** HTML だけが Vue によってパースされます。
@ -149,9 +149,9 @@ Markdown ファイルで、Vue コンポーネントを直接インポートし
**入力**
```md
This <span v-pre>{{ will be displayed as-is }}</span>
```
```md
This <span v-pre>{{ will be displayed as-is }}</span>
```
**出力**
@ -161,19 +161,19 @@ Markdown ファイルで、Vue コンポーネントを直接インポートし
段落全体を `v-pre` のカスタムコンテナで囲む方法もあります。
```md
::: v-pre
{{ This will be displayed as-is }}
:::
```
```md
::: v-pre
{{ This will be displayed as-is }}
:::
```
**出力**
<div class="escape-demo">
::: v-pre
{{ This will be displayed as-is }}
:::
::: v-pre
{{ This will be displayed as-is }}
:::
</div>
@ -183,17 +183,17 @@ Markdown ファイルで、Vue コンポーネントを直接インポートし
**入力**
````md
```js-vue
Hello {{ 1 + 1 }}
```
````
````md
```js-vue
Hello {{ 1 + 1 }}
```
````
**出力**
```js-vue
Hello {{ 1 + 1 }}
```
```js-vue
Hello {{ 1 + 1 }}
```
この方法では、一部のトークンが正しくシンタックスハイライトされない場合があります。
@ -201,25 +201,25 @@ Markdown ファイルで、Vue コンポーネントを直接インポートし
VitePress は CSS プリプロセッサ(`.scss`、`.sass`、`.less`、`.styl`、`.stylus`)を[標準サポート](https://vitejs.dev/guide/features.html#css-pre-processors)しています。Vite 固有のプラグインは不要ですが、各プリプロセッサ本体のインストールは必要です。
```
# .scss / .sass
npm install -D sass
```
# .scss / .sass
npm install -D sass
# .less
npm install -D less
# .less
npm install -D less
# .styl / .stylus
npm install -D stylus
```
# .styl / .stylus
npm install -D stylus
```
その後、Markdown やテーマコンポーネントで次のように使えます。
```vue
<style lang="sass">
.title
font-size: 20px
</style>
```
```vue
<style lang="sass">
.title
font-size: 20px
</style>
```
## Teleport の利用 {#using-teleports}
@ -231,15 +231,15 @@ VitePress は CSS プリプロセッサ(`.scss`、`.sass`、`.less`、`.styl`
<<< @/components/ModalDemo.vue
:::
```md
<ClientOnly>
<Teleport to="#modal">
<div>
// ...
</div>
</Teleport>
</ClientOnly>
```
```md
<ClientOnly>
<Teleport to="#modal">
<div>
// ...
</div>
</Teleport>
</ClientOnly>
```
<script setup>
import ModalDemo from '../../components/ModalDemo.vue'
@ -262,27 +262,27 @@ Vue は [Vue - Official VS Code plugin](https://marketplace.visualstudio.com/ite
1. tsconfig/jsconfig の `include``vueCompilerOptions.vitePressExtensions``.md` パターンを追加します。
::: code-group
```json [tsconfig.json]
{
"include": [
"docs/**/*.ts",
"docs/**/*.vue",
"docs/**/*.md",
],
"vueCompilerOptions": {
"vitePressExtensions": [".md"],
},
}
```
:::
::: code-group
```json [tsconfig.json]
{
"include": [
"docs/**/*.ts",
"docs/**/*.vue",
"docs/**/*.md"
],
"vueCompilerOptions": {
"vitePressExtensions": [".md"]
}
}
```
:::
2. VS Code の設定で、`vue.server.includeLanguages` に `markdown` を追加します。
::: code-group
```json [.vscode/settings.json]
{
"vue.server.includeLanguages": ["vue", "markdown"]
}
```
:::
::: code-group
```json [.vscode/settings.json]
{
"vue.server.includeLanguages": ["vue", "markdown"]
}
```
:::

@ -6,24 +6,24 @@
### 使い方 {#usage}
```sh
# カレントディレクトリで起動(`dev` を省略)
vitepress
```sh
# カレントディレクトリで起動(`dev` を省略)
vitepress
# サブディレクトリで起動
vitepress dev [root]
```
# サブディレクトリで起動
vitepress dev [root]
```
### オプション {#options}
| オプション | 説明 |
| ------------------ | -------------------------------------------------------------------- |
| `--open [path]` | 起動時にブラウザを開く(`boolean \| string` |
| `--port <port>` | ポート番号を指定(`number` |
| `--base <path>` | 公開時のベースパス(既定: `/``string` |
| `--cors` | CORS を有効化 |
| `--strictPort` | 指定ポートが使用中なら終了(`boolean` |
| `--force` | 最適化時にキャッシュを無視して再バンドル(`boolean` |
| オプション | 説明 |
| --------------- | ----------------------------------------------------- |
| `--open [path]` | 起動時にブラウザを開く(`boolean \| string` |
| `--port <port>` | ポート番号を指定(`number` |
| `--base <path>` | 公開時のベースパス(既定: `/``string` |
| `--cors` | CORS を有効化 |
| `--strictPort` | 指定ポートが使用中なら終了(`boolean` |
| `--force` | 最適化時にキャッシュを無視して再バンドル(`boolean` |
## `vitepress build`
@ -31,19 +31,19 @@
### 使い方 {#usage-1}
```sh
vitepress build [root]
```
```sh
vitepress build [root]
```
### オプション {#options-1}
| オプション | 説明 |
| ----------------------------- | -------------------------------------------------------------------------------------------------- |
| `--mpa`(実験的) | クライアント側ハイドレーションなしの [MPA モード](../guide/mpa-mode) でビルド(`boolean` |
| `--base <path>` | 公開時のベースパス(既定: `/``string` |
| `--target <target>` | トランスパイルターゲット(既定: `"modules"``string` |
| `--outDir <dir>` | 出力先ディレクトリ(**cwd** からの相対)(既定: `<root>/.vitepress/dist``string` |
| `--assetsInlineLimit <number>`| 静的アセットを base64 インライン化する閾値(バイト)(既定: `4096``number` |
| オプション | 説明 |
| ------------------------------ | ------------------------------------------------------------------------------------------ |
| `--mpa`(実験的) | クライアント側ハイドレーションなしの [MPA モード](../guide/mpa-mode) でビルド(`boolean` |
| `--base <path>` | 公開時のベースパス(既定: `/``string` |
| `--target <target>` | トランスパイルターゲット(既定: `"modules"``string` |
| `--outDir <dir>` | 出力先ディレクトリ(**cwd** からの相対)(既定: `<root>/.vitepress/dist``string` |
| `--assetsInlineLimit <number>` | 静的アセットを base64 インライン化する閾値(バイト)(既定: `4096``number` |
## `vitepress preview`
@ -51,16 +51,16 @@
### 使い方 {#usage-2}
```sh
vitepress preview [root]
```
```sh
vitepress preview [root]
```
### オプション {#options-2}
| オプション | 説明 |
| ------------------ | ----------------------------------------- |
| `--base <path>` | 公開時のベースパス(既定: `/``string` |
| `--port <port>` | ポート番号を指定(`number` |
| オプション | 説明 |
| --------------- | ------------------------------------------- |
| `--base <path>` | 公開時のベースパス(既定: `/``string` |
| `--port <port>` | ポート番号を指定(`number` |
## `vitepress init`
@ -68,6 +68,6 @@
### 使い方 {#usage-3}
```sh
vitepress init
```
```sh
vitepress init
```

@ -6,12 +6,12 @@
グローバルに利用可能な `Badge` コンポーネントを使用します。
```html
### Title <Badge type="info" text="default" />
### Title <Badge type="tip" text="^1.9.0" />
### Title <Badge type="warning" text="beta" />
### Title <Badge type="danger" text="caution" />
```
```md
### Title <Badge type="info" text="default" />
### Title <Badge type="tip" text="^1.9.0" />
### Title <Badge type="warning" text="beta" />
### Title <Badge type="danger" text="caution" />
```
上記のコードは次のように表示されます:
@ -24,9 +24,9 @@
`<Badge>` は子要素(`children`)を受け取り、バッジ内に表示できます。
```html
### Title <Badge type="info">custom element</Badge>
```
```md
### Title <Badge type="info">custom element</Badge>
```
### Title <Badge type="info">custom element</Badge>
@ -34,36 +34,36 @@
CSS 変数を上書きすることで、バッジのスタイルをカスタマイズできます。以下はデフォルト値です:
```css
:root {
--vp-badge-info-border: transparent;
--vp-badge-info-text: var(--vp-c-text-2);
--vp-badge-info-bg: var(--vp-c-default-soft);
```css
:root {
--vp-badge-info-border: transparent;
--vp-badge-info-text: var(--vp-c-text-2);
--vp-badge-info-bg: var(--vp-c-default-soft);
--vp-badge-tip-border: transparent;
--vp-badge-tip-text: var(--vp-c-brand-1);
--vp-badge-tip-bg: var(--vp-c-brand-soft);
--vp-badge-tip-border: transparent;
--vp-badge-tip-text: var(--vp-c-brand-1);
--vp-badge-tip-bg: var(--vp-c-brand-soft);
--vp-badge-warning-border: transparent;
--vp-badge-warning-text: var(--vp-c-warning-1);
--vp-badge-warning-bg: var(--vp-c-warning-soft);
--vp-badge-warning-border: transparent;
--vp-badge-warning-text: var(--vp-c-warning-1);
--vp-badge-warning-bg: var(--vp-c-warning-soft);
--vp-badge-danger-border: transparent;
--vp-badge-danger-text: var(--vp-c-danger-1);
--vp-badge-danger-bg: var(--vp-c-danger-soft);
}
```
--vp-badge-danger-border: transparent;
--vp-badge-danger-text: var(--vp-c-danger-1);
--vp-badge-danger-bg: var(--vp-c-danger-soft);
}
```
## `<Badge>`
`<Badge>` コンポーネントは次の props を受け取ります。
```ts
interface Props {
// `<slot>` が渡された場合、この値は無視されます。
text?: string
```ts
interface Props {
// `<slot>` が渡された場合、この値は無視されます。
text?: string
// 既定値は `tip`
type?: 'info' | 'tip' | 'warning' | 'danger'
}
```
// 既定値は `tip`
type?: 'info' | 'tip' | 'warning' | 'danger'
}
```

@ -2,21 +2,21 @@
VitePress は [Carbon Ads](https://www.carbonads.net/) をネイティブにサポートしています。設定で Carbon Ads の認証情報を定義すると、ページ上に広告が表示されます。
```js
export default {
themeConfig: {
carbonAds: {
code: 'your-carbon-code',
placement: 'your-carbon-placement'
}
}
}
```
```js
export default {
themeConfig: {
carbonAds: {
code: 'your-carbon-code',
placement: 'your-carbon-placement'
}
}
}
```
これらの値は、次のように Carbon の CDN スクリプトを呼び出すために使用されます。
```js
`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}`
```
```js
`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}`
```
Carbon Ads の設定について詳しくは、[Carbon Ads のウェブサイト](https://www.carbonads.net/)を参照してください。

@ -2,20 +2,20 @@
テーマ設定では、テーマのカスタマイズができます。設定ファイルの `themeConfig` オプションで定義します。
```ts
export default {
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
// テーマ関連の設定
themeConfig: {
logo: '/logo.svg',
nav: [...],
sidebar: { ... }
}
}
```
```ts
export default {
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
// テーマ関連の設定
themeConfig: {
logo: '/logo.svg',
nav: [...],
sidebar: { ... }
}
}
```
**このページで説明するオプションは、デフォルトテーマにのみ適用されます。** テーマによって期待する設定は異なります。カスタムテーマを使用する場合、ここで定義したテーマ設定オブジェクトはテーマへ渡され、テーマ側がそれに基づいて条件付きの挙動を定義できます。
@ -31,20 +31,20 @@
サイトタイトルの直前に、ナビゲーションバーに表示されるロゴ。パス文字列、またはライト/ダークモードで異なるロゴを設定するオブジェクトを受け取ります。
```ts
export default {
themeConfig: {
logo: '/logo.svg'
}
}
```
```ts
type ThemeableImage =
| string
| { src: string; alt?: string }
| { light: string; dark: string; alt?: string }
```
```ts
export default {
themeConfig: {
logo: '/logo.svg'
}
}
```
```ts
type ThemeableImage =
| string
| { src: string; alt?: string }
| { light: string; dark: string; alt?: string }
```
## siteTitle
@ -52,13 +52,13 @@
ナビゲーション内の既定サイトタイトル(アプリ設定の `title`)を置き換えます。`false` の場合、ナビのタイトルを非表示にします。ロゴ自体にサイト名が含まれている場合に便利です。
```ts
export default {
themeConfig: {
siteTitle: 'Hello World'
}
}
```
```ts
export default {
themeConfig: {
siteTitle: 'Hello World'
}
}
```
## nav
@ -66,47 +66,47 @@
ナビゲーションメニューの設定。[デフォルトテーマ: ナビ](./default-theme-nav#navigation-links) を参照してください。
```ts
export default {
themeConfig: {
nav: [
{ text: 'Guide', link: '/guide' },
{
text: 'Dropdown Menu',
items: [
{ text: 'Item A', link: '/item-1' },
{ text: 'Item B', link: '/item-2' },
{ text: 'Item C', link: '/item-3' }
]
}
]
}
}
```
```ts
type NavItem = NavItemWithLink | NavItemWithChildren
interface NavItemWithLink {
text: string
link: string | ((payload: PageData) => string)
activeMatch?: string
target?: string
rel?: string
noIcon?: boolean
}
interface NavItemChildren {
text?: string
items: NavItemWithLink[]
}
interface NavItemWithChildren {
text?: string
items: (NavItemChildren | NavItemWithLink)[]
activeMatch?: string
}
```
```ts
export default {
themeConfig: {
nav: [
{ text: 'Guide', link: '/guide' },
{
text: 'Dropdown Menu',
items: [
{ text: 'Item A', link: '/item-1' },
{ text: 'Item B', link: '/item-2' },
{ text: 'Item C', link: '/item-3' }
]
}
]
}
}
```
```ts
type NavItem = NavItemWithLink | NavItemWithChildren
interface NavItemWithLink {
text: string
link: string | ((payload: PageData) => string)
activeMatch?: string
target?: string
rel?: string
noIcon?: boolean
}
interface NavItemChildren {
text?: string
items: NavItemWithLink[]
}
interface NavItemWithChildren {
text?: string
items: (NavItemChildren | NavItemWithLink)[]
activeMatch?: string
}
```
## sidebar
@ -114,69 +114,69 @@
サイドバーメニューの設定。[デフォルトテーマ: サイドバー](./default-theme-sidebar) を参照してください。
```ts
export default {
themeConfig: {
sidebar: [
{
text: 'Guide',
items: [
{ text: 'Introduction', link: '/introduction' },
{ text: 'Getting Started', link: '/getting-started' },
...
]
}
]
}
}
```
```ts
export type Sidebar = SidebarItem[] | SidebarMulti
export interface SidebarMulti {
[path: string]: SidebarItem[] | { items: SidebarItem[]; base: string }
}
export type SidebarItem = {
/**
* 項目のテキストラベル
*/
text?: string
/**
* 項目のリンク
*/
link?: string
/**
* 子項目
*/
items?: SidebarItem[]
/**
* 指定しない場合、グループは折りたたみ不可。
*
* `true` なら折りたたみ可能でデフォルト折りたたみ
*
* `false` なら折りたたみ可能だがデフォルト展開
*/
collapsed?: boolean
/**
* 子項目のベースパス
*/
base?: string
/**
* 前/次リンクのフッターに表示するテキストをカスタマイズ
*/
docFooterText?: string
rel?: string
target?: string
}
```
```ts
export default {
themeConfig: {
sidebar: [
{
text: 'Guide',
items: [
{ text: 'Introduction', link: '/introduction' },
{ text: 'Getting Started', link: '/getting-started' },
...
]
}
]
}
}
```
```ts
export type Sidebar = SidebarItem[] | SidebarMulti
export interface SidebarMulti {
[path: string]: SidebarItem[] | { items: SidebarItem[]; base: string }
}
export type SidebarItem = {
/**
* 項目のテキストラベル
*/
text?: string
/**
* 項目のリンク
*/
link?: string
/**
* 子項目
*/
items?: SidebarItem[]
/**
* 指定しない場合、グループは折りたたみ不可。
*
* `true` なら折りたたみ可能でデフォルト折りたたみ
*
* `false` なら折りたたみ可能だがデフォルト展開
*/
collapsed?: boolean
/**
* 子項目のベースパス
*/
base?: string
/**
* 前/次リンクのフッターに表示するテキストをカスタマイズ
*/
docFooterText?: string
rel?: string
target?: string
}
```
## aside
@ -197,26 +197,26 @@
`false` でアウトラインコンテナの描画を無効化。詳細は以下を参照:
```ts
interface Outline {
/**
* アウトラインに表示する見出しレベル
* 単一の数値なら、そのレベルのみ表示
* タプルなら最小レベルと最大レベル
* `'deep'``[2, 6]` と同じ(`<h2>` 〜 `<h6>` を表示)
*
* @default 2
*/
level?: number | [number, number] | 'deep'
/**
* アウトラインに表示するタイトル
*
* @default 'On this page'
*/
label?: string
}
```
```ts
interface Outline {
/**
* アウトラインに表示する見出しレベル
* 単一の数値なら、そのレベルのみ表示
* タプルなら最小レベルと最大レベル
* `'deep'``[2, 6]` と同じ(`<h2>` 〜 `<h6>` を表示)
*
* @default 2
*/
level?: number | [number, number] | 'deep'
/**
* アウトラインに表示するタイトル
*
* @default 'On this page'
*/
label?: string
}
```
## socialLinks
@ -224,34 +224,34 @@
ナビゲーションにアイコン付きのソーシャルリンクを表示します。
```ts
export default {
themeConfig: {
socialLinks: [
// simple-icons (https://simpleicons.org/) の任意のアイコンを指定可能
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
{ icon: 'twitter', link: '...' },
// SVG 文字列を渡してカスタムアイコンも可
{
icon: {
svg: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Dribbble</title><path d="M12...6.38z"/></svg>'
},
link: '...',
// アクセシビリティ向けにカスタムラベルも指定可(推奨)
ariaLabel: 'cool link'
}
]
}
}
```
```ts
interface SocialLink {
icon: string | { svg: string }
link: string
ariaLabel?: string
}
```
```ts
export default {
themeConfig: {
socialLinks: [
// simple-icons (https://simpleicons.org/) の任意のアイコンを指定可能
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
{ icon: 'twitter', link: '...' },
// SVG 文字列を渡してカスタムアイコンも可
{
icon: {
svg: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Dribbble</title><path d="M12...6.38z"/></svg>'
},
link: '...',
// アクセシビリティ向けにカスタムラベルも指定可(推奨)
ariaLabel: 'cool link'
}
]
}
}
```
```ts
interface SocialLink {
icon: string | { svg: string }
link: string
ariaLabel?: string
}
```
## footer
@ -260,23 +260,23 @@
フッター設定。メッセージや著作権表示を追加できますが、ページにサイドバーがある場合はデザイン上表示されません。
```ts
export default {
themeConfig: {
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2019-present Evan You'
}
}
}
```
```ts
export interface Footer {
message?: string
copyright?: string
}
```
```ts
export default {
themeConfig: {
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2019-present Evan You'
}
}
}
```
```ts
export interface Footer {
message?: string
copyright?: string
}
```
## editLink
@ -285,23 +285,23 @@
「このページを編集」リンクを表示しますGitHub/GitLab など)。詳細は [デフォルトテーマ: 編集リンク](./default-theme-edit-link) を参照。
```ts
export default {
themeConfig: {
editLink: {
pattern: 'https://github.com/vuejs/vitepress/edit/main/docs/:path',
text: 'Edit this page on GitHub'
}
}
}
```
```ts
export interface EditLink {
pattern: string
text?: string
}
```
```ts
export default {
themeConfig: {
editLink: {
pattern: 'https://github.com/vuejs/vitepress/edit/main/docs/:path',
text: 'Edit this page on GitHub'
}
}
}
```
```ts
export interface EditLink {
pattern: string
text?: string
}
```
## lastUpdated
@ -309,34 +309,34 @@
最終更新の文言と日付フォーマットをカスタマイズします。
```ts
export default {
themeConfig: {
lastUpdated: {
text: 'Updated at',
formatOptions: {
dateStyle: 'full',
timeStyle: 'medium'
}
}
}
}
```
```ts
export interface LastUpdatedOptions {
/**
* @default 'Last updated'
*/
text?: string
/**
* @default
* { dateStyle: 'short', timeStyle: 'short' }
*/
formatOptions?: Intl.DateTimeFormatOptions & { forceLocale?: boolean }
}
```
```ts
export default {
themeConfig: {
lastUpdated: {
text: 'Updated at',
formatOptions: {
dateStyle: 'full',
timeStyle: 'medium'
}
}
}
}
```
```ts
export interface LastUpdatedOptions {
/**
* @default 'Last updated'
*/
text?: string
/**
* @default
* { dateStyle: 'short', timeStyle: 'short' }
*/
formatOptions?: Intl.DateTimeFormatOptions & { forceLocale?: boolean }
}
```
## algolia
@ -344,11 +344,11 @@
[Algolia DocSearch](https://docsearch.algolia.com/docs/what-is-docsearch) によるサイト内検索の設定。[デフォルトテーマ: 検索](./default-theme-search) を参照。
```ts
export interface AlgoliaSearchOptions extends DocSearchProps {
locales?: Record<string, Partial<DocSearchProps>>
}
```
```ts
export interface AlgoliaSearchOptions extends DocSearchProps {
locales?: Record<string, Partial<DocSearchProps>>
}
```
完全なオプションは[こちら](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts)。
@ -358,23 +358,23 @@
[Carbon Ads](https://www.carbonads.net/) を表示します。
```ts
export default {
themeConfig: {
carbonAds: {
code: 'your-carbon-code',
placement: 'your-carbon-placement'
}
}
}
```
```ts
export interface CarbonAdsOptions {
code: string
placement: string
}
```
```ts
export default {
themeConfig: {
carbonAds: {
code: 'your-carbon-code',
placement: 'your-carbon-placement'
}
}
}
```
```ts
export interface CarbonAdsOptions {
code: string
placement: string
}
```
詳細は [デフォルトテーマ: Carbon Ads](./default-theme-carbon-ads) を参照。
@ -384,23 +384,23 @@
前/次リンクの上に表示される文言をカスタマイズします。英語以外のドキュメントで便利。前/次リンク自体をグローバルに無効化することも可能。ページごとに切り替えたい場合は [frontmatter](./default-theme-prev-next-links) を使用します。
```ts
export default {
themeConfig: {
docFooter: {
prev: 'Pagina prior',
next: 'Proxima pagina'
}
}
}
```
```ts
export interface DocFooter {
prev?: string | false
next?: string | false
}
```
```ts
export default {
themeConfig: {
docFooter: {
prev: 'Pagina prior',
next: 'Proxima pagina'
}
}
}
```
```ts
export interface DocFooter {
prev?: string | false
next?: string | false
}
```
## darkModeSwitchLabel
@ -462,33 +462,33 @@ Markdown 内の外部リンクの横に外部リンクアイコンを表示す
レイアウト関連のデータを返します。返り値の型は次のとおりです。
```ts
interface {
isHome: ComputedRef<boolean>
```ts
interface {
isHome: ComputedRef<boolean>
sidebar: Readonly<ShallowRef<DefaultTheme.SidebarItem[]>>
sidebarGroups: ComputedRef<DefaultTheme.SidebarItem[]>
hasSidebar: ComputedRef<boolean>
isSidebarEnabled: ComputedRef<boolean>
sidebar: Readonly<ShallowRef<DefaultTheme.SidebarItem[]>>
sidebarGroups: ComputedRef<DefaultTheme.SidebarItem[]>
hasSidebar: ComputedRef<boolean>
isSidebarEnabled: ComputedRef<boolean>
hasAside: ComputedRef<boolean>
leftAside: ComputedRef<boolean>
hasAside: ComputedRef<boolean>
leftAside: ComputedRef<boolean>
headers: Readonly<ShallowRef<DefaultTheme.OutlineItem[]>>
hasLocalNav: ComputedRef<boolean>
}
```
headers: Readonly<ShallowRef<DefaultTheme.OutlineItem[]>>
hasLocalNav: ComputedRef<boolean>
}
```
**例:**
```vue
<script setup>
import { useLayout } from 'vitepress/theme'
```vue
<script setup>
import { useLayout } from 'vitepress/theme'
const { hasSidebar } = useLayout()
</script>
const { hasSidebar } = useLayout()
</script>
<template>
<div v-if="hasSidebar">サイドバーがあるときだけ表示</div>
</template>
```
<template>
<div v-if="hasSidebar">サイドバーがあるときだけ表示</div>
</template>
```

@ -4,57 +4,57 @@
編集リンクは、GitHub や GitLab などの Git 管理サービスでそのページを編集できるリンクを表示します。有効化するには、設定に `themeConfig.editLink` オプションを追加します。
```js
export default {
themeConfig: {
editLink: {
pattern: 'https://github.com/vuejs/vitepress/edit/main/docs/:path'
}
}
}
```
```js
export default {
themeConfig: {
editLink: {
pattern: 'https://github.com/vuejs/vitepress/edit/main/docs/:path'
}
}
}
```
`pattern` オプションはリンクの URL 構造を定義します。`:path` はページパスに置き換えられます。
また、引数に [`PageData`](./runtime-api#usedata) を受け取り、URL 文字列を返す純粋関数を指定することもできます。
```js
export default {
themeConfig: {
editLink: {
pattern: ({ filePath }) => {
if (filePath.startsWith('packages/')) {
return `https://github.com/acme/monorepo/edit/main/${filePath}`
} else {
return `https://github.com/acme/monorepo/edit/main/docs/${filePath}`
}
}
}
}
}
```
```js
export default {
themeConfig: {
editLink: {
pattern: ({ filePath }) => {
if (filePath.startsWith('packages/')) {
return `https://github.com/acme/monorepo/edit/main/${filePath}`
} else {
return `https://github.com/acme/monorepo/edit/main/docs/${filePath}`
}
}
}
}
}
```
この関数はブラウザでシリアライズされ実行されるため、副作用を持たず、スコープ外のものへアクセスしないでください。
既定では、ドキュメント下部に「Edit this page」というリンクテキストが表示されます。`text` オプションでこの文言をカスタマイズできます。
```js
export default {
themeConfig: {
editLink: {
pattern: 'https://github.com/vuejs/vitepress/edit/main/docs/:path',
text: 'GitHub でこのページを編集'
}
}
}
```
```js
export default {
themeConfig: {
editLink: {
pattern: 'https://github.com/vuejs/vitepress/edit/main/docs/:path',
text: 'GitHub でこのページを編集'
}
}
}
```
## フロントマターでの設定 {#frontmatter-config}
ページごとに無効化するには、フロントマターで `editLink` オプションを使用します。
```yaml
---
editLink: false
---
```
```yaml
---
editLink: false
---
```

@ -186,3 +186,4 @@ hero:
npm init
npx vitepress init
```
````

@ -29,6 +29,7 @@ export default defineConfig({
- <https://www.npmjs.com/package/vitepress-plugin-search>
- <https://www.npmjs.com/package/vitepress-plugin-pagefind>
- <https://www.npmjs.com/package/@orama/plugin-vitepress>
- <https://www.npmjs.com/package/vitepress-plugin-typesense>
### i18n {#local-search-i18n}
@ -43,25 +44,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
zh: { // 既定ロケールの文言も翻訳したい場合はこれを `root`
ja: { // 既定ロケールを翻訳する場合は `root` にしてください
translations: {
button: {
buttonText: '索',
buttonAriaLabel: '索'
buttonText: '索',
buttonAriaLabel: '索'
},
modal: {
displayDetails: '显示详细列表',
resetButtonTitle: '重置搜索',
backButtonTitle: '关闭搜索',
noResultsText: '没有结果',
displayDetails: '詳細一覧を表示',
resetButtonTitle: '検索をリセット',
backButtonTitle: '検索を閉じる',
noResultsText: '結果が見つかりません',
footer: {
selectText: '选择',
selectKeyAriaLabel: '输入',
navigateText: '导航',
navigateUpKeyAriaLabel: '上箭头',
navigateDownKeyAriaLabel: '下箭头',
closeText: '关闭',
closeKeyAriaLabel: 'esc'
selectText: '選択',
selectKeyAriaLabel: 'Enter',
navigateText: '移動',
navigateUpKeyAriaLabel: '上矢印',
navigateDownKeyAriaLabel: '下矢印',
closeText: '閉じる',
closeKeyAriaLabel: 'Esc'
}
}
}
@ -73,7 +74,7 @@ export default defineConfig({
})
```
### miniSearch のオプション {#mini-search-options}
### miniSearch のオプション {#minisearch-options}
MiniSearch の設定例です。
@ -178,7 +179,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.title)
return await md.renderAsync(`# ${env.frontmatter.title}`) + html
return (await md.renderAsync(`# ${env.frontmatter.title}`)) + html
return html
}
}
@ -212,6 +213,19 @@ export default defineConfig({
多言語検索の設定例です。
<details>
<summary>クリックして展開</summary>
<<< @/snippets/algolia-i18n.ts
</details>
詳しくは[公式 Algolia ドキュメント](https://docsearch.algolia.com/docs/api#translations)を参照してください。すぐに始めるには、このサイトで使っている翻訳を[GitHub リポジトリ](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code)からコピーすることもできます。
### Algolia Ask AI のサポート {#ask-ai}
**Ask AI** を有効にするには、`options` 内に `askAi` オプション(またはその一部)を指定します。
```ts
import { defineConfig } from 'vitepress'
@ -223,79 +237,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
clearButtonTitle: '清除查询条件',
clearButtonAriaLabel: '清除查询条件',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档',
placeholderTextAskAi: '向 AI 提问:',
placeholderTextAskAiStreaming: '回答中...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键字搜索',
backToKeywordSearchButtonAriaLabel: '返回关键字搜索'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除',
recentConversationsTitle: '最近的对话',
removeRecentConversationButtonTitle: '从历史记录中删除对话'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
},
resultsScreen: {
askAiPlaceholder: '向 AI 提问: '
},
askAiScreen: {
disclaimerText: '答案由 AI 生成,可能不准确,请自行验证。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '赞',
dislikeButtonTitle: '踩',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索 ',
afterToolCallText: '已搜索'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: 'Enter 键',
navigateText: '切换',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '搜索提供者'
}
}
}
}
// askAi: "あなたのアシスタントID"
// または
askAi: {
// 最低限、Algolia から受け取った assistantId を指定する必要があります
assistantId: 'XXXYYY',
// 任意の上書き — 省略した場合は上位の appId/apiKey/indexName を再利用
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -303,11 +253,13 @@ export default defineConfig({
})
```
[これらのオプション](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) は上書きできます。詳細は Algolia の公式ドキュメントを参照してください。
::: warning 注意
キーワード検索を既定にして Ask AI を使わない場合は、`askAi` を指定しないでください。
:::
### Algolia Ask AI のサポート {#ask-ai}
### Ask AI サイドパネル {#ask-ai-side-panel}
**Ask AI** を有効にするには、`options` 内に `askAi` オプション(またはその一部)を指定します。
DocSearch v4.5+ はオプションの **Ask AI サイドパネル**をサポートしています。有効にすると、デフォルトで **Ctrl/Cmd+I** で開くことができます。[サイドパネル API リファレンス](https://docsearch.algolia.com/docs/sidepanel/api-reference)にオプションの完全なリストがあります。
```ts
import { defineConfig } from 'vitepress'
@ -320,15 +272,18 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
// askAi: "YOUR-ASSISTANT-ID"
// または
askAi: {
// 少なくとも Algolia から受け取った assistantId を指定
assistantId: 'XXXYYY',
// 任意の上書き — 省略時は上位の appId/apiKey/indexName を再利用
// apiKey: '...',
// appId: '...',
// indexName: '...'
sidePanel: {
// @docsearch/sidepanel-js SidepanelProps API をミラー
panel: {
variant: 'floating', // または 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
@ -336,116 +291,70 @@ export default defineConfig({
})
```
::: warning 注意
キーワード検索を既定にして Ask AI を使わない場合は、`askAi` を指定しないでください。
:::
キーボードショートカットを無効にする必要がある場合は、サイドパネルの `keyboardShortcuts` オプションを使用してください:
Ask AI UI の翻訳は `options.translations.modal.askAiScreen``options.translations.resultsScreen` にあります。すべてのキーは[型定義](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts)を参照してください。
```ts
import { defineConfig } from 'vitepress'
### クローラー設定 {#crawler-config}
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
このサイトで使用している設定を元にした例です。
#### モード (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
VitePress がキーワード検索と Ask AI を統合する方法をオプションで制御できます:
- `mode: 'auto'`(デフォルト):キーワード検索が設定されている場合は `hybrid` を推論し、それ以外の場合は Ask AI サイドパネルが設定されている場合は `sidePanel` を推論します。
- `mode: 'sidePanel'`:サイドパネルのみを強制(キーワード検索ボタンを非表示)。
- `mode: 'hybrid'`:キーワード検索モーダル + Ask AI サイドパネルを有効化(キーワード検索設定が必要)。
- `mode: 'modal'`Ask AI を DocSearch モーダル内に保持(サイドパネルを設定した場合でも)。
#### Ask AI のみ(キーワード検索なし) {#ask-ai-only}
**Ask AI サイドパネルのみ**を使用する場合は、トップレベルのキーワード検索設定を省略し、`askAi` の下に認証情報を提供できます:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
### クローラー設定 {#crawler-config}
このサイトで使用している設定を元にした例です。
<<< @/snippets/algolia-crawler.js

@ -49,7 +49,7 @@ interface PageData {
titleTemplate?: string | boolean
description: string
relativePath: string
filePath: string,
filePath: string
headers: Header[]
frontmatter: Record<string, any>
params?: Record<string, any>

@ -223,7 +223,6 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: '문서 검색',
translations: {
button: {
buttonText: '검색',
@ -231,44 +230,67 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
},
modal: {
searchBox: {
clearButtonTitle: '검색 지우기',
clearButtonAriaLabel: '검색 지우기',
clearButtonTitle: '지우기',
clearButtonAriaLabel: '검색 지우기',
closeButtonText: '닫기',
closeButtonAriaLabel: '닫기',
placeholderText: '문서 검색',
placeholderTextAskAi: 'AI에게 물어보기: ',
placeholderTextAskAiStreaming: '답변 작성 중...',
placeholderText: '문서 검색하거나 Ask AI에 질문',
placeholderTextAskAi: '다른 질문하기...',
placeholderTextAskAiStreaming: '답변 중...',
searchInputLabel: '검색',
backToKeywordSearchButtonText: '키워드 검색으로 돌아가기',
backToKeywordSearchButtonAriaLabel: '키워드 검색으로 돌아가기'
backToKeywordSearchButtonAriaLabel: '키워드 검색으로 돌아가기',
newConversationPlaceholder: '질문하기',
conversationHistoryTitle: '내 대화 기록',
startNewConversationText: '새 대화 시작',
viewConversationHistoryText: '대화 기록',
threadDepthErrorPlaceholder: '대화 한도에 도달했습니다'
},
newConversation: {
newConversationTitle: '오늘 무엇을 도와드릴까요?',
newConversationDescription:
'문서를 검색해 설정 가이드, 기능 설명, 문제 해결 팁을 빠르게 찾아드립니다.'
},
footer: {
selectText: '선택',
submitQuestionText: '질문 제출',
selectKeyAriaLabel: 'Enter 키',
navigateText: '이동',
navigateUpKeyAriaLabel: '위 화살표',
navigateDownKeyAriaLabel: '아래 화살표',
closeText: '닫기',
backToSearchText: '검색으로 돌아가기',
closeKeyAriaLabel: 'Escape 키',
poweredByText: '제공'
},
errorScreen: {
titleText: '결과를 불러올 수 없습니다',
helpText: '네트워크 연결을 확인해 주세요.'
},
startScreen: {
recentSearchesTitle: '검색 기록',
noRecentSearchesText: '최근 검색 없음',
saveRecentSearchButtonTitle: '검색 기록에 저장',
removeRecentSearchButtonTitle: '검색 기록에서 삭제',
recentSearchesTitle: '최근',
noRecentSearchesText: '최근 검색이 없습니다',
saveRecentSearchButtonTitle: '검색 저장',
removeRecentSearchButtonTitle: '기록에서 이 검색 제거',
favoriteSearchesTitle: '즐겨찾기',
removeFavoriteSearchButtonTitle: '즐겨찾기에서 삭제',
removeFavoriteSearchButtonTitle: '즐겨찾기에서 이 검색 제거',
recentConversationsTitle: '최근 대화',
removeRecentConversationButtonTitle: '대화를 기록에서 삭제'
},
errorScreen: {
titleText: '결과를 가져올 수 없습니다',
helpText: '네트워크 연결을 확인하세요'
removeRecentConversationButtonTitle: '기록에서 이 대화 제거'
},
noResultsScreen: {
noResultsText: '결과를 찾을 수 없습니다',
suggestedQueryText: '다른 검색어를 시도해 보세요',
reportMissingResultsText: '결과가 있어야 한다고 생각하나요?',
reportMissingResultsLinkText: '피드백 보내기'
noResultsText: '다음에 대한 결과를 찾을 수 없습니다',
suggestedQueryText: '다음을 검색해 보세요',
reportMissingResultsText: '이 검색은 결과가 있어야 하나요?',
reportMissingResultsLinkText: '알려주세요.'
},
resultsScreen: {
askAiPlaceholder: 'AI에게 물어보기: '
askAiPlaceholder: 'AI에게 묻기: ',
noResultsAskAiPlaceholder: '문서에서 찾지 못했나요? Ask AI에 문의: '
},
askAiScreen: {
disclaimerText:
'AI가 생성한 답변으로 오류가 있을 수 있습니다. 반드시 확인하세요.',
relatedSourcesText: '관련 소스',
'답변은 AI가 생성하며 오류가 있을 수 있습니다. 확인해 주세요.',
relatedSourcesText: '관련 출처',
thinkingText: '생각 중...',
copyButtonText: '복사',
copyButtonCopiedText: '복사됨!',
@ -277,21 +299,67 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
dislikeButtonTitle: '싫어요',
thanksForFeedbackText: '피드백 감사합니다!',
preToolCallText: '검색 중...',
duringToolCallText: '검색 중 ',
afterToolCallText: '검색 완료',
aggregatedToolCallText: '검색 완료'
duringToolCallText: '검색 중...',
afterToolCallText: '검색함',
stoppedStreamingText: '이 응답을 중지했습니다',
errorTitleText: '채팅 오류',
threadDepthExceededMessage:
'정확성을 유지하기 위해 이 대화는 종료되었습니다.',
startNewConversationButtonText: '새 대화 시작'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'AI에게 묻기',
buttonAriaLabel: 'AI에게 묻기'
}
},
footer: {
selectText: '선택',
submitQuestionText: '질문 보내기',
selectKeyAriaLabel: 'Enter 키',
navigateText: '탐색',
navigateUpKeyAriaLabel: '위쪽 화살표',
navigateDownKeyAriaLabel: '아래쪽 화살표',
closeText: '닫기',
backToSearchText: '검색으로 돌아가기',
closeKeyAriaLabel: 'Esc 키',
poweredByText: '제공: '
panel: {
translations: {
header: {
title: 'AI에게 묻기',
conversationHistoryTitle: '내 대화 기록',
newConversationText: '새 대화 시작',
viewConversationHistoryText: '대화 기록'
},
promptForm: {
promptPlaceholderText: '질문하기',
promptAnsweringText: '답변 중...',
promptAskAnotherQuestionText: '다른 질문하기',
promptDisclaimerText:
'답변은 AI가 생성하며 오류가 있을 수 있습니다.',
promptLabelText: 'Enter로 전송, Shift+Enter로 줄바꿈.',
promptAriaLabelText: '프롬프트 입력'
},
conversationScreen: {
preToolCallText: '검색 중...',
searchingText: '검색 중...',
toolCallResultText: '검색함',
conversationDisclaimer:
'답변은 AI가 생성하며 오류가 있을 수 있습니다. 확인해 주세요.',
reasoningText: '추론 중...',
thinkingText: '생각 중...',
relatedSourcesText: '관련 출처',
stoppedStreamingText: '이 응답을 중지했습니다',
copyButtonText: '복사',
copyButtonCopiedText: '복사됨!',
likeButtonTitle: '좋아요',
dislikeButtonTitle: '싫어요',
thanksForFeedbackText: '피드백 감사합니다!',
errorTitleText: '채팅 오류'
},
newConversationScreen: {
titleText: '오늘 무엇을 도와드릴까요?',
introductionText:
'문서를 검색해 설정 가이드, 기능 설명, 문제 해결 팁을 빠르게 찾아드립니다.'
},
logo: {
poweredByText: '제공'
}
}
}
}
}

@ -105,13 +105,13 @@ Cache-Control: max-age=31536000,immutable
## 플랫폼 가이드 {#platform-guides}
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#generic}
새 프로젝트를 설정하고 대시보드를 사용하여 다음 설정을 변경하세요:
- **빌드 명령어:** `npm run docs:build`
- **출력 디렉토리:** `docs/.vitepress/dist`
- **노드 버전:** `18` (또는 그 이상)
- **노드 버전:** `20` (또는 그 이상)
::: warning
HTML 코드에 대해 _Auto Minify_ 옵션을 활성화하지 마세요. 이는 Vue에 의미가 있는 주석을 출력에서 제거할 것입니다. 제거되면 하이드레이션 불일치 오류가 발생할 수 있습니다.
@ -220,7 +220,7 @@ HTML 코드에 대해 _Auto Minify_ 옵션을 활성화하지 마세요. 이는
- main
```
### Azure 정적 Web 앱 {#azure-static-web-apps}
### Azure
1. [공식 문서](https://docs.microsoft.com/en-us/azure/static-web-apps/build-configuration)를 따르세요.
@ -230,7 +230,11 @@ HTML 코드에 대해 _Auto Minify_ 옵션을 활성화하지 마세요. 이는
- **`output_location`**: `docs/.vitepress/dist`
- **`app_build_command`**: `npm run docs:build`
### Firebase {#firebase}
### CloudRay
[CloudRay](https://cloudray.io/)로 VitePress 프로젝트를 배포하려면 이 [지침](https://cloudray.io/articles/how-to-deploy-vitepress-site)을 따르세요.
### Firebase
1. 프로젝트 루트에 `firebase.json``.firebaserc`를 생성하세요:
@ -261,14 +265,6 @@ HTML 코드에 대해 _Auto Minify_ 옵션을 활성화하지 마세요. 이는
firebase deploy
```
### Surge
1. `npm run docs:build`를 실행한 후, 배포하기 위해 이 명령어를 실행하세요:
```sh
npx surge docs/.vitepress/dist
```
### Heroku
1. [`heroku-buildpack-static`](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-static)에 제공된 문서와 가이드를 따르세요.
@ -281,11 +277,11 @@ HTML 코드에 대해 _Auto Minify_ 옵션을 활성화하지 마세요. 이는
}
```
### Edgio
### Hostinger
[Edgio에 VitePress 앱 생성 및 배포하기](https://docs.edg.io/guides/vitepress)를 참고하세요.
[Hostinger](https://www.hostinger.com/web-apps-hosting)로 VitePress 프로젝트를 배포하려면 이 [지침](https://www.hostinger.com/support/how-to-deploy-a-nodejs-website-in-hostinger/)을 따르세요. 빌드 설정을 구성할 때 프레임워크로 VitePress를 선택하고 루트 디렉터리를 `./docs`로 조정하세요.
### Kinsta 정적 사이트 호스팅 {#kinsta-static-site-hosting}
### Kinsta
[VitePress](https://kinsta.com/static-site-hosting/) 웹사이트를 [Kinsta](https://kinsta.com/static-site-hosting/)에 배포하려면 이 [지침](https://kinsta.com/docs/vitepress-static-site-example/)을 따르세요.
@ -293,6 +289,14 @@ HTML 코드에 대해 _Auto Minify_ 옵션을 활성화하지 마세요. 이는
[VitePress](https://stormkit.io) 프로젝트를 [Stormkit](https://www.stormkit.io)에 배포하려면 이 [지침](https://stormkit.io/blog/how-to-deploy-vitepress)을 따르세요.
### Surge
1. `npm run docs:build`를 실행한 후, 배포하기 위해 이 명령어를 실행하세요:
```sh
npx surge docs/.vitepress/dist
```
### Nginx
다음은 Nginx 서버 블록 구성의 예입니다. 이 설정은 일반적인 텍스트 기반 에셋에 대한 gzip 압축, VitePress 사이트의 정적 파일을 적절한 캐싱 헤더와 함께 제공하는 규칙 및 `cleanUrls: true`를 처리하는 규칙을 포함합니다.

@ -771,7 +771,7 @@ export default config
## Basics
<!--@include: ./parts/basics.md-->
<!--@@include: ./parts/basics.md-->
```
**해당 파일** (`parts/basics.md`)
@ -807,7 +807,7 @@ Can be created using `.foorc.json`.
## Basics
<!--@include: ./parts/basics.md{3,}-->
<!--@@include: ./parts/basics.md{3,}-->
```
**해당 파일** (`parts/basics.md`)
@ -843,8 +843,8 @@ Can be created using `.foorc.json`.
## Basics
<!--@include: ./parts/basics.md#basic-usage{,2}-->
<!--@include: ./parts/basics.md#basic-usage{5,}-->
<!--@@include: ./parts/basics.md#basic-usage{,2}-->
<!--@@include: ./parts/basics.md#basic-usage{5,}-->
```
**해당 파일** (`parts/basics.md`)

@ -39,7 +39,7 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
ko: { // 기본 로케일을 번역하려면 이것을 `root`로 만드십시오.
ko: { // 기본 로케일을 번역하려면 `root`로 설정하세요
translations: {
button: {
buttonText: '검색',
@ -47,17 +47,17 @@ export default defineConfig({
},
modal: {
displayDetails: '상세 목록 표시',
resetButtonTitle: '검색 지우기',
resetButtonTitle: '검색 재설정',
backButtonTitle: '검색 닫기',
noResultsText: '결과를 찾을 수 없습니다',
noResultsText: '결과 없습니다',
footer: {
selectText: '선택',
selectKeyAriaLabel: '선택하기',
navigateText: '탐색',
navigateUpKeyAriaLabel: '위',
navigateDownKeyAriaLabel: '아래',
selectKeyAriaLabel: 'Enter',
navigateText: '이동',
navigateUpKeyAriaLabel: '위쪽 화살표',
navigateDownKeyAriaLabel: '아래쪽 화살표',
closeText: '닫기',
closeKeyAriaLabel: 'esc'
closeKeyAriaLabel: 'Esc'
}
}
}
@ -69,7 +69,7 @@ export default defineConfig({
})
```
### MiniSearch 옵션 {#mini-search-options}
### MiniSearch 옵션 {#minisearch-options}
MiniSearch를 다음과 같이 구성할 수 있습니다:
@ -123,7 +123,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// return html string
// HTML 문자열을 반환
}
}
}
@ -174,7 +174,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.title)
return await md.renderAsync(`# ${env.frontmatter.title}`) + html
return (await md.renderAsync(`# ${env.frontmatter.title}`)) + html
return html
}
}
@ -208,6 +208,19 @@ export default defineConfig({
다국어 검색을 사용하려면 다음과 같이 구성해야 합니다:
<details>
<summary>클릭하여 펼치기</summary>
<<< @/snippets/algolia-i18n.ts
</details>
자세한 내용은 [공식 Algolia 문서](https://docsearch.algolia.com/docs/api#translations)를 참고하세요. 빠르게 시작하려면 이 사이트에서 사용하는 번역을 [GitHub 저장소](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code)에서 복사할 수도 있습니다.
### Algolia Ask AI 지원 {#ask-ai}
**Ask AI** 기능을 사용하려면 `askAi` 옵션을 추가하세요:
```ts
import { defineConfig } from 'vitepress'
@ -219,48 +232,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
ko: {
placeholder: '문서 검색',
translations: {
button: {
buttonText: '검색',
buttonAriaLabel: '검색'
},
modal: {
searchBox: {
resetButtonTitle: '검색 지우기',
resetButtonAriaLabel: '검색 지우기',
cancelButtonText: '취소',
cancelButtonAriaLabel: '취소'
},
startScreen: {
recentSearchesTitle: '검색 기록',
noRecentSearchesText: '최근 검색 없음',
saveRecentSearchButtonTitle: '검색 기록에 저장',
removeRecentSearchButtonTitle: '검색 기록에서 삭제',
favoriteSearchesTitle: '즐겨찾기',
removeFavoriteSearchButtonTitle: '즐겨찾기에서 삭제'
},
errorScreen: {
titleText: '결과를 가져올 수 없습니다',
helpText: '네트워크 연결을 확인하세요'
},
footer: {
selectText: '선택',
navigateText: '탐색',
closeText: '닫기',
searchByText: '검색 기준'
},
noResultsScreen: {
noResultsText: '결과를 찾을 수 없습니다',
suggestedQueryText: '새로운 검색을 시도할 수 있습니다',
reportMissingResultsText: '해당 검색어에 대한 결과가 있어야 합니까?',
reportMissingResultsLinkText: '피드백 보내기 클릭'
}
}
}
}
// askAi: "내-어시스턴트-ID"
// 또는
askAi: {
// 최소한 Algolia에서 받은 assistantId를 제공해야 합니다
assistantId: 'XXXYYY',
// 선택적 재정의 — 생략하면 상위 appId/apiKey/indexName 값이 재사용됩니다
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -268,129 +248,108 @@ export default defineConfig({
})
```
[이 옵션들](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts)은 재작성 할 수 있습니다. 이에 대해 자세히 알고 싶다면 Algolia 공식 문서를 참고하세요.
::: warning 참고
Ask AI를 사용하지 않으려면 `askAi` 옵션을 생략하면 됩니다.
:::
### 크롤러 구성 {#crawler-config}
### Ask AI 사이드 패널 {#ask-ai-side-panel}
이 사이트에서 사용하는 예제 구성을 소개합니다:
DocSearch v4.5+는 선택적 **Ask AI 사이드 패널**을 지원합니다. 활성화되면 기본적으로 **Ctrl/Cmd+I**로 열 수 있습니다. [사이드 패널 API 참조](https://docsearch.algolia.com/docs/sidepanel/api-reference)에 전체 옵션 목록이 있습니다.
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// @docsearch/sidepanel-js SidepanelProps API 반영
panel: {
variant: 'floating', // 또는 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
})
```
키보드 단축키를 비활성화해야 하는 경우 사이드 패널의 `keyboardShortcuts` 옵션을 사용하세요:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
### Algolia Ask AI 지원 {#ask-ai}
#### 모드 (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
**Ask AI** 기능을 사용하려면 `askAi` 옵션을 추가하세요:
VitePress가 키워드 검색과 Ask AI를 통합하는 방식을 선택적으로 제어할 수 있습니다:
- `mode: 'auto'` (기본값): 키워드 검색이 구성된 경우 `hybrid`를 추론하고, 그렇지 않으면 Ask AI 사이드 패널이 구성된 경우 `sidePanel`을 추론합니다.
- `mode: 'sidePanel'`: 사이드 패널만 강제 (키워드 검색 버튼 숨김).
- `mode: 'hybrid'`: 키워드 검색 모달 + Ask AI 사이드 패널 활성화 (키워드 검색 구성 필요).
- `mode: 'modal'`: Ask AI를 DocSearch 모달 내부에 유지 (사이드 패널을 구성한 경우에도).
#### Ask AI만 (키워드 검색 없음) {#ask-ai-only}
**Ask AI 사이드 패널만** 사용하려면 최상위 키워드 검색 구성을 생략하고 `askAi` 아래에 자격 증명을 제공할 수 있습니다:
```ts
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: { assistantId: 'XXXYYY' }
}
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
}
})
```
::: warning 참고
Ask AI를 사용하지 않으려면 `askAi` 옵션을 생략하면 됩니다.
:::
### 크롤러 구성 {#crawler-config}
이 사이트에서 사용하는 예제 구성을 소개합니다:
<<< @/snippets/algolia-crawler.js

@ -49,7 +49,7 @@ interface PageData {
titleTemplate?: string | boolean
description: string
relativePath: string
filePath: string,
filePath: string
headers: Header[]
frontmatter: Record<string, any>
params?: Record<string, any>

@ -15,7 +15,7 @@
"open-cli": "^8.0.0",
"postcss-rtlcss": "^5.7.1",
"vitepress": "workspace:*",
"vitepress-plugin-group-icons": "^1.6.5",
"vitepress-plugin-llms": "^1.9.3"
"vitepress-plugin-group-icons": "^1.7.1",
"vitepress-plugin-llms": "^1.11.0"
}
}

@ -178,53 +178,79 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'Pesquisar documentos',
translations: {
button: {
buttonText: 'Pesquisar',
buttonAriaLabel: 'Pesquisar'
buttonText: 'Buscar',
buttonAriaLabel: 'Buscar'
},
modal: {
searchBox: {
clearButtonTitle: 'Limpar pesquisa',
clearButtonAriaLabel: 'Limpar pesquisa',
clearButtonTitle: 'Limpar',
clearButtonAriaLabel: 'Limpar a consulta',
closeButtonText: 'Fechar',
closeButtonAriaLabel: 'Fechar',
placeholderText: 'Pesquisar documentos',
placeholderTextAskAi: 'Pergunte à IA: ',
placeholderText: 'Buscar na documentação ou perguntar ao Ask AI',
placeholderTextAskAi: 'Faça outra pergunta...',
placeholderTextAskAiStreaming: 'Respondendo...',
searchInputLabel: 'Pesquisar',
backToKeywordSearchButtonText: 'Voltar à pesquisa por palavras-chave',
searchInputLabel: 'Buscar',
backToKeywordSearchButtonText:
'Voltar para a busca por palavra-chave',
backToKeywordSearchButtonAriaLabel:
'Voltar à pesquisa por palavras-chave'
'Voltar para a busca por palavra-chave',
newConversationPlaceholder: 'Faça uma pergunta',
conversationHistoryTitle: 'Meu histórico de conversas',
startNewConversationText: 'Iniciar uma nova conversa',
viewConversationHistoryText: 'Histórico de conversas',
threadDepthErrorPlaceholder: 'Limite de conversa atingido'
},
newConversation: {
newConversationTitle: 'Como posso ajudar hoje?',
newConversationDescription:
'Eu busco na sua documentação para ajudar a encontrar guias de configuração, detalhes de funcionalidades e dicas de solução de problemas rapidamente.'
},
footer: {
selectText: 'Selecionar',
submitQuestionText: 'Enviar pergunta',
selectKeyAriaLabel: 'Tecla Enter',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Seta para cima',
navigateDownKeyAriaLabel: 'Seta para baixo',
closeText: 'Fechar',
backToSearchText: 'Voltar à busca',
closeKeyAriaLabel: 'Tecla Escape',
poweredByText: 'Com tecnologia de'
},
errorScreen: {
titleText: 'Não foi possível obter resultados',
helpText: 'Talvez você queira verificar sua conexão de rede.'
},
startScreen: {
recentSearchesTitle: 'Histórico de pesquisa',
recentSearchesTitle: 'Recentes',
noRecentSearchesText: 'Nenhuma pesquisa recente',
saveRecentSearchButtonTitle: 'Salvar no histórico de pesquisas',
removeRecentSearchButtonTitle: 'Remover do histórico de pesquisas',
saveRecentSearchButtonTitle: 'Salvar esta pesquisa',
removeRecentSearchButtonTitle: 'Remover esta pesquisa do histórico',
favoriteSearchesTitle: 'Favoritos',
removeFavoriteSearchButtonTitle: 'Remover dos favoritos',
removeFavoriteSearchButtonTitle:
'Remover esta pesquisa dos favoritos',
recentConversationsTitle: 'Conversas recentes',
removeRecentConversationButtonTitle:
'Remover esta conversa do histórico'
},
errorScreen: {
titleText: 'Não foi possível obter resultados',
helpText: 'Verifique sua conexão de rede'
},
noResultsScreen: {
noResultsText: 'Nenhum resultado encontrado',
suggestedQueryText: 'Você pode tentar uma nova consulta',
reportMissingResultsText: 'Acha que deveria haver resultados?',
reportMissingResultsLinkText: 'Clique para enviar feedback'
noResultsText: 'Nenhum resultado encontrado para',
suggestedQueryText: 'Tente pesquisar por',
reportMissingResultsText:
'Acha que esta consulta deveria retornar resultados?',
reportMissingResultsLinkText: 'Avise-nos.'
},
resultsScreen: {
askAiPlaceholder: 'Pergunte à IA: '
askAiPlaceholder: 'Perguntar à IA: ',
noResultsAskAiPlaceholder:
'Não encontrou nos documentos? Peça ajuda ao Ask AI: '
},
askAiScreen: {
disclaimerText:
'As respostas são geradas por IA e podem conter erros. Verifique as respostas.',
'As respostas são geradas por IA e podem conter erros. Verifique.',
relatedSourcesText: 'Fontes relacionadas',
thinkingText: 'Pensando...',
copyButtonText: 'Copiar',
@ -232,23 +258,70 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
copyButtonTitle: 'Copiar',
likeButtonTitle: 'Curtir',
dislikeButtonTitle: 'Não curtir',
thanksForFeedbackText: 'Obrigado pelo feedback!',
preToolCallText: 'Pesquisando...',
duringToolCallText: 'Pesquisando ',
afterToolCallText: 'Pesquisa concluída',
aggregatedToolCallText: 'Pesquisa concluída'
thanksForFeedbackText: 'Obrigado pelo seu feedback!',
preToolCallText: 'Buscando...',
duringToolCallText: 'Buscando...',
afterToolCallText: 'Pesquisado',
stoppedStreamingText: 'Você interrompeu esta resposta',
errorTitleText: 'Erro no chat',
threadDepthExceededMessage:
'Esta conversa foi encerrada para manter respostas precisas.',
startNewConversationButtonText: 'Iniciar uma nova conversa'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'Perguntar à IA',
buttonAriaLabel: 'Perguntar à IA'
}
},
footer: {
selectText: 'Selecionar',
submitQuestionText: 'Enviar pergunta',
selectKeyAriaLabel: 'Tecla Enter',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Seta para cima',
navigateDownKeyAriaLabel: 'Seta para baixo',
closeText: 'Fechar',
backToSearchText: 'Voltar à pesquisa',
closeKeyAriaLabel: 'Tecla Escape',
poweredByText: 'Pesquisa por'
panel: {
translations: {
header: {
title: 'Perguntar à IA',
conversationHistoryTitle: 'Meu histórico de conversas',
newConversationText: 'Iniciar uma nova conversa',
viewConversationHistoryText: 'Histórico de conversas'
},
promptForm: {
promptPlaceholderText: 'Faça uma pergunta',
promptAnsweringText: 'Respondendo...',
promptAskAnotherQuestionText: 'Faça outra pergunta',
promptDisclaimerText:
'As respostas são geradas por IA e podem conter erros.',
promptLabelText:
'Pressione Enter para enviar ou Shift+Enter para nova linha.',
promptAriaLabelText: 'Entrada do prompt'
},
conversationScreen: {
preToolCallText: 'Buscando...',
searchingText: 'Buscando...',
toolCallResultText: 'Pesquisado',
conversationDisclaimer:
'As respostas são geradas por IA e podem conter erros. Verifique.',
reasoningText: 'Raciocinando...',
thinkingText: 'Pensando...',
relatedSourcesText: 'Fontes relacionadas',
stoppedStreamingText: 'Você interrompeu esta resposta',
copyButtonText: 'Copiar',
copyButtonCopiedText: 'Copiado!',
likeButtonTitle: 'Curtir',
dislikeButtonTitle: 'Não curtir',
thanksForFeedbackText: 'Obrigado pelo seu feedback!',
errorTitleText: 'Erro no chat'
},
newConversationScreen: {
titleText: 'Como posso ajudar hoje?',
introductionText:
'Eu busco na sua documentação para ajudar a encontrar guias de configuração, detalhes de funcionalidades e dicas de solução de problemas rapidamente.'
},
logo: {
poweredByText: 'Com tecnologia de'
}
}
}
}
}

@ -105,13 +105,13 @@ Nota: o arquivo `vercel.json` deve ser colocado na raiz do seu **repositório**.
## Guias de Plataforma {#platform-guides}
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#generic}
Configure um novo projeto e altere estas configurações usando seu painel:
- **Comando de Compilação:** `npm run docs:build`
- **Diretório de Saída:** `docs/.vitepress/dist`
- **Versão do Node:** `18` (ou superior)
- **Versão do Node:** `20` (ou superior)
::: warning
Não ative opções como _Auto Minify_ para código HTML. Isso removerá comentários da saída que têm significado para Vue. Haverão erros de incompatibilidade de hidratação se forem removidos.
@ -169,10 +169,8 @@ Não ative opções como _Auto Minify_ para código HTML. Isso removerá coment
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci # ou pnpm install / yarn install / bun install
- name: Build with VitePress
run: |
npm run docs:build # ou pnpm docs:build / yarn docs:build / bun run docs:build
touch docs/.vitepress/dist/.nojekyll
- name: Build with VitePress
run: npm run docs:build # ou pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
@ -202,7 +200,7 @@ Não ative opções como _Auto Minify_ para código HTML. Isso removerá coment
### GitLab Pages
1. Defina `outDir` na configuração VitePress como `../public`. Configure a opção `base` para `'/<repository>/'` se você deseja implantar em `https://<username>.gitlab.io/<repository>/`.
1. Defina `outDir` na configuração VitePress como `../public`. Configure a opção `base` para `'/<repository>/'` se você deseja implantar em `https://<username>.gitlab.io/<repository>/`. Você não precisa de `base` se estiver implantando em um domínio personalizado, páginas de usuário ou grupo, ou se a configuração "Use unique domain" estiver habilitada no GitLab.
2. Crie um arquivo chamado `.gitlab-ci.yml` na raiz do seu projeto com o conteúdo abaixo. Isso construirá e implantará seu site sempre que você fizer alterações no conteúdo:
@ -223,7 +221,7 @@ Não ative opções como _Auto Minify_ para código HTML. Isso removerá coment
- main
```
### Azure Static Web Apps {#azure-static-web-apps}
### Azure
1. Siga a [documentação oficial](https://docs.microsoft.com/en-us/azure/static-web-apps/build-configuration).
@ -233,7 +231,11 @@ Não ative opções como _Auto Minify_ para código HTML. Isso removerá coment
- **`output_location`**: `docs/.vitepress/dist`
- **`app_build_command`**: `npm run docs:build`
### Firebase {#firebase}
### CloudRay
Você pode implantar seu projeto VitePress com a [CloudRay](https://cloudray.io/) seguindo estas [instruções](https://cloudray.io/articles/how-to-deploy-vitepress-site).
### Firebase
1. Crie `firebase.json` e `.firebaserc` na raiz do seu projeto:
@ -264,14 +266,6 @@ Não ative opções como _Auto Minify_ para código HTML. Isso removerá coment
firebase deploy
```
### Surge
1. Após executar `npm run docs:build`, execute este comando para implantar:
```sh
npx surge docs/.vitepress/dist
```
### Heroku
1. Siga a documentação e o guia fornecidos em [`heroku-buildpack-static`](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-static).
@ -284,10 +278,66 @@ Não ative opções como _Auto Minify_ para código HTML. Isso removerá coment
}
```
### Edgio
### Hostinger
Consulte [Criar e Implantar um Aplicativo VitePress no Edgio](https://docs.edg.io/guides/vitepress).
Você pode implantar seu projeto VitePress com a [Hostinger](https://www.hostinger.com/web-apps-hosting) seguindo estas [instruções](https://www.hostinger.com/support/how-to-deploy-a-nodejs-website-in-hostinger/). Ao configurar as opções de build, escolha VitePress como framework e ajuste o diretório raiz para `./docs`.
### Kinsta Static Site Hosting {#kinsta-static-site-hosting}
### Kinsta
Você pode implantar seu site VitePress em [Kinsta](https://kinsta.com/static-site-hosting/) seguindo estas [instruções](https://kinsta.com/docs/vitepress-static-site-example/).
### Stormkit
Você pode implantar seu projeto VitePress na [Stormkit](https://www.stormkit.io) seguindo estas [instruções](https://stormkit.io/blog/how-to-deploy-vitepress).
### Surge
1. Após executar `npm run docs:build`, execute este comando para implantar:
```sh
npx surge docs/.vitepress/dist
```
### Nginx
Aqui está um exemplo de configuração de bloco de servidor Nginx. Essa configuração inclui compressão gzip para ativos comuns baseados em texto, regras para servir os arquivos estáticos do seu site VitePress com cabeçalhos de cache apropriados, assim como lidar com `cleanUrls: true`.
```nginx
server {
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
listen 80;
server_name _;
index index.html;
location / {
# content location
root /app;
# exact matches -> reverse clean urls -> folders -> not found
try_files $uri $uri.html $uri/ =404;
# non existent pages
error_page 404 /404.html;
# a folder without index.html raises 403 in this setup
error_page 403 /404.html;
# adjust caching headers
# files in the assets folder have hashes filenames
location ~* ^/assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
}
```
Essa configuração presume que o site VitePress compilado está localizado no diretório `/app` no seu servidor. Ajuste a diretiva `root` de acordo caso os arquivos do site estejam em outro lugar.
::: warning Não use index.html por padrão
A resolução de try_files não deve padronizar para index.html como em outras aplicações Vue. Isso resultará em um estado de página inválido.
:::
Mais informações podem ser encontradas na [documentação oficial do nginx](https://nginx.org/en/docs/), nestas issues [#2837](https://github.com/vuejs/vitepress/discussions/2837), [#3235](https://github.com/vuejs/vitepress/issues/3235) assim como neste [post do blog](https://blog.mehdi.cc/articles/vitepress-cleanurls-on-nginx-environment#readings) de Mehdi Merah.

@ -771,7 +771,7 @@ Por exemplo, você pode incluir um arquivo markdown relativo usando isto:
## Conceitos Básicos
<!--@include: ./parts/basics.md-->
<!--@@include: ./parts/basics.md-->
```
**Arquivo da Parte** (`parts/basics.md`)
@ -807,7 +807,7 @@ Também suporta a seleção de um intervalo de linhas:
## Conceitos Básicos
<!--@include: ./parts/basics.md{3,}-->
<!--@@include: ./parts/basics.md{3,}-->
```
**Arquivo da Parte** (`parts/basics.md`)

@ -39,18 +39,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
zh: {
pt: { // torne isto `root` se quiser traduzir a localidade padrão
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
buttonText: 'Pesquisar',
buttonAriaLabel: 'Pesquisar'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
displayDetails: 'Mostrar lista detalhada',
resetButtonTitle: 'Redefinir pesquisa',
backButtonTitle: 'Fechar pesquisa',
noResultsText: 'Nenhum resultado',
footer: {
selectText: '选择',
navigateText: '切换'
selectText: 'Selecionar',
selectKeyAriaLabel: 'Enter',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Seta para cima',
navigateDownKeyAriaLabel: 'Seta para baixo',
closeText: 'Fechar',
closeKeyAriaLabel: 'Esc'
}
}
}
@ -62,7 +69,7 @@ export default defineConfig({
})
```
### Opções MiniSearch {#mini-search-options}
### Opções MiniSearch {#minisearch-options}
Você pode configurar o MiniSearch assim:
@ -116,7 +123,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// retorne a string HTML
// retorna uma string HTML
}
}
}
@ -141,7 +148,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.search === false) return ''
if (env.relativePath.startsWith('algum/caminho')) return ''
if (env.relativePath.startsWith('some/path')) return ''
return html
}
}
@ -167,7 +174,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.title)
return await md.renderAsync(`# ${env.frontmatter.title}`) + html
return (await md.renderAsync(`# ${env.frontmatter.title}`)) + html
return html
}
}
@ -197,10 +204,23 @@ export default defineConfig({
})
```
### i18n {#algolia-search-i18n} {#algolia-search-i18n}
### i18n {#algolia-search-i18n}
Você pode usar uma configuração como esta para usar a pesquisa multilínguas:
<details>
<summary>Clique para expandir</summary>
<<< @/snippets/algolia-i18n.ts
</details>
Consulte a [documentação oficial da Algolia](https://docsearch.algolia.com/docs/api#translations) para saber mais. Para começar rapidamente, você também pode copiar as traduções usadas por este site do [nosso repositório no GitHub](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code).
### Suporte ao Algolia Ask AI {#ask-ai}
Se quiser incluir o **Ask AI**, adicione `askAi` em `options`:
```ts
import { defineConfig } from 'vitepress'
@ -212,48 +232,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除查询条件',
resetButtonAriaLabel: '清除查询条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭',
searchByText: '搜索提供者'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
}
}
}
}
// askAi: "SEU-ID-DO-ASSISTENTE"
// OU
askAi: {
// no mínimo, você deve fornecer o assistantId recebido da Algolia
assistantId: 'XXXYYY',
// substituições opcionais — se omitidas, os valores appId/apiKey/indexName de nível superior são reutilizados
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -261,129 +248,108 @@ export default defineConfig({
})
```
[Essas opções](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) podem ser sobrepostas. Consulte a documentação oficial Algolia para obter mais informações sobre elas.
::: warning Nota
Caso queira apenas a pesquisa por palavra-chave, omita `askAi`.
:::
### Configuração _Crawler_ {#crawler-config}
### Painel Lateral do Ask AI {#ask-ai-side-panel}
Aqui está um exemplo de configuração baseado na qual este site usa:
O DocSearch v4.5+ suporta um **painel lateral do Ask AI** opcional. Quando habilitado, pode ser aberto com **Ctrl/Cmd+I** por padrão. A [Referência da API do Painel Lateral](https://docsearch.algolia.com/docs/sidepanel/api-reference) contém a lista completa de opções.
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// Espelha a API do @docsearch/sidepanel-js SidepanelProps
panel: {
variant: 'floating', // ou 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
})
```
Se precisar desabilitar o atalho de teclado, use a opção `keyboardShortcuts` do painel lateral:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
### Suporte ao Algolia Ask AI {#ask-ai}
#### Modo (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
Se quiser incluir o **Ask AI**, adicione `askAi` em `options`:
Você pode controlar opcionalmente como o VitePress integra a pesquisa por palavra-chave e o Ask AI:
- `mode: 'auto'` (padrão): infere `hybrid` quando a pesquisa por palavra-chave está configurada, caso contrário `sidePanel` quando o painel lateral do Ask AI está configurado.
- `mode: 'sidePanel'`: força apenas o painel lateral (oculta o botão de pesquisa por palavra-chave).
- `mode: 'hybrid'`: habilita o modal de pesquisa por palavra-chave + painel lateral do Ask AI (requer configuração de pesquisa por palavra-chave).
- `mode: 'modal'`: mantém o Ask AI dentro do modal do DocSearch (mesmo se você configurou o painel lateral).
#### Apenas Ask AI (sem pesquisa por palavra-chave) {#ask-ai-only}
Se quiser usar **apenas o painel lateral do Ask AI**, você pode omitir a configuração de pesquisa por palavra-chave de nível superior e fornecer as credenciais em `askAi`:
```ts
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: { assistantId: 'XXXYYY' }
}
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
}
})
```
::: warning Nota
Caso queira apenas a pesquisa por palavra-chave, omita `askAi`.
:::
### Configuração _Crawler_ {#crawler-config}
Aqui está um exemplo de configuração baseado na qual este site usa:
<<< @/snippets/algolia-crawler.js

@ -45,7 +45,7 @@ interface PageData {
titleTemplate?: string | boolean
description: string
relativePath: string
filePath: string,
filePath: string
headers: Header[]
frontmatter: Record<string, any>
params?: Record<string, any>

@ -178,7 +178,6 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'Поиск в документации',
translations: {
button: {
buttonText: 'Поиск',
@ -186,45 +185,70 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
},
modal: {
searchBox: {
clearButtonTitle: 'Очистить поиск',
clearButtonAriaLabel: 'Очистить поиск',
clearButtonTitle: 'Очистить',
clearButtonAriaLabel: 'Очистить запрос',
closeButtonText: 'Закрыть',
closeButtonAriaLabel: 'Закрыть',
placeholderText: 'Поиск в документации',
placeholderTextAskAi: 'Задайте вопрос ИИ: ',
placeholderTextAskAiStreaming: 'Формируется ответ...',
placeholderText: 'Поиск по документации или задайте вопрос Ask AI',
placeholderTextAskAi: 'Задайте другой вопрос...',
placeholderTextAskAiStreaming: 'Отвечаю...',
searchInputLabel: 'Поиск',
backToKeywordSearchButtonText:
'Вернуться к поиску по ключевым словам',
backToKeywordSearchButtonText: 'Назад к поиску по ключевым словам',
backToKeywordSearchButtonAriaLabel:
'Вернуться к поиску по ключевым словам'
'Назад к поиску по ключевым словам',
newConversationPlaceholder: 'Задайте вопрос',
conversationHistoryTitle: 'Моя история разговоров',
startNewConversationText: 'Начать новый разговор',
viewConversationHistoryText: 'История разговоров',
threadDepthErrorPlaceholder: 'Достигнут лимит разговора'
},
startScreen: {
recentSearchesTitle: 'История поиска',
noRecentSearchesText: 'Нет истории поиска',
saveRecentSearchButtonTitle: 'Сохранить в истории поиска',
removeRecentSearchButtonTitle: 'Удалить из истории поиска',
favoriteSearchesTitle: 'Избранное',
removeFavoriteSearchButtonTitle: 'Удалить из избранного',
recentConversationsTitle: 'Недавние диалоги',
removeRecentConversationButtonTitle: 'Удалить этот диалог из истории'
newConversation: {
newConversationTitle: 'Чем могу помочь сегодня?',
newConversationDescription:
'Я ищу по вашей документации, чтобы быстро помочь найти руководства по настройке, детали функций и советы по устранению неполадок.'
},
footer: {
selectText: 'Выбрать',
submitQuestionText: 'Отправить вопрос',
selectKeyAriaLabel: 'Клавиша Enter',
navigateText: 'Навигация',
navigateUpKeyAriaLabel: 'Стрелка вверх',
navigateDownKeyAriaLabel: 'Стрелка вниз',
closeText: 'Закрыть',
backToSearchText: 'Назад к поиску',
closeKeyAriaLabel: 'Клавиша Escape',
poweredByText: 'При поддержке'
},
errorScreen: {
titleText: 'Невозможно получить результаты',
helpText: 'Проверьте подключение к Интернету'
titleText: 'Не удалось получить результаты',
helpText: 'Возможно, стоит проверить подключение к сети.'
},
startScreen: {
recentSearchesTitle: 'Недавние',
noRecentSearchesText: 'Нет недавних поисков',
saveRecentSearchButtonTitle: 'Сохранить этот поиск',
removeRecentSearchButtonTitle: 'Удалить этот поиск из истории',
favoriteSearchesTitle: 'Избранное',
removeFavoriteSearchButtonTitle: 'Удалить этот поиск из избранного',
recentConversationsTitle: 'Недавние разговоры',
removeRecentConversationButtonTitle:
'Удалить этот разговор из истории'
},
noResultsScreen: {
noResultsText: 'Ничего не найдено',
suggestedQueryText: 'Попробуйте изменить запрос',
reportMissingResultsText: 'Считаете, что результаты должны быть?',
reportMissingResultsLinkText: 'Сообщите об этом'
noResultsText: 'Не найдено результатов для',
suggestedQueryText: 'Попробуйте поискать',
reportMissingResultsText:
'Считаете, что по этому запросу должны быть результаты?',
reportMissingResultsLinkText: 'Сообщите нам.'
},
resultsScreen: {
askAiPlaceholder: 'Задайте вопрос ИИ: '
askAiPlaceholder: 'Спросить ИИ: ',
noResultsAskAiPlaceholder:
'Не нашли в документации? Попросите Ask AI помочь: '
},
askAiScreen: {
disclaimerText:
'Ответы генерируются ИИ и могут содержать ошибки. Проверяйте информацию.',
'Ответы генерируются ИИ и могут содержать ошибки. Проверьте их.',
relatedSourcesText: 'Связанные источники',
thinkingText: 'Думаю...',
copyButtonText: 'Копировать',
@ -233,22 +257,69 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
likeButtonTitle: 'Нравится',
dislikeButtonTitle: 'Не нравится',
thanksForFeedbackText: 'Спасибо за отзыв!',
preToolCallText: 'Поиск...',
duringToolCallText: 'Поиск ',
afterToolCallText: 'Поиск завершён',
aggregatedToolCallText: 'Поиск завершён'
preToolCallText: 'Ищу...',
duringToolCallText: 'Ищу...',
afterToolCallText: 'Искал',
stoppedStreamingText: 'Вы остановили этот ответ',
errorTitleText: 'Ошибка чата',
threadDepthExceededMessage:
'Этот разговор закрыт, чтобы сохранить точность ответов.',
startNewConversationButtonText: 'Начать новый разговор'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'Спросить ИИ',
buttonAriaLabel: 'Спросить ИИ'
}
},
footer: {
selectText: 'выбрать',
submitQuestionText: 'Отправить вопрос',
selectKeyAriaLabel: 'Клавиша Enter',
navigateText: 'перейти',
navigateUpKeyAriaLabel: 'Стрелка вверх',
navigateDownKeyAriaLabel: 'Стрелка вниз',
closeText: 'закрыть',
backToSearchText: 'Вернуться к поиску',
closeKeyAriaLabel: 'Клавиша Esc',
poweredByText: 'поиск от'
panel: {
translations: {
header: {
title: 'Спросить ИИ',
conversationHistoryTitle: 'Моя история разговоров',
newConversationText: 'Начать новый разговор',
viewConversationHistoryText: 'История разговоров'
},
promptForm: {
promptPlaceholderText: 'Задайте вопрос',
promptAnsweringText: 'Отвечаю...',
promptAskAnotherQuestionText: 'Задайте другой вопрос',
promptDisclaimerText:
'Ответы генерируются ИИ и могут содержать ошибки.',
promptLabelText:
'Нажмите Enter, чтобы отправить, или Shift+Enter для новой строки.',
promptAriaLabelText: 'Ввод запроса'
},
conversationScreen: {
preToolCallText: 'Ищу...',
searchingText: 'Ищу...',
toolCallResultText: 'Искал',
conversationDisclaimer:
'Ответы генерируются ИИ и могут содержать ошибки. Проверьте их.',
reasoningText: 'Рассуждаю...',
thinkingText: 'Думаю...',
relatedSourcesText: 'Связанные источники',
stoppedStreamingText: 'Вы остановили этот ответ',
copyButtonText: 'Копировать',
copyButtonCopiedText: 'Скопировано!',
likeButtonTitle: 'Нравится',
dislikeButtonTitle: 'Не нравится',
thanksForFeedbackText: 'Спасибо за отзыв!',
errorTitleText: 'Ошибка чата'
},
newConversationScreen: {
titleText: 'Чем могу помочь сегодня?',
introductionText:
'Я ищу по вашей документации, чтобы быстро помочь найти руководства по настройке, детали функций и советы по устранению неполадок.'
},
logo: {
poweredByText: 'При поддержке'
}
}
}
}
}

@ -105,7 +105,7 @@ Cache-Control: max-age=31536000,immutable
## Руководства по платформам {#platform-guides}
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#netlify-vercel-cloudflare-pages-aws-amplify-render}
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#generic}
Создайте новый проект и измените эти настройки с помощью панели управления:
@ -117,7 +117,7 @@ Cache-Control: max-age=31536000,immutable
Не включайте такие опции, как _Auto Minify_ для HTML-кода. Он удалит из вывода комментарии, которые имеют значение для Vue. При их удалении могут возникать ошибки несоответствия гидратации.
:::
### GitHub Pages {#github-pages}
### GitHub Pages
1. Создайте файл с именем `deploy.yml` в директории `.github/workflows` вашего проекта с примерно таким содержанием:
@ -198,7 +198,7 @@ Cache-Control: max-age=31536000,immutable
3. Внесите свои изменения в ветку `main` и дождитесь завершения процесса GitHub Actions. Вы должны увидеть, что ваш сайт развёрнут по адресу `https://<username>.github.io/[repository]/` или `https://<custom-domain>/` в зависимости от ваших настроек. Ваш сайт будет автоматически разворачиваться при каждом внесении изменений в ветке `main`.
### GitLab Pages {#gitlab-pages}
### GitLab Pages
1. Установите значение `../public` для параметра `outDir` в конфигурации VitePress. Настройте опцию `base` на `'/<репозиторий>/'`, если вы хотите развернуть ваш проект по адресу `https://<имя пользователя>.gitlab.io/<репозиторий>/`. Вам не нужна опция `base`, если вы выполняете развёртывание на личном домене, страницах пользователя или группы, или если в GitLab включен параметр «Использовать уникальный домен».
@ -221,7 +221,7 @@ Cache-Control: max-age=31536000,immutable
- main
```
### Статические веб-приложения Azure {#azure-static-web-apps}
### Azure
1. Следуйте [официальной документации](https://docs.microsoft.com/ru-ru/azure/static-web-apps/build-configuration).
@ -231,7 +231,11 @@ Cache-Control: max-age=31536000,immutable
- **`output_location`**: `docs/.vitepress/dist`
- **`app_build_command`**: `npm run docs:build`
### Firebase {#firebase}
### CloudRay
Вы можете развернуть свой проект VitePress с [CloudRay](https://cloudray.io/), следуя этим [инструкциям](https://cloudray.io/articles/how-to-deploy-vitepress-site).
### Firebase
1. Создайте `firebase.json` и `.firebaserc` в корне вашего проекта:
@ -262,15 +266,7 @@ Cache-Control: max-age=31536000,immutable
firebase deploy
```
### Surge {#surge}
1. После запуска `npm run docs:build` выполните эту команду для развёртывания:
```sh
npx surge docs/.vitepress/dist
```
### Heroku {#heroku}
### Heroku
1. Следуйте документации и руководству, приведённому в [`heroku-buildpack-static`](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-static).
@ -282,11 +278,11 @@ Cache-Control: max-age=31536000,immutable
}
```
### Edgio {#edgio}
### Hostinger
См. [Создание и развёртывание приложения VitePress в Edgio](https://docs.edg.io/applications/v6/sites_frameworks/getting_started/vitepress).
Вы можете развернуть свой проект VitePress на [Hostinger](https://www.hostinger.com/web-apps-hosting), следуя этим [инструкциям](https://www.hostinger.com/support/how-to-deploy-a-nodejs-website-in-hostinger/). При настройке параметров сборки выберите VitePress в качестве фреймворка и укажите корневой каталог `./docs`.
### Хостинг статических файлов Kinsta {#kinsta-static-site-hosting}
### Kinsta
Вы можете развернуть свой сайт VitePress на [Kinsta](https://kinsta.com/static-site-hosting/), следуя этим [инструкциям](https://kinsta.com/docs/vitepress-static-site-example/).
@ -294,6 +290,14 @@ Cache-Control: max-age=31536000,immutable
Вы можете развернуть свой проект VitePress на [Stormkit](https://www.stormkit.io), следуя следующим [инструкциям](https://stormkit.io/blog/how-to-deploy-vitepress).
### Surge
1. После запуска `npm run docs:build` выполните эту команду для развёртывания:
```sh
npx surge docs/.vitepress/dist
```
### Nginx
Вот пример конфигурации блока сервера Nginx. Эта настройка включает сжатие gzip для общих текстовых ресурсов, правила обслуживания статических файлов вашего сайта VitePress с правильными заголовками кэширования и обработку параметра `cleanUrls: true`.

@ -795,7 +795,7 @@ export default config
## Основы
<!--@include: ./parts/basics.md-->
<!--@@include: ./parts/basics.md-->
```
**Файл части** (`parts/basics.md`)
@ -831,7 +831,7 @@ export default config
## Основы
<!--@include: ./parts/basics.md{3,}-->
<!--@@include: ./parts/basics.md{3,}-->
```
**Файл части** (`parts/basics.md`)
@ -867,8 +867,8 @@ export default config
## Основы
<!--@include: ./parts/basics.md#basic-usage{,2}-->
<!--@include: ./parts/basics.md#basic-usage{5,}-->
<!--@@include: ./parts/basics.md#basic-usage{,2}-->
<!--@@include: ./parts/basics.md#basic-usage{5,}-->
```
**Часть файла** (`parts/basics.md`)
@ -919,7 +919,7 @@ export default config
```md
## Мой дополнительный раздел
<!--@include: ./parts/basics.md#мои-основнои-раздел-->
<!--@@include: ./parts/basics.md#мои-основнои-раздел-->
```
**Соответствующий код**
@ -943,7 +943,7 @@ export default config
и включить его следующим образом:
```md
<!--@include: ./parts/basics.md#custom-id-->
<!--@@include: ./parts/basics.md#custom-id-->
```
## Математические уравнения {#math-equations}

@ -29,6 +29,7 @@ export default defineConfig({
- <https://www.npmjs.com/package/vitepress-plugin-search>
- <https://www.npmjs.com/package/vitepress-plugin-pagefind>
- <https://www.npmjs.com/package/@orama/plugin-vitepress>
- <https://www.npmjs.com/package/vitepress-plugin-typesense>
### i18n {#local-search-i18n}
@ -43,25 +44,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
ru: { // используйте ключ `root`, если хотите перевести локаль по умолчанию
ru: { // используйте `root`, если хотите перевести локаль по умолчанию
translations: {
button: {
buttonText: 'Поиск',
buttonAriaLabel: 'Поиск'
},
modal: {
displayDetails: 'Отобразить подробный список',
displayDetails: 'Показать подробный список',
resetButtonTitle: 'Сбросить поиск',
backButtonTitle: 'Закрыть поиск',
noResultsText: 'Нет результатов по запросу',
noResultsText: 'Нет результатов',
footer: {
selectText: 'выбрать',
selectKeyAriaLabel: 'выбрать',
navigateText: 'перейти',
navigateUpKeyAriaLabel: 'стрелка вверх',
navigateDownKeyAriaLabel: 'стрелка вниз',
closeText: 'закрыть',
closeKeyAriaLabel: 'esc'
selectText: 'Выбрать',
selectKeyAriaLabel: 'Enter',
navigateText: 'Навигация',
navigateUpKeyAriaLabel: 'Стрелка вверх',
navigateDownKeyAriaLabel: 'Стрелка вниз',
closeText: 'Закрыть',
closeKeyAriaLabel: 'Esc'
}
}
}
@ -127,7 +128,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// возвращаем html
// вернуть строку HTML
}
}
}
@ -178,7 +179,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.title)
return await md.renderAsync(`# ${env.frontmatter.title}`) + html
return (await md.renderAsync(`# ${env.frontmatter.title}`)) + html
return html
}
}
@ -212,6 +213,19 @@ export default defineConfig({
Пример конфигурации для использования многоязычного поиска:
<details>
<summary>Нажмите, чтобы развернуть</summary>
<<< @/snippets/algolia-i18n.ts
</details>
Подробности см. в [официальной документации Algolia](https://docsearch.algolia.com/docs/api#translations). Чтобы быстрее начать, можно также скопировать переводы, используемые на этом сайте, из [нашего репозитория GitHub](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code).
### Поддержка Ask AI в Algolia {#ask-ai}
Если вы хотите добавить функцию **Ask AI**, передайте параметр `askAi` (или любые из его отдельных полей) внутри объекта `options`:
```ts
import { defineConfig } from 'vitepress'
@ -223,79 +237,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
ru: {
placeholder: 'Поиск в документации',
translations: {
button: {
buttonText: 'Поиск',
buttonAriaLabel: 'Поиск'
},
modal: {
searchBox: {
clearButtonTitle: 'Очистить поиск',
clearButtonAriaLabel: 'Очистить поиск',
closeButtonText: 'Закрыть',
closeButtonAriaLabel: 'Закрыть',
placeholderText: 'Поиск в документации',
placeholderTextAskAi: 'Задайте вопрос ИИ:',
placeholderTextAskAiStreaming: 'Формируется ответ...',
searchInputLabel: 'Поиск',
backToKeywordSearchButtonText: 'Вернуться к поиску по ключевым словам',
backToKeywordSearchButtonAriaLabel: 'Вернуться к поиску по ключевым словам'
},
startScreen: {
recentSearchesTitle: 'История поиска',
noRecentSearchesText: 'Нет истории поиска',
saveRecentSearchButtonTitle: 'Сохранить в истории поиска',
removeRecentSearchButtonTitle: 'Удалить из истории поиска',
favoriteSearchesTitle: 'Избранное',
removeFavoriteSearchButtonTitle: 'Удалить из избранного',
recentConversationsTitle: 'Последние диалоги',
removeRecentConversationButtonTitle: 'Удалить диалог из истории'
},
errorScreen: {
titleText: 'Невозможно получить результаты',
helpText: 'Проверьте подключение к Интернету'
},
noResultsScreen: {
noResultsText: 'Ничего не найдено',
suggestedQueryText: 'Попробуйте изменить запрос',
reportMissingResultsText: 'Считаете, что результаты должны быть?',
reportMissingResultsLinkText: 'Сообщите об этом'
},
resultsScreen: {
askAiPlaceholder: 'Задайте вопрос ИИ: '
},
askAiScreen: {
disclaimerText: 'Ответ сгенерирован ИИ и может быть неточным. Пожалуйста, проверьте информацию самостоятельно.',
relatedSourcesText: 'Связанные источники',
thinkingText: 'Думаю...',
copyButtonText: 'Копировать',
copyButtonCopiedText: 'Скопировано!',
copyButtonTitle: 'Копировать',
likeButtonTitle: 'Нравится',
dislikeButtonTitle: 'Не нравится',
thanksForFeedbackText: 'Спасибо за ваш отзыв!',
preToolCallText: 'Идёт поиск...',
duringToolCallText: 'Поиск ',
afterToolCallText: 'Поиск выполнен'
},
footer: {
selectText: 'выбрать',
submitQuestionText: 'Отправить вопрос',
selectKeyAriaLabel: 'Клавиша Enter',
navigateText: 'перейти',
navigateUpKeyAriaLabel: 'Стрелка вверх',
navigateDownKeyAriaLabel: 'Стрелка вниз',
closeText: 'закрыть',
backToSearchText: 'Вернуться к поиску',
closeKeyAriaLabel: 'Клавиша Esc',
poweredByText: 'поиск от'
}
}
}
}
// askAi: "ВАШ-ID-АССИСТЕНТА"
// ИЛИ
askAi: {
// как минимум нужно указать assistantId, полученный от Algolia
assistantId: 'XXXYYY',
// необязательные переопределения — если их нет, используются значения appId/apiKey/indexName верхнего уровня
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -303,11 +253,13 @@ export default defineConfig({
})
```
[Эти параметры](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) можно переопределить. Чтобы узнать о них больше, обратитесь к официальной документации Algolia.
::: warning Примечание
Если вы хотите использовать обычный поиск по ключевым словам без Ask AI, просто не указывайте свойство `askAi`
:::
### Поддержка Ask AI в Algolia {#ask-ai}
### Боковая панель Ask AI {#ask-ai-side-panel}
Если вы хотите добавить функцию **Ask AI**, передайте параметр `askAi` (или любые из его отдельных полей) внутри объекта `options`:
DocSearch v4.5+ поддерживает опциональную **боковую панель Ask AI**. Когда она включена, её можно открыть с помощью **Ctrl/Cmd+I** по умолчанию. [Справочник API боковой панели](https://docsearch.algolia.com/docs/sidepanel/api-reference) содержит полный список опций.
```ts
import { defineConfig } from 'vitepress'
@ -320,15 +272,18 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
// askAi: "ID-ВАШЕГО-АССИСТЕНТА"
// ИЛИ
askAi: {
// минимум вы должны указать assistantId, полученный от Algolia
assistantId: 'XXXYYY',
// опциональные переопределения если не указаны, используются значения appId/apiKey/indexName верхнего уровня
// apiKey: '...',
// appId: '...',
// indexName: '...'
sidePanel: {
// Отражает API @docsearch/sidepanel-js SidepanelProps
panel: {
variant: 'floating', // или 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
@ -336,116 +291,70 @@ export default defineConfig({
})
```
::: warning Примечание
Если вы хотите использовать обычный поиск по ключевым словам без Ask AI, просто не указывайте свойство `askAi`
:::
Если вам нужно отключить сочетание клавиш, используйте опцию `keyboardShortcuts` боковой панели:
Переводы для интерфейса Ask AI находятся в `options.translations.modal.askAiScreen` и `options.translations.resultsScreen` — полный список ключей смотрите в [типах](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts).
```ts
import { defineConfig } from 'vitepress'
### Конфигурация поискового робота {#crawler-config}
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
Вот пример конфигурации, основанной на той, что используется на этом сайте:
#### Режим (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
Вы можете опционально контролировать, как VitePress интегрирует поиск по ключевым словам и Ask AI:
- `mode: 'auto'` (по умолчанию): выводит `hybrid`, когда настроен поиск по ключевым словам, иначе `sidePanel`, когда настроена боковая панель Ask AI.
- `mode: 'sidePanel'`: принудительно использовать только боковую панель (скрывает кнопку поиска по ключевым словам).
- `mode: 'hybrid'`: включает модальное окно поиска по ключевым словам + боковую панель Ask AI (требует настройки поиска по ключевым словам).
- `mode: 'modal'`: сохраняет Ask AI внутри модального окна DocSearch (даже если вы настроили боковую панель).
#### Только Ask AI (без поиска по ключевым словам) {#ask-ai-only}
Если вы хотите использовать **только боковую панель Ask AI**, вы можете опустить конфигурацию поиска по ключевым словам верхнего уровня и предоставить учётные данные в `askAi`:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
```
### Конфигурация поискового робота {#crawler-config}
Вот пример конфигурации, основанной на той, что используется на этом сайте:
<<< @/snippets/algolia-crawler.js

@ -0,0 +1,101 @@
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})

@ -0,0 +1,155 @@
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
translations: {
button: {
buttonText: '搜索',
buttonAriaLabel: '搜索'
},
modal: {
searchBox: {
clearButtonTitle: '清除',
clearButtonAriaLabel: '清除查询',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档或向 AI 提问',
placeholderTextAskAi: '再问一个问题...',
placeholderTextAskAiStreaming: '正在回答...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键词搜索',
backToKeywordSearchButtonAriaLabel: '返回关键词搜索',
newConversationPlaceholder: '提问',
conversationHistoryTitle: '我的对话历史',
startNewConversationText: '开始新的对话',
viewConversationHistoryText: '对话历史',
threadDepthErrorPlaceholder: '对话已达上限'
},
newConversation: {
newConversationTitle: '我今天能帮你什么?',
newConversationDescription:
'我会搜索你的文档,快速帮你找到设置指南、功能细节和故障排除提示。'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: '回车键',
navigateText: '导航',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '由…提供支持'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查网络连接。'
},
startScreen: {
recentSearchesTitle: '最近',
noRecentSearchesText: '暂无最近搜索',
saveRecentSearchButtonTitle: '保存此搜索',
removeRecentSearchButtonTitle: '从历史记录中移除此搜索',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除此搜索',
recentConversationsTitle: '最近对话',
removeRecentConversationButtonTitle: '从历史记录中移除此对话'
},
noResultsScreen: {
noResultsText: '未找到相关结果',
suggestedQueryText: '尝试搜索',
reportMissingResultsText: '认为此查询应该有结果?',
reportMissingResultsLinkText: '告诉我们。'
},
resultsScreen: {
askAiPlaceholder: '询问 AI',
noResultsAskAiPlaceholder: '文档里没找到?让 Ask AI 帮忙:'
},
askAiScreen: {
disclaimerText: '回答由 AI 生成,可能会出错。请核实。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '喜欢',
dislikeButtonTitle: '不喜欢',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索中...',
afterToolCallText: '已搜索',
stoppedStreamingText: '你已停止此回复',
errorTitleText: '聊天错误',
threadDepthExceededMessage: '为保持回答准确,此对话已关闭。',
startNewConversationButtonText: '开始新的对话'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: '询问 AI',
buttonAriaLabel: '询问 AI'
}
},
panel: {
translations: {
header: {
title: '询问 AI',
conversationHistoryTitle: '我的对话历史',
newConversationText: '开始新的对话',
viewConversationHistoryText: '对话历史'
},
promptForm: {
promptPlaceholderText: '提问',
promptAnsweringText: '正在回答...',
promptAskAnotherQuestionText: '再问一个问题',
promptDisclaimerText: '回答由 AI 生成,可能会出错。',
promptLabelText: '按回车发送Shift+回车换行。',
promptAriaLabelText: '问题输入'
},
conversationScreen: {
preToolCallText: '搜索中...',
searchingText: '搜索中...',
toolCallResultText: '已搜索',
conversationDisclaimer:
'回答由 AI 生成,可能会出错。请核实。',
reasoningText: '推理中...',
thinkingText: '思考中...',
relatedSourcesText: '相关来源',
stoppedStreamingText: '你已停止此回复',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
likeButtonTitle: '喜欢',
dislikeButtonTitle: '不喜欢',
thanksForFeedbackText: '感谢你的反馈!',
errorTitleText: '聊天错误'
},
newConversationScreen: {
titleText: '我今天能帮你什么?',
introductionText:
'我会搜索你的文档,快速帮你找到设置指南、功能细节和故障排除提示。'
},
logo: {
poweredByText: '由…提供支持'
}
}
}
}
}
}
}
}
}
}
})

@ -171,74 +171,139 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
buttonText: '搜索',
buttonAriaLabel: '搜索'
},
modal: {
searchBox: {
clearButtonTitle: '清除查询条件',
clearButtonAriaLabel: '清除查询条件',
clearButtonTitle: '清除',
clearButtonAriaLabel: '清除查询',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档',
placeholderTextAskAi: '向 AI 提问:',
placeholderTextAskAiStreaming: '回答...',
placeholderText: '搜索文档或向 AI 提问',
placeholderTextAskAi: '再问一个问题...',
placeholderTextAskAiStreaming: '正在回答...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键字搜索',
backToKeywordSearchButtonAriaLabel: '返回关键字搜索'
backToKeywordSearchButtonText: '返回关键词搜索',
backToKeywordSearchButtonAriaLabel: '返回关键词搜索',
newConversationPlaceholder: '提问',
conversationHistoryTitle: '我的对话历史',
startNewConversationText: '开始新的对话',
viewConversationHistoryText: '对话历史',
threadDepthErrorPlaceholder: '对话已达上限'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除',
recentConversationsTitle: '最近的对话',
removeRecentConversationButtonTitle: '从历史记录中删除对话'
newConversation: {
newConversationTitle: '我今天能帮你什么?',
newConversationDescription:
'我会搜索你的文档,快速帮你找到设置指南、功能细节和故障排除提示。'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: '回车键',
navigateText: '导航',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '由…提供支持'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
helpText: '你可能需要检查网络连接。'
},
startScreen: {
recentSearchesTitle: '最近',
noRecentSearchesText: '暂无最近搜索',
saveRecentSearchButtonTitle: '保存此搜索',
removeRecentSearchButtonTitle: '从历史记录中移除此搜索',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除此搜索',
recentConversationsTitle: '最近对话',
removeRecentConversationButtonTitle: '从历史记录中移除此对话'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
noResultsText: '找到相关结果',
suggestedQueryText: '尝试搜索',
reportMissingResultsText: '认为此查询应该有结果?',
reportMissingResultsLinkText: '告诉我们。'
},
resultsScreen: {
askAiPlaceholder: '向 AI 提问: '
askAiPlaceholder: '询问 AI',
noResultsAskAiPlaceholder: '文档里没找到?让 Ask AI 帮忙:'
},
askAiScreen: {
disclaimerText: '答案由 AI 生成,可能不准确,请自行验证。',
disclaimerText: '回答由 AI 生成,可能会出错。请核实。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '',
dislikeButtonTitle: '',
likeButtonTitle: '喜欢',
dislikeButtonTitle: '不喜欢',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索 ',
duringToolCallText: '搜索中...',
afterToolCallText: '已搜索',
aggregatedToolCallText: '已搜索'
stoppedStreamingText: '你已停止此回复',
errorTitleText: '聊天错误',
threadDepthExceededMessage: '为保持回答准确,此对话已关闭。',
startNewConversationButtonText: '开始新的对话'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: '询问 AI',
buttonAriaLabel: '询问 AI'
}
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: 'Enter 键',
navigateText: '切换',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '搜索提供者'
panel: {
translations: {
header: {
title: '询问 AI',
conversationHistoryTitle: '我的对话历史',
newConversationText: '开始新的对话',
viewConversationHistoryText: '对话历史'
},
promptForm: {
promptPlaceholderText: '提问',
promptAnsweringText: '正在回答...',
promptAskAnotherQuestionText: '再问一个问题',
promptDisclaimerText: '回答由 AI 生成,可能会出错。',
promptLabelText: '按回车发送Shift+回车换行。',
promptAriaLabelText: '问题输入'
},
conversationScreen: {
preToolCallText: '搜索中...',
searchingText: '搜索中...',
toolCallResultText: '已搜索',
conversationDisclaimer: '回答由 AI 生成,可能会出错。请核实。',
reasoningText: '推理中...',
thinkingText: '思考中...',
relatedSourcesText: '相关来源',
stoppedStreamingText: '你已停止此回复',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
likeButtonTitle: '喜欢',
dislikeButtonTitle: '不喜欢',
thanksForFeedbackText: '感谢你的反馈!',
errorTitleText: '聊天错误'
},
newConversationScreen: {
titleText: '我今天能帮你什么?',
introductionText:
'我会搜索你的文档,快速帮你找到设置指南、功能细节和故障排除提示。'
},
logo: {
poweredByText: '由…提供支持'
}
}
}
}
}

@ -105,7 +105,7 @@ Cache-Control: max-age=31536000,immutable
## 各平台部署指南 {#platform-guides}
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render {#generic}
使用仪表板创建新项目并更改这些设置:
@ -200,7 +200,7 @@ Cache-Control: max-age=31536000,immutable
### GitLab Pages
1. 如果你想部署到 `https://<username> .gitlab.io/<repository> /`,将 VitePress 配置中的 `outDir` 设置为 `../public`。将 `base` 选项配置为 `'/<repository>/'`
1. 如果你想部署到 `https://<username> .gitlab.io/<repository> /`,将 VitePress 配置中的 `outDir` 设置为 `../public`。将 `base` 选项配置为 `'/<repository>/'`如果你部署到自定义域名、用户或组织页面,或在 GitLab 中启用了“Use unique domain”设置则不需要 `base`
2. 在项目的根目录中创建一个名为 `.gitlab-ci.yml` 的文件,其中包含以下内容。每当你更改内容时,这都会构建和部署你的站点:
@ -221,7 +221,7 @@ Cache-Control: max-age=31536000,immutable
- main
```
### Azure 静态 web 应用 {#azure-static-web-apps}
### Azure
1. 参考[官方文档](https://docs.microsoft.com/en-us/azure/static-web-apps/build-configuration)。
@ -231,7 +231,11 @@ Cache-Control: max-age=31536000,immutable
- **`output_location`**: `docs/.vitepress/dist`
- **`app_build_command`**: `npm run docs:build`
### Firebase {#firebase}
### CloudRay
你可以按照这些[说明](https://cloudray.io/articles/how-to-deploy-vitepress-site)使用 [CloudRay](https://cloudray.io/) 部署你的 VitePress 项目。
### Firebase
1. 在项目的根目录下创建 `firebase.json``.firebaserc`
@ -262,14 +266,6 @@ Cache-Control: max-age=31536000,immutable
firebase deploy
```
### Surge
1. 运行 `npm run docs:build` 后,运行此命令进行部署:
```sh
npx surge docs/.vitepress/dist
```
### Heroku
1. 参考 [`heroku-buildpack-static`](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-static) 中给出的文档和指南。
@ -282,11 +278,11 @@ Cache-Control: max-age=31536000,immutable
}
```
### Edgio
### Hostinger
请参阅[创建并部署 VitePress 应用程序到 Edgio](https://docs.edg.io/guides/vitepress)
你可以按照这些[说明](https://www.hostinger.com/support/how-to-deploy-a-nodejs-website-in-hostinger/)使用 [Hostinger](https://www.hostinger.com/web-apps-hosting) 部署你的 VitePress 项目。在配置构建设置时,选择 VitePress 作为框架,并将根目录调整为 `./docs`
### Kinsta 静态站点托管 {#kinsta-static-site-hosting}
### Kinsta
你可以按照这些[说明](https://kinsta.com/docs/vitepress-static-site-example/) 在 [Kinsta](https://kinsta.com/static-site-hosting/) 上部署 VitePress 站点。
@ -294,6 +290,14 @@ Cache-Control: max-age=31536000,immutable
你可以按照这些[说明](https://stormkit.io/blog/how-to-deploy-vitepress)将你的 VitePress 项目部署到 [Stormkit](https://www.stormkit.io)。
### Surge
1. 运行 `npm run docs:build` 后,运行此命令进行部署:
```sh
npx surge docs/.vitepress/dist
```
### Nginx
下面是一个 Nginx 服务器块配置示例。此配置包括对基于文本的常见资源的 gzip 压缩、使用适当缓存头为 VitePress 站点静态文件提供服务的规则以及处理 `cleanUrls: true` 的方法。

@ -771,7 +771,7 @@ export default config
## Basics
<!--@include: ./parts/basics.md-->
<!--@@include: ./parts/basics.md-->
```
**Part file** (`parts/basics.md`)
@ -807,7 +807,7 @@ Can be created using `.foorc.json`.
## Basics
<!--@include: ./parts/basics.md{3,}-->
<!--@@include: ./parts/basics.md{3,}-->
```
**Part file** (`parts/basics.md`)

@ -39,18 +39,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
zh: {
zh: { // 如果你想翻译默认语言,请将此处设为 `root`
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
buttonText: '搜索',
buttonAriaLabel: '搜索'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
displayDetails: '显示详细列表',
resetButtonTitle: '重置搜索',
backButtonTitle: '关闭搜索',
noResultsText: '没有结果',
footer: {
selectText: '选择',
navigateText: '切换'
selectKeyAriaLabel: '输入',
navigateText: '导航',
navigateUpKeyAriaLabel: '上箭头',
navigateDownKeyAriaLabel: '下箭头',
closeText: '关闭',
closeKeyAriaLabel: 'Esc'
}
}
}
@ -62,7 +69,7 @@ export default defineConfig({
})
```
### MiniSearch 配置项 {#mini-search-options}
### MiniSearch 配置项 {#minisearch-options}
你可以像这样配置 MiniSearch
@ -116,7 +123,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// 返回 html 字符串
// 返回 HTML 字符串
}
}
}
@ -167,7 +174,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.title)
return await md.renderAsync(`# ${env.frontmatter.title}`) + html
return (await md.renderAsync(`# ${env.frontmatter.title}`)) + html
return html
}
}
@ -201,6 +208,19 @@ export default defineConfig({
你可以使用这样的配置来使用多语言搜索:
<details>
<summary>点击展开</summary>
<<< @/snippets/algolia-i18n.ts
</details>
更多信息请参考[官方 Algolia 文档](https://docsearch.algolia.com/docs/api#translations)。想要快速开始,你也可以从[我们的 GitHub 仓库](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code)复制此站点使用的翻译。
### Algolia Ask AI 支持 {#ask-ai}
如果需要启用 **Ask AI**,只需在 `options` 中添加 `askAi`
```ts
import { defineConfig } from 'vitepress'
@ -212,72 +232,51 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: { buttonText: '搜索文档', buttonAriaLabel: '搜索文档' },
modal: {
searchBox: {
clearButtonTitle: '清除查询条件',
clearButtonAriaLabel: '清除查询条件',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档',
placeholderTextAskAi: '向 AI 提问:',
placeholderTextAskAiStreaming: '回答中...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键字搜索',
backToKeywordSearchButtonAriaLabel: '返回关键字搜索'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除',
recentConversationsTitle: '最近的对话',
removeRecentConversationButtonTitle: '从历史记录中删除对话'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '请检查网络连接'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
},
resultsScreen: { askAiPlaceholder: '向 AI 提问: ' },
askAiScreen: {
disclaimerText: '答案由 AI 生成,可能不准确,请自行验证。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '赞',
dislikeButtonTitle: '踩',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索 ',
afterToolCallText: '已搜索'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: 'Enter 键',
navigateText: '切换',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '搜索提供者'
}
}
// askAi: "你的助手ID"
// 或
askAi: {
// 至少需要提供从 Algolia 获取的 assistantId
assistantId: 'XXXYYY',
// 可选覆盖 — 若省略,将复用顶层 appId/apiKey/indexName 的值
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
}
})
```
::: warning 提示
若仅需关键词搜索,可省略 `askAi`
:::
### Ask AI 侧边栏 {#ask-ai-side-panel}
DocSearch v4.5+ 支持可选的 **Ask AI 侧边栏**。启用后,默认可通过 **Ctrl/Cmd+I** 打开。完整的选项列表请参阅[侧边栏 API 参考](https://docsearch.algolia.com/docs/sidepanel/api-reference)。
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// 镜像 @docsearch/sidepanel-js SidepanelProps API
panel: {
variant: 'floating', // 或 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
@ -287,131 +286,70 @@ export default defineConfig({
})
```
### Algolia Ask AI 支持 {#ask-ai}
如果需要启用 **Ask AI**,只需在 `options` 中添加 `askAi`
如果需要禁用键盘快捷键,请使用侧边栏的 `keyboardShortcuts` 选项:
```ts
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY'
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
}
})
```
::: warning 提示
若仅需关键词搜索,可省略 `askAi`
:::
#### 模式 (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
[这些选项](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts)可以被覆盖。请参阅 Algolia 官方文档以了解更多信息。
你可以选择性地控制 VitePress 如何集成关键词搜索和 Ask AI
### 爬虫配置 {#crawler-config}
- `mode: 'auto'`(默认):当配置了关键词搜索时推断为 `hybrid`,否则当配置了 Ask AI 侧边栏时推断为 `sidePanel`
- `mode: 'sidePanel'`:强制仅使用侧边栏(隐藏关键词搜索按钮)。
- `mode: 'hybrid'`:启用关键词搜索模态框 + Ask AI 侧边栏(需要关键词搜索配置)。
- `mode: 'modal'`:将 Ask AI 保留在 DocSearch 模态框内(即使你配置了侧边栏)。
以下是基于此站点使用的示例配置:
#### 仅 Ask AI无关键词搜索 {#ask-ai-only}
如果你想**仅使用 Ask AI 侧边栏**,可以省略顶级关键词搜索配置,并在 `askAi` 下提供凭据:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
### 爬虫配置 {#crawler-config}
以下是基于此站点使用的示例配置:
<<< @/snippets/algolia-crawler.js

@ -1,6 +1,6 @@
{
"name": "vitepress",
"version": "2.0.0-alpha.15",
"version": "2.0.0-alpha.16",
"description": "Vite & Vue powered static site generator",
"keywords": [
"vite",
@ -51,14 +51,15 @@
"lib"
],
"scripts": {
"dev": "rimraf dist && pnpm dev:shared && pnpm dev:start",
"clean": "node -e \"require('node:fs').rmSync('./dist',{recursive:!0,force:!0,maxRetries:10})\"",
"dev": "pnpm clean && pnpm dev:shared && pnpm dev:start",
"dev:start": "pnpm --stream '/^dev:(client|node|watch)$/'",
"dev:client": "tsc --sourcemap -w --preserveWatchOutput -p src/client",
"dev:node": "DEV=true pnpm build:node -w",
"dev:shared": "node scripts/copyShared",
"dev:watch": "node scripts/watchAndCopy",
"build": "pnpm build:prepare && pnpm build:client && pnpm build:node",
"build:prepare": "rimraf dist && node scripts/copyShared",
"build:prepare": "pnpm clean && node scripts/copyShared",
"build:client": "vue-tsc --noEmit -p src/client && tsc -p src/client && node scripts/copyClient",
"build:node": "tsc -p src/node --noEmit && rollup --config rollup.config.ts --configPlugin esbuild",
"test": "pnpm --aggregate-output --reporter=append-only '/^test:(unit|e2e|init)$/'",
@ -95,28 +96,29 @@
"*": "prettier --experimental-cli --ignore-unknown --write"
},
"dependencies": {
"@docsearch/css": "^4.3.2",
"@docsearch/js": "^4.3.2",
"@iconify-json/simple-icons": "^1.2.59",
"@shikijs/core": "^3.15.0",
"@shikijs/transformers": "^3.15.0",
"@shikijs/types": "^3.15.0",
"@docsearch/css": "^4.5.3",
"@docsearch/js": "^4.5.3",
"@docsearch/sidepanel-js": "^4.5.3",
"@iconify-json/simple-icons": "^1.2.69",
"@shikijs/core": "^3.22.0",
"@shikijs/transformers": "^3.22.0",
"@shikijs/types": "^3.22.0",
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/devtools-api": "^8.0.5",
"@vue/shared": "^3.5.24",
"@vueuse/core": "^14.0.0",
"@vueuse/integrations": "^14.0.0",
"focus-trap": "^7.6.6",
"@vue/shared": "^3.5.27",
"@vueuse/core": "^14.2.0",
"@vueuse/integrations": "^14.2.0",
"focus-trap": "^8.0.0",
"mark.js": "8.11.1",
"minisearch": "^7.2.0",
"shiki": "^3.15.0",
"vite": "^7.2.2",
"vue": "^3.5.24"
"shiki": "^3.22.0",
"vite": "^7.3.1",
"vue": "^3.5.27"
},
"devDependencies": {
"@clack/prompts": "^1.0.0-alpha.6",
"@iconify/utils": "^3.0.2",
"@clack/prompts": "^1.0.0",
"@iconify/utils": "^3.1.0",
"@mdit-vue/plugin-component": "^3.0.2",
"@mdit-vue/plugin-frontmatter": "^3.0.2",
"@mdit-vue/plugin-headers": "^3.0.2",
@ -135,51 +137,50 @@
"@types/lodash.template": "^4.5.3",
"@types/mark.js": "^8.11.12",
"@types/markdown-it-attrs": "^4.1.3",
"@types/markdown-it-container": "^2.0.10",
"@types/markdown-it-container": "^4.0.0",
"@types/markdown-it-emoji": "^3.0.1",
"@types/minimist": "^1.2.5",
"@types/node": "^24.10.1",
"@types/node": "^25.2.0",
"@types/picomatch": "^4.0.2",
"@types/prompts": "^2.4.9",
"chokidar": "^4.0.3",
"conventional-changelog": "^7.1.1",
"conventional-changelog-angular": "^8.1.0",
"cross-spawn": "^7.0.6",
"esbuild": "^0.25.12",
"execa": "^9.6.0",
"fs-extra": "^11.3.2",
"esbuild": "^0.27.2",
"execa": "^9.6.1",
"fs-extra": "^11.3.3",
"get-port": "^7.1.0",
"gray-matter": "^4.0.3",
"lint-staged": "^16.2.6",
"lint-staged": "^16.2.7",
"lodash.template": "^4.5.0",
"lru-cache": "^11.2.2",
"lru-cache": "^11.2.5",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-async": "^2.2.0",
"markdown-it-attrs": "^4.3.1",
"markdown-it-cjk-friendly": "^1.3.2",
"markdown-it-cjk-friendly": "^2.0.1",
"markdown-it-container": "^4.0.0",
"markdown-it-emoji": "^3.0.0",
"markdown-it-mathjax3": "^4.3.2",
"minimist": "^1.2.8",
"nanoid": "^5.1.6",
"obug": "^2.0.0",
"ora": "^9.0.0",
"obug": "^2.1.1",
"ora": "^9.1.0",
"oxc-minify": "^0.98.0",
"p-map": "^7.0.4",
"package-directory": "^8.1.0",
"package-directory": "^8.2.0",
"path-to-regexp": "^6.3.0",
"picocolors": "^1.1.1",
"picomatch": "^4.0.3",
"playwright-chromium": "^1.56.1",
"playwright-chromium": "^1.58.1",
"polka": "^1.0.0-next.28",
"postcss": "^8.5.6",
"postcss-selector-parser": "^7.1.0",
"prettier": "^3.6.2",
"postcss-selector-parser": "^7.1.1",
"prettier": "^3.8.1",
"prompts": "^2.4.2",
"punycode": "^2.3.1",
"rimraf": "^6.1.0",
"rollup": "^4.53.2",
"rollup": "^4.57.1",
"rollup-plugin-dts": "6.1.1",
"rollup-plugin-esbuild": "^6.2.1",
"semver": "^7.7.3",
@ -189,7 +190,7 @@
"tinyglobby": "^0.2.15",
"typescript": "^5.9.3",
"vitest": "4.0.0-beta.4",
"vue-tsc": "^3.1.4",
"vue-tsc": "^3.2.4",
"wait-on": "^9.0.3"
},
"peerDependencies": {
@ -208,5 +209,5 @@
"optional": true
}
},
"packageManager": "pnpm@10.22.0"
"packageManager": "pnpm@10.28.2"
}

File diff suppressed because it is too large Load Diff

@ -48,7 +48,7 @@ export function useCopyCode() {
async function copyToClipboard(text: string) {
try {
return navigator.clipboard.writeText(text)
await navigator.clipboard.writeText(text)
} catch {
const element = document.createElement('textarea')
const previouslyFocusedElement = document.activeElement

@ -1,84 +1,238 @@
<script setup lang="ts">
import docsearch from '@docsearch/js'
import { useRouter } from 'vitepress'
import type { DocSearchInstance, DocSearchProps } from '@docsearch/js'
import type { SidepanelInstance, SidepanelProps } from '@docsearch/sidepanel-js'
import { inBrowser, useRouter } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { nextTick, onMounted, watch } from 'vue'
import { nextTick, onUnmounted, watch } from 'vue'
import type { DocSearchAskAi } from '../../../../types/docsearch'
import { useData } from '../composables/data'
import { resolveMode, validateCredentials } from '../support/docsearch'
import '../styles/docsearch.css'
const props = defineProps<{
algolia: DefaultTheme.AlgoliaSearchOptions
algoliaOptions: DefaultTheme.AlgoliaSearchOptions
openRequest?: {
target: 'search' | 'askAi' | 'toggleAskAi'
nonce: number
} | null
}>()
const router = useRouter()
const { site, localeIndex, lang } = useData()
const { site } = useData()
onMounted(update)
watch(localeIndex, update)
let cleanup = () => {}
let docsearchInstance: DocSearchInstance | undefined
let sidepanelInstance: SidepanelInstance | undefined
let openOnReady: 'search' | 'askAi' | null = null
let initializeCount = 0
let docsearchLoader: Promise<typeof import('@docsearch/js')> | undefined
let sidepanelLoader: Promise<typeof import('@docsearch/sidepanel-js')> | undefined
let lastFocusedElement: HTMLElement | null = null
let skipEventDocsearch = false
let skipEventSidepanel = false
async function update() {
await nextTick()
const options = {
...props.algolia,
...props.algolia.locales?.[localeIndex.value]
}
const rawFacetFilters = options.searchParameters?.facetFilters ?? []
const facetFilters = [
...(Array.isArray(rawFacetFilters)
? rawFacetFilters
: [rawFacetFilters]
).filter((f) => !f.startsWith('lang:')),
`lang:${lang.value}`
]
// Rebuild the askAi prop as an object:
// If the askAi prop is a string, treat it as the assistantId and use
// the default indexName, apiKey and appId from the main options.
// If the askAi prop is an object, spread its explicit values.
const askAiProp = options.askAi
const isAskAiString = typeof askAiProp === 'string'
const askAi = askAiProp
? {
indexName: isAskAiString ? options.indexName : askAiProp.indexName,
apiKey: isAskAiString ? options.apiKey : askAiProp.apiKey,
appId: isAskAiString ? options.appId : askAiProp.appId,
assistantId: isAskAiString ? askAiProp : askAiProp.assistantId,
// Re-use the merged facetFilters from the search parameters so that
// Ask AI uses the same language filtering as the regular search.
searchParameters: facetFilters.length ? { facetFilters } : undefined
watch(() => props.algoliaOptions, update, { immediate: true })
onUnmounted(cleanup)
watch(
() => props.openRequest?.nonce,
() => {
const req = props.openRequest
if (!req) return
if (req.target === 'search') {
if (docsearchInstance?.isReady) {
onBeforeOpen('docsearch', () => docsearchInstance?.open())
} else {
openOnReady = 'search'
}
} else if (req.target === 'toggleAskAi') {
if (sidepanelInstance?.isOpen) {
sidepanelInstance.close()
} else {
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
}
} else {
// askAi - open sidepanel or fallback to docsearch modal
if (sidepanelInstance?.isReady) {
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
} else if (sidepanelInstance) {
openOnReady = 'askAi'
} else if (docsearchInstance?.isReady) {
onBeforeOpen('docsearch', () => docsearchInstance?.openAskAi())
} else {
openOnReady = 'askAi'
}
: undefined
}
},
{ immediate: true }
)
initialize({
...options,
searchParameters: {
...options.searchParameters,
facetFilters
},
askAi
async function update(options: DefaultTheme.AlgoliaSearchOptions) {
if (!inBrowser) return
await nextTick()
const askAi = options.askAi as DocSearchAskAi | undefined
const { valid, ...credentials } = validateCredentials({
appId: options.appId ?? askAi?.appId,
apiKey: options.apiKey ?? askAi?.apiKey,
indexName: options.indexName ?? askAi?.indexName
})
if (!valid) {
console.warn('[vitepress] Algolia search cannot be initialized: missing appId/apiKey/indexName.')
return
}
await initialize({ ...options, ...credentials })
}
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
const options = Object.assign({}, userOptions, {
container: '#docsearch',
async function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
const currentInitialize = ++initializeCount
// Always tear down previous instances first (e.g. on locale changes)
cleanup()
const { useSidePanel } = resolveMode(userOptions)
const askAi = userOptions.askAi as DocSearchAskAi | undefined
const { default: docsearch } = await loadDocsearch()
if (currentInitialize !== initializeCount) return
if (useSidePanel && askAi?.sidePanel) {
const { default: sidepanel } = await loadSidepanel()
if (currentInitialize !== initializeCount) return
sidepanelInstance = sidepanel({
...(askAi.sidePanel === true ? {} : askAi.sidePanel),
container: '#vp-docsearch-sidepanel',
indexName: askAi.indexName ?? userOptions.indexName,
appId: askAi.appId ?? userOptions.appId,
apiKey: askAi.apiKey ?? userOptions.apiKey,
assistantId: askAi.assistantId,
onOpen: focusInput,
onClose: onClose.bind(null, 'sidepanel'),
onReady: () => {
if (openOnReady === 'askAi') {
openOnReady = null
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
}
},
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
} as SidepanelProps)
}
const options = {
...userOptions,
container: '#vp-docsearch',
navigator: {
navigate(item: { itemUrl: string }) {
navigate(item) {
router.go(item.itemUrl)
}
},
transformItems(items: { url: string }[]) {
return items.map((item) => {
return Object.assign({}, item, {
url: getRelativePath(item.url)
})
})
transformItems: (items) => items.map((item) => ({ ...item, url: getRelativePath(item.url) })),
// When sidepanel is enabled, intercept Ask AI events to open it instead (hybrid mode)
...(useSidePanel && sidepanelInstance && {
interceptAskAiEvent: (initialMessage) => {
onBeforeOpen('sidepanel', () => sidepanelInstance?.open(initialMessage))
return true
}
}),
onOpen: focusInput,
onClose: onClose.bind(null, 'docsearch'),
onReady: () => {
if (openOnReady === 'search') {
openOnReady = null
onBeforeOpen('docsearch', () => docsearchInstance?.open())
} else if (openOnReady === 'askAi' && !sidepanelInstance) {
// No sidepanel configured, use docsearch modal for askAi
openOnReady = null
onBeforeOpen('docsearch', () => docsearchInstance?.openAskAi())
}
},
keyboardShortcuts: {
'/': false,
'Ctrl/Cmd+K': false
}
} as DocSearchProps
docsearchInstance = docsearch(options)
cleanup = () => {
docsearchInstance?.destroy()
sidepanelInstance?.destroy()
docsearchInstance = undefined
sidepanelInstance = undefined
openOnReady = null
lastFocusedElement = null
}
}
function focusInput() {
requestAnimationFrame(() => {
const input =
document.querySelector<HTMLInputElement>('#docsearch-input') ||
document.querySelector<HTMLInputElement>('#docsearch-sidepanel textarea')
input?.focus()
})
}
docsearch(options as any)
function onBeforeOpen(target: 'docsearch' | 'sidepanel', cb: () => void) {
if (target === 'docsearch') {
if (sidepanelInstance?.isOpen) {
skipEventSidepanel = true
sidepanelInstance.close()
} else if (!docsearchInstance?.isOpen) {
if (document.activeElement instanceof HTMLElement) {
lastFocusedElement = document.activeElement
}
}
} else if (target === 'sidepanel') {
if (docsearchInstance?.isOpen) {
skipEventDocsearch = true
docsearchInstance.close()
} else if (!sidepanelInstance?.isOpen) {
if (document.activeElement instanceof HTMLElement) {
lastFocusedElement = document.activeElement
}
}
}
setTimeout(cb, 0)
}
function onClose(target: 'docsearch' | 'sidepanel') {
if (target === 'docsearch') {
if (skipEventDocsearch) {
skipEventDocsearch = false
return
}
} else if (target === 'sidepanel') {
if (skipEventSidepanel) {
skipEventSidepanel = false
return
}
}
if (lastFocusedElement) {
lastFocusedElement.focus()
lastFocusedElement = null
}
}
function loadDocsearch() {
if (!docsearchLoader) {
docsearchLoader = import('@docsearch/js')
}
return docsearchLoader
}
function loadSidepanel() {
if (!sidepanelLoader) {
sidepanelLoader = import('@docsearch/sidepanel-js')
}
return sidepanelLoader
}
function getRelativePath(url: string) {
@ -88,5 +242,6 @@ function getRelativePath(url: string) {
</script>
<template>
<div id="docsearch" />
<div id="vp-docsearch" />
<div id="vp-docsearch-sidepanel" />
</template>

@ -41,8 +41,10 @@ defineProps<{
/>
<div v-else-if="icon" class="icon" v-html="icon"></div>
<h2 class="title" v-html="title"></h2>
<p v-if="details" class="details" v-html="details"></p>
<ul v-if="Array.isArray(details)" class="details">
<li v-for="item in details" :key="item" v-html="item"></li>
</ul>
<p v-else-if="details" class="details" v-html="details"></p>
<div v-if="linkText" class="link-text">
<p class="link-text-value">
{{ linkText }} <span class="vpi-arrow-right link-text-icon" />
@ -105,6 +107,11 @@ defineProps<{
color: var(--vp-c-text-2);
}
ul.details {
list-style-type: disc;
padding-left: 14px;
}
.link-text {
padding-top: 8px;
}

@ -2,7 +2,7 @@
import localSearchIndex from '@localSearchIndex'
import {
computedAsync,
debouncedWatch,
watchDebounced,
onKeyStroke,
useEventListener,
useLocalStorage,
@ -26,7 +26,7 @@ import {
watchEffect,
type Ref
} from 'vue'
import type { ModalTranslations } from '../../../../types/local-search'
import type { LocalSearchTranslations } from '../../../../types/local-search'
import { pathToFile } from '../../app/utils'
import { escapeRegExp } from '../../shared'
import { useData } from '../composables/data'
@ -113,16 +113,6 @@ const disableDetailedView = computed(() => {
)
})
const buttonText = computed(() => {
const options = theme.value.search?.options ?? theme.value.algolia
return (
options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
options?.translations?.button?.buttonText ||
'Search'
)
})
watchEffect(() => {
if (disableDetailedView.value) {
showDetailedList.value = false
@ -144,7 +134,7 @@ const mark = computedAsync(async () => {
const cache = new LRUCache<string, Map<string, string>>(16) // 16 files
debouncedWatch(
watchDebounced(
() => [searchIndex.value, filterText.value, showDetailedList.value] as const,
async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
if (old?.[0] !== index) {
@ -339,8 +329,12 @@ onKeyStroke('Escape', () => {
emit('close')
})
// Translations
const defaultTranslations: { modal: ModalTranslations } = {
/* Translations */
const defaultTranslations: LocalSearchTranslations = {
button: {
buttonText: 'Search'
},
modal: {
displayDetails: 'Display detailed list',
resetButtonTitle: 'Reset search',
@ -360,7 +354,7 @@ const defaultTranslations: { modal: ModalTranslations } = {
const translate = createSearchTranslate(defaultTranslations)
// Back
/* Back */
onMounted(() => {
// Prevents going to previous site
@ -373,6 +367,7 @@ useEventListener('popstate', (event) => {
})
/** Lock body */
const isLocked = useScrollLock(inBrowser ? document.body : null)
onMounted(() => {
@ -432,7 +427,7 @@ function onMouseMove(e: MouseEvent) {
@submit.prevent=""
>
<label
:title="buttonText"
:title="translate('button.buttonText')"
id="localsearch-label"
for="localsearch-input"
>
@ -461,7 +456,7 @@ function onMouseMove(e: MouseEvent) {
id="localsearch-input"
enterkeyhint="go"
maxlength="64"
:placeholder="buttonText"
:placeholder="translate('button.buttonText')"
spellcheck="false"
type="search"
/>

@ -7,6 +7,7 @@ import VPLink from './VPLink.vue'
const props = defineProps<{
item: T
rel?: string
}>()
const { page } = useData()
@ -17,6 +18,14 @@ const href = computed(() =>
: props.item.link
)
const isActiveLink = computed(() =>
isActive(
page.value.relativePath,
props.item.activeMatch || href.value,
!!props.item.activeMatch
)
)
defineOptions({ inheritAttrs: false })
</script>
@ -24,16 +33,10 @@ defineOptions({ inheritAttrs: false })
<div class="VPMenuLink">
<VPLink
v-bind="$attrs"
:class="{
active: isActive(
page.relativePath,
item.activeMatch || href,
!!item.activeMatch
)
}"
:class="{ active: isActiveLink }"
:href
:target="item.target"
:rel="item.rel"
:rel="props.rel ?? item.rel"
:no-icon="item.noIcon"
>
<span v-html="item.text"></span>

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { ref, watchPostEffect } from 'vue'
import { useLayout } from '../composables/layout'
import VPNavBarAppearance from './VPNavBarAppearance.vue'
import VPNavBarExtra from './VPNavBarExtra.vue'
@ -21,21 +20,18 @@ defineEmits<{
const { y } = useWindowScroll()
const { isHome, hasSidebar } = useLayout()
const classes = ref<Record<string, boolean>>({})
watchPostEffect(() => {
classes.value = {
'has-sidebar': hasSidebar.value,
'home': isHome.value,
'top': y.value === 0,
'screen-open': props.isScreenOpen
}
})
</script>
<template>
<div class="VPNavBar" :class="classes">
<div
class="VPNavBar"
:class="{
'has-sidebar': hasSidebar,
'home': isHome,
'top': y === 0,
'screen-open': isScreenOpen
}"
>
<div class="wrapper">
<div class="container">
<div class="title">
@ -55,7 +51,11 @@ watchPostEffect(() => {
<VPNavBarSocialLinks class="social-links" />
<VPNavBarExtra class="extra" />
<slot name="nav-bar-content-after" />
<VPNavBarHamburger class="hamburger" :active="isScreenOpen" @click="$emit('toggle-screen')" />
<VPNavBarHamburger
class="hamburger"
:active="isScreenOpen"
@click="$emit('toggle-screen')"
/>
</div>
</div>
</div>
@ -206,12 +206,6 @@ watchPostEffect(() => {
}
}
@media (max-width: 767px) {
.content-body {
column-gap: 0.5rem;
}
}
.menu + .translations::before,
.menu + .appearance::before,
.menu + .social-links::before,

@ -0,0 +1,31 @@
<template>
<button type="button" class="VPNavBarAskAiButton">
<span class="vpi-sparkles" aria-hidden="true"></span>
</button>
</template>
<style scoped>
.VPNavBarAskAiButton {
display: flex;
align-items: center;
height: var(--vp-nav-height);
padding: 8px 14px;
font-size: 20px;
}
@media (min-width: 768px) {
.VPNavBarAskAiButton {
height: auto;
padding: 11.5px;
transition: color 0.3s ease;
background-color: var(--vp-c-bg-alt);
border-radius: 8px;
font-size: 15px;
color: var(--vp-c-text-2);
}
.VPNavBarAskAiButton:hover {
color: var(--vp-c-brand-1);
}
}
</style>

@ -31,7 +31,13 @@ const hasExtraContent = computed(
<p class="trans-title">{{ currentLang.label }}</p>
<template v-for="locale in localeLinks" :key="locale.link">
<VPMenuLink :item="locale" :lang="locale.lang" :dir="locale.dir" />
<VPMenuLink
:item="locale"
:lang="locale.lang"
:hreflang="locale.lang"
rel="alternate"
:dir="locale.dir"
/>
</template>
</div>

@ -11,31 +11,36 @@ const props = defineProps<{
const { page } = useData()
const isChildActive = (navItem: DefaultTheme.NavItem) => {
const isActiveGroup = computed(() => {
if (props.item.activeMatch) {
return isActive(page.value.relativePath, props.item.activeMatch, true)
}
return isChildActive(props.item)
})
function isChildActive(navItem: DefaultTheme.NavItem): boolean {
if ('component' in navItem) return false
if ('link' in navItem) {
const href =
typeof navItem.link === 'function'
? navItem.link(page.value)
: navItem.link
return isActive(
page.value.relativePath,
typeof navItem.link === "function" ? navItem.link(page.value) : navItem.link,
!!props.item.activeMatch
navItem.activeMatch || href,
!!navItem.activeMatch
)
}
return navItem.items.some(isChildActive)
}
const childrenActive = computed(() => isChildActive(props.item))
</script>
<template>
<VPFlyout
:class="{
VPNavBarMenuGroup: true,
active:
isActive(page.relativePath, item.activeMatch, !!item.activeMatch) ||
childrenActive
}"
:class="{ VPNavBarMenuGroup: true, active: isActiveGroup }"
:button="item.text"
:items="item.items"
/>

@ -16,18 +16,19 @@ const href = computed(() =>
? props.item.link(page.value)
: props.item.link
)
const isActiveLink = computed(() =>
isActive(
page.value.relativePath,
props.item.activeMatch || href.value,
!!props.item.activeMatch
)
)
</script>
<template>
<VPLink
:class="{
VPNavBarMenuLink: true,
active: isActive(
page.relativePath,
item.activeMatch || href,
!!item.activeMatch
)
}"
:class="{ VPNavBarMenuLink: true, active: isActiveLink }"
:href
:target="item.target"
:rel="item.rel"

@ -1,9 +1,11 @@
<script lang="ts" setup>
import '@docsearch/css'
import { onKeyStroke } from '@vueuse/core'
import type { DefaultTheme } from 'vitepress/theme'
import { defineAsyncComponent, onMounted, onUnmounted, ref } from 'vue'
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
import { useData } from '../composables/data'
import { resolveMode, resolveOptionsForLanguage } from '../support/docsearch'
import { smartComputed } from '../support/reactivity'
import VPNavBarAskAiButton from './VPNavBarAskAiButton.vue'
import VPNavBarSearchButton from './VPNavBarSearchButton.vue'
const VPLocalSearchBox = __VP_LOCAL_SEARCH__
@ -14,7 +16,37 @@ const VPAlgoliaSearchBox = __ALGOLIA__
? defineAsyncComponent(() => import('./VPAlgoliaSearchBox.vue'))
: () => null
const { theme } = useData()
const { theme, localeIndex, lang } = useData()
const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
// #region Algolia Search
const algoliaOptions = smartComputed<DefaultTheme.AlgoliaSearchOptions>(() => {
return resolveOptionsForLanguage(
theme.value.search?.options || {},
localeIndex.value,
lang.value
)
})
const resolvedMode = computed(() => resolveMode(algoliaOptions.value))
const askAiSidePanelConfig = computed(() => {
if (!resolvedMode.value.useSidePanel) return null
const askAi = algoliaOptions.value.askAi
if (!askAi || typeof askAi === 'string') return null
if (!askAi.sidePanel) return null
return askAi.sidePanel === true ? {} : askAi.sidePanel
})
const askAiShortcutEnabled = computed(() => {
return askAiSidePanelConfig.value?.keyboardShortcuts?.['Ctrl/Cmd+I'] !== false
})
type OpenTarget = 'search' | 'askAi' | 'toggleAskAi'
type OpenRequest = { target: OpenTarget; nonce: number }
const openRequest = ref<OpenRequest | null>(null)
let openNonce = 0
// to avoid loading the docsearch js upfront (which is more than 1/3 of the
// payload), we delay initializing it until the user has actually clicked or
@ -22,86 +54,74 @@ const { theme } = useData()
const loaded = ref(false)
const actuallyLoaded = ref(false)
const preconnect = () => {
onMounted(() => {
if (!__ALGOLIA__) return
const id = 'VPAlgoliaPreconnect'
if (document.getElementById(id)) return
const appId =
algoliaOptions.value.appId ||
(typeof algoliaOptions.value.askAi === 'object'
? algoliaOptions.value.askAi?.appId
: undefined)
if (!appId) return
const rIC = window.requestIdleCallback || setTimeout
rIC(() => {
const preconnect = document.createElement('link')
preconnect.id = id
preconnect.rel = 'preconnect'
preconnect.href = `https://${
((theme.value.search?.options as DefaultTheme.AlgoliaSearchOptions) ??
theme.value.algolia)!.appId
}-dsn.algolia.net`
preconnect.href = `https://${appId}-dsn.algolia.net`
preconnect.crossOrigin = ''
document.head.appendChild(preconnect)
})
}
onMounted(() => {
if (!__ALGOLIA__) {
return
}
preconnect()
})
const handleSearchHotKey = (event: KeyboardEvent) => {
if (__ALGOLIA__) {
onKeyStroke('k', (event) => {
if (
(event.key?.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) ||
(!isEditingContent(event) && event.key === '/')
resolvedMode.value.showKeywordSearch &&
(event.ctrlKey || event.metaKey)
) {
event.preventDefault()
load()
remove()
loadAndOpen('search')
}
}
const remove = () => {
window.removeEventListener('keydown', handleSearchHotKey)
}
})
window.addEventListener('keydown', handleSearchHotKey)
onKeyStroke('i', (event) => {
if (
askAiSidePanelConfig.value &&
askAiShortcutEnabled.value &&
(event.ctrlKey || event.metaKey)
) {
event.preventDefault()
loadAndOpen('askAi')
}
})
onUnmounted(remove)
})
onKeyStroke('/', (event) => {
if (resolvedMode.value.showKeywordSearch && !isEditingContent(event)) {
event.preventDefault()
loadAndOpen('search')
}
})
}
function load() {
function loadAndOpen(target: OpenTarget) {
if (!loaded.value) {
loaded.value = true
setTimeout(poll, 16)
}
}
function poll() {
// programmatically open the search box after initialize
const e = new Event('keydown') as any
e.key = 'k'
e.metaKey = true
window.dispatchEvent(e)
setTimeout(() => {
if (!document.querySelector('.DocSearch-Modal')) {
poll()
}
}, 16)
// This will either be handled immediately if DocSearch is ready,
// or queued by the AlgoliaSearchBox until its instances become ready.
openRequest.value = { target, nonce: ++openNonce }
}
function isEditingContent(event: KeyboardEvent): boolean {
const element = event.target as HTMLElement
const tagName = element.tagName
return (
element.isContentEditable ||
tagName === 'INPUT' ||
tagName === 'SELECT' ||
tagName === 'TEXTAREA'
)
}
// #endregion
// Local search
// #region Local Search
const showSearch = ref(false)
@ -121,37 +141,60 @@ if (__VP_LOCAL_SEARCH__) {
})
}
const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
// #endregion
function isEditingContent(event: KeyboardEvent): boolean {
const element = event.target as HTMLElement
const tagName = element.tagName
return (
element.isContentEditable ||
tagName === 'INPUT' ||
tagName === 'SELECT' ||
tagName === 'TEXTAREA'
)
}
</script>
<template>
<div class="VPNavBarSearch">
<template v-if="provider === 'local'">
<VPLocalSearchBox
v-if="showSearch"
@close="showSearch = false"
<template v-if="provider === 'algolia'">
<VPNavBarSearchButton
v-if="resolvedMode.showKeywordSearch"
:text="algoliaOptions.translations?.button?.buttonText || 'Search'"
:aria-label="algoliaOptions.translations?.button?.buttonAriaLabel || 'Search'"
:aria-keyshortcuts="'/ control+k meta+k'"
@click="loadAndOpen('search')"
/>
<VPNavBarAskAiButton
v-if="askAiSidePanelConfig"
:aria-label="askAiSidePanelConfig.button?.translations?.buttonAriaLabel || 'Ask AI'"
:aria-keyshortcuts="askAiShortcutEnabled ? 'control+i meta+i' : undefined"
@click="actuallyLoaded ? loadAndOpen('toggleAskAi') : loadAndOpen('askAi')"
/>
<div id="local-search">
<VPNavBarSearchButton @click="showSearch = true" />
</div>
</template>
<template v-else-if="provider === 'algolia'">
<VPAlgoliaSearchBox
v-if="loaded"
:algolia="theme.search?.options ?? theme.algolia"
:algolia-options
:open-request
@vue:beforeMount="actuallyLoaded = true"
/>
<div v-if="!actuallyLoaded" id="docsearch">
<VPNavBarSearchButton @click="load" />
</div>
</template>
<template v-else-if="provider === 'local'">
<VPNavBarSearchButton
:text="algoliaOptions.translations?.button?.buttonText || 'Search'"
:aria-label="algoliaOptions.translations?.button?.buttonAriaLabel || 'Search'"
:aria-keyshortcuts="'/ control+k meta+k'"
@click="showSearch = true"
/>
<VPLocalSearchBox
v-if="showSearch"
@close="showSearch = false"
/>
</template>
</div>
</template>
<style>
<style scoped>
.VPNavBarSearch {
display: flex;
align-items: center;
@ -159,6 +202,7 @@ const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
@media (min-width: 768px) {
.VPNavBarSearch {
gap: 8px;
flex-grow: 1;
padding-left: 24px;
}

@ -1,147 +1,67 @@
<script lang="ts" setup>
import type { ButtonTranslations } from '../../../../types/local-search'
import { createSearchTranslate } from '../support/translation'
// button translations
const defaultTranslations: { button: ButtonTranslations } = {
button: {
buttonText: 'Search',
buttonAriaLabel: 'Search'
}
}
const translate = createSearchTranslate(defaultTranslations)
defineProps<{
text: string
}>()
</script>
<template>
<button
type="button"
:aria-label="translate('button.buttonAriaLabel')"
aria-keyshortcuts="/ control+k meta+k"
class="DocSearch DocSearch-Button"
>
<span class="DocSearch-Button-Container">
<span class="vpi-search DocSearch-Search-Icon"></span>
<span class="DocSearch-Button-Placeholder">{{ translate('button.buttonText') }}</span>
</span>
<span class="DocSearch-Button-Keys">
<kbd class="DocSearch-Button-Key"></kbd>
<kbd class="DocSearch-Button-Key"></kbd>
<button type="button" class="VPNavBarSearchButton">
<span class="vpi-search" aria-hidden="true"></span>
<span class="text">{{ text }}</span>
<span class="keys" aria-hidden="true">
<kbd class="key-cmd">&#x2318;</kbd>
<kbd class="key-ctrl">Ctrl</kbd>
<kbd>K</kbd>
</span>
</button>
</template>
<style>
[class*='DocSearch'] {
--docsearch-actions-height: auto;
--docsearch-actions-width: auto;
--docsearch-background-color: var(--vp-c-bg-soft);
--docsearch-container-background: var(--vp-backdrop-bg-color);
--docsearch-focus-color: var(--vp-c-brand-1);
--docsearch-footer-background: var(--vp-c-bg);
--docsearch-highlight-color: var(--vp-c-brand-1);
--docsearch-hit-background: var(--vp-c-default-soft);
--docsearch-hit-color: var(--vp-c-text-1);
--docsearch-hit-highlight-color: var(--vp-c-brand-soft);
--docsearch-icon-color: var(--vp-c-text-2);
--docsearch-key-background: transparent;
--docsearch-key-color: var(--vp-c-text-2);
--docsearch-modal-background: var(--vp-c-bg-soft);
--docsearch-muted-color: var(--vp-c-text-2);
--docsearch-primary-color: var(--vp-c-brand-1);
--docsearch-searchbox-focus-background: transparent;
--docsearch-secondary-text-color: var(--vp-c-text-2);
--docsearch-soft-primary-color: var(--vp-c-brand-soft);
--docsearch-subtle-color: var(--vp-c-divider);
--docsearch-success-color: var(--vp-c-brand-soft);
--docsearch-text-color: var(--vp-c-text-1);
}
.dark [class*='DocSearch'] {
--docsearch-modal-shadow: none;
}
.DocSearch-Clear {
padding: 0 8px;
}
.DocSearch-Commands-Key {
padding: 4px;
border: 1px solid var(--docsearch-subtle-color);
border-radius: 4px;
}
.DocSearch-Hit a:focus-visible {
outline: 2px solid var(--docsearch-focus-color);
}
.DocSearch-Logo [class^='cls-'] {
fill: currentColor;
<style scoped>
.VPNavBarSearchButton {
display: flex;
align-items: center;
gap: 8px;
height: var(--vp-nav-height);
padding: 8px 14px;
font-size: 20px;
}
.DocSearch-SearchBar + .DocSearch-Footer {
border-top-color: transparent;
}
.DocSearch-Title {
font-size: revert;
line-height: revert;
}
.DocSearch-Button {
--docsearch-muted-color: var(--docsearch-text-color);
--docsearch-searchbox-background: transparent;
width: auto;
padding: 2px 12px;
border: none;
border-radius: 8px;
.text,
.keys,
:root.mac .key-ctrl,
:root:not(.mac) .key-cmd {
display: none;
}
.DocSearch-Search-Icon {
color: inherit !important;
width: 20px;
height: 20px;
kbd {
font-family: inherit;
font-weight: 500;
}
@media (min-width: 768px) {
.DocSearch-Button {
--docsearch-muted-color: var(--docsearch-secondary-text-color);
--docsearch-searchbox-background: var(--vp-c-bg-alt);
.VPNavBarSearchButton {
height: auto;
padding: 8px 12px;
background-color: var(--vp-c-bg-alt);
border-radius: 8px;
font-size: 14px;
line-height: 1;
color: var(--vp-c-text-2);
}
.DocSearch-Search-Icon {
width: 15px;
height: 15px;
}
.DocSearch-Button-Placeholder {
.text {
display: inline;
font-size: 13px;
}
}
.DocSearch-Button-Keys {
min-width: auto;
margin: 0;
padding: 4px 6px;
background-color: var(--docsearch-key-background);
border: 1px solid var(--docsearch-subtle-color);
border-radius: 4px;
font-size: 12px;
line-height: 1;
color: var(--docsearch-key-color);
}
.DocSearch-Button-Keys > * {
display: none;
}
.DocSearch-Button-Keys:after {
/*rtl:ignore*/
direction: ltr;
content: 'Ctrl K';
}
.mac .DocSearch-Button-Keys:after {
content: '\2318 K';
.keys {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 6px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 12px;
}
}
</style>

@ -19,7 +19,13 @@ const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
<p class="title">{{ currentLang.label }}</p>
<template v-for="locale in localeLinks" :key="locale.link">
<VPMenuLink :item="locale" :lang="locale.lang" :dir="locale.dir" />
<VPMenuLink
:item="locale"
:lang="locale.lang"
:hreflang="locale.lang"
rel="alternate"
:dir="locale.dir"
/>
</template>
</div>
</VPFlyout>

@ -1,7 +1,6 @@
<script setup lang="ts">
import { useScrollLock } from '@vueuse/core'
import { inBrowser } from 'vitepress'
import { ref } from 'vue'
import VPNavScreenAppearance from './VPNavScreenAppearance.vue'
import VPNavScreenMenu from './VPNavScreenMenu.vue'
import VPNavScreenSocialLinks from './VPNavScreenSocialLinks.vue'
@ -11,7 +10,6 @@ defineProps<{
open: boolean
}>()
const screen = ref<HTMLElement | null>(null)
const isLocked = useScrollLock(inBrowser ? document.body : null)
</script>
@ -21,7 +19,7 @@ const isLocked = useScrollLock(inBrowser ? document.body : null)
@enter="isLocked = true"
@after-leave="isLocked = false"
>
<div v-if="open" class="VPNavScreen" ref="screen" id="VPNavScreen">
<div v-if="open" class="VPNavScreen" id="VPNavScreen">
<div class="container">
<slot name="nav-screen-content-before" />
<VPNavScreenMenu class="menu" />

@ -44,6 +44,8 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-autospace: normal;
text-spacing-trim: normal;
}
main {

@ -0,0 +1,120 @@
@import '@docsearch/css/dist/style.css';
@import '@docsearch/css/dist/sidepanel.css';
#vp-docsearch,
#vp-docsearch-sidepanel,
.DocSearch-SidepanelButton {
display: none;
}
:root:root {
--docsearch-actions-height: auto;
--docsearch-actions-width: auto;
--docsearch-background-color: var(--vp-c-bg-soft);
--docsearch-container-background: var(--vp-backdrop-bg-color);
--docsearch-dropdown-menu-background: var(--vp-c-bg-elv);
--docsearch-dropdown-menu-item-hover-background: var(--vp-c-default-soft);
--docsearch-focus-color: var(--vp-c-brand-1);
--docsearch-footer-background: var(--vp-c-bg-alt);
--docsearch-highlight-color: var(--vp-c-brand-1);
--docsearch-hit-background: var(--vp-c-bg);
--docsearch-hit-color: var(--vp-c-text-1);
--docsearch-hit-highlight-color: var(--vp-c-brand-soft);
--docsearch-icon-color: var(--vp-c-text-2);
--docsearch-key-background: var(--vp-code-bg);
--docsearch-modal-background: var(--vp-c-bg-soft);
--docsearch-muted-color: var(--vp-c-text-2);
--docsearch-primary-color: var(--vp-c-brand-1);
--docsearch-searchbox-background: var(--vp-c-bg-alt);
--docsearch-searchbox-focus-background: transparent;
--docsearch-secondary-text-color: var(--vp-c-text-2);
--docsearch-sidepanel-accent-muted: var(--vp-c-text-3);
--docsearch-sidepanel-text-base: var(--vp-c-text-1);
--docsearch-soft-muted-color: var(--vp-c-default-soft);
--docsearch-soft-primary-color: var(--vp-c-brand-soft);
--docsearch-subtle-color: var(--vp-c-divider);
--docsearch-success-color: var(--vp-c-brand-soft);
--docsearch-text-color: var(--vp-c-text-1);
}
:root.dark {
--docsearch-modal-shadow: none;
}
.DocSearch-AskAiScreen-RelatedSources-Item-Link {
padding: 8px 12px 8px 10px;
}
.DocSearch-AskAiScreen-RelatedSources-Item-Link svg {
width: 16px;
height: 16px;
}
.DocSearch-AskAiScreen-RelatedSources-Title {
padding-bottom: 0;
font-size: 12px;
}
.DocSearch-Clear {
padding-right: 6px;
}
.DocSearch-Commands-Key {
padding: 4px;
border: 1px solid var(--docsearch-subtle-color);
border-radius: 4px;
}
.DocSearch-Hit a:focus-visible {
outline: 2px solid var(--docsearch-focus-color);
}
.DocSearch-Logo [class^='cls-'] {
fill: currentColor;
}
.DocSearch-Markdown-Content code {
padding: 0.2em 0.4em;
}
.DocSearch-Menu-content {
margin-top: -4px;
padding: 6px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
box-shadow: var(--vp-shadow-2);
}
.DocSearch-Menu-item {
border-radius: 4px;
}
.DocSearch-SearchBar + .DocSearch-Footer {
border-top-color: transparent;
}
.DocSearch-Sidepanel-Prompt--form {
border-color: var(--docsearch-subtle-color);
transition: border-color 0.2s;
}
.DocSearch-Sidepanel-Prompt--submit {
background-color: var(--docsearch-soft-primary-color);
color: var(--docsearch-primary-color);
}
.DocSearch-Sidepanel-Prompt--submit:hover {
background-color: var(--vp-button-brand-hover-bg);
color: var(--vp-button-brand-text);
}
.DocSearch-Sidepanel-Prompt--submit:disabled,
.DocSearch-Sidepanel-Prompt--submit[aria-disabled='true'] {
background-color: var(--docsearch-soft-muted-color);
color: var(--docsearch-muted-color);
}
.DocSearch-Title {
font-size: revert;
line-height: revert;
}

@ -9,7 +9,7 @@
font-display: swap;
src: url('../fonts/inter-roman-cyrillic-ext.woff2') format('woff2');
unicode-range:
U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
@ -59,8 +59,9 @@
font-display: swap;
src: url('../fonts/inter-roman-latin-ext.woff2') format('woff2');
unicode-range:
U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
@ -71,8 +72,8 @@
src: url('../fonts/inter-roman-latin.woff2') format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
U+2215, U+FEFF, U+FFFD;
}
@font-face {
@ -82,7 +83,7 @@
font-display: swap;
src: url('../fonts/inter-italic-cyrillic-ext.woff2') format('woff2');
unicode-range:
U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
@ -132,8 +133,9 @@
font-display: swap;
src: url('../fonts/inter-italic-latin-ext.woff2') format('woff2');
unicode-range:
U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
@ -144,8 +146,8 @@
src: url('../fonts/inter-italic-latin.woff2') format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
U+2215, U+FEFF, U+FFFD;
}
@font-face {
@ -192,4 +194,4 @@
U+007E, U+002F;
}
/* Generate the subsetted fonts using: `pyftsubset <file>.woff2 --unicodes="<range>" --output-file="inter-<style>-<subset>.woff2" --flavor=woff2` */
/* Generate the subsetted fonts using: `pyftsubset <file>.woff2 --unicodes="<range>" --layout-features+=pnum,tnum --flavor=woff2 --output-file="inter-<style>-<subset>.woff2"` */

@ -74,7 +74,10 @@
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2c-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E");
}
.vpi-search {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.6'%3E%3Cpath d='m21 21l-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8' stroke-width='1.4'/%3E%3C/g%3E%3C/svg%3E");
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='m21 21l-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/g%3E%3C/svg%3E");
}
.vpi-sparkles {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.8'%3E%3Cpath d='M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594zM20 2v4m2-2h-4'/%3E%3Ccircle cx='4' cy='20' r='2'/%3E%3C/g%3E%3C/svg%3E");
}
.vpi-layout-list {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7m-7 5h7m-7 6h7m-7 5h7'/%3E%3C/g%3E%3C/svg%3E");

@ -385,6 +385,9 @@
:lang(zh) {
--vp-code-copy-copied-text-content: '已复制';
}
:lang(ja) {
--vp-code-copy-copied-text-content: 'コピー完了';
}
/**
* Component: Button

@ -0,0 +1,253 @@
import type { DefaultTheme } from 'vitepress/theme'
import type { DocSearchAskAi } from '../../../../types/docsearch'
import { isObject } from '../../shared'
export type FacetFilter = string | string[] | FacetFilter[]
export interface ValidatedCredentials {
valid: boolean
appId?: string
apiKey?: string
indexName?: string
}
export type DocSearchMode = 'auto' | 'sidePanel' | 'hybrid' | 'modal'
export interface ResolvedMode {
mode: DocSearchMode
showKeywordSearch: boolean
useSidePanel: boolean
}
/**
* Resolves the effective mode based on config and available features.
*
* - 'auto': infer hybrid vs sidePanel-only from provided config
* - 'sidePanel': force sidePanel-only even if keyword search is configured
* - 'hybrid': force hybrid (error if keyword search is not configured)
* - 'modal': force modal even if sidePanel is configured
*/
export function resolveMode(
options: Pick<
DefaultTheme.AlgoliaSearchOptions,
'appId' | 'apiKey' | 'indexName' | 'askAi' | 'mode'
>
): ResolvedMode {
const mode = options.mode ?? 'auto'
const hasKeyword = hasKeywordSearch(options)
const askAi = options.askAi
const hasSidePanelConfig = Boolean(
askAi && typeof askAi === 'object' && askAi.sidePanel
)
switch (mode) {
case 'sidePanel':
// Force sidePanel-only - hide keyword search
return {
mode,
showKeywordSearch: false,
useSidePanel: true
}
case 'hybrid':
// Force hybrid - keyword search must be configured
if (!hasKeyword) {
console.error(
'[vitepress] mode: "hybrid" requires keyword search credentials (appId, apiKey, indexName).'
)
}
return {
mode,
showKeywordSearch: hasKeyword,
useSidePanel: true
}
case 'modal':
// Force modal - don't use sidepanel for askai, even if configured
return {
mode,
showKeywordSearch: hasKeyword,
useSidePanel: false
}
case 'auto':
default:
// Auto-detect based on config
return {
mode: 'auto',
showKeywordSearch: hasKeyword,
useSidePanel: hasSidePanelConfig
}
}
}
export function hasKeywordSearch(
options: Pick<
DefaultTheme.AlgoliaSearchOptions,
'appId' | 'apiKey' | 'indexName'
>
): boolean {
return Boolean(options.appId && options.apiKey && options.indexName)
}
export function hasAskAi(
askAi: DefaultTheme.AlgoliaSearchOptions['askAi']
): boolean {
if (!askAi) return false
if (typeof askAi === 'string') return askAi.length > 0
return Boolean(askAi.assistantId)
}
/**
* Removes existing `lang:` filters and appends `lang:${lang}`.
* Handles both flat arrays and nested arrays (for OR conditions).
*/
export function mergeLangFacetFilters(
rawFacetFilters: FacetFilter | FacetFilter[] | undefined,
lang: string
): FacetFilter[] {
const input = Array.isArray(rawFacetFilters)
? rawFacetFilters
: rawFacetFilters
? [rawFacetFilters]
: []
const filtered = input
.map((filter) => {
if (Array.isArray(filter)) {
// Handle nested arrays (OR conditions)
return filter.filter(
(f) => typeof f === 'string' && !f.startsWith('lang:')
)
}
return filter
})
.filter((filter) => {
if (typeof filter === 'string') {
return !filter.startsWith('lang:')
}
// Keep nested arrays with remaining filters
return Array.isArray(filter) && filter.length > 0
})
return [...filtered, `lang:${lang}`]
}
/**
* Validates that required Algolia credentials are present.
*/
export function validateCredentials(
options: Pick<
DefaultTheme.AlgoliaSearchOptions,
'appId' | 'apiKey' | 'indexName'
>
): ValidatedCredentials {
const appId = options.appId
const apiKey = options.apiKey
const indexName = options.indexName
return {
valid: Boolean(appId && apiKey && indexName),
appId,
apiKey,
indexName
}
}
/**
* Builds Ask AI configuration from various input formats.
*/
export function buildAskAiConfig(
askAiProp: NonNullable<DefaultTheme.AlgoliaSearchOptions['askAi']>,
options: DefaultTheme.AlgoliaSearchOptions,
lang: string
): DocSearchAskAi {
const isAskAiString = typeof askAiProp === 'string'
const askAiSearchParameters =
!isAskAiString && askAiProp.searchParameters
? { ...askAiProp.searchParameters }
: undefined
// If Ask AI defines its own facetFilters, merge lang filtering into those.
// Otherwise, reuse the keyword search facetFilters so Ask AI follows the
// same language filtering behavior by default.
const askAiFacetFiltersSource =
askAiSearchParameters?.facetFilters ??
options.searchParameters?.facetFilters
const askAiFacetFilters = mergeLangFacetFilters(
askAiFacetFiltersSource as FacetFilter | FacetFilter[] | undefined,
lang
)
const mergedAskAiSearchParameters = {
...askAiSearchParameters,
facetFilters: askAiFacetFilters.length ? askAiFacetFilters : undefined
}
const result: Record<string, any> = {
...(isAskAiString ? {} : askAiProp),
indexName: isAskAiString ? options.indexName : askAiProp.indexName,
apiKey: isAskAiString ? options.apiKey : askAiProp.apiKey,
appId: isAskAiString ? options.appId : askAiProp.appId,
assistantId: isAskAiString ? askAiProp : askAiProp.assistantId
}
// Keep `searchParameters` undefined unless it has at least one key.
if (Object.values(mergedAskAiSearchParameters).some((v) => v != null)) {
result.searchParameters = mergedAskAiSearchParameters
}
return result
}
/**
* Resolves Algolia search options for the given language,
* merging in locale-specific overrides and language facet filters.
*/
export function resolveOptionsForLanguage(
options: DefaultTheme.AlgoliaSearchOptions,
localeIndex: string,
lang: string
): DefaultTheme.AlgoliaSearchOptions {
options = deepMerge(options, options.locales?.[localeIndex] || {})
const facetFilters = mergeLangFacetFilters(
options.searchParameters?.facetFilters,
lang
)
const askAi = options.askAi
? buildAskAiConfig(options.askAi, options, lang)
: undefined
return {
...options,
searchParameters: { ...options.searchParameters, facetFilters },
askAi
}
}
function deepMerge<T>(target: T, source: Partial<T>): T {
const result = { ...target } as any
for (const key in source) {
const value = source[key]
if (value === undefined) continue
// special case: replace entirely
if (key === 'searchParameters') {
result[key] = value
continue
}
// deep-merge only plain objects; arrays are replaced entirely
if (isObject(value) && isObject(result[key])) {
result[key] = deepMerge(result[key], value)
} else {
result[key] = value
}
}
delete result.locales
return result
}

@ -0,0 +1,14 @@
import { type ComputedRef, computed } from 'vue'
export function smartComputed<T>(
getter: () => T,
comparator = (oldValue: T, newValue: T) =>
JSON.stringify(oldValue) === JSON.stringify(newValue)
): ComputedRef<T> {
return computed((oldValue) => {
const newValue = getter()
return oldValue === undefined || !comparator(oldValue, newValue)
? newValue
: oldValue
})
}

@ -6,7 +6,6 @@ import path from 'node:path'
import { pathToFileURL } from 'node:url'
import pMap from 'p-map'
import { packageDirectorySync } from 'package-directory'
import { rimraf } from 'rimraf'
import * as vite from 'vite'
import type { BuildOptions, Rollup } from 'vite'
import { resolveConfig, type SiteConfig } from '../config'
@ -176,7 +175,13 @@ export async function build(
)
} finally {
unlinkVue()
if (!process.env.DEBUG) await rimraf(siteConfig.tempDir)
if (!process.env.DEBUG) {
fs.rmSync(siteConfig.tempDir, {
recursive: true,
force: true,
maxRetries: 10
})
}
}
await generateSitemap(siteConfig)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save