@movk/nuxt-docs 1.6.1 → 1.7.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 (40) hide show
  1. package/app/app.config.ts +37 -0
  2. package/app/app.vue +8 -3
  3. package/app/components/DocsAsideRightBottom.vue +17 -22
  4. package/app/components/PageHeaderLinks.vue +6 -1
  5. package/app/components/content/PageLastCommit.vue +5 -5
  6. package/app/components/header/Header.vue +1 -1
  7. package/app/components/header/HeaderBody.vue +12 -2
  8. package/app/components/header/HeaderBottom.vue +1 -0
  9. package/app/components/header/HeaderCTA.vue +2 -2
  10. package/app/components/header/HeaderCenter.vue +1 -1
  11. package/app/components/header/HeaderLogo.vue +1 -1
  12. package/app/layouts/default.vue +3 -1
  13. package/app/layouts/docs.vue +1 -1
  14. package/app/pages/docs/[...slug].vue +3 -2
  15. package/app/templates/releases.vue +98 -0
  16. package/app/types/index.d.ts +149 -0
  17. package/content.config.ts +24 -2
  18. package/modules/ai-chat/index.ts +75 -24
  19. package/modules/ai-chat/runtime/components/AiChat.vue +4 -10
  20. package/modules/ai-chat/runtime/components/AiChatDisabled.vue +3 -0
  21. package/modules/ai-chat/runtime/components/AiChatFloatingInput.vue +24 -9
  22. package/modules/ai-chat/runtime/components/AiChatModelSelect.vue +2 -0
  23. package/modules/ai-chat/runtime/components/AiChatPanel.vue +318 -0
  24. package/modules/ai-chat/runtime/components/AiChatPreStream.vue +1 -0
  25. package/modules/ai-chat/runtime/components/AiChatReasoning.vue +3 -3
  26. package/modules/ai-chat/runtime/components/AiChatSlideoverFaq.vue +2 -5
  27. package/modules/ai-chat/runtime/composables/useAIChat.ts +48 -0
  28. package/modules/ai-chat/runtime/composables/useModels.ts +3 -6
  29. package/modules/ai-chat/runtime/server/api/ai-chat.ts +92 -0
  30. package/modules/ai-chat/runtime/server/utils/docs_agent.ts +23 -15
  31. package/modules/ai-chat/runtime/types.ts +6 -0
  32. package/modules/css.ts +3 -2
  33. package/modules/routing.ts +26 -0
  34. package/nuxt.config.ts +2 -0
  35. package/nuxt.schema.ts +493 -0
  36. package/package.json +11 -9
  37. package/app/composables/useFaq.ts +0 -21
  38. package/modules/ai-chat/runtime/components/AiChatSlideover.vue +0 -255
  39. package/modules/ai-chat/runtime/server/api/search.ts +0 -84
  40. /package/{app → modules/ai-chat/runtime}/composables/useHighlighter.ts +0 -0
package/app/app.config.ts CHANGED
@@ -70,5 +70,42 @@ export default defineAppConfig({
70
70
  timeZone: 'Asia/Shanghai'
71
71
  }
72
72
  }
73
+ },
74
+ aiChat: {
75
+ floatingInput: true,
76
+ explainWithAi: true,
77
+ shortcuts: {
78
+ focusInput: 'meta_i'
79
+ },
80
+ texts: {
81
+ title: 'AI 助手',
82
+ collapse: '折叠',
83
+ expand: '展开',
84
+ clearChat: '清除聊天记录',
85
+ close: '关闭',
86
+ loading: 'Loading...',
87
+ askAnything: '问我任何事情...',
88
+ askMeAnythingDescription: '我可以帮助您浏览文档、解释概念并回答您的问题。',
89
+ faq: 'FAQ 建议',
90
+ placeholder: '输入你的问题...',
91
+ lineBreak: '换行',
92
+ trigger: '与 AI 聊天',
93
+ streaming: '思考中...',
94
+ streamed: '思考过程',
95
+ explainWithAi: '用 AI 解释此页面'
96
+ },
97
+ icons: {
98
+ loading: 'i-lucide-loader',
99
+ trigger: 'i-lucide-sparkles',
100
+ explain: 'i-lucide-brain',
101
+ close: 'i-lucide-x',
102
+ clearChat: 'i-lucide-trash-2',
103
+ streaming: 'i-lucide-chevron-down',
104
+ providers: {
105
+ mistral: 'i-simple-icons-mistralai',
106
+ kwaipilot: 'i-lucide-wand',
107
+ zai: 'i-lucide-wand'
108
+ }
109
+ }
73
110
  }
