Skip to content

Commit 9aa0b03

Browse files
authored
docs: add PageHeaderLinks and MCP server (#705)
1 parent 030a0ce commit 9aa0b03

File tree

10 files changed

+872
-104
lines changed

10 files changed

+872
-104
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<script setup lang="ts">
2+
import { useClipboard } from '@vueuse/core'
3+
4+
const route = useRoute()
5+
const toast = useToast()
6+
const { copy, copied } = useClipboard()
7+
const site = useSiteConfig()
8+
const isCopying = ref(false)
9+
10+
const mdPath = computed(() => `${site.url}/raw${route.path}.md`)
11+
12+
const items = [
13+
[
14+
{
15+
label: 'Copy Markdown link',
16+
icon: 'i-lucide-link',
17+
onSelect() {
18+
copy(mdPath.value)
19+
toast.add({
20+
title: 'Copied to clipboard',
21+
icon: 'i-lucide-check-circle'
22+
})
23+
}
24+
},
25+
{
26+
label: 'View as Markdown',
27+
icon: 'i-simple-icons:markdown',
28+
target: '_blank',
29+
to: `/raw${route.path}.md`
30+
},
31+
{
32+
label: 'Open in ChatGPT',
33+
icon: 'i-simple-icons:openai',
34+
target: '_blank',
35+
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
36+
},
37+
{
38+
label: 'Open in Claude',
39+
icon: 'i-simple-icons:anthropic',
40+
target: '_blank',
41+
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
42+
}
43+
],
44+
[
45+
{
46+
label: 'Copy MCP Server URL',
47+
icon: 'i-lucide-link',
48+
onSelect() {
49+
copy(`https://hub.nuxt.com/mcp`)
50+
toast.add({
51+
title: 'Copied to clipboard',
52+
icon: 'i-lucide-check-circle'
53+
})
54+
}
55+
},
56+
{
57+
label: 'Add MCP Server',
58+
icon: 'i-simple-icons:cursor',
59+
target: '_blank',
60+
to: `/mcp/deeplink`
61+
}
62+
]
63+
]
64+
65+
async function copyPage() {
66+
isCopying.value = true
67+
copy(await $fetch<string>(`/raw${route.path}.md`))
68+
isCopying.value = false
69+
}
70+
</script>
71+
72+
<template>
73+
<UFieldGroup>
74+
<UButton
75+
label="Copy page"
76+
:icon="copied ? 'i-lucide-clipboard-check' : 'i-lucide-clipboard'"
77+
color="neutral"
78+
variant="soft"
79+
size="sm"
80+
:loading="isCopying"
81+
:ui="{
82+
leadingIcon: 'size-3.5'
83+
}"
84+
@click="copyPage"
85+
/>
86+
<UDropdownMenu
87+
:items="items"
88+
:content="{
89+
align: 'end',
90+
side: 'bottom',
91+
sideOffset: 8
92+
}"
93+
:ui="{
94+
content: 'w-48'
95+
}"
96+
>
97+
<UButton
98+
icon="i-lucide-chevron-down"
99+
size="sm"
100+
color="neutral"
101+
variant="soft"
102+
class="border-l border-muted"
103+
aria-label="Open copy options"
104+
:ui="{
105+
leadingIcon: 'size-3.5'
106+
}"
107+
/>
108+
</UDropdownMenu>
109+
</UFieldGroup>
110+
</template>

