diff --git a/layer/app/app.vue b/layer/app/app.vue index 8726c3874..793d18895 100644 --- a/layer/app/app.vue +++ b/layer/app/app.vue @@ -4,6 +4,8 @@ import * as nuxtUiLocales from '@nuxt/ui/locale' import { transformNavigation } from './utils/navigation' import { useDocusColorMode } from './composables/useDocusColorMode' import { useSubNavigation } from './composables/useSubNavigation' +import { useVersion } from './composables/useVersion' +import { useCollectionName } from './composables/useCollectionName' const appConfig = useAppConfig() const { seo } = appConfig @@ -11,11 +13,12 @@ const { forced: forcedColorMode } = useDocusColorMode() const site = useSiteConfig() const { locale, locales, isEnabled, switchLocalePath } = useDocusI18n() const { isEnabled: isAssistantEnabled, panelWidth: assistantPanelWidth, shouldPushContent } = useAssistant() +const { version, isVersioned } = useVersion() const nuxtUiLocale = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales] || nuxtUiLocales.en) const lang = computed(() => nuxtUiLocale.value.code) const dir = computed(() => nuxtUiLocale.value.dir) -const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs') +const collectionName = useCollectionName('docs') useHead({ meta: [ @@ -50,12 +53,12 @@ if (isEnabled.value) { } const { data: navigation } = await useAsyncData(() => `navigation_${collectionName.value}`, () => queryCollectionNavigation(collectionName.value as keyof PageCollections), { - transform: (data: ContentNavigationItem[]) => transformNavigation(data, isEnabled.value, locale.value), - watch: [locale], + transform: (data: ContentNavigationItem[]) => transformNavigation(data, isEnabled.value, locale.value, isVersioned.value, version.value), + watch: [locale, version], }) const { data: files } = useLazyAsyncData(`search_${collectionName.value}`, () => queryCollectionSearchSections(collectionName.value as keyof PageCollections), { server: false, - watch: [locale], + watch: [locale, version], }) provide('navigation', navigation) diff --git a/layer/app/components/VersionSelect.vue b/layer/app/components/VersionSelect.vue new file mode 100644 index 000000000..9b9a533e0 --- /dev/null +++ b/layer/app/components/VersionSelect.vue @@ -0,0 +1,56 @@ + + + diff --git a/layer/app/composables/useCollectionName.ts b/layer/app/composables/useCollectionName.ts new file mode 100644 index 000000000..61b484c23 --- /dev/null +++ b/layer/app/composables/useCollectionName.ts @@ -0,0 +1,17 @@ +import { computed } from 'vue' + +export const useCollectionName = (type: 'docs' | 'landing' = 'docs') => { + const { version, isVersioned } = useVersion() + const { locale, isEnabled: isI18n } = useDocusI18n() + + return computed(() => { + let name: string = type + if (type === 'docs' && isVersioned.value && version.value) { + name += `_${version.value}` + } + if (isI18n.value) { + name += `_${locale.value}` + } + return name + }) +} diff --git a/layer/app/composables/useVersion.ts b/layer/app/composables/useVersion.ts new file mode 100644 index 000000000..de2bc0498 --- /dev/null +++ b/layer/app/composables/useVersion.ts @@ -0,0 +1,105 @@ +import { useRuntimeConfig, useCookie, useRoute, navigateTo } from '#imports' +import { ref, computed } from 'vue' + +export interface VersionItem { + label: string + value: string + tag?: string +} + +export interface VersionsRuntimeConfig { + strategy: 'prefix' | 'state' + default: string + items: VersionItem[] +} + +export const useVersion = () => { + const config = useRuntimeConfig().public + const versionsConfig = (config.docus as { versions?: VersionsRuntimeConfig } | undefined)?.versions + const isVersioned = ref(!!versionsConfig?.items?.length) + + if (!isVersioned.value || !versionsConfig) { + return { + isVersioned, + version: ref(''), + versions: [] as VersionItem[], + switchVersion: (_v: string) => {}, + versionStrategy: 'prefix' as const, + } + } + + const items = versionsConfig.items + const strategy = versionsConfig.strategy || 'prefix' + const defaultVersion = versionsConfig.default || items[0]!.value + + const resolveVersionFromRoute = (): string => { + if (strategy !== 'prefix') return defaultVersion + const route = useRoute() + const segments = route.path.split('/').filter(Boolean) + + const { isEnabled: isI18n } = useDocusI18n() + const startIdx = isI18n.value ? 1 : 0 + + const candidate = segments[startIdx] + if (candidate && items.some(v => v.value === candidate)) { + return candidate + } + return defaultVersion + } + + const versionCookie = strategy === 'state' + ? useCookie('docus-version', { default: () => defaultVersion }) + : null + + const version = computed({ + get: () => { + if (strategy === 'state') { + return versionCookie!.value || defaultVersion + } + return resolveVersionFromRoute() + }, + set: (v: string) => { + if (strategy === 'state' && versionCookie) { + versionCookie.value = v + } + }, + }) + + const switchVersion = (targetVersion: string) => { + if (!items.some(v => v.value === targetVersion)) return + + if (strategy === 'state') { + version.value = targetVersion + reloadNuxtApp() + return + } + + const route = useRoute() + const currentVersion = version.value + const currentPath = route.path + + let newPath: string + if (currentVersion && currentPath.includes(`/${currentVersion}`)) { + newPath = currentPath.replace(`/${currentVersion}`, `/${targetVersion}`) + } + else { + const { isEnabled: isI18n, locale } = useDocusI18n() + if (isI18n.value) { + newPath = currentPath.replace(`/${locale.value}`, `/${locale.value}/${targetVersion}`) + } + else { + newPath = `/${targetVersion}${currentPath}` + } + } + + navigateTo(newPath) + } + + return { + isVersioned, + version, + versions: items, + switchVersion, + versionStrategy: strategy, + } +} diff --git a/layer/app/error.vue b/layer/app/error.vue index f19e070fb..2fd1cdbd9 100644 --- a/layer/app/error.vue +++ b/layer/app/error.vue @@ -4,6 +4,8 @@ import type { ContentNavigationItem, PageCollections } from '@nuxt/content' import * as nuxtUiLocales from '@nuxt/ui/locale' import { transformNavigation } from './utils/navigation' import { useDocusColorMode } from './composables/useDocusColorMode' +import { useVersion } from './composables/useVersion' +import { useCollectionName } from './composables/useCollectionName' const props = defineProps<{ error: NuxtError @@ -11,6 +13,7 @@ const props = defineProps<{ const { forced: forcedColorMode } = useDocusColorMode() const { locale, locales, isEnabled, t, switchLocalePath } = useDocusI18n() +const { version, isVersioned } = useVersion() const nuxtUiLocale = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales] || nuxtUiLocales.en) const lang = computed(() => nuxtUiLocale.value.code) @@ -47,11 +50,11 @@ if (isEnabled.value) { }) } -const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs') +const collectionName = useCollectionName('docs') const { data: navigation } = await useAsyncData(`navigation_${collectionName.value}`, () => queryCollectionNavigation(collectionName.value as keyof PageCollections), { - transform: (data: ContentNavigationItem[]) => transformNavigation(data, isEnabled.value, locale.value), - watch: [locale], + transform: (data: ContentNavigationItem[]) => transformNavigation(data, isEnabled.value, locale.value, isVersioned.value, version.value), + watch: [locale, version], }) const { data: files } = useLazyAsyncData(`search_${collectionName.value}`, () => queryCollectionSearchSections(collectionName.value as keyof PageCollections), { server: false, diff --git a/layer/app/layouts/docs.vue b/layer/app/layouts/docs.vue index bb69a7c8a..888daeda8 100644 --- a/layer/app/layouts/docs.vue +++ b/layer/app/layouts/docs.vue @@ -1,9 +1,22 @@ + +