feat(theme): add plugin-llms and <PageContextMenu /> component (#753)

This commit is contained in:
pengzhanbo 2025-11-19 16:51:49 +08:00 committed by GitHub
parent 5b780c28d0
commit 20728f504d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 844 additions and 160 deletions

View File

@ -3,6 +3,7 @@ import { defineMermaidConfig } from '@vuepress/plugin-markdown-chart/client'
import { defineAsyncComponent, h } from 'vue'
import { Layout } from 'vuepress-theme-plume/client'
import VPPostItem from 'vuepress-theme-plume/components/Posts/VPPostItem.vue'
import PageContextMenu from 'vuepress-theme-plume/features/PageContextMenu.vue'
import { defineClientConfig } from 'vuepress/client'
import AsideNav from '~/components/AsideNav.vue'
import { setupThemeColors } from '~/composables/theme-colors.js'
@ -25,6 +26,7 @@ export default defineClientConfig({
layouts: {
Layout: h(Layout, null, {
'aside-outline-after': () => h(AsideNav),
'doc-title-after': () => h(PageContextMenu),
}),
},
}) as ClientConfig

View File

@ -40,6 +40,7 @@ export const themeConfig: ThemeCollectionItem = defineCollection({
'shiki',
'search',
'reading-time',
'llms',
'markdown-enhance',
'markdown-power',
'markdown-image',

View File

@ -40,6 +40,7 @@ export const themeConfig: ThemeCollectionItem = defineCollection({
'shiki',
'search',
'reading-time',
'llms',
'markdown-enhance',
'markdown-power',
'markdown-image',

View File

@ -3,9 +3,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { viteBundler } from '@vuepress/bundler-vite'
import { addViteOptimizeDepsInclude, addViteSsrExternal } from '@vuepress/helper'
import { llmsPlugin } from '@vuepress/plugin-llms'
import { defineUserConfig } from 'vuepress'
import { tocGetter } from './llmstxtTOC.js'
import { theme } from './theme.js'
const pnpmWorkspace = fs.readFileSync(path.resolve(__dirname, '../../pnpm-workspace.yaml'), 'utf-8')
@ -47,21 +45,6 @@ export default defineUserConfig({
'~/composables': path.resolve(__dirname, './themes/composables'),
},
plugins: [
llmsPlugin({
llmsTxtTemplateGetter: {
description: (_, { currentLocale }) => {
return currentLocale === '/'
? '一个简约易用的,功能丰富的 vuepress 文档&博客 主题'
: 'An easy-to-use and feature-rich vuepress documentation and blog theme'
},
details: '',
toc: tocGetter,
},
locale: 'all',
}),
],
bundler: viteBundler(),
shouldPrefetch: false,

View File

@ -1,131 +0,0 @@
import type { LLMPage, LLMState } from '@vuepress/plugin-llms'
import type { ThemeSidebarItem } from 'vuepress-theme-plume'
import { generateTOCLink as rawGenerateTOCLink } from '@vuepress/plugin-llms'
import { ensureEndingSlash, ensureLeadingSlash } from 'vuepress/shared'
import { path } from 'vuepress/utils'
import { enCollections, zhCollections } from './collections/index.js'
function normalizePath(prefix: string, path = ''): string {
if (path.startsWith('/'))
return path
return `${ensureEndingSlash(prefix)}${path}`
}
function withBase(url = '', base = '/'): string {
if (!url)
return ''
if (url.startsWith(base))
return normalizePath(url)
return path.join(base, url)
}
function genStarsWith(stars: string | undefined, locale: string) {
return (url: string): boolean => {
if (!stars)
return false
return url.startsWith(withBase(stars, locale))
}
}
export function tocGetter(llmPages: LLMPage[], llmState: LLMState): string {
const { currentLocale } = llmState
const isZh = currentLocale === '/'
const collections = isZh ? zhCollections : enCollections
let tableOfContent = ''
const usagePages: LLMPage[] = []
collections
.filter(item => item.type === 'post')
.forEach(({ title, linkPrefix, link }) => {
tableOfContent += `### ${title}\n\n`
const withLinkPrefix = genStarsWith(linkPrefix, currentLocale)
const withLink = genStarsWith(link, currentLocale)
const withFallback = genStarsWith('/article/', currentLocale)
const list: string[] = []
llmPages.forEach((page) => {
if (withLinkPrefix(page.path) || withLink(page.path) || withFallback(page.path)) {
usagePages.push(page)
list.push(rawGenerateTOCLink(page, llmState))
}
})
tableOfContent += `${list.filter(Boolean).join('')}\n`
})
const generateTOCLink = (path: string): string => {
const filepath = path.endsWith('/') ? `${path}README.md` : path.endsWith('.md') ? path : `${path || 'README'}.md`
const link = path.endsWith('/') ? `${path}index.html` : `${path}.html`
const page = llmPages.find((item) => {
return ensureLeadingSlash(item.filePathRelative || '') === filepath || link === item.path
})
if (page) {
usagePages.push(page)
return rawGenerateTOCLink(page, llmState)
}
return ''
}
const processAutoSidebar = (prefix: string): string[] => {
const list: string[] = []
llmPages.forEach((page) => {
if (ensureLeadingSlash(page.filePathRelative || '').startsWith(prefix)) {
usagePages.push(page)
list.push(rawGenerateTOCLink(page, llmState))
}
})
return list.filter(Boolean)
}
const processSidebar = (items: (string | ThemeSidebarItem)[], prefix: string): string[] => {
const result: string[] = []
items.forEach((item) => {
if (typeof item === 'string') {
result.push(generateTOCLink(normalizePath(prefix, item)))
}
else {
if (item.link) {
result.push(generateTOCLink(normalizePath(prefix, item.link)))
}
if (item.items === 'auto') {
result.push(...processAutoSidebar(normalizePath(prefix, item.prefix)))
}
else if (item.items?.length) {
result.push(...processSidebar(item.items, normalizePath(prefix, item.prefix)))
}
}
})
return result
}
// Collections
collections
.filter(collection => collection.type === 'doc')
.forEach(({ dir, title, sidebar = [] }) => {
tableOfContent += `### ${title}\n\n`
const prefix = normalizePath(ensureLeadingSlash(withBase(dir, currentLocale)))
if (sidebar === 'auto') {
tableOfContent += `${processAutoSidebar(prefix).join('')}\n`
}
else if (sidebar.length) {
const home = generateTOCLink(ensureEndingSlash(prefix))
const list = processSidebar(sidebar, prefix)
if (home && !list.includes(home)) {
list.unshift(home)
}
tableOfContent += `${list.join('')}\n`
}
})
// Others
const unUsagePages = llmPages.filter(page => !usagePages.includes(page))
if (unUsagePages.length) {
tableOfContent += '### Others\n\n'
tableOfContent += unUsagePages
.map(page => rawGenerateTOCLink(page, llmState))
.join('')
}
return tableOfContent
}

View File

@ -76,4 +76,16 @@ export const theme: Theme = plumeTheme({
content: 'vuepress-theme-plume',
},
},
llmstxt: {
locale: 'all',
llmsTxtTemplateGetter: {
description: (_, { currentLocale }) => {
return currentLocale === '/'
? '一个简约易用的,功能丰富的 vuepress 文档&博客 主题'
: 'An easy-to-use and feature-rich vuepress documentation and blog theme'
},
details: '',
},
},
})

104
docs/config/plugins/llms.md Normal file
View File

@ -0,0 +1,104 @@
---
title: LLMs txt
createTime: 2025/11/19 14:48:35
permalink: /config/plugins/llmstxt/
---
## 概述
为站点添加 [llms.txt](https://llmstxt.org/),以提供对 LLM 友好的内容。
**关联插件** [@vuepress/plugin-llms](https://ecosystem.vuejs.press/zh/plugins/ai/llms.html)
## 为什么需要 llms.txt
大型语言模型越来越依赖网站信息,但面临一个关键限制:上下文窗口太小,无法完整处理大多数网站。将包含导航、广告和 JavaScript 的复杂 HTML 页面转换为适合 LLM 的纯文本既困难又不精确。
虽然网站同时为人类读者和 LLM 服务,但后者受益于在一个可访问的位置收集的更简洁、专家级的信息。这对于开发环境等使用案例尤其重要,因为 LLM 需要快速访问编程文档和 API。
向网站添加 `/llms.txt` Markdown 文件,以提供对 LLM 友好的内容。此文件提供了简短的背景信息、指南和指向详细 Markdown 文件的链接。
## 功能
插件通过检索你的文档源目录中的所有 Markdown 文件,并将其转换为 LLM 友好的纯文本文件。
::: file-tree
- .vuepress/dist
- llms.txt
- llms-full.txt
- markdown-examples.html
- markdown-examples.md
- …
:::
点击以下链接查看本文档站点的 llms.txt 文件:
- [llms.txt](/llms.txt){.no-icon}
- [llms-full.txt](/llms-full.txt){.no-icon}
::: tip
插件仅在生产构建时,即执行 `vuepress build` 命令时,生成 `llms.txt` 文件,以及其它 LLM 友好的文档文件,并将它们输出到 `.vuepress/dist` 目录中。
:::
[完整功能说明请查看 **插件官方文档**](https://ecosystem.vuejs.press/zh/plugins/ai/llms.html#%E6%8F%92%E4%BB%B6%E5%8A%9F%E8%83%BD){.read-more}
## 配置
主题默认不启用此功能,你可以通过 `llmstxt` 配置项启用它:
```ts title=".vuepress/config.ts"
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
// 使用主题内置的默认配置
// llmstxt: true,
// 使用自定义配置
llmstxt: {
locale: '/',
// ...其它配置
},
// 也可以在 `plugins.llmstxt` 配置,但不推荐
plugins: {
llmstxt: true
}
}),
})
```
[完整配置项说明请查看 **插件官方文档**](https://ecosystem.vuejs.press/zh/plugins/ai/llms.html#options){.read-more}
## 组件
为进一步增强 文档站点 与 LLMs 的互动,你可以在文档站点中添加 `<PageContextMenu />` 组件。
该组件不作为内置组件,而是主题额外的 `features` 实现,因此你需要手动引入它,
并在合适的位置,通过 [组件插槽](../../guide/custom/slots.md) 添加到文档站点中:
```ts title=".vuepress/client.ts"
import { defineAsyncComponent, h } from 'vue'
import { Layout } from 'vuepress-theme-plume/client'
import PageContextMenu from 'vuepress-theme-plume/features/PageContextMenu.vue' // [!code ++]
import { defineClientConfig } from 'vuepress/client'
export default defineClientConfig({
layouts: {
Layout: h(Layout, null, {
// 将 PageContextMenu 添加到 doc-title-after 插槽,即文章标题的右侧
'doc-title-after': () => h(PageContextMenu), // [!code ++]
}),
},
})
```
你可以在当前页面的标题的右侧,体验该组件的功能。
::: important
此组件完全依赖于 `@vuepress/plugin-llms` 插件,仅当你启用了此插件功能后,才能使用它。
因此,此组件提供的功能 **仅在构建后的生产包中才可用**
:::

View File

@ -0,0 +1,112 @@
---
title: LLMs txt
createTime: 2025/11/19 14:48:35
permalink: /en/config/plugins/llmstxt/
---
## Overview
Add [llms.txt](https://llmstxt.org/) to your site to provide LLM-friendly content.
**Related Plugin**: [@vuepress/plugin-llms](https://ecosystem.vuejs.press/plugins/ai/llms.html)
## Why llms.txt?
Large Language Models increasingly rely on website information but face a key limitation:
their context window is too small to fully process most websites.
Converting complex HTML pages containing navigation, ads, and JavaScript into LLM-friendly plain text is both difficult and imprecise.
While websites serve both human readers and LLMs, the latter benefit from more concise,
expert-level information collected in one accessible location.
This is particularly important for use cases like development environments where LLMs need quick access to programming documentation and APIs.
Add a `/llms.txt` Markdown file to your website to provide LLM-friendly content.
This file provides brief background information, guidelines, and links to detailed Markdown files.
## Features
The plugin retrieves all Markdown files from your documentation source directory and converts them into LLM-friendly plain text files.
::: file-tree
- .vuepress/dist
- llms.txt
- llms-full.txt
- markdown-examples.html
- markdown-examples.md
- …
:::
Click the links below to view the llms.txt files for this documentation site:
- [llms.txt](/llms.txt){.no-icon}
- [llms-full.txt](/llms-full.txt){.no-icon}
::: tip
The plugin only generates `llms.txt` files and other LLM-friendly documentation files during
production builds, i.e., when executing the `vuepress build` command, and outputs them to the `.vuepress/dist` directory.
:::
[View the complete feature description in the **Plugin Official Documentation**](https://ecosystem.vuejs.press/plugins/ai/llms.html#%E6%8F%92%E4%BB%B6%E5%8A%9F%E8%83%BD){.read-more}
## Configuration
This feature is not enabled by default in the theme. You can enable it through the `llmstxt` configuration option:
```ts title=".vuepress/config.ts"
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
// Use the theme's built-in default configuration
// llmstxt: true,
// Use custom configuration
llmstxt: {
locale: '/',
// ...other configurations
},
// Can also configure via `plugins.llmstxt`, but not recommended
plugins: {
llmstxt: true
}
}),
})
```
[View the complete configuration options in the **Plugin Official Documentation**](https://ecosystem.vuejs.press/plugins/ai/llms.html#options){.read-more}
## Components
To further enhance interaction between your documentation site and LLMs,
you can add the `<PageContextMenu />` component to your documentation site.
This component is not built-in but is implemented as an additional feature of the theme.
Therefore, you need to manually import it and place it in an appropriate location through [component slots](../../guide/custom/slots.md):
```ts title=".vuepress/client.ts"
import { defineAsyncComponent, h } from 'vue'
import { Layout } from 'vuepress-theme-plume/client'
import PageContextMenu from 'vuepress-theme-plume/features/PageContextMenu.vue' // [!code ++]
import { defineClientConfig } from 'vuepress/client'
export default defineClientConfig({
layouts: {
Layout: h(Layout, null, {
// Add PageContextMenu to the doc-title-after slot, i.e., to the right of the article title
'doc-title-after': () => h(PageContextMenu), // [!code ++]
}),
},
})
```
You can experience this component's functionality to the right of the current page's title.
::: important
This component relies entirely on the `@vuepress/plugin-llms` plugin and can only be used when you have enabled this plugin's functionality.
Therefore, the functionality provided by this component **is only available in the built production package**.
:::

View File

@ -106,6 +106,8 @@ You can preview <https://plume-layout-slots.netlify.app> to see the positions of
- `doc-footer-before`
- `doc-before`
- `doc-after`
- `doc-title-before`
- `doc-title-after`
- `doc-meta-top`
- `doc-meta-bottom`
- `doc-meta-before`

View File

@ -104,6 +104,8 @@ export default defineClientConfig({
- `doc-footer-before`
- `doc-before`
- `doc-after`
- `doc-title-before`
- `doc-title-after`
- `doc-meta-top`
- `doc-meta-bottom`
- `doc-meta-before`

View File

@ -19,7 +19,6 @@
"@lunariajs/core": "catalog:dev",
"@simonwep/pickr": "catalog:dev",
"@vuepress/bundler-vite": "catalog:vuepress",
"@vuepress/plugin-llms": "catalog:vuepress",
"@vuepress/shiki-twoslash": "catalog:vuepress",
"chart.js": "catalog:prod",
"echarts": "catalog:prod",

View File

@ -30,6 +30,8 @@ export default defineClientConfig({
'doc-footer-before': () => h(SlotDemo, { name: 'doc-footer-before' }),
'doc-before': () => h(SlotDemo, { name: 'doc-before', mt: 16 }),
'doc-after': () => h(SlotDemo, { name: 'doc-after' }),
'doc-title-before': () => h(SlotDemo, { name: 'doc-title-before', h: 24 }),
'doc-title-after': () => h(SlotDemo, { name: 'doc-title-after', h: 24 }),
'doc-meta-before': () => h(SlotDemo, { name: 'doc-meta-before', h: 24 }),
'doc-meta-after': () => h(SlotDemo, { name: 'doc-meta-after', h: 24 }),
'doc-meta-top': () => h(SlotDemo, { name: 'doc-meta-top' }),

6
pnpm-lock.yaml generated
View File

@ -551,9 +551,6 @@ importers:
'@vuepress/bundler-vite':
specifier: catalog:vuepress
version: 2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1)
'@vuepress/plugin-llms':
specifier: catalog:vuepress
version: 2.0.0-rc.118(typescript@5.9.3)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
'@vuepress/shiki-twoslash':
specifier: catalog:vuepress
version: 2.0.0-rc.118(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
@ -817,6 +814,9 @@ importers:
'@vuepress/plugin-git':
specifier: catalog:vuepress
version: 2.0.0-rc.118(typescript@5.9.3)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
'@vuepress/plugin-llms':
specifier: catalog:vuepress
version: 2.0.0-rc.118(typescript@5.9.3)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
'@vuepress/plugin-markdown-chart':
specifier: catalog:vuepress
version: 2.0.0-rc.118(chart.js@4.5.1)(echarts@6.0.0)(flowchart.ts@3.0.1)(markdown-it@14.1.0)(markmap-lib@0.18.12(markmap-common@0.18.9))(markmap-toolbar@0.18.12(markmap-common@0.18.9))(markmap-view@0.18.12(markmap-common@0.18.9))(mermaid@11.12.1)(typescript@5.9.3)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))

View File

@ -113,6 +113,7 @@
"@vuepress/plugin-copy-code": "catalog:vuepress",
"@vuepress/plugin-docsearch": "catalog:vuepress",
"@vuepress/plugin-git": "catalog:vuepress",
"@vuepress/plugin-llms": "catalog:vuepress",
"@vuepress/plugin-markdown-chart": "catalog:vuepress",
"@vuepress/plugin-markdown-hint": "catalog:vuepress",
"@vuepress/plugin-markdown-image": "catalog:vuepress",

View File

@ -147,6 +147,12 @@ watch(
<slot name="doc-bottom" />
</template>
<template #doc-title-before>
<slot name="doc-title-before" />
</template>
<template #doc-title-after>
<slot name="doc-title-after" />
</template>
<template #doc-meta-before>
<slot name="doc-meta-before" />
</template>

View File

@ -122,6 +122,12 @@ watch(
<slot name="doc-meta-top" />
<VPDocMeta>
<template #doc-title-before>
<slot name="doc-title-before" />
</template>
<template #doc-title-after>
<slot name="doc-title-after" />
</template>
<template #doc-meta-before>
<slot name="doc-meta-before" />
</template>

View File

@ -53,11 +53,15 @@ const hasMeta = computed(() =>
</script>
<template>
<h1 class="vp-doc-title page-title" :class="{ padding: !hasMeta }">
<div class="vp-doc-title">
<slot name="doc-title-before" />
<h1 class="page-title" :class="{ padding: !hasMeta }">
<VPBadge v-if="page.frontmatter.draft" type="warning" text="DRAFT" />
{{ page.title }}
<VPBadge v-if="badge" :type="badge.type || 'tip'" :text="badge.text" />
</h1>
<slot name="doc-title-after" />
</div>
<div v-if="hasMeta" class="vp-doc-meta">
<slot name="doc-meta-before" />
@ -90,7 +94,18 @@ const hasMeta = computed(() =>
</template>
<style scoped>
@media (min-width: 768px) {
.vp-doc-title {
display: flex;
gap: 16px;
align-items: center;
justify-content: flex-start;
}
}
.page-title {
flex: 1;
min-width: 0;
margin-bottom: 0.7rem;
font-size: 28px;
font-weight: 600;
@ -99,7 +114,7 @@ const hasMeta = computed(() =>
transition: color var(--vp-t-color);
}
.vp-doc-title.padding {
.page-title.padding {
padding-bottom: 4rem;
}

View File

@ -0,0 +1,306 @@
<script setup lang="ts">
import { onClickOutside, useClipboard, useToggle } from '@vueuse/core'
import { computed, onMounted, ref, useTemplateRef } from 'vue'
import { withBase } from 'vuepress/client'
import { ensureEndingSlash } from 'vuepress/shared'
import { useData } from '../../composables/index.js'
import '@vuepress/helper/transition/fade-in.css'
interface MenuItem {
link: string
text: string
tagline: string
icon: string
}
const { claude = true, chatgpt = true } = defineProps<{
claude?: boolean
chatgpt?: boolean
}>()
const { page, frontmatter, theme } = useData()
const markdownLink = computed(() => {
const url = withBase(page.value.path)
if (url.endsWith('.html'))
return `${url.slice(0, -5)}.md`
return `${ensureEndingSlash(url)}index.md`
})
const message = computed(() => {
if (__VUEPRESS_SSR__) {
return ''
}
return encodeURIComponent(
(theme.value.askAIMessage ?? 'Read {link} and answer content-related questions.')
.replace('{link}', location.origin + markdownLink.value),
)
})
const menuList = computed(() => {
const list: MenuItem[] = []
list.push({
link: markdownLink.value,
text: theme.value.viewMarkdown ?? 'View as Markdown',
tagline: theme.value.viewMarkdownTagline ?? 'View this page as plain text',
icon: 'vpi-markdown',
})
if (chatgpt) {
list.push({
link: `https://chat.openai.com/?prompt=${message.value}`,
text: (theme.value.askAIText ?? 'Open in {name}').replace('{name}', 'ChatGPT'),
tagline: (theme.value.askAITagline ?? 'Ask {name} about this page').replace('{name}', 'ChatGPT'),
icon: 'vpi-chatgpt',
})
}
if (claude) {
list.push({
link: `https://claude.ai/new?q=${message.value}`,
text: (theme.value.askAIText ?? 'Open in {name}').replace('{name}', 'Claude'),
tagline: (theme.value.askAITagline ?? 'Ask {name} about this page').replace('{name}', 'Claude'),
icon: 'vpi-claude',
})
}
return list
})
const markdownContent = ref('')
const loaded = ref(true)
const { copy, copied } = useClipboard()
async function onCopy() {
if (!markdownContent.value) {
loaded.value = false
await fetchMarkdownContent()
loaded.value = true
}
markdownContent.value && copy(markdownContent.value)
}
let promise: Promise<void> | null = null
async function fetchMarkdownContent() {
if (promise)
return
promise = fetch(location.origin + markdownLink.value)
.then(res => res.text())
.then((text) => {
markdownContent.value = text.trimStart().replace(/^---[\s\S]+?---/, '').trimStart()
})
.finally(() => {
promise = null
})
await promise
}
onMounted(() => {
const idl = window.requestIdleCallback || window.requestAnimationFrame || (cb => setTimeout(cb, 0))
idl(fetchMarkdownContent)
})
const menuRef = useTemplateRef('menu')
const toggleRef = useTemplateRef('toggle')
const [open, toggleMenu] = useToggle(false)
onClickOutside(menuRef, () => toggleMenu(false), { ignore: [toggleRef] })
const copyPageText = computed(() => {
const copyText = theme.value.copyPageText ?? 'Copy page'
const copiedText = theme.value.copiedPageText ?? 'Copied !'
const copyingText = theme.value.copingPageText ?? 'Copying..'
return copied.value ? copiedText : !loaded.value ? copyingText : copyText
})
</script>
<template>
<div v-if="frontmatter.llmstxt !== false" class="vp-page-context-menu">
<div class="page-context-button" type="button">
<span class="page-context-copy" @click="onCopy">
<span class="vpi-copy" :class="{ loading: !loaded, copied }" />
<span class="text">{{ copyPageText }}</span>
</span>
<span ref="toggle" class="page-context-toggle" :class="{ open }" @click="() => toggleMenu()">
<span class="vpi-chevron-down" />
</span>
</div>
<Transition name="fade-in">
<ul v-show="open" ref="menu" class="page-context-menu">
<li>
<a href="javascript:void(0)" @click="onCopy">
<span class="vpi-copy" :class="{ loading: !loaded, copied }" />
<span>
{{ copyPageText }}
<small>{{ theme.copyTagline ?? 'Copy page as Markdown for LLMs' }}</small>
</span>
</a>
</li>
<li v-for="item in menuList" :key="item.text">
<a
:href="item.link" target="_blank" rel="noopener noreferrer"
:aria-label="item.text" data-allow-mismatch
>
<span :class="item.icon" />
<span>
{{ item.text }} <span class="vpi-external-link" />
<small>{{ item.tagline }}</small>
</span>
</a>
</li>
</ul>
</Transition>
</div>
</template>
<style scoped>
.vp-page-context-menu {
position: relative;
display: inline-block;
align-self: flex-start;
width: fit-content;
}
.page-context-button {
height: 32px;
overflow: hidden;
border: solid 1px var(--vp-c-divider);
border-radius: 6px;
}
.page-context-button,
.page-context-copy {
display: flex;
align-items: center;
}
.page-context-copy {
gap: 4px;
padding: 0 8px;
font-size: 14px;
}
.page-context-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
border-left: solid 1px var(--vp-c-divider);
}
.page-context-toggle {
color: var(--vp-c-text-3);
}
.page-context-copy,
.page-context-toggle {
height: 30px;
cursor: pointer;
background-color: transparent;
transition: background-color var(--vp-t-color);
}
.page-context-copy:hover,
.page-context-toggle:hover,
.page-context-toggle.open {
background-color: var(--vp-c-bg-soft);
}
.page-context-copy .text {
flex: 1;
text-align: center;
}
.page-context-toggle .vpi-chevron-down {
transition: transform var(--vp-t-color);
}
.page-context-toggle.open .vpi-chevron-down {
transform: rotate(270deg);
}
.vpi-copy {
--icon: var(--code-copy-icon);
}
.vpi-copy.copied {
--icon: var(--code-copied-icon);
}
.vpi-copy.loading {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-dasharray='16' stroke-dashoffset='16' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 3c4.97 0 9 4.03 9 9'%3E%3Canimate fill='freeze' attributeName='stroke-dashoffset' dur='0.2s' values='16;0'/%3E%3CanimateTransform attributeName='transform' dur='1.5s' repeatCount='indefinite' type='rotate' values='0 12 12;360 12 12'/%3E%3C/path%3E%3C/svg%3E");
}
.page-context-menu {
position: absolute;
top: calc(100% + 12px);
left: 0;
z-index: 20;
width: max-content;
padding: 8px 4px;
list-style: none;
background-color: var(--vp-c-bg);
border: solid 1px var(--vp-c-divider);
border-radius: 6px;
box-shadow: var(--vp-shadow-2);
}
@media (min-width: 768px) {
.page-context-menu {
right: 0;
left: unset;
}
}
.page-context-menu li a {
display: flex;
gap: 12px;
align-items: center;
padding: 4px 8px;
font-size: 14px;
font-weight: bold;
color: var(--vp-c-text-1);
cursor: pointer;
background-color: transparent;
border-radius: 6px;
transition: background-color var(--vp-t-color);
}
.page-context-menu li a:hover {
background-color: var(--vp-c-bg-soft);
}
.page-context-menu li a > [class*="vpi-"] {
display: inline-block;
width: 24px;
height: 24px;
overflow: hidden;
color: var(--vp-c-text-2);
border: solid 1px var(--vp-c-divider);
border-radius: 4px;
}
.page-context-menu li a small {
display: block;
font-size: 12px;
font-weight: normal;
color: var(--vp-c-text-3);
}
.page-context-menu .vpi-external-link {
color: var(--vp-c-text-3);
}
.vpi-markdown {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M20.56 18H3.44C2.65 18 2 17.37 2 16.59V7.41C2 6.63 2.65 6 3.44 6h17.12c.79 0 1.44.63 1.44 1.41v9.18c0 .78-.65 1.41-1.44 1.41M6.81 15.19v-3.66l1.92 2.35l1.92-2.35v3.66h1.93V8.81h-1.93l-1.92 2.35l-1.92-2.35H4.89v6.38zM19.69 12h-1.92V8.81h-1.92V12h-1.93l2.89 3.28z'/%3E%3C/svg%3E");
}
.vpi-chatgpt {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z'/%3E%3Cpath fill='%23000' d='M10 2a4 4 0 0 1 3.46 1.99l.098.182l.638-.368a4 4 0 0 1 5.475 5.446l-.113.186l.638.368a4 4 0 0 1-1.979 7.464L18 17.264V18a4 4 0 0 1-7.459 2.01l-.1-.182l-.637.368a4 4 0 0 1-5.475-5.446l.113-.186l-.638-.368a4 4 0 0 1 1.979-7.464L6 6.736V6a4 4 0 0 1 4-4m4.702 10.788l-.068 4.059a1 1 0 0 1-.391.777l-.109.072l-1.956 1.13a2.002 2.002 0 0 0 3.817-.677L16 18v-4.434l-1.298-.779Zm-2.033 1.946l-3.55 1.97a1 1 0 0 1-.985-.008l-1.956-1.13a2.001 2.001 0 0 0 2.626 2.898l3.84-2.217zm2.687-5.415l-1.323.735l3.482 2.089A1 1 0 0 1 18 13v2.259a2.002 2.002 0 0 0 1.196-3.723zM6 8.74a2.001 2.001 0 0 0-1.328 3.64l.132.083l3.84 2.217l1.323-.735l-3.482-2.088a1 1 0 0 1-.477-.728L6 11zM10 4a2 2 0 0 0-2 2v4.434l1.298.779l.068-4.06a1 1 0 0 1 .5-.85l1.956-1.129A2 2 0 0 0 10 4m7.928 2.268a2 2 0 0 0-2.594-.805l-.138.073l-3.84 2.217l-.025 1.513l3.55-1.97a1 1 0 0 1 .868-.05l.117.058l1.957 1.13c.442-.62.51-1.465.105-2.166'/%3E%3C/g%3E%3C/svg%3E");
}
.vpi-claude {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m5.92 15.3l3.94-2.2l.06-.2l-.06-.1h-.2L9 12.76l-2.24-.06l-1.96-.1l-1.9-.1l-.48-.1l-.42-.6l.04-.3l.4-.26l.58.04l1.26.1l1.9.12l1.38.08l2.04.24h.32l.04-.14l-.1-.08l-.08-.08L7.8 10.2L5.68 8.8l-1.12-.82l-.6-.4l-.3-.4l-.12-.84l.54-.6l.74.06l.18.04l.74.58l1.6 1.22L9.4 9.2l.3.24l.12-.08l.02-.06l-.14-.22L8.6 7L7.4 4.92l-.54-.86l-.14-.52c-.06-.2-.08-.4-.08-.6l.6-.84l.36-.1l.84.12l.32.28l.52 1.2l.82 1.86l1.3 2.52l.4.76l.2.68l.06.2h.14v-.1l.1-1.44l.2-1.74l.2-2.24l.06-.64l.32-.76l.6-.4l.52.22l.4.58l-.06.36L14.32 5l-.52 2.42l-.3 1.64h.18l.2-.22l.82-1.08l1.38-1.72l.6-.7l.72-.74l.46-.36h.86l.62.94l-.28.98l-.88 1.12l-.74.94l-1.06 1.42l-.64 1.14l.06.08h.14l2.4-.52l1.28-.22l1.52-.26l.7.32l.08.32l-.28.68l-1.64.4l-1.92.4l-2.86.66l-.04.02l.04.06l1.28.12l.56.04h1.36l2.52.2l.66.4l.38.54l-.06.4l-1.02.52l-1.36-.32l-3.2-.76l-1.08-.26h-.16v.08l.92.9l1.66 1.5l2.12 1.94l.1.48l-.26.4l-.28-.04l-1.84-1.4l-.72-.6l-1.6-1.36h-.1v.14l.36.54l1.96 2.94l.1.9l-.14.28l-.52.2l-.54-.12l-1.16-1.6l-1.2-1.8l-.94-1.64l-.1.08l-.58 6.04l-.26.3l-.6.24l-.5-.4l-.28-.6l.28-1.24l.32-1.6l.26-1.28l.24-1.58l.14-.52v-.04h-.14l-1.2 1.66l-1.8 2.46l-1.44 1.52l-.34.14l-.6-.3l.06-.56l.32-.46l2-2.56l1.2-1.58l.8-.92l-.02-.1h-.06l-5.28 3.44l-.94.12l-.4-.4l.04-.6l.2-.2l1.6-1.1z'/%3E%3C/svg%3E");
}
</style>

View File

@ -100,6 +100,12 @@ useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
<template #page-bottom>
<slot name="page-bottom" />
</template>
<template #doc-title-before>
<slot name="doc-title-before" />
</template>
<template #doc-title-after>
<slot name="doc-title-after" />
</template>
<template #doc-meta-before>
<slot name="doc-meta-before" />
</template>

View File

@ -28,6 +28,7 @@ const EXCLUDE_LIST: (keyof ThemeOptions)[] = [
'watermark',
'readingTime',
'copyCode',
'llmstxt',
]
// 过滤不需要出现在多语言配置中的字段
const EXCLUDE_LOCALE_LIST: (keyof ThemeOptions)[] = [...EXCLUDE_LIST, 'blog', 'appearance']

View File

@ -22,6 +22,7 @@ export const PLUGINS_SUPPORTED_FIELDS: (keyof ThemeBuiltinPlugins)[] = [
'readingTime',
'watermark',
'replaceAssets',
'llmstxt',
]
export const MARKDOWN_CHART_FIELDS: (keyof MarkdownChartPluginOptions)[] = [

View File

@ -51,6 +51,16 @@ export const deLocale: ThemeLocaleText = {
message:
'Unterstützt von <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
},
copyPageText: 'Seite kopieren',
copiedPageText: 'Kopieren !',
copingPageText: 'Wird kopiert..',
copyTagline: 'Seite als Markdown für LLMs kopieren',
viewMarkdown: 'Als Markdown anzeigen',
viewMarkdownTagline: 'Diese Seite als Nur-Text anzeigen',
askAIText: 'In {name} öffnen',
askAITagline: '{name} zu dieser Seite befragen',
askAIMessage: 'Lese {link} und beantworte Fragen zum Inhalt.',
}
export const dePresetLocale: PresetLocale = {

View File

@ -38,6 +38,16 @@ export const enLocale: ThemeLocaleText = {
message:
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
},
copyPageText: 'Copy page',
copiedPageText: 'Copied !',
copingPageText: 'Copying..',
copyTagline: 'Copy page as Markdown for LLMs',
viewMarkdown: 'View as Markdown',
viewMarkdownTagline: 'View this page as plain text',
askAIText: 'Open in {name}',
askAITagline: 'Ask {name} about this page',
askAIMessage: 'Read {link} and answer content-related questions.',
}
export const enPresetLocale: PresetLocale = {

View File

@ -51,6 +51,16 @@ export const frLocale: ThemeLocaleText = {
message:
'Propulsé par <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
},
copyPageText: 'Copier la page',
copiedPageText: 'Copie réussie',
copingPageText: 'Copie en cours..',
copyTagline: 'Copier la page au format Markdown pour une utilisation avec des LLM',
viewMarkdown: 'Voir en Markdown',
viewMarkdownTagline: 'Voir cette page en texte brut',
askAIText: 'Ouvrir dans {name}',
askAITagline: 'Interroger {name} sur cette page',
askAIMessage: 'Lisez {link} et répondez aux questions concernant son contenu.',
}
export const frPresetLocale: PresetLocale = {

View File

@ -51,6 +51,16 @@ export const jaLocale: ThemeLocaleText = {
message:
'<a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a> によって提供されています',
},
copyPageText: 'ページをコピー',
copiedPageText: 'コピーしました',
copingPageText: 'コピー中..',
copyTagline: 'ページをMarkdown形式でコピーしてLLMで使用',
viewMarkdown: 'Markdown形式で表示',
viewMarkdownTagline: 'このページをプレーンテキストで表示',
askAIText: '{name} で開く',
askAITagline: 'このページについて {name} に質問する',
askAIMessage: '{link} を読み、内容に関する質問に答えてください。',
}
export const jaPresetLocale: PresetLocale = {

View File

@ -51,6 +51,16 @@ export const koLocale: ThemeLocaleText = {
message:
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
},
copyPageText: '페이지 복사',
copiedPageText: '복사 완료',
copingPageText: '복사 중..',
copyTagline: '페이지를 마크다운 형식으로 복사하여 LLM에서 사용',
viewMarkdown: 'Markdown 형식으로 보기',
viewMarkdownTagline: '이 페이지를 일반 텍스트로 보기',
askAIText: '{name} 에서 열기',
askAITagline: '이 페이지에 대해 {name} 에 질문하기',
askAIMessage: '{link} 을(를) 읽고 내용과 관련된 질문에 답변해 주세요.',
}
export const koPresetLocale: PresetLocale = {

View File

@ -51,6 +51,16 @@ export const ruLocale: ThemeLocaleText = {
message:
'Работает на <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
},
copyPageText: 'Копировать страницу',
copiedPageText: 'Скопировано успешно',
copingPageText: 'Копируется...',
copyTagline: 'Скопировать страницу в формате Markdown для использования в LLM',
viewMarkdown: 'Просмотреть в Markdown',
viewMarkdownTagline: 'Просмотреть эту страницу в виде простого текста',
askAIText: 'Открыть в {name}',
askAITagline: 'Спросить {name} об этой странице',
askAIMessage: 'Прочитайте {link} и ответьте на вопросы, связанные с содержанием.',
}
export const ruPresetLocale: PresetLocale = {

View File

@ -51,6 +51,16 @@ export const zhTwLocale: ThemeLocaleText = {
message:
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
},
copyPageText: '複製頁面',
copiedPageText: '複製成功',
copingPageText: '複製中..',
copyTagline: '將頁面以 Markdown 格式複製供 LLMs 使用',
viewMarkdown: '以 Markdown 格式檢視',
viewMarkdownTagline: '以純文字檢視此頁面',
askAIText: '在 {name} 中開啟',
askAITagline: '向 {name} 提問有關此頁面',
askAIMessage: '閱讀 {link} 並回答內容相關的問題。',
}
export const zhTwPresetLocale: PresetLocale = {

View File

@ -50,6 +50,16 @@ export const zhLocale: ThemeLocaleText = {
message:
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
},
copyPageText: '复制页面',
copiedPageText: '复制成功',
copingPageText: '复制中..',
copyTagline: '将页面以 Markdown 格式复制供 LLMs 使用',
viewMarkdown: '以 Markdown 格式查看',
viewMarkdownTagline: '以纯文本查看此页面',
askAIText: '在 {name} 中打开',
askAITagline: '向 {name} 提问有关此页面',
askAIMessage: '阅读 {link} 并回答内容相关的问题。',
}
export const zhPresetLocale: PresetLocale = {

View File

@ -0,0 +1,142 @@
import type { LLMPage, LlmsPluginOptions, LLMState } from '@vuepress/plugin-llms'
import type { App, PluginConfig } from 'vuepress'
import type { ThemeSidebarItem } from '../../shared/index.js'
import { generateTOCLink as rawGenerateTOCLink, llmsPlugin as rawLlmsPlugin } from '@vuepress/plugin-llms'
import { ensureEndingSlash, ensureLeadingSlash, isPlainObject } from 'vuepress/shared'
import { getThemeConfig } from '../loadConfig/index.js'
import { withBase } from '../utils/index.js'
export function llmsPlugin(app: App, userOptions: true | LlmsPluginOptions): PluginConfig {
if (!app.env.isBuild)
return []
const { llmsTxtTemplateGetter, ...userLLMsTxt } = isPlainObject(userOptions) ? userOptions : {}
function tocGetter(llmPages: LLMPage[], llmState: LLMState): string {
const options = getThemeConfig()
const { currentLocale } = llmState
const collections = options.locales?.[currentLocale]?.collections || []
if (!collections.length)
return ''
let tableOfContent = ''
const usagePages: LLMPage[] = []
collections
.filter(item => item.type === 'post')
.forEach(({ title, linkPrefix, link }) => {
tableOfContent += `### ${title}\n\n`
const withLinkPrefix = genStarsWith(linkPrefix, currentLocale)
const withLink = genStarsWith(link, currentLocale)
const withFallback = genStarsWith('/article/', currentLocale)
const list: string[] = []
llmPages.forEach((page) => {
if (withLinkPrefix(page.path) || withLink(page.path) || withFallback(page.path)) {
usagePages.push(page)
list.push(rawGenerateTOCLink(page, llmState))
}
})
tableOfContent += `${list.filter(Boolean).join('')}\n`
})
const generateTOCLink = (path: string): string => {
const filepath = path.endsWith('/') ? `${path}README.md` : path.endsWith('.md') ? path : `${path || 'README'}.md`
const link = path.endsWith('/') ? `${path}index.html` : `${path}.html`
const page = llmPages.find((item) => {
return ensureLeadingSlash(item.filePathRelative || '') === filepath || link === item.path
})
if (page) {
usagePages.push(page)
return rawGenerateTOCLink(page, llmState)
}
return ''
}
const processAutoSidebar = (prefix: string): string[] => {
const list: string[] = []
llmPages.forEach((page) => {
if (ensureLeadingSlash(page.filePathRelative || '').startsWith(prefix)) {
usagePages.push(page)
list.push(rawGenerateTOCLink(page, llmState))
}
})
return list.filter(Boolean)
}
const processSidebar = (items: (string | ThemeSidebarItem)[], prefix: string): string[] => {
const result: string[] = []
items.forEach((item) => {
if (typeof item === 'string') {
result.push(generateTOCLink(normalizePath(prefix, item)))
}
else {
if (item.link) {
result.push(generateTOCLink(normalizePath(prefix, item.link)))
}
if (item.items === 'auto') {
result.push(...processAutoSidebar(normalizePath(prefix, item.prefix)))
}
else if (item.items?.length) {
result.push(...processSidebar(item.items, normalizePath(prefix, item.prefix)))
}
}
})
return result
}
// Collections
collections
.filter(collection => collection.type === 'doc')
.forEach(({ dir, title, sidebar = [] }) => {
tableOfContent += `### ${title}\n\n`
const prefix = normalizePath(ensureLeadingSlash(withBase(dir, currentLocale)))
if (sidebar === 'auto') {
tableOfContent += `${processAutoSidebar(prefix).join('')}\n`
}
else if (sidebar.length) {
const home = generateTOCLink(ensureEndingSlash(prefix))
const list = processSidebar(sidebar, prefix)
if (home && !list.includes(home)) {
list.unshift(home)
}
tableOfContent += `${list.join('')}\n`
}
})
// Others
const unUsagePages = llmPages.filter(page => !usagePages.includes(page))
if (unUsagePages.length) {
tableOfContent += '### Others\n\n'
tableOfContent += unUsagePages
.map(page => rawGenerateTOCLink(page, llmState))
.join('')
}
return tableOfContent
}
return [rawLlmsPlugin({
...userLLMsTxt,
llmsTxtTemplateGetter: {
toc: tocGetter,
...llmsTxtTemplateGetter,
},
})]
}
function genStarsWith(stars: string | undefined, locale: string) {
return (url: string): boolean => {
if (!stars)
return false
return url.startsWith(withBase(stars, locale))
}
}
function normalizePath(prefix: string, path = ''): string {
if (path.startsWith('/'))
return path
return `${ensureEndingSlash(prefix)}${path}`
}

View File

@ -18,6 +18,7 @@ import { watermarkPlugin } from '@vuepress/plugin-watermark'
import { getThemeConfig } from '../loadConfig/index.js'
import { codePlugins } from './code.js'
import { gitPlugin } from './git.js'
import { llmsPlugin } from './llms.js'
import { markdownPlugins } from './markdown.js'
export function setupPlugins(
@ -114,6 +115,11 @@ export function setupPlugins(
plugins.push(replaceAssetsPlugin(replaceAssets))
}
const llmstxt = options.llmstxt ?? pluginOptions.llmstxt
if (llmstxt) {
plugins.push(...llmsPlugin(app, llmstxt))
}
/**
* hostname
*/

View File

@ -396,4 +396,14 @@ export interface ThemeLocaleText {
* placeholder
*/
encryptPlaceholder?: string
copyPageText?: string
copiedPageText?: string
copingPageText?: string
copyTagline?: string
viewMarkdown?: string
viewMarkdownTagline?: string
askAIText?: string
askAITagline?: string
askAIMessage?: string
}

View File

@ -1,6 +1,7 @@
import type { CommentPluginOptions } from '@vuepress/plugin-comment'
import type { CopyCodePluginOptions } from '@vuepress/plugin-copy-code'
import type { ChangelogOptions, ContributorsOptions } from '@vuepress/plugin-git'
import type { LlmsPluginOptions } from '@vuepress/plugin-llms'
import type { ReadingTimePluginOptions } from '@vuepress/plugin-reading-time'
import type { ReplaceAssetsPluginOptions } from '@vuepress/plugin-replace-assets'
import type { ShikiPluginOptions } from '@vuepress/plugin-shiki'
@ -94,7 +95,10 @@ export interface ThemeFeatureOptions {
/**
*
*
* @default { provider: 'local' }
* @default
* ```
* { provider: 'local' }
* ```
*/
search?: boolean | SearchOptions
@ -132,4 +136,9 @@ export interface ThemeFeatureOptions {
*
*/
replaceAssets?: false | ReplaceAssetsPluginOptions
/**
* llmstxt
*/
llmstxt?: boolean | LlmsPluginOptions
}

View File

@ -3,6 +3,7 @@ import type { CachePluginOptions } from '@vuepress/plugin-cache'
import type { CommentPluginOptions } from '@vuepress/plugin-comment'
import type { CopyCodePluginOptions } from '@vuepress/plugin-copy-code'
import type { DocSearchOptions } from '@vuepress/plugin-docsearch'
import type { LlmsPluginOptions } from '@vuepress/plugin-llms'
import type { MarkdownChartPluginOptions } from '@vuepress/plugin-markdown-chart'
import type { MarkdownImagePluginOptions } from '@vuepress/plugin-markdown-image'
import type { MarkdownIncludePluginOptions } from '@vuepress/plugin-markdown-include'
@ -130,4 +131,9 @@ export interface ThemeBuiltinPlugins {
*
*/
replaceAssets?: false | ReplaceAssetsPluginOptions
/**
* llmstxt
*/
llmstxt?: boolean | LlmsPluginOptions
}