@movk/nuxt-docs 1.13.1 → 1.14.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.
Files changed (43) hide show
  1. package/app/app.config.ts +1 -1
  2. package/app/assets/css/main.css +16 -0
  3. package/app/assets/icons/LICENSE +14 -0
  4. package/app/assets/icons/ai.svg +1 -0
  5. package/app/components/OgImage/Nuxt.vue +2 -4
  6. package/app/components/content/CommitChangelog.vue +8 -3
  7. package/app/components/content/ComponentEmits.vue +1 -1
  8. package/app/components/content/ComponentExample.vue +98 -72
  9. package/app/components/content/ComponentProps.vue +3 -3
  10. package/app/components/content/ComponentPropsSchema.vue +1 -1
  11. package/app/components/content/ComponentSlots.vue +1 -1
  12. package/app/components/content/HighlightInlineType.vue +1 -1
  13. package/app/components/content/PageLastCommit.vue +6 -5
  14. package/app/components/header/HeaderLogo.vue +1 -1
  15. package/app/composables/cachedParseMarkdown.ts +12 -0
  16. package/app/composables/fetchComponentExample.ts +5 -22
  17. package/app/composables/fetchComponentMeta.ts +5 -22
  18. package/app/mdc.config.ts +12 -0
  19. package/app/pages/docs/[...slug].vue +8 -2
  20. package/app/templates/releases.vue +3 -1
  21. package/app/types/index.d.ts +1 -1
  22. package/app/utils/shiki-transformer-icon-highlight.ts +89 -0
  23. package/app/workers/prettier.js +26 -17
  24. package/modules/ai-chat/index.ts +1 -1
  25. package/modules/component-example.ts +65 -30
  26. package/modules/config.ts +24 -1
  27. package/modules/css.ts +1 -1
  28. package/nuxt.config.ts +40 -2
  29. package/nuxt.schema.ts +4 -4
  30. package/package.json +17 -17
  31. package/server/api/component-example.get.ts +5 -5
  32. package/server/api/github/{commits.get.ts → commits.json.get.ts} +7 -4
  33. package/server/api/github/{last-commit.get.ts → last-commit.json.get.ts} +12 -9
  34. package/server/api/github/releases.json.get.ts +2 -2
  35. package/server/mcp/resources/documentation-pages.ts +26 -0
  36. package/server/mcp/resources/examples.ts +17 -0
  37. package/server/mcp/tools/get-example.ts +1 -1
  38. package/server/mcp/tools/list-examples.ts +4 -8
  39. package/server/mcp/tools/list-getting-started-guides.ts +29 -0
  40. package/server/routes/raw/[...slug].md.get.ts +3 -5
  41. package/server/utils/stringifyMinimark.ts +345 -0
  42. package/server/utils/transformMDC.ts +14 -5
  43. package/utils/meta.ts +1 -1