74
111
  })
package/app/app.vue CHANGED
@@ -7,6 +7,7 @@ const site = useSiteConfig()
7
7
  const appConfig = useAppConfig()
8
8
  const colorMode = useColorMode()
9
9
  const route = useRoute()
10
+ const { isEnabled: isAiChatEnabled, panelWidth: aiChatPanelWidth, shouldPushContent } = useAIChat()
10
11
 
11
12
  const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs', ['category', 'description']))
12
13
  const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
@@ -48,9 +49,9 @@ provide('navigation', rootNavigation)
48
49
  <Analytics v-if="appConfig.vercelAnalytics" :debug="appConfig.vercelAnalytics?.debug" />
49
50
  <SpeedInsights v-if="appConfig.vercelAnalytics" :debug="appConfig.vercelAnalytics?.debug" />
50
51
 
51
- <div :class="{ root: route.path.startsWith('/docs/') }">
52
+ <div :class="{ root: route.path.startsWith('/docs/') }" :style="{ marginRight: shouldPushContent ? `${aiChatPanelWidth}px` : '0' }">
52
53
  <template v-if="!route.path.startsWith('/examples')">
53
- <Header />
54
+ <Header v-if="$route.meta.header !== false" />
54
55
  </template>
55
56
 
56
57
  <NuxtLayout>
@@ -58,10 +59,14 @@ provide('navigation', rootNavigation)
58
59
  </NuxtLayout>
59
60
 
60
61
  <template v-if="!route.path.startsWith('/examples')">
61
- <Footer />
62
+ <Footer v-if="$route.meta.footer !== false" />
62
63
 
63
64
  <ClientOnly>
64
65
  <LazyUContentSearch :files="files" :navigation="rootNavigation" :fuse="{ resultLimit: 1000 }" />
66
+ <template v-if="isAiChatEnabled">
67
+ <LazyAiChatFloatingInput />
68
+ <LazyAiChatPanel />
69
+ </template>
65
70
  </ClientOnly>
66
71
  </template>
67
72
  </div>
@@ -1,29 +1,24 @@
1
1
  <script setup lang="ts">
2
- const { aiChat } = useRuntimeConfig().public
3
2
  const route = useRoute()
4
-
5
3
  const pageUrl = route.path
6
- const { open } = useAIChat()
7
- const { faqQuestions } = useFaq()
4
+
5
+ const { aiChat } = useAppConfig()
6
+ const { isEnabled, open } = useAIChat()
7
+
8
+ const showExplainWithAi = computed(() => {
9
+ return isEnabled.value && aiChat.explainWithAi !== false
10
+ })
8
11
  </script>
9
12
 
10
13
  <template>
11
- <div v-if="aiChat.enable">
12
- <UButton
13
- icon="i-lucide-brain"
14
- target="_blank"
15
- label="用 AI 解释此页面"
16
- size="sm"
17
- variant="ghost"
18
- color="neutral"
19
- @click="open(`解释此页面 ${pageUrl}`, true)"
20
- />
21
- <AiChatSlideover :faq-questions="faqQuestions" />
22
-
23
- <Teleport to="body">
24
- <ClientOnly>
25
- <LazyAiChatFloatingInput />
26
- </ClientOnly>
27
- </Teleport>
28
- </div>
14
+ <UButton
15
+ v-if="showExplainWithAi"
16
+ :icon="aiChat.icons.explain"
17
+ target="_blank"
18
+ :label="aiChat.texts.explainWithAi"
19
+ size="sm"
20
+ variant="ghost"
21
+ color="neutral"
22
+ @click="open(`解释此页面 ${pageUrl}`, true)"
23
+ />
29
24
  </template>
@@ -106,7 +106,12 @@ async function copyPage() {
106
106
  content: 'w-48'
107
107
  }"
108
108
  >
109
- <UButton :icon="ui.icons.chevronDown" color="neutral" variant="outline" />
109
+ <UButton
110
+ :icon="ui.icons.chevronDown"
111
+ color="neutral"
112
+ variant="outline"
113
+ aria-label="Toggle Dropdown"
114
+ />
110
115
  </UDropdownMenu>
