docus 5.4.4 → 5.5.1
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.vue +16 -6
- package/app/components/app/AppHeader.vue +5 -0
- package/app/components/docs/DocsAsideRightBottom.vue +28 -1
- package/app/composables/useSeo.ts +207 -0
- package/app/pages/[[lang]]/[...slug].vue +17 -9
- package/app/plugins/i18n.ts +28 -11
- package/app/templates/landing.vue +4 -10
- package/app/types/index.d.ts +49 -0
- package/app/utils/navigation.ts +31 -0
- package/i18n/locales/en.json +27 -0
- package/i18n/locales/fr.json +28 -1
- package/modules/assistant/README.md +213 -0
- package/modules/assistant/index.ts +100 -0
- package/modules/assistant/runtime/components/AssistantChat.vue +21 -0
- package/modules/assistant/runtime/components/AssistantChatDisabled.vue +3 -0
- package/modules/assistant/runtime/components/AssistantFloatingInput.vue +110 -0
- package/modules/assistant/runtime/components/AssistantLoading.vue +164 -0
- package/modules/assistant/runtime/components/AssistantMatrix.vue +92 -0
- package/modules/assistant/runtime/components/AssistantPanel.vue +329 -0
- package/modules/assistant/runtime/components/AssistantPreStream.vue +46 -0
- package/modules/assistant/runtime/composables/useAssistant.ts +107 -0
- package/modules/assistant/runtime/composables/useHighlighter.ts +34 -0
- package/modules/assistant/runtime/server/api/search.ts +111 -0
- package/modules/assistant/runtime/types.ts +7 -0
- package/modules/config.ts +6 -4
- package/modules/css.ts +6 -2
- package/modules/markdown-rewrite.ts +130 -0
- package/nuxt.config.ts +22 -1
- package/nuxt.schema.ts +63 -0
- package/package.json +24 -15
- package/server/routes/sitemap.xml.ts +93 -0
- package/utils/meta.ts +9 -3
package/app/app.vue
CHANGED
|
@@ -5,6 +5,7 @@ import * as nuxtUiLocales from '@nuxt/ui/locale'
|
|
|
5
5
|
const { seo } = useAppConfig()
|
|
6
6
|
const site = useSiteConfig()
|
|
7
7
|
const { locale, locales, isEnabled, switchLocalePath } = useDocusI18n()
|
|
8
|
+
const { isEnabled: isAssistantEnabled, panelWidth: assistantPanelWidth, shouldPushContent } = useAssistant()
|
|
8
9
|
|
|
9
10
|
const nuxtUiLocale = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales] || nuxtUiLocales.en)
|
|
10
11
|
const lang = computed(() => nuxtUiLocale.value.code)
|
|
@@ -47,7 +48,7 @@ const { data: navigation } = await useAsyncData(() => `navigation_${collectionNa
|
|
|
47
48
|
transform: (data: ContentNavigationItem[]) => {
|
|
48
49
|
const rootResult = data.find(item => item.path === '/docs')?.children || data || []
|
|
49
50
|
|
|
50
|
-
return rootResult.find(item => item.path === `/${locale.value}`)?.children || rootResult
|
|
51
|
+
return rootResult.find((item: ContentNavigationItem) => item.path === `/${locale.value}`)?.children || rootResult
|
|
51
52
|
},
|
|
52
53
|
watch: [locale],
|
|
53
54
|
})
|
|
@@ -63,17 +64,26 @@ provide('navigation', navigation)
|
|
|
63
64
|
<UApp :locale="nuxtUiLocale">
|
|
64
65
|
<NuxtLoadingIndicator color="var(--ui-primary)" />
|
|
65
66
|
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
<div
|
|
68
|
+
class="transition-[margin-right] duration-200 ease-linear will-change-[margin-right]"
|
|
69
|
+
:style="{ marginRight: shouldPushContent ? `${assistantPanelWidth}px` : '0' }"
|
|
70
|
+
>
|
|
71
|
+
<AppHeader v-if="$route.meta.header !== false" />
|
|
72
|
+
<NuxtLayout>
|
|
73
|
+
<NuxtPage />
|
|
74
|
+
</NuxtLayout>
|
|
75
|
+
<AppFooter v-if="$route.meta.footer !== false" />
|
|
76
|
+
</div>
|
|
71
77
|
|
|
72
78
|
<ClientOnly>
|
|
73
79
|
<LazyUContentSearch
|
|
74
80
|
:files="files"
|
|
75
81
|
:navigation="navigation"
|
|
76
82
|
/>
|
|
83
|
+
<template v-if="isAssistantEnabled">
|
|
84
|
+
<LazyAssistantPanel />
|
|
85
|
+
<LazyAssistantFloatingInput />
|
|
86
|
+
</template>
|
|
77
87
|
</ClientOnly>
|
|
78
88
|
</UApp>
|
|
79
89
|
</template>
|
|
@@ -4,6 +4,7 @@ import { useDocusI18n } from '../../composables/useDocusI18n'
|
|
|
4
4
|
const appConfig = useAppConfig()
|
|
5
5
|
const site = useSiteConfig()
|
|
6
6
|
|
|
7
|
+
const { isEnabled: isAssistantEnabled } = useAssistant()
|
|
7
8
|
const { localePath, isEnabled, locales } = useDocusI18n()
|
|
8
9
|
|
|
9
10
|
const links = computed(() => appConfig.github && appConfig.github.url
|
|
@@ -33,6 +34,10 @@ const links = computed(() => appConfig.github && appConfig.github.url
|
|
|
33
34
|
<template #right>
|
|
34
35
|
<AppHeaderCTA />
|
|
35
36
|
|
|
37
|
+
<template v-if="isAssistantEnabled">
|
|
38
|
+
<AssistantChat />
|
|
39
|
+
</template>
|
|
40
|
+
|
|
36
41
|
<template v-if="isEnabled && locales.length > 1">
|
|
37
42
|
<ClientOnly>
|
|
38
43
|
<LanguageSelect />
|
|
@@ -1,18 +1,45 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
const route = useRoute()
|
|
3
|
+
|
|
4
|
+
const pageUrl = route.path
|
|
2
5
|
const appConfig = useAppConfig()
|
|
3
6
|
const { t } = useDocusI18n()
|
|
7
|
+
const { isEnabled, open } = useAssistant()
|
|
8
|
+
|
|
9
|
+
const showExplainWithAi = computed(() => {
|
|
10
|
+
return isEnabled.value && appConfig.assistant?.explainWithAi !== false
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const explainIcon = computed(() => appConfig.assistant?.icons?.explain || 'i-lucide-brain')
|
|
4
14
|
</script>
|
|
5
15
|
|
|
6
16
|
<template>
|
|
7
17
|
<div
|
|
8
|
-
v-if="appConfig.toc?.bottom?.links?.length"
|
|
18
|
+
v-if="appConfig.toc?.bottom?.links?.length || showExplainWithAi"
|
|
9
19
|
class="hidden lg:block space-y-6"
|
|
10
20
|
>
|
|
11
21
|
<USeparator type="dashed" />
|
|
12
22
|
|
|
13
23
|
<UPageLinks
|
|
24
|
+
v-if="appConfig.toc?.bottom?.links?.length"
|
|
14
25
|
:title="appConfig.toc?.bottom?.title || t('docs.links')"
|
|
15
26
|
:links="appConfig.toc?.bottom?.links"
|
|
16
27
|
/>
|
|
28
|
+
|
|
29
|
+
<USeparator
|
|
30
|
+
v-if="appConfig.toc?.bottom?.links?.length && showExplainWithAi"
|
|
31
|
+
type="dashed"
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<UButton
|
|
35
|
+
v-if="showExplainWithAi"
|
|
36
|
+
:icon="explainIcon"
|
|
37
|
+
:label="t('assistant.explainWithAi')"
|
|
38
|
+
size="sm"
|
|
39
|
+
variant="link"
|
|
40
|
+
class="p-0 text-sm"
|
|
41
|
+
color="neutral"
|
|
42
|
+
@click="open(`Explain the page ${pageUrl}`, true)"
|
|
43
|
+
/>
|
|
17
44
|
</div>
|
|
18
45
|
</template>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import type { BreadcrumbItem } from '../utils/navigation'
|
|
3
|
+
import { joinURL, withoutTrailingSlash } from 'ufo'
|
|
4
|
+
|
|
5
|
+
export interface UseSeoOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Page title
|
|
8
|
+
*/
|
|
9
|
+
title: MaybeRefOrGetter<string | undefined>
|
|
10
|
+
/**
|
|
11
|
+
* Page description
|
|
12
|
+
*/
|
|
13
|
+
description: MaybeRefOrGetter<string | undefined>
|
|
14
|
+
/**
|
|
15
|
+
* Page type for og:type (default: 'article' for docs, 'website' for landing)
|
|
16
|
+
*/
|
|
17
|
+
type?: MaybeRefOrGetter<'website' | 'article'>
|
|
18
|
+
/**
|
|
19
|
+
* Custom OG image URL (absolute)
|
|
20
|
+
*/
|
|
21
|
+
ogImage?: MaybeRefOrGetter<string | undefined>
|
|
22
|
+
/**
|
|
23
|
+
* Published date for article schema
|
|
24
|
+
*/
|
|
25
|
+
publishedAt?: MaybeRefOrGetter<string | undefined>
|
|
26
|
+
/**
|
|
27
|
+
* Modified date for article schema
|
|
28
|
+
*/
|
|
29
|
+
modifiedAt?: MaybeRefOrGetter<string | undefined>
|
|
30
|
+
/**
|
|
31
|
+
* Breadcrumb items for BreadcrumbList schema
|
|
32
|
+
*/
|
|
33
|
+
breadcrumbs?: MaybeRefOrGetter<BreadcrumbItem[] | undefined>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Composable for comprehensive SEO setup including:
|
|
38
|
+
* - Meta tags (title, description, og:*, twitter:*)
|
|
39
|
+
* - Canonical URLs
|
|
40
|
+
* - Hreflang tags for i18n
|
|
41
|
+
* - JSON-LD structured data
|
|
42
|
+
*/
|
|
43
|
+
export function useSeo(options: UseSeoOptions) {
|
|
44
|
+
const route = useRoute()
|
|
45
|
+
const site = useSiteConfig()
|
|
46
|
+
const { locale, locales, isEnabled: isI18nEnabled, switchLocalePath } = useDocusI18n()
|
|
47
|
+
|
|
48
|
+
const title = computed(() => toValue(options.title))
|
|
49
|
+
const description = computed(() => toValue(options.description))
|
|
50
|
+
const type = computed(() => toValue(options.type) || 'article')
|
|
51
|
+
const ogImage = computed(() => toValue(options.ogImage))
|
|
52
|
+
const publishedAt = computed(() => toValue(options.publishedAt))
|
|
53
|
+
const modifiedAt = computed(() => toValue(options.modifiedAt))
|
|
54
|
+
const breadcrumbs = computed(() => toValue(options.breadcrumbs))
|
|
55
|
+
|
|
56
|
+
// Build canonical URL
|
|
57
|
+
const canonicalUrl = computed(() => {
|
|
58
|
+
if (!site.url) return undefined
|
|
59
|
+
return joinURL(site.url, route.path)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Base URL for building other URLs
|
|
63
|
+
const baseUrl = computed(() => site.url ? withoutTrailingSlash(site.url) : '')
|
|
64
|
+
|
|
65
|
+
// Set meta tags
|
|
66
|
+
useSeoMeta({
|
|
67
|
+
title,
|
|
68
|
+
description,
|
|
69
|
+
ogTitle: title,
|
|
70
|
+
ogDescription: description,
|
|
71
|
+
ogType: type,
|
|
72
|
+
ogUrl: canonicalUrl,
|
|
73
|
+
ogLocale: computed(() => isI18nEnabled.value ? locale.value : undefined),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Set canonical link
|
|
77
|
+
useHead({
|
|
78
|
+
link: computed(() => {
|
|
79
|
+
const links: Array<{ rel: string, href?: string, hreflang?: string }> = []
|
|
80
|
+
|
|
81
|
+
// Canonical URL
|
|
82
|
+
if (canonicalUrl.value) {
|
|
83
|
+
links.push({
|
|
84
|
+
rel: 'canonical',
|
|
85
|
+
href: canonicalUrl.value,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Hreflang tags for i18n
|
|
90
|
+
if (isI18nEnabled.value && baseUrl.value) {
|
|
91
|
+
for (const loc of locales) {
|
|
92
|
+
const localePath = switchLocalePath(loc.code)
|
|
93
|
+
if (localePath) {
|
|
94
|
+
links.push({
|
|
95
|
+
rel: 'alternate',
|
|
96
|
+
hreflang: loc.code,
|
|
97
|
+
href: joinURL(baseUrl.value, localePath),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// x-default hreflang (points to default locale)
|
|
103
|
+
const defaultLocalePath = switchLocalePath(locales[0]?.code || 'en')
|
|
104
|
+
if (defaultLocalePath) {
|
|
105
|
+
links.push({
|
|
106
|
+
rel: 'alternate',
|
|
107
|
+
hreflang: 'x-default',
|
|
108
|
+
href: joinURL(baseUrl.value, defaultLocalePath),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return links
|
|
114
|
+
}),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Custom OG image handling
|
|
118
|
+
if (ogImage.value) {
|
|
119
|
+
useSeoMeta({
|
|
120
|
+
ogImage: ogImage.value,
|
|
121
|
+
twitterImage: ogImage.value,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// JSON-LD structured data
|
|
126
|
+
useHead({
|
|
127
|
+
script: computed(() => {
|
|
128
|
+
const scripts: Array<{ type: string, innerHTML: string }> = []
|
|
129
|
+
|
|
130
|
+
if (!baseUrl.value || !title.value) return scripts
|
|
131
|
+
|
|
132
|
+
const pageUrl = joinURL(baseUrl.value, route.path)
|
|
133
|
+
|
|
134
|
+
// Article schema for documentation pages
|
|
135
|
+
if (type.value === 'article') {
|
|
136
|
+
const articleSchema: Record<string, unknown> = {
|
|
137
|
+
'@context': 'https://schema.org',
|
|
138
|
+
'@type': 'Article',
|
|
139
|
+
'headline': title.value,
|
|
140
|
+
'description': description.value,
|
|
141
|
+
'url': pageUrl,
|
|
142
|
+
'mainEntityOfPage': {
|
|
143
|
+
'@type': 'WebPage',
|
|
144
|
+
'@id': pageUrl,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (publishedAt.value) {
|
|
149
|
+
articleSchema.datePublished = publishedAt.value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (modifiedAt.value) {
|
|
153
|
+
articleSchema.dateModified = modifiedAt.value
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (site.name) {
|
|
157
|
+
articleSchema.publisher = {
|
|
158
|
+
'@type': 'Organization',
|
|
159
|
+
'name': site.name,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
scripts.push({
|
|
164
|
+
type: 'application/ld+json',
|
|
165
|
+
innerHTML: JSON.stringify(articleSchema),
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// WebSite schema for landing pages
|
|
170
|
+
if (type.value === 'website') {
|
|
171
|
+
const websiteSchema: Record<string, unknown> = {
|
|
172
|
+
'@context': 'https://schema.org',
|
|
173
|
+
'@type': 'WebSite',
|
|
174
|
+
'name': site.name || title.value,
|
|
175
|
+
'description': description.value,
|
|
176
|
+
'url': baseUrl.value,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
scripts.push({
|
|
180
|
+
type: 'application/ld+json',
|
|
181
|
+
innerHTML: JSON.stringify(websiteSchema),
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// BreadcrumbList schema for navigation
|
|
186
|
+
if (breadcrumbs.value && breadcrumbs.value.length > 0) {
|
|
187
|
+
const breadcrumbSchema = {
|
|
188
|
+
'@context': 'https://schema.org',
|
|
189
|
+
'@type': 'BreadcrumbList',
|
|
190
|
+
'itemListElement': breadcrumbs.value.map((item, index) => ({
|
|
191
|
+
'@type': 'ListItem',
|
|
192
|
+
'position': index + 1,
|
|
193
|
+
'name': item.title,
|
|
194
|
+
'item': joinURL(baseUrl.value, item.path),
|
|
195
|
+
})),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
scripts.push({
|
|
199
|
+
type: 'application/ld+json',
|
|
200
|
+
innerHTML: JSON.stringify(breadcrumbSchema),
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return scripts
|
|
205
|
+
}),
|
|
206
|
+
})
|
|
207
|
+
}
|
|
@@ -11,6 +11,7 @@ const route = useRoute()
|
|
|
11
11
|
const { locale, isEnabled, t } = useDocusI18n()
|
|
12
12
|
const appConfig = useAppConfig()
|
|
13
13
|
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
|
14
|
+
const { shouldPushContent: shouldHideToc } = useAssistant()
|
|
14
15
|
|
|
15
16
|
const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')
|
|
16
17
|
|
|
@@ -30,14 +31,16 @@ if (!page.value) {
|
|
|
30
31
|
const title = page.value.seo?.title || page.value.title
|
|
31
32
|
const description = page.value.seo?.description || page.value.description
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
const headline = ref(findPageHeadline(navigation?.value, page.value?.path))
|
|
35
|
+
const breadcrumbs = computed(() => findPageBreadcrumbs(navigation?.value, page.value?.path || ''))
|
|
36
|
+
|
|
37
|
+
useSeo({
|
|
34
38
|
title,
|
|
35
|
-
ogTitle: title,
|
|
36
39
|
description,
|
|
37
|
-
|
|
40
|
+
type: 'article',
|
|
41
|
+
modifiedAt: (page.value as unknown as Record<string, unknown>).modifiedAt as string | undefined,
|
|
42
|
+
breadcrumbs,
|
|
38
43
|
})
|
|
39
|
-
|
|
40
|
-
const headline = ref(findPageHeadline(navigation?.value, page.value?.path))
|
|
41
44
|
watch(() => navigation?.value, () => {
|
|
42
45
|
headline.value = findPageHeadline(navigation?.value, page.value?.path) || headline.value
|
|
43
46
|
})
|
|
@@ -62,10 +65,16 @@ const editLink = computed(() => {
|
|
|
62
65
|
`${page.value?.stem}.${page.value?.extension}`,
|
|
63
66
|
].filter(Boolean).join('/')
|
|
64
67
|
})
|
|
68
|
+
|
|
69
|
+
// Add the page path to the prerender list
|
|
70
|
+
addPrerenderPath(`/raw${route.path}.md`)
|
|
65
71
|
</script>
|
|
66
72
|
|
|
67
73
|
<template>
|
|
68
|
-
<UPage
|
|
74
|
+
<UPage
|
|
75
|
+
v-if="page"
|
|
76
|
+
:key="`page-${shouldHideToc}`"
|
|
77
|
+
>
|
|
69
78
|
<UPageHeader
|
|
70
79
|
:title="page.title"
|
|
71
80
|
:description="page.description"
|
|
@@ -92,9 +101,8 @@ const editLink = computed(() => {
|
|
|
92
101
|
:value="page"
|
|
93
102
|
/>
|
|
94
103
|
|
|
95
|
-
<USeparator>
|
|
104
|
+
<USeparator v-if="github">
|
|
96
105
|
<div
|
|
97
|
-
v-if="github"
|
|
98
106
|
class="flex items-center gap-2 text-sm text-muted"
|
|
99
107
|
>
|
|
100
108
|
<UButton
|
|
@@ -124,7 +132,7 @@ const editLink = computed(() => {
|
|
|
124
132
|
</UPageBody>
|
|
125
133
|
|
|
126
134
|
<template
|
|
127
|
-
v-if="page?.body?.toc?.links?.length"
|
|
135
|
+
v-if="page?.body?.toc?.links?.length && !shouldHideToc"
|
|
128
136
|
#right
|
|
129
137
|
>
|
|
130
138
|
<UContentToc
|
package/app/plugins/i18n.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import en from '../../i18n/locales/en.json'
|
|
2
1
|
import type { RouteLocationNormalized } from 'vue-router'
|
|
2
|
+
import { consola } from 'consola'
|
|
3
|
+
|
|
4
|
+
const log = consola.withTag('Docus')
|
|
5
|
+
|
|
6
|
+
// Lazy import functions for locale files (bundled but not eagerly loaded)
|
|
7
|
+
const localeFiles = import.meta.glob<{ default: Record<string, unknown> }>('../../i18n/locales/*.json')
|
|
3
8
|
|
|
4
9
|
export default defineNuxtPlugin(async () => {
|
|
5
10
|
const nuxtApp = useNuxtApp()
|
|
@@ -8,19 +13,31 @@ export default defineNuxtPlugin(async () => {
|
|
|
8
13
|
|
|
9
14
|
// If i18n is not enabled, fetch and provide the configured locale in app config
|
|
10
15
|
if (!i18nConfig) {
|
|
11
|
-
let locale = 'en'
|
|
12
|
-
let resolvedMessages: Record<string, unknown> = en
|
|
13
|
-
|
|
14
16
|
const appConfig = useAppConfig()
|
|
15
|
-
const configuredLocale = appConfig.docus.locale
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const configuredLocale = appConfig.docus.locale || 'en'
|
|
18
|
+
|
|
19
|
+
let locale = configuredLocale
|
|
20
|
+
let resolvedMessages: Record<string, unknown>
|
|
21
|
+
|
|
22
|
+
// Try to load the requested locale file
|
|
23
|
+
const localeKey = `../../i18n/locales/${configuredLocale}.json`
|
|
24
|
+
const localeLoader = localeFiles[localeKey]
|
|
25
|
+
|
|
26
|
+
if (localeLoader) {
|
|
27
|
+
const localeModule = await localeLoader()
|
|
28
|
+
resolvedMessages = localeModule.default
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
log.warn(`Missing locale file for "${configuredLocale}". Falling back to "en".`)
|
|
32
|
+
locale = 'en'
|
|
33
|
+
const fallbackKey = '../../i18n/locales/en.json'
|
|
34
|
+
const fallbackLoader = localeFiles[fallbackKey]
|
|
35
|
+
if (fallbackLoader) {
|
|
36
|
+
const fallbackModule = await fallbackLoader()
|
|
37
|
+
resolvedMessages = fallbackModule.default
|
|
20
38
|
}
|
|
21
39
|
else {
|
|
22
|
-
|
|
23
|
-
resolvedMessages = localeMessages
|
|
40
|
+
resolvedMessages = {} as Record<string, unknown>
|
|
24
41
|
}
|
|
25
42
|
}
|
|
26
43
|
|
|
@@ -15,20 +15,14 @@ if (!page.value) {
|
|
|
15
15
|
const title = page.value.seo?.title || page.value.title
|
|
16
16
|
const description = page.value.seo?.description || page.value.description
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
useSeo({
|
|
19
19
|
title,
|
|
20
20
|
description,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
type: 'website',
|
|
22
|
+
ogImage: page.value?.seo?.ogImage as string | undefined,
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
if (page.value?.seo?.ogImage) {
|
|
26
|
-
useSeoMeta({
|
|
27
|
-
ogImage: page.value.seo.ogImage,
|
|
28
|
-
twitterImage: page.value.seo.ogImage,
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
25
|
+
if (!page.value?.seo?.ogImage) {
|
|
32
26
|
defineOgImageComponent('Landing', {
|
|
33
27
|
title,
|
|
34
28
|
description,
|
package/app/types/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { FaqQuestions, LocalizedFaqQuestions } from '../../modules/assistant/runtime/types'
|
|
2
|
+
|
|
3
|
+
export type { FaqCategory, FaqQuestions, LocalizedFaqQuestions } from '../../modules/assistant/runtime/types'
|
|
4
|
+
|
|
1
5
|
declare module 'nuxt/schema' {
|
|
2
6
|
interface AppConfig {
|
|
3
7
|
docus: {
|
|
@@ -36,6 +40,51 @@ declare module 'nuxt/schema' {
|
|
|
36
40
|
branch: string
|
|
37
41
|
rootDir?: string
|
|
38
42
|
} | false
|
|
43
|
+
assistant?: {
|
|
44
|
+
/**
|
|
45
|
+
* Show the floating input at the bottom of documentation pages.
|
|
46
|
+
* @default true
|
|
47
|
+
*/
|
|
48
|
+
floatingInput?: boolean
|
|
49
|
+
/**
|
|
50
|
+
* Show the "Explain with AI" button in the documentation sidebar.
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
explainWithAi?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* FAQ questions to display in the chat slideover.
|
|
56
|
+
* Can be a simple array of strings, an array of categories, or a locale-based object.
|
|
57
|
+
* @example Simple format: ['How to install?', 'How to configure?']
|
|
58
|
+
* @example Category format: [{ category: 'Getting Started', items: ['How to install?'] }]
|
|
59
|
+
* @example Localized format: { en: ['How to install?'], fr: ['Comment installer ?'] }
|
|
60
|
+
*/
|
|
61
|
+
faqQuestions?: FaqQuestions | LocalizedFaqQuestions
|
|
62
|
+
/**
|
|
63
|
+
* Keyboard shortcuts configuration.
|
|
64
|
+
*/
|
|
65
|
+
shortcuts?: {
|
|
66
|
+
/**
|
|
67
|
+
* Shortcut to focus the floating input.
|
|
68
|
+
* @default 'meta_i'
|
|
69
|
+
*/
|
|
70
|
+
focusInput?: string
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Icons configuration.
|
|
74
|
+
*/
|
|
75
|
+
icons?: {
|
|
76
|
+
/**
|
|
77
|
+
* Icon for the assistant trigger button and slideover header.
|
|
78
|
+
* @default 'i-lucide-sparkles'
|
|
79
|
+
*/
|
|
80
|
+
trigger?: string
|
|
81
|
+
/**
|
|
82
|
+
* Icon for the "Explain with AI" button.
|
|
83
|
+
* @default 'i-lucide-brain'
|
|
84
|
+
*/
|
|
85
|
+
explain?: string
|
|
86
|
+
}
|
|
87
|
+
}
|
|
39
88
|
}
|
|
40
89
|
}
|
|
41
90
|
|
package/app/utils/navigation.ts
CHANGED
|
@@ -5,3 +5,34 @@ export const flattenNavigation = (items?: ContentNavigationItem[]): ContentNavig
|
|
|
5
5
|
? flattenNavigation(item.children)
|
|
6
6
|
: [item],
|
|
7
7
|
) || []
|
|
8
|
+
|
|
9
|
+
export interface BreadcrumbItem {
|
|
10
|
+
title: string
|
|
11
|
+
path: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find breadcrumb path to a page in the navigation tree
|
|
16
|
+
*/
|
|
17
|
+
export function findPageBreadcrumbs(
|
|
18
|
+
navigation: ContentNavigationItem[] | undefined,
|
|
19
|
+
path: string,
|
|
20
|
+
currentPath: BreadcrumbItem[] = [],
|
|
21
|
+
): BreadcrumbItem[] | undefined {
|
|
22
|
+
if (!navigation) return undefined
|
|
23
|
+
|
|
24
|
+
for (const item of navigation) {
|
|
25
|
+
const itemPath = [...currentPath, { title: item.title, path: item.path }]
|
|
26
|
+
|
|
27
|
+
if (item.path === path) {
|
|
28
|
+
return itemPath
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (item.children) {
|
|
32
|
+
const found = findPageBreadcrumbs(item.children, path, itemPath)
|
|
33
|
+
if (found) return found
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
package/i18n/locales/en.json
CHANGED
|
@@ -18,5 +18,32 @@
|
|
|
18
18
|
"toc": "On this page",
|
|
19
19
|
"report": "Report an issue",
|
|
20
20
|
"edit": "Edit this page"
|
|
21
|
+
},
|
|
22
|
+
"assistant": {
|
|
23
|
+
"title": "Ask AI",
|
|
24
|
+
"placeholder": "Ask a question...",
|
|
25
|
+
"tooltip": "Ask AI a question",
|
|
26
|
+
"tryAsking": "Try asking a question",
|
|
27
|
+
"askAnything": "Ask anything...",
|
|
28
|
+
"clearChat": "Clear chat",
|
|
29
|
+
"close": "Close",
|
|
30
|
+
"expand": "Expand",
|
|
31
|
+
"collapse": "Collapse",
|
|
32
|
+
"thinking": "Thinking...",
|
|
33
|
+
"askMeAnything": "Ask anything",
|
|
34
|
+
"askMeAnythingDescription": "Get help navigating the documentation, understanding concepts, and finding answers.",
|
|
35
|
+
"faq": "FAQ",
|
|
36
|
+
"chatCleared": "Chat is cleared on refresh",
|
|
37
|
+
"lineBreak": "Line break",
|
|
38
|
+
"explainWithAi": "Explain with AI",
|
|
39
|
+
"toolListPages": "Listed documentation pages",
|
|
40
|
+
"toolReadPage": "Read",
|
|
41
|
+
"loading": {
|
|
42
|
+
"searching": "Searching the documentation",
|
|
43
|
+
"reading": "Reading through the docs",
|
|
44
|
+
"analyzing": "Analyzing the content",
|
|
45
|
+
"finding": "Finding the best answer",
|
|
46
|
+
"finished": "Sources used"
|
|
47
|
+
}
|
|
21
48
|
}
|
|
22
49
|
}
|
package/i18n/locales/fr.json
CHANGED
|
@@ -18,5 +18,32 @@
|
|
|
18
18
|
"toc": "Sur cette page",
|
|
19
19
|
"report": "Signaler un problème",
|
|
20
20
|
"edit": "Éditer cette page"
|
|
21
|
+
},
|
|
22
|
+
"assistant": {
|
|
23
|
+
"title": "Demander à l'IA",
|
|
24
|
+
"placeholder": "Posez une question...",
|
|
25
|
+
"tooltip": "Poser une question à l'IA",
|
|
26
|
+
"tryAsking": "Essayez de poser une question",
|
|
27
|
+
"askAnything": "Demandez n'importe quoi...",
|
|
28
|
+
"clearChat": "Effacer le chat",
|
|
29
|
+
"close": "Fermer",
|
|
30
|
+
"expand": "Agrandir",
|
|
31
|
+
"collapse": "Réduire",
|
|
32
|
+
"thinking": "Réflexion...",
|
|
33
|
+
"askMeAnything": "Posez une question",
|
|
34
|
+
"askMeAnythingDescription": "Obtenez de l'aide pour naviguer dans la documentation, comprendre des concepts et trouver des réponses.",
|
|
35
|
+
"faq": "FAQ",
|
|
36
|
+
"chatCleared": "Le chat est effacé au rechargement",
|
|
37
|
+
"lineBreak": "Retour à la ligne",
|
|
38
|
+
"explainWithAi": "Expliquer avec l'IA",
|
|
39
|
+
"toolListPages": "Pages de documentation listées",
|
|
40
|
+
"toolReadPage": "Lecture de",
|
|
41
|
+
"loading": {
|
|
42
|
+
"searching": "Recherche dans la documentation",
|
|
43
|
+
"reading": "Lecture des documents",
|
|
44
|
+
"analyzing": "Analyse du contenu",
|
|
45
|
+
"finding": "Recherche de la meilleure réponse",
|
|
46
|
+
"finished": "Sources utilisées"
|
|
47
|
+
}
|
|
21
48
|
}
|
|
22
|
-
}
|
|
49
|
+
}
|