Stelle docs-site auf Nuxt-UI-Docs-Template-Stil um

This commit is contained in:
2026-04-22 19:07:15 +02:00
parent 76f86e87c1
commit 63b1c563c1
23 changed files with 13114 additions and 215 deletions

3
docs-site/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
.nuxt
.output

View File

@@ -1,8 +1,8 @@
FROM node:20-alpine AS builder
WORKDIR /app/docs-site
COPY docs-site/package.json ./
RUN npm install
COPY docs-site/package.json docs-site/package-lock.json ./
RUN npm ci
COPY docs-site ./
COPY docs /app/docs

View File

@@ -1,6 +1,6 @@
# FEDEO Docs Site (Nuxt Content)
# FEDEO Docs Site (Nuxt UI + Nuxt Content)
Diese Docs-App basiert auf Nuxt Content und rendert die Inhalte aus dem Repository-Ordner `docs/`.
Diese Docs-App nutzt den Standardstil des offiziellen Nuxt-UI-Docs-Templates und rendert Inhalte aus `docs/`.
## Lokale Entwicklung
@@ -11,20 +11,21 @@ npm install
npm run dev
```
Die App ist danach unter `http://localhost:3005` erreichbar.
Danach ist die App unter `http://localhost:3005` erreichbar.
## Build
```bash
npm run build
npm run preview
```
## Production-Deploy
Das Docker-Image startet einen Node-Server auf Port `3000`.
In der Haupt-`docker-compose.yml` wird die App hinter Traefik unter `/docs` veröffentlicht.
Das Docker-Image startet einen Nuxt Node-Server auf Port `3000`.
In der Haupt-`docker-compose.yml` ist der Service hinter Traefik unter `/docs` veröffentlicht.
## Content-Quelle
## Content-Synchronisierung
Vor `dev` und `build` wird automatisch synchronisiert:

View File

@@ -1,3 +0,0 @@
<template>
<NuxtPage />
</template>

View File

@@ -0,0 +1,57 @@
export default defineAppConfig({
ui: {
colors: {
primary: 'green',
neutral: 'slate'
},
footer: {
slots: {
root: 'border-t border-default',
left: 'text-sm text-muted'
}
}
},
seo: {
siteName: 'FEDEO Docs'
},
header: {
title: 'FEDEO Docs',
to: '/',
search: true,
colorMode: true,
links: [
{
icon: 'i-simple-icons-github',
to: 'https://git.federspiel.tech/flfeders/FEDEO',
target: '_blank',
'aria-label': 'Repository'
}
]
},
footer: {
credits: `Built with Nuxt UI • © ${new Date().getFullYear()} FEDEO`,
colorMode: false,
links: [
{
icon: 'i-simple-icons-github',
to: 'https://git.federspiel.tech/flfeders/FEDEO',
target: '_blank',
'aria-label': 'Repository'
}
]
},
toc: {
title: 'Inhaltsverzeichnis',
bottom: {
title: 'Links',
links: [
{
icon: 'i-lucide-book-open',
label: 'FEDEO Projekt',
to: 'https://git.federspiel.tech/flfeders/FEDEO',
target: '_blank'
}
]
}
}
})

44
docs-site/app/app.vue Normal file
View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
const { seo } = useAppConfig()
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
server: false
})
useHead({
meta: [{ name: 'viewport', content: 'width=device-width, initial-scale=1' }],
htmlAttrs: { lang: 'de' }
})
useSeoMeta({
titleTemplate: `%s - ${seo?.siteName}`,
ogSiteName: seo?.siteName,
twitterCard: 'summary_large_image'
})
provide('navigation', navigation)
</script>
<template>
<UApp>
<NuxtLoadingIndicator />
<AppHeader />
<UMain>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UMain>
<AppFooter />
<ClientOnly>
<LazyUContentSearch
:files="files"
:navigation="navigation"
/>
</ClientOnly>
</UApp>
</template>

View File

@@ -0,0 +1,25 @@
@import "tailwindcss";
@import "@nuxt/ui";
@source "../../../content/**/*";
@theme static {
--container-8xl: 90rem;
--font-sans: 'Public Sans', sans-serif;
--color-green-50: #EFFDF5;
--color-green-100: #D9FBE8;
--color-green-200: #B3F5D1;
--color-green-300: #75EDAE;
--color-green-400: #00DC82;
--color-green-500: #00C16A;
--color-green-600: #00A155;
--color-green-700: #007F45;
--color-green-800: #016538;
--color-green-900: #0A5331;
--color-green-950: #052E16;
}
:root {
--ui-container: var(--container-8xl);
}

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
const { footer } = useAppConfig()
</script>
<template>
<UFooter>
<template #left>
{{ footer.credits }}
</template>
<template #right>
<UColorModeButton v-if="footer?.colorMode" />
<template v-if="footer?.links">
<UButton
v-for="(link, index) of footer?.links"
:key="index"
v-bind="{ color: 'neutral', variant: 'ghost', ...link }"
/>
</template>
</template>
</UFooter>
</template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content'
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const { header } = useAppConfig()
</script>
<template>
<UHeader
:ui="{ center: 'flex-1' }"
:to="header?.to || '/'"
>
<UContentSearchButton
v-if="header?.search"
:collapsed="false"
class="w-full"
/>
<template #left>
<NuxtLink :to="header?.to || '/'">
<AppLogo class="w-auto h-6 shrink-0" />
</NuxtLink>
</template>
<template #right>
<UContentSearchButton
v-if="header?.search"
class="lg:hidden"
/>
<UColorModeButton v-if="header?.colorMode" />
<template v-if="header?.links">
<UButton
v-for="(link, index) of header.links"
:key="index"
v-bind="{ color: 'neutral', variant: 'ghost', ...link }"
/>
</template>
</template>
<template #body>
<UContentNavigation
highlight
:navigation="navigation"
/>
</template>
</UHeader>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<span class="font-semibold text-primary">FEDEO Docs</span>
</template>

