@uxfront/layer-docs 0.1.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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/app/app.config.ts +82 -0
- package/app/app.vue +138 -0
- package/app/assets/css/main.css +15 -0
- package/app/components/IconMenuToggle.vue +79 -0
- package/app/components/LanguageSelect.vue +73 -0
- package/app/components/MorphingGradientBackground.vue +261 -0
- package/app/components/app/AppFooter.vue +13 -0
- package/app/components/app/AppFooterCenter.vue +17 -0
- package/app/components/app/AppFooterLeft.vue +21 -0
- package/app/components/app/AppFooterRight.vue +33 -0
- package/app/components/app/AppHeader.vue +105 -0
- package/app/components/app/AppHeaderBody.vue +14 -0
- package/app/components/app/AppHeaderCTA.vue +31 -0
- package/app/components/app/AppHeaderCenter.vue +10 -0
- package/app/components/app/AppHeaderLogo.vue +16 -0
- package/app/components/app/AppSearch.vue +59 -0
- package/app/components/app/AppSubHeader.vue +21 -0
- package/app/components/content/BrowserFrame.vue +28 -0
- package/app/components/content/FrameworkSwitcher.vue +47 -0
- package/app/components/content/Video.vue +103 -0
- package/app/components/docs/DocsAsideLeftBody.vue +20 -0
- package/app/components/docs/DocsAsideRightBottom.vue +15 -0
- package/app/components/docs/DocsPageHeaderLinks.vue +74 -0
- package/app/composables/useDocsSections.ts +50 -0
- package/app/composables/useDocusI18n.ts +49 -0
- package/app/composables/useFramework.ts +70 -0
- package/app/constants/sections.ts +25 -0
- package/app/error.vue +140 -0
- package/app/layouts/default.vue +23 -0
- package/app/pages/[[lang]]/[...slug].vue +48 -0
- package/app/pages/[[lang]]/docs/[section]/[...slug].vue +171 -0
- package/app/plugins/i18n.ts +21 -0
- package/app/plugins/posthog.client.ts +31 -0
- package/app/types/non-route-categories.ts +12 -0
- package/app/utils/flattenNavigation.ts +22 -0
- package/app/utils/foldNonRouteCategories.ts +47 -0
- package/app/utils/prerender.ts +9 -0
- package/i18n/locales/en.json +22 -0
- package/modules/config.ts +122 -0
- package/modules/routing.ts +20 -0
- package/nuxt.config.ts +130 -0
- package/package.json +98 -0
- package/server/plugins/llms-redirect.ts +60 -0
- package/server/routes/raw/[...slug].md.get.ts +74 -0
- package/tsconfig.json +17 -0
- package/utils/content.ts +118 -0
- package/utils/git.ts +114 -0
- package/utils/meta.ts +28 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { kebabCase } from "scule";
|
|
3
|
+
import type { ContentNavigationItem, Collections, PageCollectionItemBase } from "@nuxt/content";
|
|
4
|
+
|
|
5
|
+
type DocsPageItem = PageCollectionItemBase & {
|
|
6
|
+
links?: Array<{
|
|
7
|
+
label: string;
|
|
8
|
+
icon: string;
|
|
9
|
+
to: string;
|
|
10
|
+
target?: string;
|
|
11
|
+
}>;
|
|
12
|
+
seo?: { title?: string; description?: string };
|
|
13
|
+
};
|
|
14
|
+
import { findPageHeadline } from "@nuxt/content/utils";
|
|
15
|
+
|
|
16
|
+
definePageMeta({
|
|
17
|
+
layout: "default",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const route = useRoute();
|
|
21
|
+
const { locale, isEnabled, t } = useDocusI18n();
|
|
22
|
+
const appConfig = useAppConfig();
|
|
23
|
+
const navigationMap = inject<Ref<Record<string, ContentNavigationItem[]>>>("navigation");
|
|
24
|
+
|
|
25
|
+
const sectionSlug = computed(() => route.params.section as string);
|
|
26
|
+
const section = computed(() => findDocsSectionBySlug(sectionSlug.value));
|
|
27
|
+
|
|
28
|
+
if (!section.value) {
|
|
29
|
+
throw createError({
|
|
30
|
+
statusCode: 404,
|
|
31
|
+
statusMessage: "Page not found",
|
|
32
|
+
fatal: true,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const collectionName = computed(() =>
|
|
37
|
+
isEnabled.value ? `docs_${section.value!.key}_${locale.value}` : `docs_${section.value!.key}`,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const navigation = computed(() => navigationMap?.value?.[section.value!.key] ?? []);
|
|
41
|
+
|
|
42
|
+
const [{ data: page }, { data: surround }] = await Promise.all([
|
|
43
|
+
useAsyncData(
|
|
44
|
+
kebabCase(route.path),
|
|
45
|
+
() =>
|
|
46
|
+
queryCollection(collectionName.value as keyof Collections)
|
|
47
|
+
.path(route.path)
|
|
48
|
+
.first() as Promise<DocsPageItem>,
|
|
49
|
+
),
|
|
50
|
+
useAsyncData(`${kebabCase(route.path)}-surround`, () => {
|
|
51
|
+
return queryCollectionItemSurroundings(collectionName.value as keyof Collections, route.path, {
|
|
52
|
+
fields: ["description"],
|
|
53
|
+
});
|
|
54
|
+
}),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
if (!page.value) {
|
|
58
|
+
throw createError({
|
|
59
|
+
statusCode: 404,
|
|
60
|
+
statusMessage: "Page not found",
|
|
61
|
+
fatal: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add the page path to the prerender list
|
|
66
|
+
addPrerenderPath(`/raw${route.path}.md`);
|
|
67
|
+
|
|
68
|
+
const title = page.value.seo?.title || page.value.title;
|
|
69
|
+
const description = page.value.seo?.description || page.value.description;
|
|
70
|
+
|
|
71
|
+
useSeoMeta({
|
|
72
|
+
title,
|
|
73
|
+
ogTitle: title,
|
|
74
|
+
description,
|
|
75
|
+
ogDescription: description,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const headline = ref(findPageHeadline(navigation.value, page.value?.path));
|
|
79
|
+
watch(
|
|
80
|
+
() => navigation.value,
|
|
81
|
+
() => {
|
|
82
|
+
headline.value = findPageHeadline(navigation.value, page.value?.path) || headline.value;
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const github = computed(() => (appConfig.github ? appConfig.github : null));
|
|
87
|
+
|
|
88
|
+
const editLink = computed(() => {
|
|
89
|
+
if (!github.value) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
github.value.url,
|
|
95
|
+
"edit",
|
|
96
|
+
github.value.branch,
|
|
97
|
+
github.value.rootDir,
|
|
98
|
+
"content",
|
|
99
|
+
`${page.value?.stem}.${page.value?.extension}`,
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join("/");
|
|
103
|
+
});
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<UPage v-if="page">
|
|
108
|
+
<UPageHeader
|
|
109
|
+
:title="page.title"
|
|
110
|
+
:description="page.description"
|
|
111
|
+
:headline="headline"
|
|
112
|
+
:ui="{
|
|
113
|
+
wrapper: 'flex-row items-center flex-wrap justify-between',
|
|
114
|
+
}"
|
|
115
|
+
>
|
|
116
|
+
<template #links>
|
|
117
|
+
<UButton
|
|
118
|
+
v-for="(link, index) in (page as DocsPageItem).links"
|
|
119
|
+
:key="index"
|
|
120
|
+
size="sm"
|
|
121
|
+
v-bind="link"
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<DocsPageHeaderLinks />
|
|
125
|
+
</template>
|
|
126
|
+
</UPageHeader>
|
|
127
|
+
|
|
128
|
+
<UPageBody>
|
|
129
|
+
<ContentRenderer v-if="page" :value="page" />
|
|
130
|
+
|
|
131
|
+
<USeparator>
|
|
132
|
+
<div v-if="github" class="flex items-center gap-2 text-sm text-muted">
|
|
133
|
+
<UButton
|
|
134
|
+
variant="link"
|
|
135
|
+
color="neutral"
|
|
136
|
+
:to="editLink"
|
|
137
|
+
target="_blank"
|
|
138
|
+
icon="i-lucide-pen"
|
|
139
|
+
:ui="{ leadingIcon: 'size-4' }"
|
|
140
|
+
>
|
|
141
|
+
{{ t("docs.edit") }}
|
|
142
|
+
</UButton>
|
|
143
|
+
<span>{{ t("common.or") }}</span>
|
|
144
|
+
<UButton
|
|
145
|
+
variant="link"
|
|
146
|
+
color="neutral"
|
|
147
|
+
:to="`${github.url}/issues/new/choose`"
|
|
148
|
+
target="_blank"
|
|
149
|
+
icon="i-lucide-alert-circle"
|
|
150
|
+
:ui="{ leadingIcon: 'size-4' }"
|
|
151
|
+
>
|
|
152
|
+
{{ t("docs.report") }}
|
|
153
|
+
</UButton>
|
|
154
|
+
</div>
|
|
155
|
+
</USeparator>
|
|
156
|
+
<UContentSurround :surround="surround" />
|
|
157
|
+
</UPageBody>
|
|
158
|
+
|
|
159
|
+
<template v-if="page?.body?.toc?.links?.length" #right>
|
|
160
|
+
<UContentToc
|
|
161
|
+
highlight
|
|
162
|
+
:title="appConfig.toc?.title || t('docs.toc')"
|
|
163
|
+
:links="page.body?.toc?.links"
|
|
164
|
+
>
|
|
165
|
+
<template #bottom>
|
|
166
|
+
<DocsAsideRightBottom />
|
|
167
|
+
</template>
|
|
168
|
+
</UContentToc>
|
|
169
|
+
</template>
|
|
170
|
+
</UPage>
|
|
171
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default defineNuxtPlugin(() => {
|
|
2
|
+
// Consumers opt out via `app.config` (`i18nRedirect.enabled: false`).
|
|
3
|
+
if (!useAppConfig().i18nRedirect?.enabled) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const nuxtApp = useNuxtApp();
|
|
8
|
+
|
|
9
|
+
const i18nConfig = nuxtApp.$config.public.i18n as { defaultLocale?: string } | undefined;
|
|
10
|
+
if (!i18nConfig) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
addRouteMiddleware((to) => {
|
|
15
|
+
if (to.path === "/") {
|
|
16
|
+
const cookieLocale = useCookie("i18n_redirected").value || i18nConfig.defaultLocale || "en";
|
|
17
|
+
|
|
18
|
+
return navigateTo(`/${cookieLocale}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ConfigDefaults } from "posthog-js";
|
|
2
|
+
import posthog from "posthog-js";
|
|
3
|
+
import { defineNuxtPlugin } from "#app";
|
|
4
|
+
|
|
5
|
+
export default defineNuxtPlugin(() => {
|
|
6
|
+
if (import.meta.dev) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Consumers opt out via `app.config` (`analytics.enabled: false`).
|
|
11
|
+
if (!useAppConfig().analytics?.enabled) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const runtimeConfig = useRuntimeConfig();
|
|
16
|
+
|
|
17
|
+
const posthogClient = posthog.init(runtimeConfig.public.posthog.key, {
|
|
18
|
+
api_host: runtimeConfig.public.posthog.host,
|
|
19
|
+
defaults: runtimeConfig.public.posthog.defaults as ConfigDefaults,
|
|
20
|
+
person_profiles: "identified_only", // or 'always' to create profiles for anonymous users as well
|
|
21
|
+
loaded: (posthog) => {
|
|
22
|
+
if (import.meta.env.MODE === "development") posthog.debug();
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
provide: {
|
|
28
|
+
posthog: () => posthogClient,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface NonRouteCategoryMeta {
|
|
2
|
+
title: string;
|
|
3
|
+
icon?: string | false;
|
|
4
|
+
defaultOpen?: boolean;
|
|
5
|
+
order: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare module "nuxt/schema" {
|
|
9
|
+
interface AppConfig {
|
|
10
|
+
nonRouteCategories?: Record<string, NonRouteCategoryMeta>;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ContentNavigationItem } from "@nuxt/content";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flatten navigation items that are false-positive groups.
|
|
5
|
+
* Nuxt Content's index detection regex matches filenames containing "index"
|
|
6
|
+
* as a substring (e.g. "z-index"), incorrectly nesting them as groups.
|
|
7
|
+
* This flattens groups with a single child sharing the same path.
|
|
8
|
+
*/
|
|
9
|
+
export function flattenNavigation(items: ContentNavigationItem[]): ContentNavigationItem[] {
|
|
10
|
+
return items.map((item) => {
|
|
11
|
+
if (item.children?.length === 1 && item.children[0]?.path === item.path) {
|
|
12
|
+
const { children: _, ...parent } = item;
|
|
13
|
+
return { ...parent, ...item.children[0], children: undefined };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (item.children?.length) {
|
|
17
|
+
return { ...item, children: flattenNavigation(item.children) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return item;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ContentNavigationItem } from "@nuxt/content";
|
|
2
|
+
import type { NonRouteCategoryMeta } from "~/types/non-route-categories";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rebuilds the sidebar grouping for non-route categories.
|
|
6
|
+
*
|
|
7
|
+
* Pages under a `route: false` folder are flattened in the URL, so the native
|
|
8
|
+
* navigation tree lists them as direct children with the category level gone.
|
|
9
|
+
* This re-nests each such page under a synthetic group node (rendered as a
|
|
10
|
+
* non-navigating accordion header), matching pages to categories by `stem`.
|
|
11
|
+
*/
|
|
12
|
+
export function foldNonRouteCategories(
|
|
13
|
+
items: ContentNavigationItem[],
|
|
14
|
+
categories: Record<string, NonRouteCategoryMeta>,
|
|
15
|
+
): ContentNavigationItem[] {
|
|
16
|
+
return items.map((node) => {
|
|
17
|
+
if (!node.children?.length) {
|
|
18
|
+
return node;
|
|
19
|
+
}
|
|
20
|
+
const passthrough: ContentNavigationItem[] = [];
|
|
21
|
+
const groups = new Map<
|
|
22
|
+
string,
|
|
23
|
+
{ meta: NonRouteCategoryMeta; children: ContentNavigationItem[] }
|
|
24
|
+
>();
|
|
25
|
+
for (const child of node.children) {
|
|
26
|
+
const dir = child.stem ? child.stem.split("/").slice(0, -1).join("/") : "";
|
|
27
|
+
const meta = categories[dir];
|
|
28
|
+
if (meta && child.page !== false) {
|
|
29
|
+
const group = groups.get(dir) ?? { meta, children: [] };
|
|
30
|
+
group.children.push(child);
|
|
31
|
+
groups.set(dir, group);
|
|
32
|
+
} else {
|
|
33
|
+
passthrough.push(foldNonRouteCategories([child], categories)[0]!);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const groupNodes = [...groups.values()]
|
|
37
|
+
.sort((a, b) => a.meta.order - b.meta.order || a.meta.title.localeCompare(b.meta.title))
|
|
38
|
+
.map(({ meta, children }) => ({
|
|
39
|
+
title: meta.title,
|
|
40
|
+
...(meta.icon ? { icon: meta.icon } : {}),
|
|
41
|
+
...(meta.defaultOpen !== undefined ? { defaultOpen: meta.defaultOpen } : {}),
|
|
42
|
+
page: false as const,
|
|
43
|
+
children,
|
|
44
|
+
})) as ContentNavigationItem[];
|
|
45
|
+
return { ...node, children: [...passthrough, ...groupNodes] };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"or": "or",
|
|
4
|
+
"error": {
|
|
5
|
+
"title": "Page not found",
|
|
6
|
+
"description": "We are sorry but this page could not be found."
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"docs": {
|
|
10
|
+
"copy": {
|
|
11
|
+
"page": "Copy page",
|
|
12
|
+
"link": "Copy Markdown page",
|
|
13
|
+
"view": "View as Markdown",
|
|
14
|
+
"gpt": "Open in ChatGPT",
|
|
15
|
+
"claude": "Open in Claude"
|
|
16
|
+
},
|
|
17
|
+
"links": "Community",
|
|
18
|
+
"toc": "On this page",
|
|
19
|
+
"report": "Report an issue",
|
|
20
|
+
"edit": "Edit this page"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createResolver, defineNuxtModule } from "@nuxt/kit";
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { inferSiteURL, getPackageJsonMetadata } from "../utils/meta";
|
|
6
|
+
import { getGitBranch, getGitEnv, getLocalGitInfo } from "../utils/git";
|
|
7
|
+
|
|
8
|
+
export default defineNuxtModule({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "config",
|
|
11
|
+
},
|
|
12
|
+
async setup(_options, nuxt) {
|
|
13
|
+
const dir = nuxt.options.rootDir;
|
|
14
|
+
const url = inferSiteURL();
|
|
15
|
+
const meta = await getPackageJsonMetadata(dir);
|
|
16
|
+
const gitInfo = (await getLocalGitInfo(dir)) || getGitEnv();
|
|
17
|
+
const site = nuxt.options.site;
|
|
18
|
+
const siteName =
|
|
19
|
+
(typeof site === "object" ? site.name : undefined) || meta.name || gitInfo?.name || "";
|
|
20
|
+
|
|
21
|
+
nuxt.options.llms = defu(nuxt.options.llms, {
|
|
22
|
+
domain: url,
|
|
23
|
+
title: siteName,
|
|
24
|
+
description: meta.description || "",
|
|
25
|
+
full: {
|
|
26
|
+
title: siteName,
|
|
27
|
+
description: meta.description || "",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
nuxt.options.site = defu(nuxt.options.site, {
|
|
32
|
+
url,
|
|
33
|
+
name: siteName,
|
|
34
|
+
debug: false,
|
|
35
|
+
}) as typeof nuxt.options.site;
|
|
36
|
+
|
|
37
|
+
nuxt.options.appConfig.header = defu(nuxt.options.appConfig.header, {
|
|
38
|
+
title: siteName,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
nuxt.options.appConfig.seo = defu(nuxt.options.appConfig.seo, {
|
|
42
|
+
titleTemplate: `%s - ${siteName}`,
|
|
43
|
+
title: siteName,
|
|
44
|
+
description: meta.description || "",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
nuxt.options.appConfig.github = defu(nuxt.options.appConfig.github, {
|
|
48
|
+
owner: gitInfo?.owner,
|
|
49
|
+
name: gitInfo?.name,
|
|
50
|
+
url: gitInfo?.url,
|
|
51
|
+
branch: getGitBranch(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
** I18N
|
|
56
|
+
*/
|
|
57
|
+
if (nuxt.options.i18n && nuxt.options.i18n.locales) {
|
|
58
|
+
const { resolve } = createResolver(import.meta.url);
|
|
59
|
+
|
|
60
|
+
// Filter locales to only include existing ones
|
|
61
|
+
const filteredLocales = nuxt.options.i18n.locales.filter((locale) => {
|
|
62
|
+
const localeCode = typeof locale === "string" ? locale : locale.code;
|
|
63
|
+
|
|
64
|
+
// Check for JSON locale file
|
|
65
|
+
const localeFilePath = resolve("../i18n/locales", `${localeCode}.json`);
|
|
66
|
+
const hasLocaleFile = existsSync(localeFilePath);
|
|
67
|
+
|
|
68
|
+
// Check for content folder
|
|
69
|
+
const contentPath = join(nuxt.options.rootDir, "content", localeCode);
|
|
70
|
+
const hasContentFolder = existsSync(contentPath);
|
|
71
|
+
|
|
72
|
+
if (!hasLocaleFile) {
|
|
73
|
+
console.warn(
|
|
74
|
+
`[Docus] Locale file not found: ${localeCode}.json - skipping locale "${localeCode}"`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!hasContentFolder) {
|
|
79
|
+
console.warn(
|
|
80
|
+
`[Docus] Content folder not found: content/${localeCode}/ - skipping locale "${localeCode}"`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return hasLocaleFile && hasContentFolder;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Override strategy to prefix
|
|
88
|
+
nuxt.options.i18n = {
|
|
89
|
+
...nuxt.options.i18n,
|
|
90
|
+
strategy: "prefix",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Expose filtered locales
|
|
94
|
+
nuxt.options.runtimeConfig.public.docus = {
|
|
95
|
+
filteredLocales,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
nuxt.hook("i18n:registerModule", (register) => {
|
|
99
|
+
const langDir = resolve("../i18n/locales");
|
|
100
|
+
|
|
101
|
+
const locales = filteredLocales?.map((locale) => {
|
|
102
|
+
return typeof locale === "string"
|
|
103
|
+
? {
|
|
104
|
+
code: locale,
|
|
105
|
+
name: locale,
|
|
106
|
+
file: `${locale}.json`,
|
|
107
|
+
}
|
|
108
|
+
: {
|
|
109
|
+
code: locale.code,
|
|
110
|
+
name: locale.name || locale.code,
|
|
111
|
+
file: `${locale.code}.json`,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
register({
|
|
116
|
+
langDir,
|
|
117
|
+
locales,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver } from "@nuxt/kit";
|
|
2
|
+
|
|
3
|
+
export default defineNuxtModule({
|
|
4
|
+
meta: {
|
|
5
|
+
name: "routing",
|
|
6
|
+
},
|
|
7
|
+
async setup(_options, nuxt) {
|
|
8
|
+
const { resolve } = createResolver(import.meta.url);
|
|
9
|
+
|
|
10
|
+
// Ensure useDocusI18n is available in the app
|
|
11
|
+
nuxt.hook("imports:extend", (imports) => {
|
|
12
|
+
if (imports.some((i) => i.name === "useDocusI18n")) return;
|
|
13
|
+
|
|
14
|
+
imports.push({
|
|
15
|
+
name: "useDocusI18n",
|
|
16
|
+
from: resolve("../app/composables/useDocusI18n"),
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
});
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createResolver, useNuxt } from "@nuxt/kit";
|
|
2
|
+
|
|
3
|
+
const { resolve } = createResolver(import.meta.url);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `@uxfront/layer-docs` — a neutral, brandable Nuxt-layer documentation theme.
|
|
7
|
+
*
|
|
8
|
+
* Consumers extend it via `extends: ['@uxfront/layer-docs']` and supply their
|
|
9
|
+
* own branding (title, logos, socials, palette) through their `app.config.ts`,
|
|
10
|
+
* merged over the layer's neutral defaults by Nuxt's `defu` layer merge. The
|
|
11
|
+
* section topology (`DOCS_SECTIONS`) and markdown content stay in the consumer.
|
|
12
|
+
*
|
|
13
|
+
* i18n is OFF by default: `@nuxtjs/i18n` is not registered here, so a v1 single
|
|
14
|
+
* locale site "just works". A consumer that wants localisation registers the
|
|
15
|
+
* module itself; `modules/config` + `useDocusI18n` pick it up automatically.
|
|
16
|
+
*
|
|
17
|
+
* https://nuxt.com/docs/getting-started/layers
|
|
18
|
+
*/
|
|
19
|
+
export default defineNuxtConfig({
|
|
20
|
+
compatibilityDate: "2025-07-22",
|
|
21
|
+
telemetry: false,
|
|
22
|
+
css: [resolve("./app/assets/css/main.css")],
|
|
23
|
+
// Expose `app/constants/` (DOCS_SECTIONS, findDocsSectionBySlug) as
|
|
24
|
+
// auto-imports. The layer ships a neutral empty default; a consumer's own
|
|
25
|
+
// `app/constants/` overrides it via auto-import dir precedence.
|
|
26
|
+
imports: {
|
|
27
|
+
dirs: ["constants"],
|
|
28
|
+
},
|
|
29
|
+
modules: [
|
|
30
|
+
resolve("./modules/config"),
|
|
31
|
+
resolve("./modules/routing"),
|
|
32
|
+
"@nuxt/ui",
|
|
33
|
+
"@nuxt/image",
|
|
34
|
+
"@nuxt/scripts",
|
|
35
|
+
"@nuxtjs/robots",
|
|
36
|
+
"@nuxtjs/sitemap",
|
|
37
|
+
"@nuxt/content",
|
|
38
|
+
"nuxt-llms",
|
|
39
|
+
],
|
|
40
|
+
icon: {
|
|
41
|
+
serverBundle: "local",
|
|
42
|
+
fetchTimeout: 10000,
|
|
43
|
+
},
|
|
44
|
+
// Syntax-highlighting language set + MDC auto-unwrap. Shared across every
|
|
45
|
+
// docs consumer; brand-agnostic.
|
|
46
|
+
content: {
|
|
47
|
+
build: {
|
|
48
|
+
markdown: {
|
|
49
|
+
highlight: {
|
|
50
|
+
langs: [
|
|
51
|
+
"bash",
|
|
52
|
+
"diff",
|
|
53
|
+
"json",
|
|
54
|
+
"js",
|
|
55
|
+
"ts",
|
|
56
|
+
"tsx",
|
|
57
|
+
"html",
|
|
58
|
+
"css",
|
|
59
|
+
"vue",
|
|
60
|
+
"svelte",
|
|
61
|
+
"astro",
|
|
62
|
+
"shell",
|
|
63
|
+
"mdc",
|
|
64
|
+
"md",
|
|
65
|
+
"yaml",
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
remarkPlugins: {
|
|
69
|
+
"remark-mdc": {
|
|
70
|
+
options: {
|
|
71
|
+
autoUnwrap: true,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
nitro: {
|
|
79
|
+
prerender: {
|
|
80
|
+
crawlLinks: true,
|
|
81
|
+
failOnError: false,
|
|
82
|
+
autoSubfolderIndex: false,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
hooks: {
|
|
86
|
+
// Seed the prerender crawl with the site root(s). Without i18n that's just
|
|
87
|
+
// "/"; with i18n it's each locale root ("/en", "/fr", ...). Everything else
|
|
88
|
+
// is reached via crawlLinks.
|
|
89
|
+
"nitro:config"(nitroConfig) {
|
|
90
|
+
const nuxt = useNuxt();
|
|
91
|
+
const i18nOptions = nuxt.options.i18n as
|
|
92
|
+
| { locales?: Array<string | { code: string }> }
|
|
93
|
+
| undefined;
|
|
94
|
+
|
|
95
|
+
const routes: string[] = [];
|
|
96
|
+
if (!i18nOptions?.locales?.length) {
|
|
97
|
+
routes.push("/");
|
|
98
|
+
} else {
|
|
99
|
+
routes.push(
|
|
100
|
+
...i18nOptions.locales.map((locale) =>
|
|
101
|
+
typeof locale === "string" ? `/${locale}` : `/${locale.code}`,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
nitroConfig.prerender = nitroConfig.prerender || {};
|
|
107
|
+
nitroConfig.prerender.routes = nitroConfig.prerender.routes || [];
|
|
108
|
+
nitroConfig.prerender.routes.push(...routes);
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
// Generic sitemap defaults; brand values (url/name) come from the consumer's
|
|
112
|
+
// `site` config or are inferred in `modules/config`.
|
|
113
|
+
sitemap: {
|
|
114
|
+
enabled: true,
|
|
115
|
+
discoverImages: true,
|
|
116
|
+
discoverVideos: true,
|
|
117
|
+
},
|
|
118
|
+
// Neutral analytics defaults. The PostHog client plugin (ships in this layer)
|
|
119
|
+
// only initialises in production when `app.config` `analytics.enabled` is true
|
|
120
|
+
// and a key is present; consumers override the key/host from their own env.
|
|
121
|
+
runtimeConfig: {
|
|
122
|
+
public: {
|
|
123
|
+
posthog: {
|
|
124
|
+
key: "",
|
|
125
|
+
host: "https://us.i.posthog.com",
|
|
126
|
+
defaults: "2025-05-24",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|