docus 5.10.0 → 5.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/app.config.ts CHANGED
@@ -17,7 +17,7 @@ export default defineAppConfig({
17
17
  },
18
18
  contentToc: {
19
19
  defaultVariants: {
20
- highlight: true,
20
+ highlightVariant: 'circuit',
21
21
  },
22
22
  },
23
23
  contentNavigation: {
@@ -1,15 +1,20 @@
1
1
  <script setup lang="ts">
2
2
  import { useClipboard } from '@vueuse/core'
3
+ import { joinURL, withTrailingSlash } from 'ufo'
3
4
  import { useRuntimeConfig } from '#imports'
4
5
 
5
6
  const route = useRoute()
6
7
  const toast = useToast()
7
- const appBaseURL = useRuntimeConfig().app?.baseURL || '/'
8
+ const runtimeConfig = useRuntimeConfig()
9
+ const appBaseURL = runtimeConfig.app?.baseURL || '/'
10
+ const mcpRoute = (runtimeConfig.public.mcp as { route?: string } | undefined)?.route || '/mcp'
8
11
 
9
12
  const { copy, copied } = useClipboard()
10
13
  const { t } = useDocusI18n()
11
14
 
12
- const markdownLink = computed(() => `${window?.location?.origin}${appBaseURL}raw${route.path}.md`)
15
+ const markdownLink = computed(() => `${window?.location?.origin}${withTrailingSlash(appBaseURL)}raw${route.path}.md`)
16
+ const mcpServerUrl = computed(() => `${window?.location?.origin}${joinURL(appBaseURL, mcpRoute)}`)
17
+ const mcpDeeplink = joinURL(mcpRoute, 'deeplink')
13
18
  const items = [
14
19
  [{
15
20
  label: t('docs.copy.link'),
@@ -41,7 +46,7 @@ const items = [
41
46
  label: 'Copy MCP Server URL',
42
47
  icon: 'i-lucide-link',
43
48
  onSelect() {
44
- copy(`${window?.location?.origin}${appBaseURL}mcp`)
49
+ copy(mcpServerUrl.value)
45
50
  toast.add({
46
51
  title: 'Copied to clipboard',
47
52
  icon: 'i-lucide-check-circle',
@@ -52,7 +57,7 @@ const items = [
52
57
  label: 'Add MCP Server',
53
58
  icon: 'i-simple-icons:cursor',
54
59
  target: '_blank',
55
- to: `/mcp/deeplink`,
60
+ to: mcpDeeplink,
56
61
  },
57
62
  ],
58
63
  ]
@@ -76,6 +76,7 @@ addPrerenderPath(`/raw${route.path}.md`)
76
76
  <UPage
77
77
  v-if="page"
78
78
  :key="`page-${shouldHideToc}`"
79
+ :ui="{ root: 'lg:grid-cols-12', center: 'lg:col-span-9', right: 'lg:col-span-3' }"
79
80
  >
80
81
  <UPageHeader
81
82
  :title="page.title"
@@ -1,7 +1,7 @@
1
1
  import type { RouteLocationNormalized } from 'vue-router'
2
2
  import { consola } from 'consola'
3
3
 
4
- const log = consola.withTag('Docus')
4
+ const log = consola.withTag('docus')
5
5
 
6
6
  // Lazy import functions for locale files (bundled but not eagerly loaded)
7
7
  const localeFiles = import.meta.glob<{ default: Record<string, unknown> }>('../../i18n/locales/*.json')
@@ -21,7 +21,7 @@ export interface AssistantModuleOptions {
21
21
  model?: string
22
22
  }
23
23
 
24
- const log = logger.withTag('Docus')
24
+ const log = logger.withTag('docus')
25
25
 
26
26
  const defaults: Required<AssistantModuleOptions> = {
27
27
  apiPath: '/__docus__/assistant',
@@ -77,7 +77,9 @@ export default defineNuxtModule<AssistantModuleOptions>({
77
77
  )
78
78
 
79
79
  if (!hasAiGatewayAuth) {
80
- log.warn('AI assistant disabled: neither AI_GATEWAY_API_KEY nor VERCEL_OIDC_TOKEN found')
80
+ nuxt.hook('modules:done', () => {
81
+ log.warn('AI assistant disabled: neither `AI_GATEWAY_API_KEY` nor `VERCEL_OIDC_TOKEN` found')
82
+ })
81
83
  return
82
84
  }
83
85
 
@@ -1,9 +1,27 @@
1
1
  import { streamText, convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse } from 'ai'
2
2
  import type { UIMessageStreamWriter, ToolCallPart, ToolSet } from 'ai'
3
3
  import { createMCPClient } from '@ai-sdk/mcp'
4
+ import type { H3Event } from 'h3'
4
5
 
5
6
  const MAX_STEPS = 10
6
7
 
8
+ function createLocalFetch(event: H3Event): typeof fetch {
9
+ const origin = getRequestURL(event).origin
10
+
11
+ return (input, init) => {
12
+ const requestUrl = input instanceof URL
13
+ ? input
14
+ : typeof input === 'string'
15
+ ? new URL(input, origin)
16
+ : new URL(input.url)
17
+ const localPath = requestUrl.origin === origin
18
+ ? `${requestUrl.pathname}${requestUrl.search}`
19
+ : requestUrl.toString()
20
+
21
+ return event.fetch(localPath, init)
22
+ }
23
+ }
24
+
7
25
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
26
  function stopWhenResponseComplete({ steps }: { steps: any[] }): boolean {
9
27
  const lastStep = steps.at(-1)
@@ -66,15 +84,29 @@ export default defineEventHandler(async (event) => {
66
84
  const mcpServer = config.assistant.mcpServer
67
85
  const isExternalUrl = mcpServer.startsWith('http://') || mcpServer.startsWith('https://')
68
86
  const baseURL = config.app?.baseURL?.replace(/\/$/, '') || ''
69
- const mcpUrl = isExternalUrl
70
- ? mcpServer
71
- : import.meta.dev
72
- ? `http://localhost:3000${baseURL}${mcpServer}`
73
- : `${getRequestURL(event).origin}${baseURL}${mcpServer}`
74
-
75
- const httpClient = await createMCPClient({
76
- transport: { type: 'http', url: mcpUrl },
77
- })
87
+
88
+ let transport: Parameters<typeof createMCPClient>[0]['transport']
89
+ if (isExternalUrl) {
90
+ transport = {
91
+ type: 'http',
92
+ url: mcpServer,
93
+ }
94
+ }
95
+ else if (import.meta.dev) {
96
+ transport = {
97
+ type: 'http',
98
+ url: `http://localhost:3000${baseURL}${mcpServer}`,
99
+ }
100
+ }
101
+ else {
102
+ transport = {
103
+ type: 'http',
104
+ url: `${getRequestURL(event).origin}${baseURL}${mcpServer}`,
105
+ fetch: createLocalFetch(event),
106
+ }
107
+ }
108
+
109
+ const httpClient = await createMCPClient({ transport })
78
110
  const mcpTools = await httpClient.tools()
79
111
 
80
112
  const stream = createUIMessageStream({
package/modules/config.ts CHANGED
@@ -5,10 +5,11 @@ import { join } from 'node:path'
5
5
  import { inferSiteURL, getPackageJsonMetadata } from '../utils/meta'
6
6
  import { getGitBranch, getGitEnv, getLocalGitInfo } from '../utils/git'
7
7
 
8
- const log = logger.withTag('Docus')
8
+ const log = logger.withTag('docus')
9
9
 
10
10
  type I18nLocale = string | { code: string, name?: string }
11
11
  type DocusI18nOptions = { locales?: I18nLocale[], strategy?: string }
12
+ type DocusMcpOptions = { route?: string, enabled?: boolean }
12
13
  type RegisterModuleOptions = {
13
14
  langDir: string
14
15
  locales: Array<{ code: string, name: string, file: string }>
@@ -39,7 +40,7 @@ export default defineNuxtModule({
39
40
  url,
40
41
  name: siteName,
41
42
  debug: false,
42
- })
43
+ }) as typeof nuxt.options.site
43
44
 
44
45
  nuxt.options.appConfig.header = defu(nuxt.options.appConfig.header, {
45
46
  title: siteName,
@@ -58,6 +59,16 @@ export default defineNuxtModule({
58
59
  branch: getGitBranch(),
59
60
  })
60
61
 
62
+ /*
63
+ ** MCP route (expose to client so the page header dropdown stays in sync
64
+ ** with the user-configured `mcp.route` from @nuxtjs/mcp-toolkit)
65
+ */
66
+ const mcpOptions = (nuxt.options as typeof nuxt.options & { mcp?: DocusMcpOptions }).mcp
67
+ nuxt.options.runtimeConfig.public.mcp = defu(
68
+ nuxt.options.runtimeConfig.public.mcp as DocusMcpOptions | undefined,
69
+ { route: mcpOptions?.route || '/mcp' },
70
+ )
71
+
61
72
  const forcedColorMode = (nuxt.options.appConfig.docus as Record<string, unknown>)?.colorMode as string | undefined
62
73
  if (forcedColorMode === 'light' || forcedColorMode === 'dark') {
63
74
  nuxt.options.colorMode = defu({ preference: forcedColorMode, fallback: forcedColorMode }, nuxt.options.colorMode || {}) as typeof nuxt.options.colorMode
package/modules/css.ts CHANGED
@@ -1,21 +1,37 @@
1
- import { defineNuxtModule, addTemplate, createResolver } from '@nuxt/kit'
2
- import { joinURL } from 'ufo'
1
+ import { defineNuxtModule, addTemplate, createResolver, logger } from '@nuxt/kit'
2
+ import { existsSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { resolve } from 'pathe'
3
5
  import { resolveModulePath } from 'exsolve'
4
6
 
7
+ const log = logger.withTag('docus')
8
+
5
9
  export default defineNuxtModule({
6
10
  meta: {
7
- name: 'css',
11
+ name: 'docus-css',
8
12
  },
9
13
  async setup(_options, nuxt) {
10
- const dir = nuxt.options.rootDir
11
14
  const resolver = createResolver(import.meta.url)
12
15
 
13
- const contentDir = joinURL(dir, 'content')
16
+ const contentDir = resolve(nuxt.options.rootDir, 'content')
14
17
  const uiPath = resolveModulePath('@nuxt/ui', { from: import.meta.url, conditions: ['style'] })
15
18
  const tailwindPath = resolveModulePath('tailwindcss', { from: import.meta.url, conditions: ['style'] })
16
19
  const layerDir = resolver.resolve('../app')
17
20
  const assistantDir = resolver.resolve('../modules/assistant')
18
21
 
22
+ let userDocusPath: string | null = resolve(nuxt.options.srcDir, 'app.css')
23
+ if (existsSync(userDocusPath)) {
24
+ const userDocusCss = await readFile(userDocusPath, 'utf-8')
25
+ if (userDocusCss.includes('@import "tailwindcss"')) {
26
+ nuxt.hook('modules:done', () => {
27
+ log.warn('`app.css` contains `@import "tailwindcss";` consider removing it to avoid duplicate css.')
28
+ })
29
+ }
30
+ }
31
+ else {
32
+ userDocusPath = null
33
+ }
34
+
19
35
  const cssTemplate = addTemplate({
20
36
  filename: 'docus.css',
21
37
  getContents: () => {
@@ -25,7 +41,12 @@ export default defineNuxtModule({
25
41
  @source "${contentDir.replace(/\\/g, '/')}/**/*";
26
42
  @source "${layerDir.replace(/\\/g, '/')}/**/*";
27
43
  @source "../../app.config.ts";
28
- @source "${assistantDir.replace(/\\/g, '/')}/**/*";`
44
+ @source "${assistantDir.replace(/\\/g, '/')}/**/*";
45
+
46
+ :root {
47
+ --ui-container: 90rem;
48
+ }
49
+ ` + (userDocusPath ? `\n@import ${JSON.stringify(userDocusPath)};` : '')
29
50
  },
30
51
  })
31
52
 
@@ -2,7 +2,7 @@ import { defineNuxtModule, logger } from '@nuxt/kit'
2
2
  import { resolve } from 'node:path'
3
3
  import { readFile, writeFile } from 'node:fs/promises'
4
4
 
5
- const log = logger.withTag('Docus')
5
+ const log = logger.withTag('docus')
6
6
 
7
7
  type I18nLocale = string | { code: string }
8
8
  type DocusI18nOptions = { locales?: I18nLocale[] }
@@ -32,16 +32,22 @@ export default defineNuxtModule({
32
32
  return
33
33
  }
34
34
 
35
- // Always redirect / to /llms.txt
35
+ // Always redirect / to /llms.txt and ensure plain text content type
36
+ const markdownHeaders = {
37
+ 'content-type': 'text/markdown; charset=utf-8',
38
+ }
39
+
36
40
  const routes = [
37
41
  {
38
42
  src: '^/$',
39
43
  dest: '/llms.txt',
44
+ headers: markdownHeaders,
40
45
  has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
41
46
  },
42
47
  {
43
48
  src: '^/$',
44
49
  dest: '/llms.txt',
50
+ headers: markdownHeaders,
45
51
  has: [{ type: 'header', key: 'user-agent', value: 'curl/.*' }],
46
52
  },
47
53
  ]
@@ -66,11 +72,13 @@ export default defineNuxtModule({
66
72
  {
67
73
  src: `^/(${localePattern})$`,
68
74
  dest: '/llms.txt',
75
+ headers: markdownHeaders,
69
76
  has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
70
77
  },
71
78
  {
72
79
  src: `^/(${localePattern})$`,
73
80
  dest: '/llms.txt',
81
+ headers: markdownHeaders,
74
82
  has: [{ type: 'header', key: 'user-agent', value: 'curl/.*' }],
75
83
  },
76
84
  )
@@ -109,11 +117,13 @@ export default defineNuxtModule({
109
117
  {
110
118
  src: `^${pagePath}$`,
111
119
  dest: rawPath,
120
+ headers: markdownHeaders,
112
121
  has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
113
122
  },
114
123
  {
115
124
  src: `^${pagePath}$`,
116
125
  dest: rawPath,
126
+ headers: markdownHeaders,
117
127
  has: [{ type: 'header', key: 'user-agent', value: 'curl/.*' }],
118
128
  },
119
129
  ]
@@ -19,7 +19,7 @@ export interface SkillsModuleOptions {
19
19
  const SKILL_NAME_REGEX = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/
20
20
  const MAX_NAME_LENGTH = 64
21
21
 
22
- const log = logger.withTag('Docus')
22
+ const log = logger.withTag('docus')
23
23
 
24
24
  const defaults: Required<SkillsModuleOptions> = {
25
25
  dir: 'skills',
@@ -38,7 +38,9 @@ export default defineNuxtModule<SkillsModuleOptions>({
38
38
  const catalog = await scanSkills(skillsDir)
39
39
  if (!catalog.length) return
40
40
 
41
- log.info(`Found ${catalog.length} agent skill${catalog.length > 1 ? 's' : ''}: ${catalog.map(s => s.name).join(', ')}`)
41
+ nuxt.hook('modules:done', () => {
42
+ log.info(`Found ${catalog.length} agent skill${catalog.length > 1 ? 's' : ''}: ${catalog.map(s => s.name).join(', ')}`)
43
+ })
42
44
 
43
45
  nuxt.options.runtimeConfig.skills = { catalog }
44
46
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "docus",
3
3
  "description": "Nuxt layer for Docus documentation theme",
4
- "version": "5.10.0",
4
+ "version": "5.11.0",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
7
7
  "repository": {
@@ -23,39 +23,40 @@
23
23
  "README.md"
24
24
  ],
25
25
  "dependencies": {
26
- "@ai-sdk/gateway": "^3.0.83",
27
- "@ai-sdk/mcp": "^1.0.30",
28
- "@ai-sdk/vue": "3.0.141",
29
- "@iconify-json/lucide": "^1.2.100",
30
- "@iconify-json/simple-icons": "^1.2.75",
26
+ "@ai-sdk/gateway": "^3.0.104",
27
+ "@ai-sdk/mcp": "^1.0.36",
28
+ "@ai-sdk/vue": "3.0.168",
29
+ "@iconify-json/lucide": "^1.2.102",
30
+ "@iconify-json/simple-icons": "^1.2.79",
31
31
  "@iconify-json/vscode-icons": "^1.2.45",
32
- "@nuxt/content": "^3.12.0",
32
+ "@nuxt/content": "^3.13.0",
33
33
  "@nuxt/image": "^2.0.0",
34
34
  "@nuxt/kit": "^4.4.2",
35
- "@nuxt/ui": "^4.6.0",
35
+ "@nuxt/ui": "^4.7.1",
36
36
  "@nuxtjs/i18n": "^10.2.4",
37
- "@nuxtjs/mcp-toolkit": "^0.13.2",
38
- "@nuxtjs/mdc": "^0.21.0",
39
- "@nuxtjs/robots": "^6.0.6",
37
+ "@nuxtjs/mcp-toolkit": "^0.13.4",
38
+ "@nuxtjs/mdc": "^0.21.1",
39
+ "@nuxtjs/robots": "^6.0.7",
40
40
  "@shikijs/core": "^4.0.2",
41
41
  "@shikijs/engine-javascript": "^4.0.2",
42
42
  "@shikijs/langs": "^4.0.2",
43
43
  "@shikijs/themes": "^4.0.2",
44
- "@takumi-rs/core": "^0.73.1",
44
+ "@takumi-rs/core": "^1.0.15",
45
45
  "@vueuse/core": "^14.2.1",
46
- "ai": "6.0.141",
47
- "defu": "^6.1.4",
46
+ "ai": "6.0.168",
47
+ "defu": "^6.1.7",
48
48
  "exsolve": "^1.0.8",
49
49
  "git-url-parse": "^16.1.0",
50
- "motion-v": "^2.2.0",
50
+ "motion-v": "^2.2.1",
51
51
  "nuxt-llms": "^0.2.0",
52
- "nuxt-og-image": "^6.3.1",
52
+ "nuxt-og-image": "^6.4.5",
53
+ "pathe": "^2.0.3",
53
54
  "pkg-types": "^2.3.0",
54
55
  "scule": "^1.3.0",
55
56
  "shiki-stream": "^0.1.4",
56
- "tailwindcss": "^4.2.2",
57
+ "tailwindcss": "^4.2.3",
57
58
  "ufo": "^1.6.3",
58
- "yaml": "^2.7.1",
59
+ "yaml": "^2.8.3",
59
60
  "zod": "^4.3.6",
60
61
  "zod-to-json-schema": "^3.25.2"
61
62
  },
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import { joinURL } from 'ufo'
2
3
  import { queryCollection } from '@nuxt/content/server'
3
4
  import type { Collections } from '@nuxt/content'
4
5
  import { getAvailableLocales, getCollectionFromPath } from '../../utils/content'
@@ -33,11 +34,13 @@ WORKFLOW: This tool returns the complete page content including title, descripti
33
34
  cache: '1h',
34
35
  handler: async ({ path }) => {
35
36
  const event = useEvent()
36
- const config = useRuntimeConfig(event).public
37
+ const config = useRuntimeConfig(event)
38
+ const publicConfig = config.public
37
39
  const siteUrl = getRequestURL(event).origin || inferSiteURL()
40
+ const baseURL = config.app?.baseURL || '/'
38
41
 
39
- const availableLocales = getAvailableLocales(config)
40
- const collectionName = config.i18n?.locales
42
+ const availableLocales = getAvailableLocales(publicConfig)
43
+ const collectionName = publicConfig.i18n?.locales
41
44
  ? getCollectionFromPath(path, availableLocales)
42
45
  : 'docs'
43
46
 
@@ -58,7 +61,7 @@ WORKFLOW: This tool returns the complete page content including title, descripti
58
61
  path: page.path,
59
62
  description: page.description,
60
63
  content,
61
- url: `${siteUrl}${page.path}`,
64
+ url: siteUrl ? joinURL(siteUrl, baseURL, page.path) : joinURL(baseURL, page.path),
62
65
  }
63
66
  }
64
67
  catch (error) {
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import { joinURL } from 'ufo'
2
3
  import { queryCollection } from '@nuxt/content/server'
3
4
  import type { Collections } from '@nuxt/content'
4
5
  import { getCollectionsToQuery, getAvailableLocales } from '../../utils/content'
@@ -39,10 +40,12 @@ OUTPUT: Returns a structured list with:
39
40
  cache: '1h',
40
41
  handler: async ({ locale }) => {
41
42
  const event = useEvent()
42
- const config = useRuntimeConfig(event).public
43
+ const config = useRuntimeConfig(event)
44
+ const publicConfig = config.public
43
45
 
44
46
  const siteUrl = getRequestURL(event).origin || inferSiteURL()
45
- const availableLocales = getAvailableLocales(config)
47
+ const baseURL = config.app?.baseURL || '/'
48
+ const availableLocales = getAvailableLocales(publicConfig)
46
49
  const collections = getCollectionsToQuery(locale, availableLocales)
47
50
 
48
51
  try {
@@ -57,7 +60,7 @@ OUTPUT: Returns a structured list with:
57
60
  path: page.path,
58
61
  description: page.description,
59
62
  locale: collectionName.replace('docs_', ''),
60
- url: `${siteUrl}${page.path}`,
63
+ url: siteUrl ? joinURL(siteUrl, baseURL, page.path) : joinURL(baseURL, page.path),
61
64
  }))
62
65
  }),
63
66
  )