docus 4.0.0-beta.8 → 4.0.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/README.md +23 -0
- package/app/app.config.ts +0 -3
- package/app/app.vue +20 -5
- package/app/components/LanguageSelect.vue +82 -0
- package/app/components/app/AppHeader.vue +27 -2
- package/app/components/docs/DocsAsideRightBottom.vue +2 -1
- package/app/components/docs/DocsPageHeaderLinks.vue +12 -7
- package/app/composables/useDocusI18n.ts +31 -0
- package/app/error.vue +7 -7
- package/app/pages/{[...slug].vue → [[lang]]/[...slug].vue} +12 -9
- package/app/pages/{index.vue → [[lang]]/index.vue} +8 -1
- package/content.config.ts +45 -12
- package/i18n/i18n.config.ts +15 -0
- package/i18n/locales/en.json +18 -0
- package/i18n/locales/fr.json +18 -0
- package/modules/default-configs.ts +11 -0
- package/nuxt.config.ts +1 -0
- package/package.json +12 -4
- package/server/routes/raw/[...slug].md.get.ts +21 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ This is the official Nuxt layer for [Docus](https://docus.dev), providing a comp
|
|
|
14
14
|
- ✨ **Beautiful Design** - Clean, modern documentation theme
|
|
15
15
|
- 📱 **Responsive** - Mobile-first responsive design
|
|
16
16
|
- 🌙 **Dark Mode** - Built-in dark/light mode support
|
|
17
|
+
- 🌍 **Internationalization** - Native i18n support with automatic routing and language switching
|
|
17
18
|
- 🔍 **Search** - Full-text search functionality
|
|
18
19
|
- 📝 **Markdown Enhanced** - Extended markdown with custom components
|
|
19
20
|
- 🎨 **Customizable** - Easy theming and customization
|
|
@@ -46,6 +47,13 @@ npm run dev
|
|
|
46
47
|
|
|
47
48
|
This creates a complete documentation project pre-configured with `docus`.
|
|
48
49
|
|
|
50
|
+
For multi-language documentation, use the i18n template:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Create a new i18n documentation project
|
|
54
|
+
npx create docus my-docs -t i18n
|
|
55
|
+
```
|
|
56
|
+
|
|
49
57
|
### Option 2: Manual Setup
|
|
50
58
|
|
|
51
59
|
#### Option 2a: Nuxt Config (recommended)
|
|
@@ -58,6 +66,21 @@ export default defineNuxtConfig({
|
|
|
58
66
|
})
|
|
59
67
|
```
|
|
60
68
|
|
|
69
|
+
For internationalization, also add the `@nuxtjs/i18n` module:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
export default defineNuxtConfig({
|
|
73
|
+
modules: ['@nuxtjs/i18n'],
|
|
74
|
+
i18n: {
|
|
75
|
+
defaultLocale: 'en',
|
|
76
|
+
locales: [
|
|
77
|
+
{ code: 'en', name: 'English' },
|
|
78
|
+
{ code: 'fr', name: 'Français' },
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
61
84
|
#### Option 2b: CLI Usage
|
|
62
85
|
|
|
63
86
|
Use directly with Nuxt CLI:
|
package/app/app.config.ts
CHANGED
package/app/app.vue
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { PageCollections } from '@nuxt/content'
|
|
3
|
+
import * as locales from '@nuxt/ui-pro/locale'
|
|
4
|
+
|
|
2
5
|
const { seo } = useAppConfig()
|
|
3
6
|
const site = useSiteConfig()
|
|
4
7
|
|
|
5
|
-
const {
|
|
6
|
-
|
|
8
|
+
const { locale, isEnabled } = useDocusI18n()
|
|
9
|
+
|
|
10
|
+
const lang = computed(() => locales[locale.value as keyof typeof locales]?.code || 'en')
|
|
11
|
+
const dir = computed(() => locales[locale.value as keyof typeof locales]?.dir || 'ltr')
|
|
12
|
+
const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')
|
|
13
|
+
|
|
14
|
+
const { data: navigation } = await useAsyncData(`navigation_${collectionName.value}`, () => queryCollectionNavigation(collectionName.value as keyof PageCollections), {
|
|
15
|
+
transform: (data) => {
|
|
16
|
+
const rootResult = data.find(item => item.path === '/docs')?.children || data || []
|
|
17
|
+
|
|
18
|
+
return rootResult.find(item => item.path === `/${locale.value}`)?.children || rootResult
|
|
19
|
+
},
|
|
20
|
+
watch: [locale],
|
|
7
21
|
})
|
|
8
|
-
const { data: files } = useLazyAsyncData(
|
|
22
|
+
const { data: files } = useLazyAsyncData(`search_${collectionName.value}`, () => queryCollectionSearchSections(collectionName.value as keyof PageCollections), {
|
|
9
23
|
server: false,
|
|
10
24
|
})
|
|
11
25
|
|
|
@@ -17,7 +31,8 @@ useHead({
|
|
|
17
31
|
{ rel: 'icon', href: '/favicon.ico' },
|
|
18
32
|
],
|
|
19
33
|
htmlAttrs: {
|
|
20
|
-
lang
|
|
34
|
+
lang,
|
|
35
|
+
dir,
|
|
21
36
|
},
|
|
22
37
|
})
|
|
23
38
|
|
|
@@ -33,7 +48,7 @@ provide('navigation', navigation)
|
|
|
33
48
|
</script>
|
|
34
49
|
|
|
35
50
|
<template>
|
|
36
|
-
<UApp>
|
|
51
|
+
<UApp :locale="locales[locale as keyof typeof locales]">
|
|
37
52
|
<NuxtLoadingIndicator color="var(--ui-primary)" />
|
|
38
53
|
|
|
39
54
|
<AppHeader />
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { locale, locales, switchLocalePath } = useDocusI18n()
|
|
3
|
+
|
|
4
|
+
function getEmojiFlag(locale: string): string {
|
|
5
|
+
const languageToCountry: Record<string, string> = {
|
|
6
|
+
ar: 'sa', // Arabic -> Saudi Arabia
|
|
7
|
+
bn: 'bd', // Bengali -> Bangladesh
|
|
8
|
+
ca: 'es', // Catalan -> Spain
|
|
9
|
+
ckb: 'iq', // Central Kurdish -> Iraq
|
|
10
|
+
cs: 'cz', // Czech -> Czech Republic (note: modern country code is actually 'cz')
|
|
11
|
+
da: 'dk', // Danish -> Denmark
|
|
12
|
+
el: 'gr', // Greek -> Greece
|
|
13
|
+
en: 'gb', // English -> Great Britain
|
|
14
|
+
et: 'ee', // Estonian -> Estonia
|
|
15
|
+
he: 'il', // Hebrew -> Israel
|
|
16
|
+
hi: 'in', // Hindi -> India
|
|
17
|
+
hy: 'am', // Armenian -> Armenia
|
|
18
|
+
ja: 'jp', // Japanese -> Japan
|
|
19
|
+
kk: 'kz', // Kazakh -> Kazakhstan
|
|
20
|
+
km: 'kh', // Khmer -> Cambodia
|
|
21
|
+
ko: 'kr', // Korean -> South Korea
|
|
22
|
+
ky: 'kg', // Kyrgyz -> Kyrgyzstan
|
|
23
|
+
lb: 'lu', // Luxembourgish -> Luxembourg
|
|
24
|
+
ms: 'my', // Malay -> Malaysia
|
|
25
|
+
nb: 'no', // Norwegian Bokmål -> Norway
|
|
26
|
+
sl: 'si', // Slovenian -> Slovenia
|
|
27
|
+
sv: 'se', // Swedish -> Sweden
|
|
28
|
+
uk: 'ua', // Ukrainian -> Ukraine
|
|
29
|
+
ur: 'pk', // Urdu -> Pakistan
|
|
30
|
+
vi: 'vn', // Vietnamese -> Vietnam
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const baseLanguage = locale.split('-')[0]?.toLowerCase() || locale
|
|
34
|
+
const countryCode = languageToCountry[baseLanguage] || locale.replace(/^.*-/, '').slice(0, 2)
|
|
35
|
+
|
|
36
|
+
return countryCode.toUpperCase()
|
|
37
|
+
.split('')
|
|
38
|
+
.map(char => String.fromCodePoint(0x1F1A5 + char.charCodeAt(0)))
|
|
39
|
+
.join('')
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<UPopover
|
|
45
|
+
mode="hover"
|
|
46
|
+
:content="{ align: 'end' }"
|
|
47
|
+
>
|
|
48
|
+
<UButton
|
|
49
|
+
color="neutral"
|
|
50
|
+
variant="ghost"
|
|
51
|
+
class="size-8"
|
|
52
|
+
>
|
|
53
|
+
<template #trailing>
|
|
54
|
+
<span class="text-lg">
|
|
55
|
+
{{ getEmojiFlag(locale) }}
|
|
56
|
+
</span>
|
|
57
|
+
</template>
|
|
58
|
+
</UButton>
|
|
59
|
+
|
|
60
|
+
<template #content>
|
|
61
|
+
<ul class="flex flex-col gap-2">
|
|
62
|
+
<li
|
|
63
|
+
v-for="localeItem in locales"
|
|
64
|
+
:key="localeItem.code"
|
|
65
|
+
>
|
|
66
|
+
<NuxtLink
|
|
67
|
+
class="flex justify-between py-2 px-1.5 gap-1 hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
|
68
|
+
:to="switchLocalePath(localeItem.code) as string"
|
|
69
|
+
:aria-label="localeItem.name"
|
|
70
|
+
>
|
|
71
|
+
<span class="text-sm">
|
|
72
|
+
{{ localeItem.name }}
|
|
73
|
+
</span>
|
|
74
|
+
<span class="size-5 text-center">
|
|
75
|
+
{{ getEmojiFlag(localeItem.code) }}
|
|
76
|
+
</span>
|
|
77
|
+
</NuxtLink>
|
|
78
|
+
</li>
|
|
79
|
+
</ul>
|
|
80
|
+
</template>
|
|
81
|
+
</UPopover>
|
|
82
|
+
</template>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { useDocusI18n } from '../../composables/useDocusI18n'
|
|
3
|
+
|
|
2
4
|
const appConfig = useAppConfig()
|
|
3
5
|
const site = useSiteConfig()
|
|
4
6
|
|
|
7
|
+
const { localePath, isEnabled } = useDocusI18n()
|
|
8
|
+
|
|
5
9
|
const links = computed(() => appConfig.github?.url
|
|
6
10
|
? [
|
|
7
11
|
{
|
|
@@ -17,7 +21,7 @@ const links = computed(() => appConfig.github?.url
|
|
|
17
21
|
<template>
|
|
18
22
|
<UHeader
|
|
19
23
|
:ui="{ center: 'flex-1' }"
|
|
20
|
-
to="/"
|
|
24
|
+
:to="localePath('/')"
|
|
21
25
|
:title="appConfig.header?.title || site.name"
|
|
22
26
|
>
|
|
23
27
|
<AppHeaderCenter />
|
|
@@ -29,9 +33,30 @@ const links = computed(() => appConfig.github?.url
|
|
|
29
33
|
<template #right>
|
|
30
34
|
<AppHeaderCTA />
|
|
31
35
|
|
|
36
|
+
<template v-if="isEnabled">
|
|
37
|
+
<ClientOnly>
|
|
38
|
+
<LanguageSelect />
|
|
39
|
+
|
|
40
|
+
<template #fallback>
|
|
41
|
+
<div class="h-8 w-8 animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-md" />
|
|
42
|
+
</template>
|
|
43
|
+
</ClientOnly>
|
|
44
|
+
|
|
45
|
+
<USeparator
|
|
46
|
+
orientation="vertical"
|
|
47
|
+
class="h-8"
|
|
48
|
+
/>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
32
51
|
<UContentSearchButton class="lg:hidden" />
|
|
33
52
|
|
|
34
|
-
<
|
|
53
|
+
<ClientOnly>
|
|
54
|
+
<UColorModeButton />
|
|
55
|
+
|
|
56
|
+
<template #fallback>
|
|
57
|
+
<div class="h-8 w-8 animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-md" />
|
|
58
|
+
</template>
|
|
59
|
+
</ClientOnly>
|
|
35
60
|
|
|
36
61
|
<template v-if="links?.length">
|
|
37
62
|
<UButton
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
const appConfig = useAppConfig()
|
|
3
|
+
const { t } = useDocusI18n()
|
|
3
4
|
</script>
|
|
4
5
|
|
|
5
6
|
<template>
|
|
@@ -10,7 +11,7 @@ const appConfig = useAppConfig()
|
|
|
10
11
|
<USeparator type="dashed" />
|
|
11
12
|
|
|
12
13
|
<UPageLinks
|
|
13
|
-
:title="appConfig.toc?.bottom?.title || '
|
|
14
|
+
:title="appConfig.toc?.bottom?.title || t('docs.links')"
|
|
14
15
|
:links="appConfig.toc?.bottom?.links"
|
|
15
16
|
/>
|
|
16
17
|
</div>
|
|
@@ -4,9 +4,9 @@ import { useClipboard } from '@vueuse/core'
|
|
|
4
4
|
const route = useRoute()
|
|
5
5
|
const toast = useToast()
|
|
6
6
|
const { copy, copied } = useClipboard()
|
|
7
|
+
const { t } = useDocusI18n()
|
|
7
8
|
|
|
8
9
|
const markdownLink = computed(() => `${window?.location?.origin}/raw${route.path}.md`)
|
|
9
|
-
|
|
10
10
|
const items = [
|
|
11
11
|
{
|
|
12
12
|
label: 'Copy Markdown link',
|
|
@@ -15,44 +15,49 @@ const items = [
|
|
|
15
15
|
copy(markdownLink.value)
|
|
16
16
|
|
|
17
17
|
toast.add({
|
|
18
|
-
title: '
|
|
18
|
+
title: t('docs.copy.link'),
|
|
19
19
|
icon: 'i-lucide-check-circle',
|
|
20
20
|
color: 'success',
|
|
21
21
|
})
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
label: '
|
|
25
|
+
label: t('docs.copy.view'),
|
|
26
26
|
icon: 'i-simple-icons:markdown',
|
|
27
27
|
target: '_blank',
|
|
28
28
|
to: markdownLink.value,
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
|
-
label: '
|
|
31
|
+
label: t('docs.copy.gpt'),
|
|
32
32
|
icon: 'i-simple-icons:openai',
|
|
33
33
|
target: '_blank',
|
|
34
34
|
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
|
-
label: '
|
|
37
|
+
label: t('docs.copy.claude'),
|
|
38
38
|
icon: 'i-simple-icons:anthropic',
|
|
39
39
|
target: '_blank',
|
|
40
40
|
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
|
|
41
41
|
},
|
|
42
42
|
]
|
|
43
|
+
|
|
44
|
+
async function copyPage() {
|
|
45
|
+
const page = await $fetch<string>(`/raw${route.path}.md`)
|
|
46
|
+
copy(page)
|
|
47
|
+
}
|
|
43
48
|
</script>
|
|
44
49
|
|
|
45
50
|
<template>
|
|
46
51
|
<UButtonGroup size="sm">
|
|
47
52
|
<UButton
|
|
48
|
-
label="
|
|
53
|
+
:label="t('docs.copy.page')"
|
|
49
54
|
:icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
|
|
50
55
|
color="neutral"
|
|
51
56
|
variant="outline"
|
|
52
57
|
:ui="{
|
|
53
58
|
leadingIcon: [copied ? 'text-primary' : 'text-neutral', 'size-3.5'],
|
|
54
59
|
}"
|
|
55
|
-
@click="
|
|
60
|
+
@click="copyPage"
|
|
56
61
|
/>
|
|
57
62
|
|
|
58
63
|
<UDropdownMenu
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import en from '../../i18n/locales/en.json'
|
|
2
|
+
|
|
3
|
+
export const useDocusI18n = () => {
|
|
4
|
+
const config = useRuntimeConfig().public
|
|
5
|
+
const isEnabled = ref(!!config.i18n)
|
|
6
|
+
|
|
7
|
+
if (!isEnabled.value) {
|
|
8
|
+
return {
|
|
9
|
+
isEnabled,
|
|
10
|
+
locale: ref('en'),
|
|
11
|
+
locales: ref([]),
|
|
12
|
+
localePath: (path: string) => path,
|
|
13
|
+
switchLocalePath: () => {},
|
|
14
|
+
t: (key: string): string => {
|
|
15
|
+
const path = key.split('.')
|
|
16
|
+
return path.reduce((acc: unknown, curr) => (acc as Record<string, unknown>)?.[curr], en) as string
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { locale, locales, t } = useI18n()
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
isEnabled,
|
|
25
|
+
locale,
|
|
26
|
+
locales,
|
|
27
|
+
t,
|
|
28
|
+
localePath: useLocalePath(),
|
|
29
|
+
switchLocalePath: useSwitchLocalePath(),
|
|
30
|
+
}
|
|
31
|
+
}
|
package/app/error.vue
CHANGED
|
@@ -16,12 +16,12 @@ useSeoMeta({
|
|
|
16
16
|
description: 'We are sorry but this page could not be found.',
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
|
|
20
|
-
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
|
|
21
|
-
|
|
22
|
-
})
|
|
19
|
+
// const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
|
|
20
|
+
// const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
|
|
21
|
+
// server: false,
|
|
22
|
+
// })
|
|
23
23
|
|
|
24
|
-
provide('navigation', navigation)
|
|
24
|
+
// provide('navigation', navigation)
|
|
25
25
|
</script>
|
|
26
26
|
|
|
27
27
|
<template>
|
|
@@ -33,10 +33,10 @@ provide('navigation', navigation)
|
|
|
33
33
|
<AppFooter />
|
|
34
34
|
|
|
35
35
|
<ClientOnly>
|
|
36
|
-
<LazyUContentSearch
|
|
36
|
+
<!-- <LazyUContentSearch
|
|
37
37
|
:files="files"
|
|
38
38
|
:navigation="navigation"
|
|
39
|
-
/>
|
|
39
|
+
/> -->
|
|
40
40
|
</ClientOnly>
|
|
41
41
|
</UApp>
|
|
42
42
|
</template>
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { kebabCase } from 'scule'
|
|
3
|
-
import type { ContentNavigationItem } from '@nuxt/content'
|
|
3
|
+
import type { ContentNavigationItem, Collections, DocsCollectionItem } from '@nuxt/content'
|
|
4
4
|
import { findPageHeadline } from '@nuxt/content/utils'
|
|
5
|
-
import { addPrerenderPath } from '
|
|
5
|
+
import { addPrerenderPath } from '../../utils/prerender'
|
|
6
6
|
|
|
7
7
|
definePageMeta({
|
|
8
8
|
layout: 'docs',
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
const route = useRoute()
|
|
12
|
+
const { locale, isEnabled, t } = useDocusI18n()
|
|
12
13
|
const appConfig = useAppConfig()
|
|
13
14
|
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
|
14
15
|
|
|
16
|
+
const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')
|
|
17
|
+
|
|
15
18
|
const [{ data: page }, { data: surround }] = await Promise.all([
|
|
16
|
-
useAsyncData(kebabCase(route.path), () => queryCollection(
|
|
19
|
+
useAsyncData(kebabCase(route.path), () => queryCollection(collectionName.value as keyof Collections).path(route.path).first()),
|
|
17
20
|
useAsyncData(`${kebabCase(route.path)}-surround`, () => {
|
|
18
|
-
return queryCollectionItemSurroundings(
|
|
21
|
+
return queryCollectionItemSurroundings(collectionName.value as keyof Collections, route.path, {
|
|
19
22
|
fields: ['description'],
|
|
20
23
|
})
|
|
21
24
|
}),
|
|
@@ -71,7 +74,7 @@ const editLink = computed(() => {
|
|
|
71
74
|
>
|
|
72
75
|
<template #links>
|
|
73
76
|
<UButton
|
|
74
|
-
v-for="(link, index) in page.links"
|
|
77
|
+
v-for="(link, index) in (page as DocsCollectionItem).links"
|
|
75
78
|
:key="index"
|
|
76
79
|
size="sm"
|
|
77
80
|
v-bind="link"
|
|
@@ -100,9 +103,9 @@ const editLink = computed(() => {
|
|
|
100
103
|
icon="i-lucide-pen"
|
|
101
104
|
:ui="{ leadingIcon: 'size-4' }"
|
|
102
105
|
>
|
|
103
|
-
|
|
106
|
+
{{ t('docs.edit') }}
|
|
104
107
|
</UButton>
|
|
105
|
-
or
|
|
108
|
+
<span>{{ t('common.or') }}</span>
|
|
106
109
|
<UButton
|
|
107
110
|
variant="link"
|
|
108
111
|
color="neutral"
|
|
@@ -111,7 +114,7 @@ const editLink = computed(() => {
|
|
|
111
114
|
icon="i-lucide-alert-circle"
|
|
112
115
|
:ui="{ leadingIcon: 'size-4' }"
|
|
113
116
|
>
|
|
114
|
-
|
|
117
|
+
{{ t('docs.report') }}
|
|
115
118
|
</UButton>
|
|
116
119
|
</div>
|
|
117
120
|
</USeparator>
|
|
@@ -124,7 +127,7 @@ const editLink = computed(() => {
|
|
|
124
127
|
>
|
|
125
128
|
<UContentToc
|
|
126
129
|
highlight
|
|
127
|
-
:title="appConfig.toc?.title || '
|
|
130
|
+
:title="appConfig.toc?.title || t('docs.toc')"
|
|
128
131
|
:links="page.body?.toc?.links"
|
|
129
132
|
>
|
|
130
133
|
<template #bottom>
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
import type { Collections } from '@nuxt/content'
|
|
3
|
+
|
|
4
|
+
const route = useRoute()
|
|
5
|
+
const { locale, isEnabled } = useDocusI18n()
|
|
6
|
+
|
|
7
|
+
const collectionName = computed(() => isEnabled.value ? `landing_${locale.value}` : 'landing')
|
|
8
|
+
|
|
9
|
+
const { data: page } = await useAsyncData(collectionName.value, () => queryCollection(collectionName.value as keyof Collections).path(route.path).first())
|
|
3
10
|
if (!page.value) {
|
|
4
11
|
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
|
5
12
|
}
|
package/content.config.ts
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
|
+
import type { DefinedCollection } from '@nuxt/content'
|
|
1
2
|
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
|
|
2
3
|
import { useNuxt } from '@nuxt/kit'
|
|
3
4
|
import { joinURL } from 'ufo'
|
|
4
5
|
|
|
5
6
|
const { options } = useNuxt()
|
|
6
7
|
const cwd = joinURL(options.rootDir, 'content')
|
|
8
|
+
const locales = options.i18n?.locales
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
const createDocsSchema = () => z.object({
|
|
11
|
+
links: z.array(z.object({
|
|
12
|
+
label: z.string(),
|
|
13
|
+
icon: z.string(),
|
|
14
|
+
to: z.string(),
|
|
15
|
+
target: z.string().optional(),
|
|
16
|
+
})).optional(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
let collections: Record<string, DefinedCollection>
|
|
20
|
+
|
|
21
|
+
if (locales && Array.isArray(locales)) {
|
|
22
|
+
collections = {}
|
|
23
|
+
for (const locale of locales) {
|
|
24
|
+
const code = typeof locale === 'string' ? locale : locale.code
|
|
25
|
+
|
|
26
|
+
collections[`landing_${code}`] = defineCollection({
|
|
27
|
+
type: 'page',
|
|
28
|
+
source: {
|
|
29
|
+
cwd,
|
|
30
|
+
include: `${code}/index.md`,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
collections[`docs_${code}`] = defineCollection({
|
|
35
|
+
type: 'page',
|
|
36
|
+
source: {
|
|
37
|
+
cwd,
|
|
38
|
+
include: `${code}/**/*.md`,
|
|
39
|
+
prefix: `/${code}`,
|
|
40
|
+
exclude: [`${code}/index.md`],
|
|
41
|
+
},
|
|
42
|
+
schema: createDocsSchema(),
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
collections = {
|
|
10
48
|
landing: defineCollection({
|
|
11
49
|
type: 'page',
|
|
12
50
|
source: {
|
|
@@ -21,14 +59,9 @@ export default defineContentConfig({
|
|
|
21
59
|
include: '**',
|
|
22
60
|
exclude: ['index.md'],
|
|
23
61
|
},
|
|
24
|
-
schema:
|
|
25
|
-
links: z.array(z.object({
|
|
26
|
-
label: z.string(),
|
|
27
|
-
icon: z.string(),
|
|
28
|
-
to: z.string(),
|
|
29
|
-
target: z.string().optional(),
|
|
30
|
-
})).optional(),
|
|
31
|
-
}),
|
|
62
|
+
schema: createDocsSchema(),
|
|
32
63
|
}),
|
|
33
|
-
}
|
|
34
|
-
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default defineContentConfig({ collections })
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default defineI18nConfig(async () => {
|
|
2
|
+
const config = useRuntimeConfig().public
|
|
3
|
+
|
|
4
|
+
const messages: Record<string, Record<string, never>> = {}
|
|
5
|
+
await Promise.all(
|
|
6
|
+
config.i18n.locales.map(async (locale) => {
|
|
7
|
+
const localeFile = await import(`./locales/${locale.code}.json`)
|
|
8
|
+
messages[locale.code] = localeFile.default
|
|
9
|
+
}),
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
messages,
|
|
14
|
+
}
|
|
15
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"or": "or"
|
|
4
|
+
},
|
|
5
|
+
"docs": {
|
|
6
|
+
"copy": {
|
|
7
|
+
"page": "Copy page",
|
|
8
|
+
"link": "Copy Markdown page",
|
|
9
|
+
"view": "View as Markdown",
|
|
10
|
+
"gpt": "Open in ChatGPT",
|
|
11
|
+
"claude": "Open in Claude"
|
|
12
|
+
},
|
|
13
|
+
"links": "Community",
|
|
14
|
+
"toc": "On this page",
|
|
15
|
+
"report": "Report an issue",
|
|
16
|
+
"edit": "Edit this page"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"or": "ou"
|
|
4
|
+
},
|
|
5
|
+
"docs": {
|
|
6
|
+
"copy": {
|
|
7
|
+
"page": "Copier la page",
|
|
8
|
+
"link": "Copier la page Markdown",
|
|
9
|
+
"view": "Voir en Markdown",
|
|
10
|
+
"gpt": "Ouvrir dans ChatGPT",
|
|
11
|
+
"claude": "Ouvrir dans Claude"
|
|
12
|
+
},
|
|
13
|
+
"links": "Communauté",
|
|
14
|
+
"toc": "Sur cette page",
|
|
15
|
+
"report": "Signaler un problème",
|
|
16
|
+
"edit": "Éditer cette page"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -50,5 +50,16 @@ export default defineNuxtModule({
|
|
|
50
50
|
nuxt.options.appConfig.toc = defu(nuxt.options.appConfig.toc, {
|
|
51
51
|
title: 'On this page',
|
|
52
52
|
})
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
** I18N
|
|
56
|
+
*/
|
|
57
|
+
if (nuxt.options.i18n && nuxt.options.i18n.locales) {
|
|
58
|
+
// Override strategy to prefix
|
|
59
|
+
nuxt.options.i18n = {
|
|
60
|
+
...nuxt.options.i18n,
|
|
61
|
+
strategy: 'prefix',
|
|
62
|
+
}
|
|
63
|
+
}
|
|
53
64
|
},
|
|
54
65
|
})
|
package/nuxt.config.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docus",
|
|
3
3
|
"description": "Nuxt layer for Docus documentation theme",
|
|
4
|
-
"version": "4.0.0
|
|
4
|
+
"version": "4.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/nuxtlabs/docus.git"
|
|
10
|
+
},
|
|
7
11
|
"private": false,
|
|
12
|
+
"license": "MIT",
|
|
8
13
|
"files": [
|
|
9
14
|
"app",
|
|
15
|
+
"i18n",
|
|
10
16
|
"content.config.ts",
|
|
11
17
|
"modules",
|
|
12
18
|
"nuxt.config.ts",
|
|
@@ -22,20 +28,22 @@
|
|
|
22
28
|
"@nuxt/content": "^3.6.3",
|
|
23
29
|
"@nuxt/image": "^1.10.0",
|
|
24
30
|
"@nuxt/kit": "^4.0.1",
|
|
25
|
-
"@nuxt/ui-pro": "^3.
|
|
31
|
+
"@nuxt/ui-pro": "^3.3.0",
|
|
32
|
+
"@nuxtjs/i18n": "^10.0.3",
|
|
26
33
|
"@nuxtjs/mdc": "^0.17.2",
|
|
27
34
|
"@nuxtjs/robots": "^5.4.0",
|
|
28
35
|
"@vueuse/core": "^13.5.0",
|
|
29
36
|
"defu": "^6.1.4",
|
|
30
37
|
"git-url-parse": "^16.1.0",
|
|
31
38
|
"minimark": "^0.2.0",
|
|
32
|
-
"motion-v": "^1.6.
|
|
39
|
+
"motion-v": "^1.6.1",
|
|
33
40
|
"nuxt-llms": "^0.1.3",
|
|
34
41
|
"nuxt-og-image": "^5.1.9",
|
|
35
42
|
"pkg-types": "^2.2.0",
|
|
36
43
|
"scule": "^1.3.0",
|
|
37
44
|
"tailwindcss": "^4.1.11",
|
|
38
|
-
"ufo": "^1.6.1"
|
|
45
|
+
"ufo": "^1.6.1",
|
|
46
|
+
"unist-util-visit": "^5.0.0"
|
|
39
47
|
},
|
|
40
48
|
"peerDependencies": {
|
|
41
49
|
"better-sqlite3": "12.x",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { withLeadingSlash } from 'ufo'
|
|
2
2
|
import { stringify } from 'minimark/stringify'
|
|
3
3
|
import { queryCollection } from '@nuxt/content/nitro'
|
|
4
|
+
import type { Collections } from '@nuxt/content'
|
|
4
5
|
|
|
5
6
|
export default eventHandler(async (event) => {
|
|
6
7
|
const slug = getRouterParams(event)['slug.md']
|
|
@@ -9,7 +10,26 @@ export default eventHandler(async (event) => {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const path = withLeadingSlash(slug.replace('.md', ''))
|
|
12
|
-
const
|
|
13
|
+
const config = useRuntimeConfig(event).public
|
|
14
|
+
|
|
15
|
+
let collectionName = 'docs'
|
|
16
|
+
if (config.i18n?.locales) {
|
|
17
|
+
const pathSegments = path.split('/').filter(Boolean)
|
|
18
|
+
const firstSegment = pathSegments[0]
|
|
19
|
+
|
|
20
|
+
const availableLocales = config.i18n.locales.map((locale: string | { code: string }) =>
|
|
21
|
+
typeof locale === 'string' ? locale : locale.code,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if (firstSegment && availableLocales.includes(firstSegment)) {
|
|
25
|
+
collectionName = `docs_${firstSegment}`
|
|
26
|
+
}
|
|
27
|
+
else if (config.i18n.defaultLocale) {
|
|
28
|
+
collectionName = `docs_${config.i18n.defaultLocale}`
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const page = await queryCollection(event, collectionName as keyof Collections).path(path).first()
|
|
13
33
|
if (!page) {
|
|
14
34
|
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
|
15
35
|
}
|