@@ -90,7 +94,18 @@ const hasMeta = computed(() =>
diff --git a/theme/src/client/layouts/Layout.vue b/theme/src/client/layouts/Layout.vue
index 19230108..bcf9233a 100644
--- a/theme/src/client/layouts/Layout.vue
+++ b/theme/src/client/layouts/Layout.vue
@@ -100,6 +100,12 @@ useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
+
+
+
+
+
+
diff --git a/theme/src/node/config/resolveThemeData.ts b/theme/src/node/config/resolveThemeData.ts
index 4649455f..4e4b5f11 100644
--- a/theme/src/node/config/resolveThemeData.ts
+++ b/theme/src/node/config/resolveThemeData.ts
@@ -28,6 +28,7 @@ const EXCLUDE_LIST: (keyof ThemeOptions)[] = [
'watermark',
'readingTime',
'copyCode',
+ 'llmstxt',
]
// 过滤不需要出现在多语言配置中的字段
const EXCLUDE_LOCALE_LIST: (keyof ThemeOptions)[] = [...EXCLUDE_LIST, 'blog', 'appearance']
diff --git a/theme/src/node/detector/fields.ts b/theme/src/node/detector/fields.ts
index 8a056432..799cb351 100644
--- a/theme/src/node/detector/fields.ts
+++ b/theme/src/node/detector/fields.ts
@@ -22,6 +22,7 @@ export const PLUGINS_SUPPORTED_FIELDS: (keyof ThemeBuiltinPlugins)[] = [
'readingTime',
'watermark',
'replaceAssets',
+ 'llmstxt',
]
export const MARKDOWN_CHART_FIELDS: (keyof MarkdownChartPluginOptions)[] = [
diff --git a/theme/src/node/locales/de.ts b/theme/src/node/locales/de.ts
index 1c6660d0..939bebe3 100644
--- a/theme/src/node/locales/de.ts
+++ b/theme/src/node/locales/de.ts
@@ -51,6 +51,16 @@ export const deLocale: ThemeLocaleText = {
message:
'Unterstützt von
VuePress &
vuepress-theme-plume',
},
+
+ 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 = {
diff --git a/theme/src/node/locales/en.ts b/theme/src/node/locales/en.ts
index c8ad2606..edb39570 100644
--- a/theme/src/node/locales/en.ts
+++ b/theme/src/node/locales/en.ts
@@ -38,6 +38,16 @@ export const enLocale: ThemeLocaleText = {
message:
'Powered by
VuePress &
vuepress-theme-plume',
},
+
+ 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 = {
diff --git a/theme/src/node/locales/fr.ts b/theme/src/node/locales/fr.ts
index bd22964a..fe6ca9c7 100644
--- a/theme/src/node/locales/fr.ts
+++ b/theme/src/node/locales/fr.ts
@@ -51,6 +51,16 @@ export const frLocale: ThemeLocaleText = {
message:
'Propulsé par
VuePress &
vuepress-theme-plume',
},
+
+ 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 = {
diff --git a/theme/src/node/locales/ja.ts b/theme/src/node/locales/ja.ts
index 04074381..598453bf 100644
--- a/theme/src/node/locales/ja.ts
+++ b/theme/src/node/locales/ja.ts
@@ -51,6 +51,16 @@ export const jaLocale: ThemeLocaleText = {
message:
'
VuePress &
vuepress-theme-plume によって提供されています',
},
+
+ copyPageText: 'ページをコピー',
+ copiedPageText: 'コピーしました',
+ copingPageText: 'コピー中..',
+ copyTagline: 'ページをMarkdown形式でコピーしてLLMで使用',
+ viewMarkdown: 'Markdown形式で表示',
+ viewMarkdownTagline: 'このページをプレーンテキストで表示',
+ askAIText: '{name} で開く',
+ askAITagline: 'このページについて {name} に質問する',
+ askAIMessage: '{link} を読み、内容に関する質問に答えてください。',
}
export const jaPresetLocale: PresetLocale = {
diff --git a/theme/src/node/locales/ko.ts b/theme/src/node/locales/ko.ts
index b9bb11cd..7bcf9d54 100644
--- a/theme/src/node/locales/ko.ts
+++ b/theme/src/node/locales/ko.ts
@@ -51,6 +51,16 @@ export const koLocale: ThemeLocaleText = {
message:
'Powered by
VuePress &
vuepress-theme-plume',
},
+
+ copyPageText: '페이지 복사',
+ copiedPageText: '복사 완료',
+ copingPageText: '복사 중..',
+ copyTagline: '페이지를 마크다운 형식으로 복사하여 LLM에서 사용',
+ viewMarkdown: 'Markdown 형식으로 보기',
+ viewMarkdownTagline: '이 페이지를 일반 텍스트로 보기',
+ askAIText: '{name} 에서 열기',
+ askAITagline: '이 페이지에 대해 {name} 에 질문하기',
+ askAIMessage: '{link} 을(를) 읽고 내용과 관련된 질문에 답변해 주세요.',
}
export const koPresetLocale: PresetLocale = {
diff --git a/theme/src/node/locales/ru.ts b/theme/src/node/locales/ru.ts
index 42300a04..dbce92c5 100644
--- a/theme/src/node/locales/ru.ts
+++ b/theme/src/node/locales/ru.ts
@@ -51,6 +51,16 @@ export const ruLocale: ThemeLocaleText = {
message:
'Работает на
VuePress &
vuepress-theme-plume',
},
+
+ copyPageText: 'Копировать страницу',
+ copiedPageText: 'Скопировано успешно',
+ copingPageText: 'Копируется...',
+ copyTagline: 'Скопировать страницу в формате Markdown для использования в LLM',
+ viewMarkdown: 'Просмотреть в Markdown',
+ viewMarkdownTagline: 'Просмотреть эту страницу в виде простого текста',
+ askAIText: 'Открыть в {name}',
+ askAITagline: 'Спросить {name} об этой странице',
+ askAIMessage: 'Прочитайте {link} и ответьте на вопросы, связанные с содержанием.',
}
export const ruPresetLocale: PresetLocale = {
diff --git a/theme/src/node/locales/zh-tw.ts b/theme/src/node/locales/zh-tw.ts
index f19cd426..6734410c 100644
--- a/theme/src/node/locales/zh-tw.ts
+++ b/theme/src/node/locales/zh-tw.ts
@@ -51,6 +51,16 @@ export const zhTwLocale: ThemeLocaleText = {
message:
'Powered by
VuePress &
vuepress-theme-plume',
},
+
+ copyPageText: '複製頁面',
+ copiedPageText: '複製成功',
+ copingPageText: '複製中..',
+ copyTagline: '將頁面以 Markdown 格式複製供 LLMs 使用',
+ viewMarkdown: '以 Markdown 格式檢視',
+ viewMarkdownTagline: '以純文字檢視此頁面',
+ askAIText: '在 {name} 中開啟',
+ askAITagline: '向 {name} 提問有關此頁面',
+ askAIMessage: '閱讀 {link} 並回答內容相關的問題。',
}
export const zhTwPresetLocale: PresetLocale = {
diff --git a/theme/src/node/locales/zh.ts b/theme/src/node/locales/zh.ts
index 0de83380..a0dd04b7 100644
--- a/theme/src/node/locales/zh.ts
+++ b/theme/src/node/locales/zh.ts
@@ -50,6 +50,16 @@ export const zhLocale: ThemeLocaleText = {
message:
'Powered by
VuePress &
vuepress-theme-plume',
},
+
+ copyPageText: '复制页面',
+ copiedPageText: '复制成功',
+ copingPageText: '复制中..',
+ copyTagline: '将页面以 Markdown 格式复制供 LLMs 使用',
+ viewMarkdown: '以 Markdown 格式查看',
+ viewMarkdownTagline: '以纯文本查看此页面',
+ askAIText: '在 {name} 中打开',
+ askAITagline: '向 {name} 提问有关此页面',
+ askAIMessage: '阅读 {link} 并回答内容相关的问题。',
}
export const zhPresetLocale: PresetLocale = {
diff --git a/theme/src/node/plugins/llms.ts b/theme/src/node/plugins/llms.ts
new file mode 100644
index 00000000..e02453fd
--- /dev/null
+++ b/theme/src/node/plugins/llms.ts
@@ -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}`
+}
diff --git a/theme/src/node/plugins/setupPlugins.ts b/theme/src/node/plugins/setupPlugins.ts
index e85346be..b39ddb23 100644
--- a/theme/src/node/plugins/setupPlugins.ts
+++ b/theme/src/node/plugins/setupPlugins.ts
@@ -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 存在时生效
*/
diff --git a/theme/src/shared/locale.ts b/theme/src/shared/locale.ts
index 07bc3d13..fa4f2e2c 100644
--- a/theme/src/shared/locale.ts
+++ b/theme/src/shared/locale.ts
@@ -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
}
diff --git a/theme/src/shared/options.ts b/theme/src/shared/options.ts
index 8fab8460..7e09726d 100644
--- a/theme/src/shared/options.ts
+++ b/theme/src/shared/options.ts
@@ -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
}
diff --git a/theme/src/shared/plugins.ts b/theme/src/shared/plugins.ts
index 016d5561..f8c8b1fa 100644
--- a/theme/src/shared/plugins.ts
+++ b/theme/src/shared/plugins.ts
@@ -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
}