111
116
  </UFieldGroup>
112
117
  </template>
@@ -67,9 +67,9 @@ const authorUrl = computed(() => {
67
67
 
68
68
  <template>
69
69
  <div v-if="commit" class="flex items-center flex-wrap gap-1.5 text-sm text-muted mt-2">
70
- <span class="text-dimmed">最后更新于</span>
70
+ <span class="text-muted">最后更新于</span>
71
71
  <time class="font-medium text-default" :datetime="commit.date">{{ commit.dateFormatted }}</time>
72
- <span class="text-dimmed">由</span>
72
+ <span class="text-muted">由</span>
73
73
  <ULink
74
74
  v-if="authorUrl"
75
75
  :to="authorUrl"
@@ -79,7 +79,7 @@ const authorUrl = computed(() => {
79
79
  <UAvatar
80
80
  v-if="showAvatar && commit.author.avatar"
81
81
  :src="commit.author.avatar"
82
- :alt="commit.author.name"
82
+ alt="Author Avatar"
83
83
  size="2xs"
84
84
  />
85
85
  <UBadge color="neutral" variant="outline" size="sm">
@@ -90,7 +90,7 @@ const authorUrl = computed(() => {
90
90
  <UAvatar
91
91
  v-if="showAvatar && commit.author.avatar"
92
92
  :src="commit.author.avatar"
93
- :alt="commit.author.name"
93
+ alt="Author Avatar"
94
94
  size="2xs"
95
95
  />
96
96
  <UBadge color="neutral" variant="outline" size="sm">
@@ -98,7 +98,7 @@ const authorUrl = computed(() => {
98
98
  </UBadge>
99
99
  </span>
100
100
  <template v-if="showMessage && commit.message">
101
- <span class="text-dimmed">提交</span>
101
+ <span class="text-muted">提交</span>
102
102
  <ULink
103
103
  v-if="commitUrl"
104
104
  :to="commitUrl"
@@ -18,7 +18,7 @@ const links = computed<ButtonProps[]>(() => github && github.url
18
18
  </script>
19
19
 
20
20
  <template>
21
- <UHeader :ui="{ left: 'min-w-0' }" class="flex flex-col">
21
+ <UHeader :ui="{ left: 'min-w-0' }" class="flex flex-col" aria-label="Site Header">
22
22
  <template #left>
23
23
  <HeaderLogo />
24
24
  </template>
@@ -9,11 +9,21 @@ const { navigationByCategory } = useNavigation(navigation!)
9
9
  </script>
10
10
 
11
11
  <template>
12
- <UNavigationMenu orientation="vertical" :items="mobileLinks" class="-mx-2.5" />
12
+ <UNavigationMenu
13
+ orientation="vertical"
14
+ :items="mobileLinks"
15
+ class="-mx-2.5"
16
+ aria-label="Mobile Navigation"
17
+ />
13
18
 
14
19
  <template v-if="route.path.startsWith('/docs/')">
15
20
  <USeparator type="dashed" class="mt-4 mb-6" />
16
21
 
17
- <UContentNavigation :navigation="navigationByCategory" highlight :ui="{ linkTrailingBadge: 'font-semibold uppercase' }" />
22
+ <UContentNavigation
23
+ :navigation="navigationByCategory"
24
+ highlight
25
+ :ui="{ linkTrailingBadge: 'font-semibold uppercase' }"
26
+ aria-label="Documentation Navigation"
27
+ />
18
28
  </template>
19
29
  </template>
@@ -21,6 +21,7 @@ const items = computed(() => mapContentNavigation(navigation?.value?.map(item =>
21
21
  variant="pill"
22
22
  highlight
23
23
  class="-mx-2.5 -mb-px"
24
+ aria-label="Category Navigation"
24
25
  />
25
26
  </UContainer>
26
27
  </template>
@@ -1,10 +1,10 @@
1
1
  <script lang="ts" setup>
2
- const { aiChat } = useRuntimeConfig().public
3
2
  const route = useRoute()
3
+ const { isEnabled: isAiChatEnabled } = useAIChat()
4
4
  </script>
5
5
 
6
6
  <template>
7
- <div v-if="aiChat.enable" class="hidden md:block">
7
+ <div v-if="isAiChatEnabled" class="hidden md:block">
8
8
  <UButton
9
9
  v-if="route.path === '/'"
10
10
  to="/docs"
@@ -3,5 +3,5 @@ const { desktopLinks } = useHeader()
3
3
  </script>
4
4
 
5
5
  <template>
6
- <UNavigationMenu :items="desktopLinks" variant="link" />
6
+ <UNavigationMenu :items="desktopLinks" variant="link" aria-label="Main Navigation" />
7
7
  </template>
@@ -4,6 +4,6 @@ const { header } = useAppConfig()
4
4
 
5
5
  <template>
6
6
  <NuxtLink :to="header.to">
7
- <UUser :avatar="{ src: header.avatar }" :name="header.title" />
7
+ <UUser :avatar="{ src: header.avatar, alt: 'Site Logo' }" :name="header.title" />
8
8
  </NuxtLink>
9
9
  </template>
@@ -1,3 +1,5 @@
1
1
  <template>
2
- <slot />
2
+ <UMain>
3
+ <slot />
4
+ </UMain>
3
5
  </template>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <UMain class="relative">
2
+ <UMain class="relative" as="main">
3
3
  <HeroBackground />
4
4
 
5
5
  <UContainer>
@@ -17,6 +17,7 @@ if (!page.value) {
17
17
  }
18
18
 
19
19
  const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
20
+ const { shouldPushContent: shouldHideToc } = useAIChat()
20
21
 
21
22
  const { data: surround } = await useAsyncData(`surround-${(kebabCase(route.path))}`, () => {
22
23
  return queryCollectionItemSurroundings('docs', route.path, {
@@ -93,7 +94,7 @@ defineOgImageComponent('Nuxt', {
93
94
  </script>
94
95
 
95
96
  <template>
96
- <UPage v-if="page">
97
+ <UPage v-if="page" :key="`page-${shouldHideToc}`">
97
98
  <UPageHeader :title="title">
98
99
  <template #headline>
99
100
  <UBreadcrumb :items="breadcrumb" />
@@ -136,7 +137,7 @@ defineOgImageComponent('Nuxt', {
136
137
  <UContentSurround :surround="surround" />
137
138
  </UPageBody>
138
139
 
139
- <template v-if="page?.body?.toc?.links?.length" #right>
140
+ <template v-if="page?.body?.toc?.links?.length && !shouldHideToc" #right>
140
141
  <UContentToc
141
142
  :title="toc?.title"
142
143
  :links="page.body?.toc?.links"
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+ import type { ButtonProps } from '@nuxt/ui'
3
+
4
+ const { data: page } = await useAsyncData('releases', () => queryCollection('releases').first())
5
+ if (!page.value) {
6
+ throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
7
+ }
8
+
9
+ const title = page.value.seo?.title || page.value.title
10
+ const description = page.value.seo?.description || page.value.description
11
+
12
+ useSeoMeta({
13
+ title,
14
+ description,
15
+ ogTitle: title,
16
+ ogDescription: description
17
+ })
18
+
19
+ defineOgImageComponent('Nuxt', {
20
+ title,
21
+ description
22
+ })
23
+
24
+ const { data: versions } = await useFetch(page.value.releases || '', {
25
+ server: false,
26
+ transform: (data: {
27
+ releases: {
28
+ name?: string
29
+ tag: string
30
+ publishedAt: string
31
+ markdown: string
32
+ }[]
33
+ }) => {
34
+ return data.releases.map(release => ({
35
+ tag: release.tag,
36
+ title: release.name || release.tag,
37
+ date: release.publishedAt,
38
+ markdown: release.markdown
39
+ }))
40
+ }
41
+ })
42
+ </script>
43
+
44
+ <template>
45
+ <main v-if="page">
46
+ <UPageHero
47
+ :title="page.hero.title"
48
+ :description="page.hero.description"
49
+ :links="(page.hero.links as ButtonProps[]) || []"
50
+ class="md:border-b border-default"
51
+ :ui="{ container: 'relative py-10 sm:py-16 lg:py-24' }"
52
+ >
53
+ <template #top>
54
+ <div class="absolute z-[-1] rounded-full bg-primary blur-[300px] size-60 sm:size-80 transform -translate-x-1/2 left-1/2 -translate-y-80" />
55
+ </template>
56
+
57
+ <LazyStarsBg />
58
+
59
+ <div aria-hidden="true" class="hidden md:block absolute z-[-1] border-x border-default inset-0 mx-4 sm:mx-6 lg:mx-8" />
60
+ </UPageHero>
61
+
62
+ <UPageSection :ui="{ container: 'py-0!' }">
63
+ <div class="py-4 md:py-8 lg:py-16 md:border-x border-default">
64
+ <UContainer class="max-w-5xl">
65
+ <UChangelogVersions
66
+ as="main"
67
+ :indicator-motion="false"
68
+ :ui="{
69
+ root: 'py-16 sm:py-24 lg:py-32',
70
+ indicator: 'inset-y-0'
71
+ }"
72
+ >
73
+ <UChangelogVersion
74
+ v-for="version in versions"
75
+ :key="version.tag"
76
+ v-bind="version"
77
+ :ui="{
78
+ root: 'flex items-start',
79
+ container: 'max-w-xl',
80
+ header: 'border-b border-default pb-4',
81
+ title: 'text-3xl',
82
+ date: 'text-xs/9 text-highlighted font-mono',
83
+ indicator: 'sticky top-0 pt-16 -mt-16 sm:pt-24 sm:-mt-24 lg:pt-32 lg:-mt-32'
84
+ }"
85
+ >
86
+ <template #body>
87
+ <MDC
88
+ v-if="version.markdown"
89
+ :value="version.markdown"
90
+ />
91
+ </template>
92
+ </UChangelogVersion>
93
+ </UChangelogVersions>
94
+ </UContainer>
95
+ </div>
96
+ </UPageSection>
97
+ </main>
98
+ </template>
@@ -61,6 +61,155 @@ declare module 'nuxt/schema' {
61
61
  options?: Intl.DateTimeFormatOptions
62
62
  }
63
63
  } | false
64
+ aiChat: {
65
+ /**
66
+ * 在文档页面底部显示浮动输入。
67
+ * @default true
68
+ */
69
+ floatingInput: boolean
70
+ /**
71
+ * 在文档侧边栏中显示“使用 AI 进行解释”按钮。
72
+ * @default true
73
+ */
74
+ explainWithAi?: boolean
75
+ /**
76
+ * 显示的常见问题解答问题。
77
+ * @example 简单格式: ['如何安装?', '如何配置?']
78
+ * @example 分类格式: [{ category: '入门', items: ['如何安装?'] }]
79
+ */
80
+ faqQuestions?: FaqQuestions
81
+ /**
82
+ * 键盘快捷键配置。
83
+ */
84
+ shortcuts: {
85
+ /**
86
+ * 快捷键,用于聚焦浮动输入框。
87
+ * @default 'meta_i'
88
+ */
89
+ focusInput: string
90
+ }
91
+ /**
92
+ * 文本配置。
93
+ */
94
+ texts: {
95
+ /**
96
+ * AI 聊天面板的标题文本。
97
+ * @default 'AI 助手'
98
+ */
99
+ title: string
100
+ /**
101
+ * 折叠按钮的文本。
102
+ * @default '折叠‘
103
+ */
104
+ collapse: string
105
+ /**
106
+ * 展开按钮的文本。
107
+ * @default '展开'
108
+ */
109
+ expand: string
110
+ /**
111
+ * 清除聊天记录按钮的文本。
112
+ * @default '清除聊天记录'
113
+ */
114
+ clearChat: string
115
+ /**
116
+ * 关闭按钮的文本。
117
+ * @default '关闭'
118
+ */
119
+ close: string
120
+ /**
121
+ * 加载时的提示文本。
122
+ * @default 'Loading...'
123
+ */
124
+ loading: string
125
+ /**
126
+ * 询问任何事情文本
127
+ * @default '问我任何事情...'
128
+ */
129
+ askAnything: string
130
+ /**
131
+ * 询问任何事情描述文本
132
+ * @default '我可以帮助您浏览文档、解释概念并回答您的问题。'
133
+ */
134
+ askMeAnythingDescription: string
135
+ /**
136
+ * FAQ 建议标题文本。
137
+ * @default 'FAQ 建议'
138
+ */
139
+ faq: string
140
+ /**
141
+ * 浮动输入框的占位符文本。
142
+ * @default '输入你的问题...'
143
+ */
144
+ placeholder: string
145
+ /**
146
+ * 换行的提示文本。
147
+ * @default '换行'
148
+ */
149
+ lineBreak: string
150
+ /**
151
+ * AI 聊天面板触发按钮的提示文本。
152
+ * @default '与 AI 聊天'
153
+ */
154
+ trigger: string
155
+ /**
156
+ * 思考时的提示文本。
157
+ * @default '思考中...'
158
+ */
159
+ streaming: string
160
+ /**
161
+ * 思考后的提示文本。
162
+ * @default '思考过程'
163
+ */
164
+ streamed: string
165
+ /**
166
+ * 使用 AI 进行解释按钮的文本。
167
+ * @default '用 AI 解释此页面
168
+ */
169
+ explainWithAi: string
170
+ }
171
+ /**
172
+ * 图标配置。
173
+ */
174
+ icons: {
175
+ /**
176
+ * 加载时的图标。
177
+ * @default i-lucide-loader
178
+ */
179
+ loading: string
180
+ /**
181
+ * AI 聊天触发按钮和滑出层头部的图标。
182
+ * @default 'i-lucide-sparkles'
183
+ */
184
+ trigger: string
185
+ /**
186
+ * "使用 AI 进行解释" 按钮的图标。
187
+ * @default 'i-lucide-brain'
188
+ */
189
+ explain: string
190
+ /**
191
+ * 思考时的图标。
192
+ * @default ui.icons.chevronDown
193
+ */
194
+ streaming: string
195
+ /**
196
+ * 清除聊天记录按钮的图标。
197
+ * @default 'i-lucide-trash-2'
198
+ */
199
+ clearChat: string
200
+ /**
201
+ * 关闭按钮的图标。
202
+ * @default 'i-lucide-x'
203
+ */
204
+ close: string
205
+ /**
206
+ * 用于映射不同 AI 提供商的图标。
207
+ * @example { mistral: 'i-simple-icons-mistralai' }
208
+ * @default { xxx: 'i-simple-xxx', mistral: 'i-simple-icons-mistralai', kwaipilot: 'i-lucide-wand', zai: 'i-lucide-wand' }
209
+ */
210
+ providers: Record<string, string>
211
+ }
212
+ }
64
213
  }
65
214
  }
66
215
 
package/content.config.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'node:fs'
1
2
  import { defineCollection, defineContentConfig } from '@nuxt/content'
2
3
  import { useNuxt } from '@nuxt/kit'
3
4
  import { asSeoCollection } from '@nuxtjs/seo/content'
@@ -7,6 +8,8 @@ import { z } from 'zod/v4'
7
8
  const { options } = useNuxt()
8
9
  const cwd = joinURL(options.rootDir, 'content')
9
10
 
11
+ const hasReleasesMd = existsSync(joinURL(cwd, 'releases.md'))
12
+
10
13
  const Avatar = z.object({
11
14
  src: z.string(),
12
15
  alt: z.string().optional()
@@ -27,6 +30,12 @@ const Button = z.object({
27
30
  class: z.string().optional()
28
31
  })
29
32
 
33
+ const PageHero = z.object({
34
+ title: z.string(),
35
+ description: z.string(),
36
+ links: z.array(Button).optional()
37
+ })
38
+
30
39
  export default defineContentConfig({
31
40
  collections: {
32
41
  landing: defineCollection(asSeoCollection({
@@ -38,10 +47,10 @@ export default defineContentConfig({
38
47
  })),
39
48
  docs: defineCollection(asSeoCollection({
40
49
  type: 'page',
41
- source: [{
50
+ source: {
42
51
  cwd,
43
52
  include: 'docs/**/*'
44
- }],
53
+ },
45
54
  schema: z.object({
46
55
  links: z.array(Button),
47
56
  category: z.string().optional(),
@@ -49,6 +58,19 @@ export default defineContentConfig({
49
58
  title: z.string().optional()
50
59
  })
51
60
  })
61
+ })),
62
+ releases: defineCollection(asSeoCollection({
63
+ type: 'page',
64
+ source: {
65
+ cwd,
66
+ include: hasReleasesMd ? 'releases.md' : 'releases.yml'
67
+ },
68
+ schema: z.object({
69
+ title: z.string(),
70
+ description: z.string(),
71
+ releases: z.string(),
72
+ hero: PageHero
73
+ })
52
74
  }))
53
75
  }
54
76
  })