docs/app/pages/docs/[...slug].vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,17 @@ import.meta.server && defineOgImageComponent('Docs', {
3636
:ui="{ wrapper: 'lg:mr-10' }"
3737
:title="page.title"
3838
:description="page.description"
39-
:links="page.links"
40-
/>
39+
>
40+
<template #links>
41+
<UButton
42+
v-for="(link, index) in page.links"
43+
:key="index"
44+
v-bind="link"
45+
/>
46+
47+
<PageHeaderLinks />
48+
</template>
49+
</UPageHeader>
4150

4251
<UPageBody prose class="dark:text-gray-300 dark:prose-pre:!bg-gray-800/60 lg:pr-10 pb-0">
4352
<ContentRenderer v-if="page.body" :value="page" />

docs/nuxt.config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export default defineNuxtConfig({
77
'@nuxt/scripts',
88
'@vueuse/nuxt',
99
'nuxt-og-image',
10-
'nuxt-llms'
10+
'nuxt-llms',
11+
'@nuxtjs/mcp-toolkit'
1112
],
1213

1314
$production: {
@@ -70,6 +71,9 @@ export default defineNuxtConfig({
7071
'/docs/guides/drizzle': { redirect: { statusCode: 301, to: '/docs/features/database' } },
7172
'/docs/recipes/pre-rendering': { redirect: { statusCode: 301, to: '/docs/guides/pre-rendering' } }
7273
},
74+
experimental: {
75+
asyncContext: true
76+
},
7377
compatibilityDate: '2025-02-11',
7478
nitro: {
7579
prerender: {
@@ -90,5 +94,8 @@ export default defineNuxtConfig({
9094
title: 'NuxtHub Complete Documentation',
9195
description: 'The complete NuxtHub documentation, blog posts and changelog written in Markdown (MDC syntax).'
9296
}
97+
},
98+
mcp: {
99+
name: 'NuxtHub Documentation'
93100
}
94101
})

docs/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@nuxt/image": "^2.0.0",
2222
"@nuxt/scripts": "^0.13.0",
2323
"@nuxt/ui": "^4.2.1",
24+
"@nuxtjs/mcp-toolkit": "0.5.2",
2425
"@tsparticles/engine": "^3.9.1",
2526
"@tsparticles/slim": "^3.9.1",
2627
"@vueuse/core": "^14.1.0",
@@ -29,6 +30,7 @@
2930
"nuxt": "^4.2.1",
3031
"nuxt-llms": "^0.1.3",
3132
"nuxt-og-image": "^5.1.12",
32-
"tailwindcss": "^4.1.17"
33+
"tailwindcss": "^4.1.17",
34+
"zod": "^4.1.13"
3335
}
3436
}

docs/server/mcp/tools/get-page.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { z } from 'zod'
2+
import { queryCollection } from '@nuxt/content/server'
3+
4+
export default defineMcpTool({
5+
description: `Retrieves the full content and details of a specific documentation page.
6+
7+
WHEN TO USE: Use this tool when you know the EXACT path to a documentation page. Common use cases:
8+
- User asks for a specific page: "Show me the getting started guide" → /getting-started
9+
- User asks about a known topic with a dedicated page
10+
- You found a relevant path from list-pages and want the full content
11+
- User references a specific section or guide they want to read
12+
13+
WHEN NOT TO USE: If you don't know the exact path and need to search/explore, use list-pages first.
14+
15+
WORKFLOW: This tool returns the complete page content including title, description, and full markdown. Use this when you need to provide detailed answers or code examples from specific documentation pages.`,
16+
inputSchema: {
17+
path: z.string().describe('The page path from list-pages or provided by the user (e.g., /getting-started/installation)')
18+
},
19+
cache: '1h',
20+
handler: async ({ path }) => {
21+
const event = useEvent()
22+
const url = getRequestURL(event)
23+
const siteUrl = import.meta.dev ? `${url.protocol}//${url.hostname}:${url.port}` : url.origin
24+
25+
try {
26+
const page = await queryCollection(event, 'docs')
27+
.where('path', '=', path)
28+
.select('title', 'path', 'description')
29+
.first()
30+
31+
if (!page) {
32+
return {
33+
content: [{ type: 'text', text: 'Page not found' }],
34+
isError: true
35+
}
36+
}
37+
38+
const content = await $fetch<string>(`/raw${path}.md`, {
39+
baseURL: siteUrl
40+
})
41+
42+
const result = {
43+
title: page.title,
44+
path: page.path,
45+
description: page.description,
46+
content,
47+
url: `${siteUrl}${page.path}`
48+
}
49+
50+
return {
51+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
52+
}
53+
} catch {
54+
return {
55+
content: [{ type: 'text', text: 'Failed to get page' }],
56+
isError: true
57+
}
58+
}
59+
}
60+
})
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { queryCollection } from '@nuxt/content/server'
2+
3+
export default defineMcpTool({
4+
description: `Lists all available documentation pages with their categories and basic information.
5+
6+
WHEN TO USE: Use this tool when you need to EXPLORE or SEARCH for documentation about a topic but don't know the exact page path. Common scenarios:
7+
- "Find documentation about markdown features" - explore available guides
8+
- "Show me all getting started guides" - browse introductory content
9+
- "Search for advanced configuration options" - find specific topics
10+
- User asks general questions without specifying exact pages
11+
- You need to understand the overall documentation structure
12+
13+
WHEN NOT TO USE: If you already know the specific page path (e.g., "/getting-started/installation"), use get-page directly instead.
14+
15+
WORKFLOW: This tool returns page titles, descriptions, and paths. After finding relevant pages, use get-page to retrieve the full content of specific pages that match the user's needs.
16+
17+
OUTPUT: Returns a structured list with:
18+
- title: Human-readable page name
19+
- path: Exact path for use with get-page
20+
- description: Brief summary of page content
21+
- url: Full URL for reference`,
22+
cache: '1h',
23+
handler: async () => {
24+
const event = useEvent()
25+
const siteUrl = import.meta.dev ? 'http://localhost:4000' : getRequestURL(event).origin
26+
27+
try {
28+
const pages = await queryCollection(event, 'docs')
29+
.select('title', 'path', 'description')
30+
.all()
31+
32+
const result = pages.map(page => ({
33+
title: page.title,
34+
path: page.path,
35+
description: page.description,
36+
url: `${siteUrl}${page.path}`
37+
}))
38+
39+
return {
40+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
41+
}
42+
} catch {
43+
return {
44+
content: [{ type: 'text', text: 'Failed to list pages' }],
45+
isError: true
46+
}
47+
}
48+
}
49+
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { withLeadingSlash } from 'ufo'
2+
import { stringify } from 'minimark/stringify'
3+
import { queryCollection } from '@nuxt/content/server'
4+
5+
export default eventHandler(async (event) => {
6+
const slug = getRouterParams(event)['slug.md']
7+
if (!slug?.endsWith('.md')) {
8+
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
9+
}
10+
11+
const path = withLeadingSlash(slug.replace('.md', ''))
12+
13+
const page = await queryCollection(event, 'docs').path(path).first()
14+
if (!page) {
15+
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
16+
}
17+
18+
// Add title and description to the top of the page if missing
19+
if (page.body.value[0]?.[0] !== 'h1') {
20+
page.body.value.unshift(['blockquote', {}, page.description])
21+
page.body.value.unshift(['h1', {}, page.title])
22+
}
23+
24+
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
25+
return stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' })
26+
})

docs/server/tsconfig.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

docs/tsconfig.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
{
2-
// https://nuxt.com/docs/guide/concepts/typescript
3-
"extends": "./.nuxt/tsconfig.json"
2+
"files": [],
3+
"references": [
4+
{
5+
"path": "./.nuxt/tsconfig.app.json"
6+
},
7+
{
8+
"path": "./.nuxt/tsconfig.server.json"
9+
},
10+
{
11+
"path": "./.nuxt/tsconfig.shared.json"
12+
},
13+
{
14+
"path": "./.nuxt/tsconfig.node.json"
15+
}
16+
]
417
}

0 commit comments

Comments
 (0)