31
docs-site/app/error.vue Normal file
View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
defineProps<{
error: NuxtError
}>()
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
server: false
})
provide('navigation', navigation)
</script>
<template>
<UApp>
<AppHeader />
<UError :error="error" />
<AppFooter />
<ClientOnly>
<LazyUContentSearch
:files="files"
:navigation="navigation"
/>
</ClientOnly>
</UApp>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content'
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
</script>
<template>
<UContainer>
<UPage>
<template #left>
<UPageAside>
<UContentNavigation
highlight
:navigation="navigation"
/>
</UPageAside>
</template>
<slot />
</UPage>
</UContainer>
</template>

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content'
import { findPageHeadline } from '@nuxt/content/utils'
definePageMeta({
layout: 'docs'
})
const route = useRoute()
const { toc } = useAppConfig()
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const { data: page } = await useAsyncData(route.path, () => queryCollection('docs').path(route.path).first())
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Seite nicht gefunden', fatal: true })
}
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
return queryCollectionItemSurroundings('docs', route.path, {
fields: ['description']
})
})
const title = page.value.seo?.title || page.value.title
const description = page.value.seo?.description || page.value.description
useSeoMeta({
title,
ogTitle: title,
description,
ogDescription: description
})
const headline = computed(() => findPageHeadline(navigation?.value, page.value?.path))
const links = computed(() => {
return [...(toc?.bottom?.links || [])].filter(Boolean)
})
</script>
<template>
<UPage v-if="page">
<UPageHeader
:title="page.title"
:description="page.description"
:headline="headline"
/>
<UPageBody>
<ContentRenderer
v-if="page"
:value="page"
/>
<USeparator v-if="surround?.length" />
<UContentSurround :surround="surround" />
</UPageBody>
<template
v-if="page?.body?.toc?.links?.length"
#right
>
<UContentToc
:title="toc?.title"
:links="page.body?.toc?.links"
>
<template
v-if="toc?.bottom"
#bottom
>
<div
class="hidden lg:block space-y-6"
:class="{ 'mt-6!': page.body?.toc?.links?.length }"
>
<USeparator
v-if="page.body?.toc?.links?.length"
type="dashed"
/>
<UPageLinks
:title="toc.bottom.title"
:links="links"
/>
</div>
</template>
</UContentToc>
</template>
</UPage>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
const { data: page } = await useAsyncData('index', () => queryCollection('landing').path('/').first())
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Seite nicht gefunden', fatal: true })
}
const title = page.value.seo?.title || page.value.title
const description = page.value.seo?.description || page.value.description
useSeoMeta({
titleTemplate: '',
title,
ogTitle: title,
description,
ogDescription: description
})
</script>
<template>
<ContentRenderer
v-if="page"
:value="page"
:prose="false"
/>
</template>

View File

