docus 4.0.0-beta.7 → 4.0.0-beta.9

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 CHANGED
@@ -7,7 +7,7 @@
7
7
  [![npm version](https://img.shields.io/npm/v/docus.svg)](https://www.npmjs.com/package/docus)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/docus.svg)](https://www.npmjs.com/package/docus)
9
9
 
10
- This is the official Nuxt layer for [Docus](https://docus.dev), providing a complete documentation theming. It works with the [Docus CLI](https://github.com/nuxtlabs/docus) for rapid project setup.
10
+ This is the official Nuxt layer for [Docus](https://docus.dev), providing a complete documentation theming. It works with the [Docus CLI](https://github.com/nuxtlabs/docus/tree/main/cli) for rapid project setup.
11
11
 
12
12
  ## 🚀 Features
13
13
 
package/app/app.config.ts CHANGED
@@ -29,7 +29,4 @@ export default defineAppConfig({
29
29
  },
30
30
  },
31
31
  },
32
- toc: {
33
- title: 'On this page',
34
- },
35
32
  })
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 { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'), {
6
- transform: data => data.find(item => item.path === '/docs')?.children || data || [],
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('search', () => queryCollectionSearchSections('docs'), {
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: 'en',
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
- <UColorModeButton />
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 || 'Links'"
14
+ :title="appConfig.toc?.bottom?.title || t('docs.links')"
14
15
  :links="appConfig.toc?.bottom?.links"
15
16
  />
16
17
  </div>
@@ -4,6 +4,7 @@ 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
 
@@ -15,26 +16,26 @@ const items = [
15
16
  copy(markdownLink.value)
16
17
 
17
18
  toast.add({
18
- title: 'Markdown link copied to clipboard',
19
+ title: t('docs.copy.link'),
19
20
  icon: 'i-lucide-check-circle',
20
21
  color: 'success',
21
22
  })
22
23
  },
23
24
  },
24
25
  {
25
- label: 'View as Markdown',
26
+ label: t('docs.copy.view'),
26
27
  icon: 'i-simple-icons:markdown',
27
28
  target: '_blank',
28
29
  to: markdownLink.value,
29
30
  },
30
31
  {
31
- label: 'Open in ChatGPT',
32
+ label: t('docs.copy.gpt'),
32
33
  icon: 'i-simple-icons:openai',
33
34
  target: '_blank',
34
35
  to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
35
36
  },
36
37
  {
37
- label: 'Open in Claude',
38
+ label: t('docs.copy.claude'),
38
39
  icon: 'i-simple-icons:anthropic',
39
40
  target: '_blank',
40
41
  to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
@@ -45,7 +46,7 @@ const items = [
45
46
  <template>
46
47
  <UButtonGroup size="sm">
47
48
  <UButton
48
- label="Copy page"
49
+ :label="t('docs.copy.page')"
49
50
  :icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
50
51
  color="neutral"
51
52
  variant="outline"
@@ -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
- server: false,
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 '../utils/prerender'
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('docs').path(route.path).first()),
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('docs', route.path, {
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
- Edit this page
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
- Report an issue
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 || 'Table of Contents'"
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
- const { data: page } = await useAsyncData('index', () => queryCollection('landing').path('/').first())
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
- export default defineContentConfig({
9
- collections: {
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: z.object({
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 })
@@ -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/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "docus",
3
3
  "description": "Nuxt layer for Docus documentation theme",
4
- "version": "4.0.0-beta.7",
4
+ "version": "4.0.0-beta.9",
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",
10
15
  "content.config.ts",
@@ -35,7 +40,11 @@
35
40
  "pkg-types": "^2.2.0",
36
41
  "scule": "^1.3.0",
37
42
  "tailwindcss": "^4.1.11",
38
- "ufo": "^1.6.1"
43
+ "ufo": "^1.6.1",
44
+ "unist-util-visit": "^5.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@nuxtjs/i18n": "^9.5.6"
39
48
  },
40
49
  "peerDependencies": {
41
50
  "better-sqlite3": "12.x",