package/app/app.config.ts CHANGED
@@ -96,7 +96,7 @@ export default defineAppConfig({
96
96
  },
97
97
  icons: {
98
98
  loading: 'i-lucide-loader',
99
- trigger: 'i-lucide-sparkles',
99
+ trigger: 'i-custom-ai',
100
100
  explain: 'i-lucide-brain',
101
101
  close: 'i-lucide-x',
102
102
  clearChat: 'i-lucide-trash-2',
@@ -22,3 +22,19 @@
22
22
  :root {
23
23
  --ui-container: var(--container-8xl);
24
24
  }
25
+
26
+ /* Shiki icon highlight transformer styles */
27
+ .shiki-icon-highlight {
28
+ display: inline-block;
29
+ width: 1.25em;
30
+ height: 1.25em;
31
+ vertical-align: -0.25em;
32
+ margin-right: 0.125em;
33
+ background-color: var(--ui-text-highlighted);
34
+ -webkit-mask-repeat: no-repeat;
35
+ mask-repeat: no-repeat;
36
+ -webkit-mask-size: 100% 100%;
37
+ mask-size: 100% 100%;
38
+ -webkit-mask-image: var(--shiki-icon-url);
39
+ mask-image: var(--shiki-icon-url);
40
+ }
@@ -0,0 +1,14 @@
1
+ Copyright © Nucleo
2
+
3
+ Version 1.3, January 3, 2024
4
+
5
+ Nucleo Icons
6
+
7
+ https://nucleoapp.com/
8
+
9
+ - Redistribution of icons is prohibited.
10
+ - Icons are restricted for use only within the product they are bundled with.
11
+
12
+ For more details:
13
+
14
+ https://nucleoapp.com/license
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" id="artificial-intelligence" aria-hidden="true" viewBox="0 0 20 20"><title>artificial-intelligence</title><g fill="currentColor"><polyline fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" points="10.5 15 7 5 6 5 2.5 15"/><line x1="9.586" x2="3.414" y1="12.5" y2="12.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><polyline fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" points="13.5 9 15.5 9 15.5 15"/><line x1="13.5" x2="17.5" y1="15" y2="15" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><path fill="currentColor" stroke-width="0" d="m17.6527,3.9647l-1.2005-.4503-.4506-1.2005c-.1562-.4185-.8468-.4185-1.0031,0l-.4506,1.2005-1.2005.4503c-.2086.0785-.3474.2783-.3474.5015s.1388.4231.3474.5015l1.2005.4503.4506,1.2005c.0781.2093.2783.3477.5015.3477s.4234-.1385.5015-.3477l.4506-1.2005,1.2005-.4503c.2086-.0785.3474-.2783.3474-.5015s-.1388-.4231-.3474-.5015h0Z"/></g></svg>
@@ -3,16 +3,14 @@
3
3
  * @credits NuxtLabs <https://nuxtlabs.com/>
4
4
  * @see https://github.com/nuxt/nuxt.com/blob/main/components/OgImage/Docs.vue
5
5
  */
6
- import { computed } from 'vue'
7
-
8
6
  const {
9
7
  title = 'title',
10
8
  description = 'description',
11
9
  headline = 'headline'
12
10
  } = defineProps<{ title?: string, description?: string, headline?: string }>()
13
11
 
14
- const computedTitle = computed(() => (title || '').slice(0, 60))
15
- const computedDescription = computed(() => (description || '').slice(0, 200))
12
+ const computedTitle = (title || '').slice(0, 60)
13
+ const computedDescription = (description || '').slice(0, 200)
16
14
  </script>
17
15
 
18
16
  <template>
@@ -91,12 +91,17 @@ const filePath = computed(() => {
91
91
  return `${basePath}/${filePrefix}${transformedName}.${fileExtension}`
92
92
  })
93
93
 
94
- const { data: commits } = await useLazyFetch<Commit[]>('/api/github/commits', {
94
+ const { data: commits } = useLazyFetch<Commit[]>('/api/github/commits.json', {
95
95
  key: `commit-changelog-${props.name ?? routeName.value}-${props.author ?? 'all'}`,
96
- query: { path: [filePath.value], author: props.author }
96
+ query: { path: [filePath.value], author: props.author },
97
+ server: false,
98
+ getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key]
97
99
  })
98
100
 
99
- const { data: releases } = await useLazyFetch<Release[]>('/api/github/releases.json')
101
+ const { data: releases } = useLazyFetch<Release[]>('/api/github/releases.json', {
102
+ server: false,
103
+ getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key]
104
+ })
100
105
 
101
106
  const groupedByRelease = computed<ReleaseGroup[]>(() => {
102
107
  if (!commits.value?.length) return []
@@ -12,7 +12,7 @@ const props = defineProps<{
12
12
  const route = useRoute()
13
13
  const componentName = camelCase(props.slug ?? route.path.split('/').pop() ?? '')
14
14
 
15
- const meta = await fetchComponentMeta(componentName as any)
15
+ const { data: meta } = await useFetchComponentMeta(componentName as any)
16
16
  </script>
17
17
 
18
18
  <template>
@@ -1,11 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import type { ChipProps } from '@nuxt/ui'
3
- import { camelCase } from 'scule'
3
+ import { camelCase, upperFirst } from 'scule'
4
4
  import { hash } from 'ohash'
5
5
  import { useElementSize } from '@vueuse/core'
6
6
  import { get, set } from '#ui/utils'
7
7
 
8
- const { preview = true, source = true, prettier = false, ...props } = defineProps<{
8
+ const props = withDefaults(defineProps<{
9
9
  name: string
10
10
  class?: any
11
11
  /**
@@ -50,6 +50,7 @@ const { preview = true, source = true, prettier = false, ...props } = defineProp
50
50
  * 链接到组件的可变属性列表
51
51
  */
52
52
  options?: Array<{
53
+ type?: string
53
54
  alias?: string
54
55
  name: string
55
56
  label: string
@@ -65,7 +66,21 @@ const { preview = true, source = true, prettier = false, ...props } = defineProp
65
66
  * 是否在包装器上添加 overflow-hidden
66
67
  */
67
68
  overflowHidden?: boolean
68
- }>()
69
+ /**
70
+ * 是否添加 background-elevated 到 wrapper
71
+ */
72
+ elevated?: boolean
73
+ lang?: string
74
+ /**
75
+ * 覆盖用于代码块的文件名
76
+ */
77
+ filename?: string
78
+ }>(), {
79
+ preview: true,
80
+ source: true,
81
+ prettier: false,
82
+ lang: 'vue'
83
+ })
69
84
 
70
85
  const slots = defineSlots<{
71
86
  options(props?: {}): any
@@ -79,7 +94,11 @@ const { width } = useElementSize(el)
79
94
 
80
95
  const camelName = camelCase(props.name)
81
96
 
82
- const data = await fetchComponentExample(camelName)
97
+ const exampleModules = import.meta.glob('~/components/content/examples/**/*.vue')
98
+ const exampleMatch = Object.entries(exampleModules).find(([path]) => path.endsWith(`/${upperFirst(camelName)}.vue`))
99
+ const resolvedComponent = exampleMatch ? defineAsyncComponent(exampleMatch[1] as any) : undefined
100
+
101
+ const { data } = await useFetchComponentExample(camelName)
83
102
 
84
103
  const componentProps = reactive({ ...(props.props || {}) })
85
104
 
@@ -87,7 +106,6 @@ const code = computed(() => {
87
106
  let code = ''
88
107
 
89
108
  if (props.collapse) {
90
- // 构建 code-collapse 的属性
91
109
  const collapseAttrs = typeof props.collapse === 'object'
92
110
  ? Object.entries(props.collapse)
93
111
  .map(([key, value]) => {
@@ -104,8 +122,8 @@ const code = computed(() => {
104
122
  `
105
123
  }
106
124
 
107
- code += `\`\`\`vue ${preview ? '' : ` [${data.pascalName}.vue]`}${props.highlights?.length ? `{${props.highlights.join('-')}}` : ''}
108
- ${data?.code ?? ''}
125
+ code += `\`\`\`${props.lang} ${props.preview ? '' : ` [${props.filename ?? data.value?.pascalName}.${props.lang}]`}${props.highlights?.length ? `{${props.highlights.join('-')}}` : ''}
126
+ ${data.value?.code ?? ''}
109
127
  \`\`\``
110
128
 
111
129
  if (props.collapse) {
@@ -116,9 +134,9 @@ ${data?.code ?? ''}
116
134
  return code
117
135
  })
118
136
 
119
- const { data: ast } = await useAsyncData(`component-example-${camelName}${hash({ props: componentProps, collapse: props.collapse })}`, async () => {
120
- if (!prettier) {
121
- return parseMarkdown(code.value)
137
+ const { data: ast } = useAsyncData(`component-example-${camelName}${hash({ props: componentProps, collapse: props.collapse })}`, async () => {
138
+ if (!props.prettier) {
139
+ return cachedParseMarkdown(code.value)
122
140
  }
123
141
 
124
142
  let formatted = ''
@@ -133,8 +151,8 @@ const { data: ast } = await useAsyncData(`component-example-${camelName}${hash({
133
151
  formatted = code.value
134
152
  }
135
153
 
136
- return parseMarkdown(formatted)
137
- }, { watch: [code] })
154
+ return cachedParseMarkdown(formatted)
155
+ }, { lazy: import.meta.client, watch: [code] })
138
156
 
139
157
  const optionsValues = ref(props.options?.reduce((acc, option) => {
140
158
  if (option.name) {
@@ -167,72 +185,80 @@ const urlSearchParams = computed(() => {
167
185
  <template>
168
186
  <div ref="el" class="my-5" :style="{ '--ui-header-height': '4rem' }">
169
187
  <template v-if="preview">
170
- <div class="border border-muted relative z-1" :class="{ 'border-b-0 rounded-t-md': source, 'rounded-md': !source, 'overflow-hidden': props.overflowHidden }">
171
- <div v-if="props.options?.length || !!slots.options" class="flex gap-4 p-4 border-b border-muted">
172
- <slot name="options" />
173
-
174
- <UFormField
175
- v-for="option in props.options"
176
- :key="option.name"
177
- :label="option.label"
178
- :name="option.name"
179
- size="sm"
180
- class="inline-flex ring ring-accented rounded-sm"
181
- :ui="{
182
- wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
183
- label: 'text-muted px-2 py-1.5',
184
- container: 'mt-0'
185
- }"
186
- >
187
- <USelectMenu
188
- v-if="option.items?.length"
189
- :model-value="get(optionsValues, option.name)"
190
- :items="option.items"
191
- :search-input="false"
192
- :value-key="option.name.toLowerCase().endsWith('color') ? 'value' : undefined"
193
- color="neutral"
194
- variant="soft"
195
- class="rounded-sm rounded-l-none min-w-12"
196
- :multiple="option.multiple"
197
- :class="{ 'pl-6': option.name.toLowerCase().endsWith('color') }"
198
- :ui="{ itemLeadingChip: 'w-2' }"
199
- @update:model-value="set(optionsValues, option.name, $event)"
188
+ <div ref="wrapperContainer" class="relative group/component">
189
+ <div class="border border-muted relative z-1" :class="[{ 'border-b-0 rounded-t-md': props.source, 'rounded-md': !props.source, 'overflow-hidden': props.overflowHidden }]">
190
+ <div v-if="props.options?.length || !!slots.options" class="flex gap-4 p-4 border-b border-muted">
191
+ <slot name="options" />
192
+
193
+ <UFormField
194
+ v-for="option in props.options"
195
+ :key="option.name"
196
+ :label="option.label"
197
+ :name="option.name"
198
+ size="sm"
199
+ class="inline-flex ring ring-accented rounded-sm"
200
+ :ui="{
201
+ wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
202
+ label: 'text-muted px-2 py-1.5',
203
+ container: 'mt-0'
204
+ }"
200
205
  >
201
- <template v-if="option.name.toLowerCase().endsWith('color')" #leading="{ modelValue, ui }">
202
- <UChip
203
- inset
204
- standalone
205
- :color="(modelValue as any)"
206
- :size="(ui.itemLeadingChipSize() as ChipProps['size'])"
207
- class="size-2"
208
- />
209
- </template>
210
- </USelectMenu>
211
- <UInput
212
- v-else
213
- :model-value="get(optionsValues, option.name)"
214
- color="neutral"
215
- variant="soft"
216
- :ui="{ base: 'rounded-sm rounded-l-none min-w-12' }"
217
- @update:model-value="set(optionsValues, option.name, $event)"
218
- />
219
- </UFormField>
220
- </div>
206
+ <USelectMenu
207
+ v-if="option.items?.length"
208
+ :model-value="get(optionsValues, option.name)"
209
+ :items="option.items"
210
+ :search-input="false"
211
+ :value-key="option.name.toLowerCase().endsWith('color') ? 'value' : undefined"
212
+ color="neutral"
213
+ variant="soft"
214
+ class="rounded-sm rounded-l-none min-w-12"
215
+ :multiple="option.multiple"
216
+ :class="[option.name.toLowerCase().endsWith('color') && 'pl-6']"
217
+ :ui="{ itemLeadingChip: 'w-2' }"
218
+ @update:model-value="set(optionsValues, option.name, $event)"
219
+ >
220
+ <template v-if="option.name.toLowerCase().endsWith('color')" #leading="{ modelValue, ui }">
221
+ <UChip
222
+ inset
223
+ standalone
224
+ :color="(modelValue as any)"
225
+ :size="(ui.itemLeadingChipSize() as ChipProps['size'])"
226
+ class="size-2"
227
+ />
228
+ </template>
229
+ </USelectMenu>
230
+ <UInput
231
+ v-else
232
+ :model-value="get(optionsValues, option.name)"
233
+ :type="option.type"
234
+ color="neutral"
235
+ variant="soft"
236
+ :ui="{ base: 'rounded-sm rounded-l-none min-w-12' }"
237
+ @update:model-value="set(optionsValues, option.name, $event)"
238
+ />
239
+ </UFormField>
240
+ </div>
221
241
 
222
- <iframe
223
- v-if="iframe"
224
- v-bind="typeof iframe === 'object' ? iframe : {}"
225
- :src="`/examples/${name}?${urlSearchParams}`"
226
- class="relative w-full"
227
- :class="[props.class, { 'lg:left-1/2 lg:-translate-x-1/2 lg:w-[1024px]': !iframeMobile }]"
228
- />
229
- <div v-else class="flex justify-center p-4" :class="props.class">
230
- <component :is="camelName" v-bind="{ ...componentProps, ...optionsValues }" />
242
+ <iframe
243
+ v-if="iframe"
244
+ v-bind="typeof iframe === 'object' ? iframe : {}"
245
+ :src="`/examples/${name}?${urlSearchParams}`"
246
+ class="relative w-full"
247
+ :class="[props.class, { 'dark:bg-neutral-950/50 rounded-t-md': props.elevated }, !iframeMobile && 'lg:left-1/2 lg:-translate-x-1/2 lg:w-[1024px]']"
248
+ />
249
+ <div
250
+ v-else-if="resolvedComponent"
251
+ ref="componentContainer"
252
+ class="flex justify-center p-4"
253
+ :class="[props.class, { 'dark:bg-neutral-950/50 rounded-t-md': props.elevated }]"
254
+ >
255
+ <component :is="resolvedComponent" v-bind="{ ...componentProps, ...optionsValues }" />
256
+ </div>
231
257
  </div>
232
258
  </div>
233
259
  </template>
234
260
 
235
- <template v-if="source">
261
+ <template v-if="props.source">
236
262
  <div v-if="!!slots.code" class="[&_pre]:rounded-t-none! [&_div.my-5]:mt-0!">
237
263
  <slot name="code" />
238
264
  </div>
@@ -45,14 +45,14 @@ const { ignore = [
45
45
  const route = useRoute()
46
46
  const camelName = camelCase(slug ?? route.path.split('/').pop() ?? '')
47
47
  const componentName = prose ? `Prose${upperFirst(camelName)}` : `${upperFirst(camelName)}`
48
- const meta = await fetchComponentMeta(componentName as any)
48
+ const { data: meta } = await useFetchComponentMeta(componentName as any)
49
49
 
50
50
  const metaProps: ComputedRef<ComponentMeta['props']> = computed(() => {
51
- if (!meta?.meta?.props?.length) {
51
+ if (!meta.value?.meta?.props?.length) {
52
52
  return []
53
53
  }
54
54
 
55
- return meta.meta.props.filter((prop) => {
55
+ return meta.value.meta.props.filter((prop) => {
56
56
  return !ignore?.includes(prop.name)
57
57
  }).map((prop) => {
58
58
  if (prop.default) {
@@ -48,7 +48,7 @@ const schemaProps = computed(() => {
48
48
  </script>
49
49
 
50
50
  <template>
51
- <ProseCollapsible v-if="schemaProps?.length" class="mt-1 mb-0">
51
+ <ProseCollapsible v-if="schemaProps?.length" :unmount-on-hide="true" class="mt-1 mb-0">
52
52
  <ProseUl>
53
53
  <ProseLi v-for="schemaProp in schemaProps" :key="schemaProp.name">
54
54
  <HighlightInlineType :type="`${schemaProp.name}${schemaProp.required === false ? '?' : ''}: ${schemaProp.type}`" />
@@ -12,7 +12,7 @@ const props = defineProps<{
12
12
  const route = useRoute()
13
13
 
14
14
  const componentName = camelCase(props.slug ?? route.path.split('/').pop() ?? '')
15
- const meta = await fetchComponentMeta(componentName as any)
15
+ const { data: meta } = await useFetchComponentMeta(componentName as any)
16
16
  </script>
17
17
 
18
18
  <template>
@@ -24,7 +24,7 @@ const type = computed(() => {
24
24
  const ast = ref<any>(null)
25
25
 
26
26
  onMounted(async () => {
27
- ast.value = await parseMarkdown(`\`\` ${type.value} \`\`{lang="ts-type"}`)
27
+ ast.value = await cachedParseMarkdown(`\`\` ${type.value} \`\`{lang="ts-type"}`)
28
28
  })
29
29
  </script>
30
30
 
@@ -49,12 +49,12 @@ const filePath = computed(() => {
49
49
  return [rootDir, 'content', `${stem}.${extension}`].filter(Boolean).join('/')
50
50
  })
51
51
 
52
- const { data: commit } = await useFetch<LastCommit | null>('/api/github/last-commit', {
52
+ const { data: commit } = useLazyFetch<LastCommit | null>('/api/github/last-commit.json', {
53
53
  key: `last-commit-${filePath.value}`,
54
54
  query: { path: filePath.value },
55
55
  default: () => null,
56
- lazy: true,
57
- server: false
56
+ server: false,
57
+ getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key]
58
58
  })
59
59
 
60
60
  const commitUrl = computed(() => commit.value?.url ?? '')
@@ -81,6 +81,7 @@ const authorUrl = computed(() => {
81
81
  :src="commit.author.avatar"
82
82
  alt="Author Avatar"
83
83
  size="2xs"
84
+ loading="lazy"
84
85
  />
85
86
  <UBadge color="neutral" variant="outline" size="sm">
86
87
  {{ commit.author.name || commit.author.login }}
@@ -103,7 +104,7 @@ const authorUrl = computed(() => {
103
104
  v-if="commitUrl"
104
105
  :to="commitUrl"
105
106
  target="_blank"
106
- class="hover:opacity-80 transition-opacity max-w-[250px]"
107
+ class="hover:opacity-80 transition-opacity max-w-62.5"
107
108
  >
108
109
  <UBadge
109
110
  color="neutral"
@@ -119,7 +120,7 @@ const authorUrl = computed(() => {
119
120
  color="neutral"
120
121
  variant="outline"
121
122
  size="sm"
122
- class="max-w-[250px] font-mono text-xs"
123
+ class="max-w-62.5 font-mono text-xs"
123
124
  >
124
125
  <span class="truncate">{{ commit.message }}</span>
125
126
  </UBadge>
@@ -4,6 +4,6 @@ const { header } = useAppConfig()
4
4
 
5
5
  <template>
6
6
  <NuxtLink :to="header.to">
7
- <UUser :avatar="{ src: header.avatar, alt: 'Site Logo' }" :name="header.title" />
7
+ <UUser :avatar="{ src: header.avatar, alt: 'Site Logo', loading: 'lazy' }" :name="header.title" />
8
8
  </NuxtLink>
9
9
  </template>
@@ -0,0 +1,12 @@
1
+ import { markRaw } from 'vue'
2
+
3
+ const _cache = new Map<string, any>()
4
+
5
+ export async function cachedParseMarkdown(markdown: string) {
6
+ const cached = _cache.get(markdown)
7
+ if (cached) return cached
8
+
9
+ const result = markRaw(await parseMarkdown(markdown))
10
+ _cache.set(markdown, result)
11
+ return result
12
+ }
@@ -1,17 +1,4 @@
1
- const useComponentExampleState = () => useState<Record<string, any>>('component-example-state', () => ({}))
2
-
3
- export async function fetchComponentExample(name: string) {
4
- const state = useComponentExampleState()
5
-
6
- if (state.value[name]?.then) {
7
- await state.value[name]
8
- return state.value[name]
9
- }
10
- if (state.value[name]) {
11
- return state.value[name]
12
- }
13
-
14
- // Add to nitro prerender
1
+ export function useFetchComponentExample(name: string) {
15
2
  if (import.meta.server) {
16
3
  const event = useRequestEvent()
17
4
  event?.node.res.setHeader(
@@ -20,13 +7,9 @@ export async function fetchComponentExample(name: string) {
20
7
  )
21
8
  }
22
9
 
23
- // Store promise to avoid multiple calls
24
- state.value[name] = $fetch(`/api/component-example/${name}.json`).then((data) => {
25
- state.value[name] = data
26
- }).catch(() => {
27
- state.value[name] = {}
10
+ return useAsyncData(`component-example-${name}`, () => $fetch(`/api/component-example/${name}.json`).catch(() => ({})), {
11
+ lazy: import.meta.client,
12
+ dedupe: 'defer',
13
+ getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key]
28
14
  })
29
-
30
- await state.value[name]
31
- return state.value[name]
32
15
  }
@@ -1,19 +1,6 @@
1
1
  import type { ComponentMeta } from 'vue-component-meta'
2
2
 
3
- const useComponentsMetaState = () => useState<Record<string, any>>('component-meta-state', () => ({}))
4
-
5
- export async function fetchComponentMeta(name: string): Promise<{ meta: ComponentMeta }> {
6
- const state = useComponentsMetaState()
7
-
8
- if (state.value[name]?.then) {
9
- await state.value[name]
10
- return state.value[name]
11
- }
12
- if (state.value[name]) {
13
- return state.value[name]
14
- }
15
-
16
- // Add to nitro prerender
3
+ export function useFetchComponentMeta(name: string) {
17
4
  if (import.meta.server) {
18
5
  const event = useRequestEvent()
19
6
  event?.node.res.setHeader(
@@ -22,13 +9,9 @@ export async function fetchComponentMeta(name: string): Promise<{ meta: Componen
22
9
  )
23
10
  }
24
11
 
25
- // Store promise to avoid multiple calls
26
- state.value[name] = $fetch(`/api/component-meta/${name}.json`).then((meta) => {
27
- state.value[name] = meta
28
- }).catch(() => {
29
- state.value[name] = {}
12
+ return useAsyncData<{ meta: ComponentMeta }>(`component-meta-${name}`, () => $fetch(`/api/component-meta/${name}.json`).catch(() => ({}) as any), {
13
+ lazy: import.meta.client,
14
+ dedupe: 'defer',
15
+ getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key]
30
16
  })
31
-
32
- await state.value[name]
33
- return state.value[name]
34
17
  }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from '@nuxtjs/mdc/config'
2
+ import { transformerColorHighlight } from 'shiki-transformer-color-highlight'
3
+ import { transformerIconHighlight } from './utils/shiki-transformer-icon-highlight'
4
+
5
+ export default defineConfig({
6
+ shiki: {
7
+ transformers: [
8
+ transformerColorHighlight(),
9
+ transformerIconHighlight()
10
+ ]
11
+ }
12
+ })
@@ -104,7 +104,8 @@ useSeoMeta({
104
104
 
105
105
  defineOgImageComponent('Nuxt', {
106
106
  title,
107
- description
107
+ description,
108
+ headline: breadcrumb.value?.[breadcrumb.value.length - 1]?.label || 'Movk Nuxt Docs'
108
109
  })
109
110
  </script>
110
111
 
@@ -137,7 +138,12 @@ defineOgImageComponent('Nuxt', {
137
138
  v-bind="link"
138
139
  >
139
140
  <template v-if="link.avatar" #leading>
140
- <UAvatar v-bind="link.avatar" size="2xs" :alt="`${link.label} avatar`" />
141
+ <UAvatar
142
+ v-bind="link.avatar"
143
+ size="2xs"
144
+ :alt="`${link.label} avatar`"
145
+ loading="lazy"
146
+ />
141
147
  </template>
142
148
  </UButton>
143
149
  <PageHeaderLinks />
@@ -22,8 +22,10 @@ defineOgImageComponent('Nuxt', {
22
22
  })
23
23
 
24
24
  const { data: versions } = page.value.releases
25
- ? await useFetch(page.value.releases, {
25
+ ? useLazyFetch(page.value.releases, {
26
+ key: 'releases-markdown',
26
27
  server: false,
28
+ getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key],
27
29
  transform: (data: {
28
30
  releases: {
29
31
  name?: string
@@ -179,7 +179,7 @@ declare module 'nuxt/schema' {
179
179
  loading: string
180
180
  /**
181
181
  * AI 聊天触发按钮和滑出层头部的图标。
182
- * @default 'i-lucide-sparkles'
182
+ * @default 'i-custom-ai'
183
183
  */
184
184
  trigger: string
185
185
  /**