@@ -1,106 +0,0 @@
:root {
--bg: #f6f8f7;
--panel: #ffffff;
--text: #1f2937;
--muted: #5f6b7a;
--accent: #0b6e4f;
--line: #d8e0dc;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
color: var(--text);
background: radial-gradient(circle at 10% 10%, #e7f2ed, transparent 35%), var(--bg);
}
.docs-layout {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
min-height: 100vh;
}
.docs-aside {
border-right: 1px solid var(--line);
background: var(--panel);
padding: 1rem;
position: sticky;
top: 0;
height: 100vh;
overflow: auto;
}
.docs-brand {
display: block;
font-weight: 700;
color: var(--accent);
text-decoration: none;
margin-bottom: 1rem;
}
.docs-sidebar ul {
list-style: none;
margin: 0;
padding: 0 0 0 0.8rem;
}
.docs-sidebar > ul {
padding-left: 0;
}
.docs-sidebar li {
margin: 0.3rem 0;
}
.docs-sidebar a {
color: var(--text);
text-decoration: none;
}
.docs-sidebar a.router-link-active {
color: var(--accent);
font-weight: 600;
}
.docs-main {
padding: 2rem;
}
.docs-article {
max-width: 900px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 14px;
padding: 2rem;
}
.docs-article h1,
.docs-article h2,
.docs-article h3 {
margin-top: 1.5rem;
}
@media (max-width: 900px) {
.docs-layout {
grid-template-columns: 1fr;
}
.docs-aside {
position: static;
height: auto;
border-right: none;
border-bottom: 1px solid var(--line);
}
.docs-main {
padding: 1rem;
}
.docs-article {
padding: 1rem;
}
}

View File

@@ -1,21 +0,0 @@
<script setup lang="ts">
interface NavItem {
title?: string
_path?: string
children?: NavItem[]
}
defineProps<{ items: NavItem[] }>()
</script>
<template>
<nav class="docs-sidebar">
<ul>
<li v-for="item in items" :key="item._path || item.title">
<NuxtLink v-if="item._path" :to="item._path">{{ item.title }}</NuxtLink>
<span v-else>{{ item.title }}</span>
<DocsSidebar v-if="item.children?.length" :items="item.children" />
</li>
</ul>
</nav>
</template>

View File

@@ -0,0 +1,25 @@
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
export default defineContentConfig({
collections: {
landing: defineCollection({
type: 'page',
source: 'index.md'
}),
docs: defineCollection({
type: 'page',
source: {
include: '**',
exclude: ['index.md']
},
schema: z.object({
links: z.array(z.object({
label: z.string(),
icon: z.string(),
to: z.string(),
target: z.string().optional()
})).optional()
})
})
}
})

View File

@@ -1,22 +1,27 @@
export default defineNuxtConfig({
modules: ['@nuxt/content'],
modules: [
'@nuxt/image',
'@nuxt/ui',
'@nuxt/content'
],
css: ['~/assets/css/main.css'],
app: {
head: {
title: 'FEDEO Docs',
meta: [
{ name: 'description', content: 'Versionierte FEDEO-Dokumentation auf Nuxt Content.' }
]
}
},
content: {
documentDriven: false,
highlight: {
theme: 'github-light'
build: {
markdown: {
toc: {
searchDepth: 1
}
}
}
},
experimental: {
asyncContext: true
},
compatibilityDate: '2024-07-11',
nitro: {
preset: 'node-server'
},
compatibilityDate: '2025-01-01'
icon: {
provider: 'iconify'
}
})

12675
docs-site/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,23 @@
{
"name": "fedeo-docs-site",
"version": "2.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "node ./scripts/sync-content.mjs && nuxi dev --host 0.0.0.0 --port 3005",
"build": "node ./scripts/sync-content.mjs && nuxi build",
"preview": "nuxi preview --host 0.0.0.0 --port 3005"
"build": "node ./scripts/sync-content.mjs && nuxt build",
"dev": "node ./scripts/sync-content.mjs && nuxt dev --host 0.0.0.0 --port 3005",
"preview": "nuxt preview --host 0.0.0.0 --port 3005",
"postinstall": "nuxt prepare"
},
"dependencies": {
"nuxt": "^3.17.7",
"@nuxt/content": "^2.13.4"
"@iconify-json/lucide": "^1.2.102",
"@iconify-json/simple-icons": "^1.2.78",
"@nuxt/content": "^3.12.0",
"@nuxt/image": "^2.0.0",
"@nuxt/ui": "^4.6.1",
"nuxt": "^4.4.2"
},
"devDependencies": {
"typescript": "^6.0.2"
},
"engines": {
"node": ">=20.0"

View File

@@ -1,30 +0,0 @@
<script setup lang="ts">
const route = useRoute()
const contentPath = computed(() => route.path)
const { data: navigation } = await useAsyncData('docs-navigation', () => fetchContentNavigation())
const { data: page } = await useAsyncData(
() => `docs-page-${contentPath.value}`,
() => queryContent().where({ _path: contentPath.value }).findOne(),
{ watch: [contentPath] }
)
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Seite nicht gefunden' })
}
</script>
<template>
<div class="docs-layout">
<aside class="docs-aside">
<NuxtLink class="docs-brand" to="/">FEDEO Docs</NuxtLink>
<DocsSidebar :items="navigation || []" />
</aside>
<main class="docs-main">
<article class="docs-article">
<ContentRenderer :value="page" />
</article>
</main>
</div>
</template>

View File

@@ -1,25 +0,0 @@
<script setup lang="ts">
const { data: navigation } = await useAsyncData('docs-navigation', () => fetchContentNavigation())
const { data: page } = await useAsyncData('docs-page-index', () =>
queryContent().where({ _path: '/' }).findOne()
)
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Seite nicht gefunden' })
}
</script>
<template>
<div class="docs-layout">
<aside class="docs-aside">
<NuxtLink class="docs-brand" to="/">FEDEO Docs</NuxtLink>
<DocsSidebar :items="navigation || []" />
</aside>
<main class="docs-main">
<article class="docs-article">
<ContentRenderer :value="page" />
</article>
</main>
</div>
</template>

View File

@@ -1,5 +1,3 @@
{
"compilerOptions": {
"types": ["@types/node"]
}
"extends": "./.nuxt/tsconfig.json"
}