first commit
This commit is contained in:
36
app/app/app.config.ts
Normal file
36
app/app/app.config.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'emerald',
|
||||
neutral: 'zinc',
|
||||
},
|
||||
},
|
||||
uiPro: {
|
||||
contentNavigation: {
|
||||
slots: {
|
||||
linkLeadingIcon: 'size-4 mr-1',
|
||||
listWithChildren: 'border-(--ui-bg-elevated)',
|
||||
linkTrailing: 'hidden',
|
||||
},
|
||||
variants: {
|
||||
active: {
|
||||
false: {
|
||||
link: 'text-toned hover:after:bg-accented',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'link',
|
||||
},
|
||||
},
|
||||
pageLinks: {
|
||||
slots: {
|
||||
linkLeadingIcon: 'size-4',
|
||||
linkLabelExternalIcon: 'size-2.5',
|
||||
},
|
||||
},
|
||||
},
|
||||
toc: {
|
||||
title: 'On this page',
|
||||
},
|
||||
})
|
56
app/app/app.vue
Normal file
56
app/app/app.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
const { seo } = useAppConfig()
|
||||
const site = useSiteConfig()
|
||||
|
||||
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'), {
|
||||
transform: data => data.find(item => item.path === '/docs')?.children || data || [],
|
||||
})
|
||||
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
|
||||
server: false,
|
||||
})
|
||||
|
||||
useHead({
|
||||
meta: [
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', href: '/favicon.ico' },
|
||||
],
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
},
|
||||
})
|
||||
|
||||
useSeoMeta({
|
||||
titleTemplate: seo.titleTemplate,
|
||||
title: seo.title,
|
||||
description: seo.description,
|
||||
ogSiteName: site.name,
|
||||
twitterCard: 'summary_large_image',
|
||||
})
|
||||
|
||||
provide('navigation', navigation)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<NuxtLoadingIndicator color="var(--ui-primary)" />
|
||||
|
||||
<AppHeader />
|
||||
|
||||
<UMain>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</UMain>
|
||||
|
||||
<AppFooter />
|
||||
|
||||
<ClientOnly>
|
||||
<LazyUContentSearch
|
||||
:files="files"
|
||||
:navigation="navigation"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</UApp>
|
||||
</template>
|
5
app/app/assets/css/main.css
Normal file
5
app/app/assets/css/main.css
Normal file
@ -0,0 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui-pro";
|
||||
|
||||
@source "../../../content/**/*";
|
||||
@source "../../app.config.ts";
|
83
app/app/components/IconMenuToggle.vue
Normal file
83
app/app/components/IconMenuToggle.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { motion } from 'motion-v'
|
||||
import type { VariantType } from 'motion-v'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
const variants: { [k: string]: VariantType | ((custom: unknown) => VariantType) } = {
|
||||
normal: {
|
||||
rotate: 0,
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
close: (custom: unknown) => {
|
||||
const c = custom as number
|
||||
return {
|
||||
rotate: c === 1 ? 45 : c === 3 ? -45 : 0,
|
||||
y: c === 1 ? 6 : c === 3 ? -6 : 0,
|
||||
opacity: c === 2 ? 0 : 1,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 260,
|
||||
damping: 20,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const state = computed(() => props.open ? 'close' : 'normal')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
class="-me-1.5"
|
||||
square
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="size-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<motion.line
|
||||
x1="4"
|
||||
y1="6"
|
||||
x2="20"
|
||||
y2="6"
|
||||
:variants="variants"
|
||||
:animate="state"
|
||||
:custom="1"
|
||||
class="outline-none"
|
||||
/>
|
||||
<motion.line
|
||||
x1="4"
|
||||
y1="12"
|
||||
x2="20"
|
||||
y2="12"
|
||||
:variants="variants"
|
||||
:animate="state"
|
||||
:custom="2"
|
||||
class="outline-none"
|
||||
/>
|
||||
<motion.line
|
||||
x1="4"
|
||||
y1="18"
|
||||
x2="20"
|
||||
y2="18"
|
||||
:variants="variants"
|
||||
:animate="state"
|
||||
:custom="3"
|
||||
class="outline-none"
|
||||
/>
|
||||
</svg>
|
||||
</UButton>
|
||||
</template>
|
76
app/app/components/OgImage/OgImageDocs.vue
Normal file
76
app/app/components/OgImage/OgImageDocs.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{ title?: string, description?: string, headline?: string }>(), {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
})
|
||||
|
||||
const title = computed(() => (props.title || '').slice(0, 60))
|
||||
const description = computed(() => (props.description || '').slice(0, 200))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col justify-center bg-neutral-900">
|
||||
<svg
|
||||
class="absolute right-0 top-0 opacity-50"
|
||||
width="629"
|
||||
height="593"
|
||||
viewBox="0 0 629 593"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g filter="url(#filter0_f_199_94966)">
|
||||
<path
|
||||
d="M628.5 -578L639.334 -94.4223L806.598 -548.281L659.827 -87.387L965.396 -462.344L676.925 -74.0787L1087.69 -329.501L688.776 -55.9396L1160.22 -164.149L694.095 -34.9354L1175.13 15.7948L692.306 -13.3422L1130.8 190.83L683.602 6.50012L1032.04 341.989L668.927 22.4412L889.557 452.891L649.872 32.7537L718.78 511.519L628.5 36.32L538.22 511.519L607.128 32.7537L367.443 452.891L588.073 22.4412L224.955 341.989L573.398 6.50012L126.198 190.83L564.694 -13.3422L81.8734 15.7948L562.905 -34.9354L96.7839 -164.149L568.224 -55.9396L169.314 -329.501L580.075 -74.0787L291.604 -462.344L597.173 -87.387L450.402 -548.281L617.666 -94.4223L628.5 -578Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_f_199_94966"
|
||||
x="0.873535"
|
||||
y="-659"
|
||||
width="1255.25"
|
||||
height="1251.52"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feGaussianBlur
|
||||
stdDeviation="40.5"
|
||||
result="effect1_foregroundBlur_199_94966"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="pl-[100px]">
|
||||
<p
|
||||
v-if="headline"
|
||||
class="uppercase text-[24px] text-emerald-500 mb-4 font-semibold"
|
||||
>
|
||||
{{ headline }}
|
||||
</p>
|
||||
<h1
|
||||
v-if="title"
|
||||
class="m-0 text-[75px] font-semibold mb-4 text-white flex items-center"
|
||||
>
|
||||
<span>{{ title }}</span>
|
||||
</h1>
|
||||
<p
|
||||
v-if="description"
|
||||
class="text-[32px] text-neutral-300 leading-tight w-[700px]"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
73
app/app/components/OgImage/OgImageLanding.vue
Normal file
73
app/app/components/OgImage/OgImageLanding.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{ title?: string, description?: string, headline?: string }>(), {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
})
|
||||
|
||||
const title = computed(() => (props.title || '').slice(0, 60))
|
||||
const description = computed(() => (props.description || '').slice(0, 200))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex items-center justify-center bg-neutral-900">
|
||||
<svg
|
||||
class="absolute right-0 top-0 opacity-50 "
|
||||
width="629"
|
||||
height="593"
|
||||
viewBox="0 0 629 593"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g filter="url(#filter0_f_199_94966)">
|
||||
<path
|
||||
d="M628.5 -578L639.334 -94.4223L806.598 -548.281L659.827 -87.387L965.396 -462.344L676.925 -74.0787L1087.69 -329.501L688.776 -55.9396L1160.22 -164.149L694.095 -34.9354L1175.13 15.7948L692.306 -13.3422L1130.8 190.83L683.602 6.50012L1032.04 341.989L668.927 22.4412L889.557 452.891L649.872 32.7537L718.78 511.519L628.5 36.32L538.22 511.519L607.128 32.7537L367.443 452.891L588.073 22.4412L224.955 341.989L573.398 6.50012L126.198 190.83L564.694 -13.3422L81.8734 15.7948L562.905 -34.9354L96.7839 -164.149L568.224 -55.9396L169.314 -329.501L580.075 -74.0787L291.604 -462.344L597.173 -87.387L450.402 -548.281L617.666 -94.4223L628.5 -578Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_f_199_94966"
|
||||
x="0.873535"
|
||||
y="-659"
|
||||
width="1255.25"
|
||||
height="1251.52"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feGaussianBlur
|
||||
stdDeviation="40.5"
|
||||
result="effect1_foregroundBlur_199_94966"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="flex flex-col justify-center p-8">
|
||||
<div class="flex justify-center mb-8">
|
||||
<AppHeaderLogo white />
|
||||
</div>
|
||||
<h1
|
||||
v-if="title"
|
||||
class="flex justify-center m-0 text-5xl font-semibold mb-4 text-white"
|
||||
>
|
||||
<span>{{ title }}</span>
|
||||
</h1>
|
||||
<p
|
||||
v-if="description"
|
||||
class="text-center text-2xl text-neutral-300 leading-tight"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
40
app/app/components/app/AppFooter.vue
Normal file
40
app/app/components/app/AppFooter.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const links = computed(() => [
|
||||
...Object.entries(appConfig.socials || {}).map(([key, url]) => ({
|
||||
'icon': `i-simple-icons-${key}`,
|
||||
'to': url,
|
||||
'target': '_blank',
|
||||
'aria-label': `${key} social link`,
|
||||
})),
|
||||
appConfig.github?.url && {
|
||||
'icon': 'i-simple-icons-github',
|
||||
'to': appConfig.github.url,
|
||||
'target': '_blank',
|
||||
'aria-label': 'GitHub repository',
|
||||
},
|
||||
].filter(Boolean))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UFooter>
|
||||
<template #left>
|
||||
<div class="text-sm text-muted">
|
||||
Copyright © {{ new Date().getFullYear() }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<template v-if="links.length">
|
||||
<UButton
|
||||
v-for="(link, index) of links"
|
||||
:key="index"
|
||||
size="sm"
|
||||
v-bind="{ color: 'neutral', variant: 'ghost', ...link }"
|
||||
/>
|
||||
</template>
|
||||
<UColorModeButton />
|
||||
</template>
|
||||
</UFooter>
|
||||
</template>
|
57
app/app/components/app/AppHeader.vue
Normal file
57
app/app/components/app/AppHeader.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
const appConfig = useAppConfig()
|
||||
const site = useSiteConfig()
|
||||
|
||||
const links = computed(() => appConfig.github?.url
|
||||
? [
|
||||
{
|
||||
'icon': 'i-simple-icons-github',
|
||||
'to': appConfig.github.url,
|
||||
'target': '_blank',
|
||||
'aria-label': 'GitHub',
|
||||
},
|
||||
]
|
||||
: [])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UHeader
|
||||
:ui="{ center: 'flex-1' }"
|
||||
to="/"
|
||||
:title="appConfig.header?.title || site.name"
|
||||
>
|
||||
<AppHeaderCenter />
|
||||
|
||||
<template #title>
|
||||
<AppHeaderLogo class="h-6 w-auto shrink-0" />
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<AppHeaderCTA />
|
||||
|
||||
<UContentSearchButton class="lg:hidden" />
|
||||
|
||||
<UColorModeButton />
|
||||
|
||||
<template v-if="links?.length">
|
||||
<UButton
|
||||
v-for="(link, index) of links"
|
||||
:key="index"
|
||||
v-bind="{ color: 'neutral', variant: 'ghost', ...link }"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #toggle="{ open, toggle }">
|
||||
<IconMenuToggle
|
||||
:open="open"
|
||||
class="lg:hidden"
|
||||
@click="toggle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<AppHeaderBody />
|
||||
</template>
|
||||
</UHeader>
|
||||
</template>
|
13
app/app/components/app/AppHeaderBody.vue
Normal file
13
app/app/components/app/AppHeaderBody.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContentNavigationItem } from '@nuxt/content'
|
||||
|
||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UContentNavigation
|
||||
highlight
|
||||
variant="link"
|
||||
:navigation="navigation"
|
||||
/>
|
||||
</template>
|
3
app/app/components/app/AppHeaderCTA.vue
Normal file
3
app/app/components/app/AppHeaderCTA.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
6
app/app/components/app/AppHeaderCenter.vue
Normal file
6
app/app/components/app/AppHeaderCenter.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<UContentSearchButton
|
||||
:collapsed="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
16
app/app/components/app/AppHeaderLogo.vue
Normal file
16
app/app/components/app/AppHeaderLogo.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const appConfig = useAppConfig()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UColorModeImage
|
||||
v-if="appConfig.header?.logo?.dark || appConfig.header?.logo?.light"
|
||||
:light="appConfig.header?.logo?.light || appConfig.header?.logo?.dark"
|
||||
:dark="appConfig.header?.logo?.dark || appConfig.header?.logo?.light"
|
||||
:alt="appConfig.header?.logo?.alt || appConfig.header?.title"
|
||||
class="h-6 w-auto shrink-0"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ appConfig.header?.title || '{appConfig.header.title}' }}
|
||||
</span>
|
||||
</template>
|
3
app/app/components/docs/DocsAsideLeftTop.vue
Normal file
3
app/app/components/docs/DocsAsideLeftTop.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
17
app/app/components/docs/DocsAsideRightBottom.vue
Normal file
17
app/app/components/docs/DocsAsideRightBottom.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
const appConfig = useAppConfig()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="appConfig.toc?.bottom?.links?.length"
|
||||
class="hidden lg:block space-y-6"
|
||||
>
|
||||
<USeparator type="dashed" />
|
||||
|
||||
<UPageLinks
|
||||
:title="appConfig.toc?.bottom?.title || 'Links'"
|
||||
:links="appConfig.toc?.bottom?.links"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
77
app/app/components/docs/DocsPageHeaderLinks.vue
Normal file
77
app/app/components/docs/DocsPageHeaderLinks.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const { copy, copied } = useClipboard()
|
||||
|
||||
const markdownLink = computed(() => `${window?.location?.origin}/raw${route.path}.md`)
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: 'Copy Markdown link',
|
||||
icon: 'i-lucide-link',
|
||||
onSelect() {
|
||||
copy(markdownLink.value)
|
||||
|
||||
toast.add({
|
||||
title: 'Markdown link copied to clipboard',
|
||||
icon: 'i-lucide-check-circle',
|
||||
color: 'success',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'View as Markdown',
|
||||
icon: 'i-simple-icons:markdown',
|
||||
target: '_blank',
|
||||
to: markdownLink.value,
|
||||
},
|
||||
{
|
||||
label: 'Open in ChatGPT',
|
||||
icon: 'i-simple-icons:openai',
|
||||
target: '_blank',
|
||||
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
|
||||
},
|
||||
{
|
||||
label: 'Open in Claude',
|
||||
icon: 'i-simple-icons:anthropic',
|
||||
target: '_blank',
|
||||
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButtonGroup size="sm">
|
||||
<UButton
|
||||
label="Copy page"
|
||||
:icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:ui="{
|
||||
leadingIcon: [copied ? 'text-primary' : 'text-neutral', 'size-3.5'],
|
||||
}"
|
||||
@click="copy(markdownLink)"
|
||||
/>
|
||||
|
||||
<UDropdownMenu
|
||||
size="sm"
|
||||
:items="items"
|
||||
:content="{
|
||||
align: 'end',
|
||||
side: 'bottom',
|
||||
sideOffset: 8,
|
||||
}"
|
||||
:ui="{
|
||||
content: 'w-48',
|
||||
}"
|
||||
>
|
||||
<UButton
|
||||
icon="i-lucide-chevron-down"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
/>
|
||||
</UDropdownMenu>
|
||||
</UButtonGroup>
|
||||
</template>
|
42
app/app/error.vue
Normal file
42
app/app/error.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import type { NuxtError } from '#app'
|
||||
|
||||
defineProps<{
|
||||
error: NuxtError
|
||||
}>()
|
||||
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
},
|
||||
})
|
||||
|
||||
useSeoMeta({
|
||||
title: 'Page not found',
|
||||
description: 'We are sorry but this page could not be found.',
|
||||
})
|
||||
|
||||
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>
|
24
app/app/layouts/docs.vue
Normal file
24
app/app/layouts/docs.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContentNavigationItem } from '@nuxt/content'
|
||||
|
||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UContainer>
|
||||
<UPage>
|
||||
<template #left>
|
||||
<UPageAside>
|
||||
<DocsAsideLeftTop />
|
||||
|
||||
<UContentNavigation
|
||||
highlight
|
||||
:navigation="navigation"
|
||||
/>
|
||||
</UPageAside>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</UPage>
|
||||
</UContainer>
|
||||
</template>
|
136
app/app/pages/[...slug].vue
Normal file
136
app/app/pages/[...slug].vue
Normal file
@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { kebabCase } from 'scule'
|
||||
import type { ContentNavigationItem } from '@nuxt/content'
|
||||
import { findPageHeadline } from '@nuxt/content/utils'
|
||||
import { addPrerenderPath } from '../utils/prerender'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'docs',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const appConfig = useAppConfig()
|
||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
||||
|
||||
const [{ data: page }, { data: surround }] = await Promise.all([
|
||||
useAsyncData(kebabCase(route.path), () => queryCollection('docs').path(route.path).first()),
|
||||
useAsyncData(`${kebabCase(route.path)}-surround`, () => {
|
||||
return queryCollectionItemSurroundings('docs', route.path, {
|
||||
fields: ['description'],
|
||||
})
|
||||
}),
|
||||
])
|
||||
|
||||
if (!page.value) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||
}
|
||||
|
||||
// Add the page path to the prerender list
|
||||
addPrerenderPath(`/raw${route.path}.md`)
|
||||
|
||||
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))
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: headline.value,
|
||||
})
|
||||
|
||||
const editLink = computed(() => {
|
||||
if (!appConfig.github) {
|
||||
return
|
||||
}
|
||||
|
||||
return [
|
||||
appConfig.github.url,
|
||||
'edit',
|
||||
appConfig.github.branch,
|
||||
appConfig.github.rootDir,
|
||||
'content',
|
||||
`${page.value?.stem}.${page.value?.extension}`,
|
||||
].filter(Boolean).join('/')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage v-if="page">
|
||||
<UPageHeader
|
||||
:title="page.title"
|
||||
:description="page.description"
|
||||
:headline="headline"
|
||||
:ui="{
|
||||
wrapper: 'flex-row items-center flex-wrap justify-between',
|
||||
}"
|
||||
>
|
||||
<template #links>
|
||||
<UButton
|
||||
v-for="(link, index) in page.links"
|
||||
:key="index"
|
||||
size="sm"
|
||||
v-bind="link"
|
||||
/>
|
||||
|
||||
<DocsPageHeaderLinks />
|
||||
</template>
|
||||
</UPageHeader>
|
||||
|
||||
<UPageBody>
|
||||
<ContentRenderer
|
||||
v-if="page"
|
||||
:value="page"
|
||||
/>
|
||||
|
||||
<USeparator>
|
||||
<div
|
||||
v-if="editLink"
|
||||
class="flex items-center gap-2 text-sm text-muted"
|
||||
>
|
||||
<UButton
|
||||
variant="link"
|
||||
color="neutral"
|
||||
:to="editLink"
|
||||
target="_blank"
|
||||
icon="i-lucide-pen"
|
||||
:ui="{ leadingIcon: 'size-4' }"
|
||||
>
|
||||
Edit this page
|
||||
</UButton>
|
||||
or
|
||||
<UButton
|
||||
variant="link"
|
||||
color="neutral"
|
||||
:to="`${appConfig.github.url}/issues/new/choose`"
|
||||
target="_blank"
|
||||
icon="i-lucide-alert-circle"
|
||||
:ui="{ leadingIcon: 'size-4' }"
|
||||
>
|
||||
Report an issue
|
||||
</UButton>
|
||||
</div>
|
||||
</USeparator>
|
||||
<UContentSurround :surround="surround" />
|
||||
</UPageBody>
|
||||
|
||||
<template
|
||||
v-if="page?.body?.toc?.links?.length"
|
||||
#right
|
||||
>
|
||||
<UContentToc
|
||||
highlight
|
||||
:title="appConfig.toc?.title || 'Table of Contents'"
|
||||
:links="page.body?.toc?.links"
|
||||
>
|
||||
<template #bottom>
|
||||
<DocsAsideRightBottom />
|
||||
</template>
|
||||
</UContentToc>
|
||||
</template>
|
||||
</UPage>
|
||||
</template>
|
39
app/app/pages/index.vue
Normal file
39
app/app/pages/index.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
const { data: page } = await useAsyncData('index', () => queryCollection('landing').path('/').first())
|
||||
if (!page.value) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||
}
|
||||
|
||||
// Reconsider it once this is implemented: https://github.com/nuxt/content/issues/3419
|
||||
const prose = page.value.meta.prose as boolean
|
||||
const title = page.value.seo?.title || page.value.title
|
||||
const description = page.value.seo?.description || page.value.description
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: title,
|
||||
ogDescription: description,
|
||||
})
|
||||
|
||||
if (page.value?.seo?.ogImage) {
|
||||
useSeoMeta({
|
||||
ogImage: page.value.seo.ogImage,
|
||||
twitterImage: page.value.seo.ogImage,
|
||||
})
|
||||
}
|
||||
else {
|
||||
defineOgImageComponent('Landing', {
|
||||
title,
|
||||
description,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentRenderer
|
||||
v-if="page"
|
||||
:value="page"
|
||||
:prose="prose || false"
|
||||
/>
|
||||
</template>
|
39
app/app/types/index.d.ts
vendored
Normal file
39
app/app/types/index.d.ts
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
declare module 'nuxt/schema' {
|
||||
interface AppConfig {
|
||||
seo: {
|
||||
titleTemplate: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
header: {
|
||||
title: string
|
||||
logo: {
|
||||
light: string
|
||||
dark: string
|
||||
alt: string
|
||||
}
|
||||
}
|
||||
socials: Record<string, string>
|
||||
toc: {
|
||||
title: string
|
||||
bottom: {
|
||||
title: string
|
||||
links: {
|
||||
icon: string
|
||||
label: string
|
||||
to: string
|
||||
target: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
github: {
|
||||
owner: string
|
||||
name: string
|
||||
url: string
|
||||
branch: string
|
||||
rootDir?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
110
app/app/utils/git.ts
Normal file
110
app/app/utils/git.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { execSync } from 'node:child_process'
|
||||
import { readGitConfig } from 'pkg-types'
|
||||
import gitUrlParse from 'git-url-parse'
|
||||
|
||||
export interface GitInfo {
|
||||
// Repository name
|
||||
name: string
|
||||
// Repository owner/organization
|
||||
owner: string
|
||||
// Repository URL
|
||||
url: string
|
||||
}
|
||||
|
||||
export function getGitBranch() {
|
||||
const envName
|
||||
= process.env.CF_PAGES_BRANCH
|
||||
|| process.env.CI_COMMIT_BRANCH
|
||||
|| process.env.VERCEL_GIT_COMMIT_REF
|
||||
|| process.env.BRANCH
|
||||
|| process.env.GITHUB_REF_NAME
|
||||
|
||||
if (envName && envName !== 'HEAD') {
|
||||
return envName
|
||||
}
|
||||
try {
|
||||
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
|
||||
if (branch && branch !== 'HEAD') {
|
||||
return branch
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
return 'main'
|
||||
}
|
||||
|
||||
export async function getLocalGitInfo(rootDir: string): Promise<GitInfo | undefined> {
|
||||
const remote = await getLocalGitRemote(rootDir)
|
||||
if (!remote) {
|
||||
return
|
||||
}
|
||||
|
||||
// https://www.npmjs.com/package/git-url-parse#clipboard-example
|
||||
const { name, owner, source } = gitUrlParse(remote)
|
||||
const url = `https://${source}/${owner}/${name}`
|
||||
|
||||
return {
|
||||
name,
|
||||
owner,
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
async function getLocalGitRemote(dir: string): Promise<string | undefined> {
|
||||
try {
|
||||
const parsed = await readGitConfig(dir)
|
||||
if (!parsed) {
|
||||
return
|
||||
}
|
||||
return parsed.remote?.['origin']?.url
|
||||
}
|
||||
catch {
|
||||
// Ignore error
|
||||
}
|
||||
}
|
||||
|
||||
export function getGitEnv(): GitInfo {
|
||||
// https://github.com/unjs/std-env/issues/59
|
||||
const envInfo = {
|
||||
// Provider
|
||||
provider: process.env.VERCEL_GIT_PROVIDER // vercel
|
||||
|| (process.env.GITHUB_SERVER_URL ? 'github' : undefined) // github
|
||||
|| '',
|
||||
// Owner
|
||||
owner: process.env.VERCEL_GIT_REPO_OWNER // vercel
|
||||
|| process.env.GITHUB_REPOSITORY_OWNER // github
|
||||
|| process.env.CI_PROJECT_PATH?.split('/').shift() // gitlab
|
||||
|| '',
|
||||
// Name
|
||||
name: process.env.VERCEL_GIT_REPO_SLUG
|
||||
|| process.env.GITHUB_REPOSITORY?.split('/').pop() // github
|
||||
|| process.env.CI_PROJECT_PATH?.split('/').splice(1).join('/') // gitlab
|
||||
|| '',
|
||||
// Url
|
||||
url: process.env.REPOSITORY_URL || '', // netlify
|
||||
}
|
||||
|
||||
if (!envInfo.url && envInfo.provider && envInfo.owner && envInfo.name) {
|
||||
envInfo.url = `https://${envInfo.provider}.com/${envInfo.owner}/${envInfo.name}`
|
||||
}
|
||||
|
||||
// If only url available (ex: Netlify)
|
||||
if (!envInfo.name && !envInfo.owner && envInfo.url) {
|
||||
try {
|
||||
const { name, owner } = gitUrlParse(envInfo.url)
|
||||
envInfo.name = name
|
||||
envInfo.owner = owner
|
||||
}
|
||||
catch {
|
||||
// Ignore error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: envInfo.name,
|
||||
owner: envInfo.owner,
|
||||
url: envInfo.url,
|
||||
}
|
||||
}
|
29
app/app/utils/meta.ts
Normal file
29
app/app/utils/meta.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
export function inferSiteURL() {
|
||||
// https://github.com/unjs/std-env/issues/59
|
||||
return (
|
||||
process.env.NUXT_SITE_URL
|
||||
|| (process.env.NEXT_PUBLIC_VERCEL_URL && `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`) // Vercel
|
||||
|| process.env.URL // Netlify
|
||||
|| process.env.CI_PAGES_URL // Gitlab Pages
|
||||
|| process.env.CF_PAGES_URL // Cloudflare Pages
|
||||
)
|
||||
}
|
||||
|
||||
export async function getPackageJsonMetadata(dir: string) {
|
||||
try {
|
||||
const packageJson = await readFile(resolve(dir, 'package.json'), 'utf-8')
|
||||
const parsed = JSON.parse(packageJson)
|
||||
return {
|
||||
name: parsed.name,
|
||||
description: parsed.description,
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
name: 'docs',
|
||||
}
|
||||
}
|
||||
}
|
12
app/app/utils/prerender.ts
Normal file
12
app/app/utils/prerender.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const addPrerenderPath = (path: string) => {
|
||||
const event = useRequestEvent()
|
||||
if (event) {
|
||||
event.node.res.setHeader(
|
||||
'x-nitro-prerender',
|
||||
[
|
||||
event.node.res.getHeader('x-nitro-prerender'),
|
||||
path,
|
||||
].filter(Boolean).join(','),
|
||||
)
|
||||
}
|
||||
}
|
31
app/content.config.ts
Normal file
31
app/content.config.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
|
||||
|
||||
export default defineContentConfig({
|
||||
collections: {
|
||||
landing: defineCollection({
|
||||
type: 'page',
|
||||
source: {
|
||||
// @ts-expect-error __DOCS_DIR__ is not defined
|
||||
cwd: globalThis.__DOCS_DIR__,
|
||||
include: 'index.md',
|
||||
},
|
||||
}),
|
||||
docs: defineCollection({
|
||||
type: 'page',
|
||||
source: {
|
||||
// @ts-expect-error __DOCS_DIR__ is not defined
|
||||
cwd: globalThis.__DOCS_DIR__,
|
||||
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(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
54
app/modules/default-configs.ts
Normal file
54
app/modules/default-configs.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { defineNuxtModule } from '@nuxt/kit'
|
||||
import { defu } from 'defu'
|
||||
import { inferSiteURL, getPackageJsonMetadata } from '../app/utils/meta'
|
||||
import { getGitBranch, getGitEnv, getLocalGitInfo } from '../app/utils/git'
|
||||
|
||||
export default defineNuxtModule({
|
||||
meta: {
|
||||
name: 'default-configs',
|
||||
},
|
||||
async setup(_options, nuxt) {
|
||||
const dir = nuxt.options.rootDir
|
||||
const url = inferSiteURL()
|
||||
const meta = await getPackageJsonMetadata(dir)
|
||||
const gitInfo = await getLocalGitInfo(dir) || getGitEnv()
|
||||
const siteName = nuxt.options?.site?.name || meta.name || gitInfo?.name || ''
|
||||
|
||||
nuxt.options.llms = defu(nuxt.options.llms, {
|
||||
domain: url,
|
||||
title: siteName,
|
||||
description: meta.description || '',
|
||||
full: {
|
||||
title: siteName,
|
||||
description: meta.description || '',
|
||||
},
|
||||
})
|
||||
|
||||
nuxt.options.site = defu(nuxt.options.site, {
|
||||
url,
|
||||
name: siteName,
|
||||
debug: false,
|
||||
})
|
||||
|
||||
nuxt.options.appConfig.header = defu(nuxt.options.appConfig.header, {
|
||||
title: siteName,
|
||||
})
|
||||
|
||||
nuxt.options.appConfig.seo = defu(nuxt.options.appConfig.seo, {
|
||||
titleTemplate: `%s - ${siteName}`,
|
||||
title: siteName,
|
||||
description: meta.description || '',
|
||||
})
|
||||
|
||||
nuxt.options.appConfig.github = defu(nuxt.options.appConfig.github, {
|
||||
owner: gitInfo?.owner,
|
||||
name: gitInfo?.name,
|
||||
url: gitInfo?.url,
|
||||
branch: getGitBranch(),
|
||||
})
|
||||
|
||||
nuxt.options.appConfig.toc = defu(nuxt.options.appConfig.toc, {
|
||||
title: 'On this page',
|
||||
})
|
||||
},
|
||||
})
|
49
app/nuxt.config.ts
Normal file
49
app/nuxt.config.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { extendViteConfig } from '@nuxt/kit'
|
||||
|
||||
// Flag enabled when developing docs theme
|
||||
const dev = !!process.env.NUXT_DOCS_DEV
|
||||
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxt/ui-pro',
|
||||
'@nuxt/content',
|
||||
'@nuxt/image',
|
||||
'@nuxtjs/robots',
|
||||
'nuxt-og-image',
|
||||
'nuxt-llms',
|
||||
() => {
|
||||
// Update @nuxt/content optimizeDeps options
|
||||
extendViteConfig((config) => {
|
||||
config.optimizeDeps ||= {}
|
||||
config.optimizeDeps.include ||= []
|
||||
config.optimizeDeps.include.push('@nuxt/content > slugify')
|
||||
config.optimizeDeps.include = config.optimizeDeps.include
|
||||
.map(id => id.replace(/^@nuxt\/content > /, 'docus > @nuxt/content > '))
|
||||
})
|
||||
},
|
||||
],
|
||||
devtools: {
|
||||
enabled: dev,
|
||||
},
|
||||
css: ['../app/assets/css/main.css'],
|
||||
content: {
|
||||
build: {
|
||||
markdown: {
|
||||
highlight: {
|
||||
langs: ['bash', 'diff', 'json', 'js', 'ts', 'html', 'css', 'vue', 'shell', 'mdc', 'md', 'yaml'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: ['/'],
|
||||
crawlLinks: true,
|
||||
failOnError: false,
|
||||
autoSubfolderIndex: false,
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
provider: 'iconify',
|
||||
},
|
||||
})
|
218
app/nuxt.schema.ts
Normal file
218
app/nuxt.schema.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import { field, group } from '@nuxt/content/preview'
|
||||
|
||||
export default defineNuxtSchema({
|
||||
appConfig: {
|
||||
ui: group({
|
||||
title: 'UI',
|
||||
description: 'UI Customization.',
|
||||
icon: 'i-lucide-palette',
|
||||
fields: {
|
||||
colors: group({
|
||||
title: 'Colors',
|
||||
description: 'Manage main colors of your application',
|
||||
icon: 'i-lucide-palette',
|
||||
fields: {
|
||||
primary: field({
|
||||
type: 'string',
|
||||
title: 'Primary',
|
||||
description: 'Primary color of your UI.',
|
||||
icon: 'i-lucide-palette',
|
||||
default: 'green',
|
||||
required: ['red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose'],
|
||||
}),
|
||||
neutral: field({
|
||||
type: 'string',
|
||||
title: 'Neutral',
|
||||
description: 'Neutral color of your UI.',
|
||||
icon: 'i-lucide-palette',
|
||||
default: 'slate',
|
||||
required: ['slate', 'gray', 'zinc', 'neutral', 'stone'],
|
||||
}),
|
||||
},
|
||||
}),
|
||||
icons: group({
|
||||
title: 'Icons',
|
||||
description: 'Manage icons used in the application.',
|
||||
icon: 'i-lucide-settings',
|
||||
fields: {
|
||||
search: field({
|
||||
type: 'icon',
|
||||
title: 'Search Bar',
|
||||
description: 'Icon to display in the search bar.',
|
||||
icon: 'i-lucide-search',
|
||||
default: 'i-lucide-search',
|
||||
}),
|
||||
dark: field({
|
||||
type: 'icon',
|
||||
title: 'Dark mode',
|
||||
description: 'Icon of color mode button for dark mode.',
|
||||
icon: 'i-lucide-moon',
|
||||
default: 'i-lucide-moon',
|
||||
}),
|
||||
light: field({
|
||||
type: 'icon',
|
||||
title: 'Light mode',
|
||||
description: 'Icon of color mode button for light mode.',
|
||||
icon: 'i-lucide-sun',
|
||||
default: 'i-lucide-sun',
|
||||
}),
|
||||
external: field({
|
||||
type: 'icon',
|
||||
title: 'External Link',
|
||||
description: 'Icon for external link.',
|
||||
icon: 'i-lucide-external-link',
|
||||
default: 'i-lucide-external-link',
|
||||
}),
|
||||
chevron: field({
|
||||
type: 'icon',
|
||||
title: 'Chevron',
|
||||
description: 'Icon for chevron.',
|
||||
icon: 'i-lucide-chevron-down',
|
||||
default: 'i-lucide-chevron-down',
|
||||
}),
|
||||
hash: field({
|
||||
type: 'icon',
|
||||
title: 'Hash',
|
||||
description: 'Icon for hash anchors.',
|
||||
icon: 'i-lucide-hash',
|
||||
default: 'i-lucide-hash',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
seo: group({
|
||||
title: 'SEO',
|
||||
description: 'SEO configuration.',
|
||||
icon: 'i-lucide-search',
|
||||
fields: {
|
||||
title: field({
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
description: 'Title to display in the header.',
|
||||
icon: 'i-lucide-type',
|
||||
default: '',
|
||||
}),
|
||||
description: field({
|
||||
type: 'string',
|
||||
title: 'Description',
|
||||
description: 'Description to display in the header.',
|
||||
icon: 'i-lucide-type',
|
||||
default: '',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
header: group({
|
||||
title: 'Header',
|
||||
description: 'Header configuration.',
|
||||
icon: 'i-lucide-layout',
|
||||
fields: {
|
||||
title: field({
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
description: 'Title to display in the header.',
|
||||
icon: 'i-lucide-type',
|
||||
default: '',
|
||||
}),
|
||||
logo: group({
|
||||
title: 'Logo',
|
||||
description: 'Header logo configuration.',
|
||||
icon: 'i-lucide-image',
|
||||
fields: {
|
||||
light: field({
|
||||
type: 'media',
|
||||
title: 'Light Mode Logo',
|
||||
description: 'Pick an image from your gallery.',
|
||||
icon: 'i-lucide-sun',
|
||||
default: '',
|
||||
}),
|
||||
dark: field({
|
||||
type: 'media',
|
||||
title: 'Dark Mode Logo',
|
||||
description: 'Pick an image from your gallery.',
|
||||
icon: 'i-lucide-moon',
|
||||
default: '',
|
||||
}),
|
||||
alt: field({
|
||||
type: 'string',
|
||||
title: 'Alt',
|
||||
description: 'Alt to display for accessibility.',
|
||||
icon: 'i-lucide-text',
|
||||
default: '',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
socials: field({
|
||||
type: 'object',
|
||||
title: 'Social Networks',
|
||||
description: 'Social links configuration.',
|
||||
icon: 'i-lucide-network',
|
||||
default: {},
|
||||
}),
|
||||
toc: group({
|
||||
title: 'Table of contents',
|
||||
description: 'TOC configuration.',
|
||||
icon: 'i-lucide-list',
|
||||
fields: {
|
||||
title: field({
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
description: 'Title of the table of contents.',
|
||||
icon: 'i-lucide-heading',
|
||||
default: 'On this page',
|
||||
}),
|
||||
bottom: group({
|
||||
title: 'Bottom',
|
||||
description: 'Bottom section of the table of contents.',
|
||||
icon: 'i-lucide-list',
|
||||
fields: {
|
||||
title: field({
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
description: 'Title of the bottom section.',
|
||||
icon: 'i-lucide-heading',
|
||||
default: 'Community',
|
||||
}),
|
||||
links: field({
|
||||
type: 'array',
|
||||
title: 'Links',
|
||||
description: 'Links to display in the bottom section.',
|
||||
icon: 'i-lucide-link',
|
||||
default: [],
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
github: group({
|
||||
title: 'GitHub',
|
||||
description: 'GitHub configuration.',
|
||||
icon: 'i-simple-icons-github',
|
||||
fields: {
|
||||
url: field({
|
||||
type: 'string',
|
||||
title: 'URL',
|
||||
description: 'GitHub URL.',
|
||||
icon: 'i-simple-icons-github',
|
||||
default: '',
|
||||
}),
|
||||
branch: field({
|
||||
type: 'string',
|
||||
title: 'Branch',
|
||||
description: 'GitHub branch.',
|
||||
icon: 'i-lucide-git-branch',
|
||||
default: 'main',
|
||||
}),
|
||||
rootDir: field({
|
||||
type: 'string',
|
||||
title: 'Root Directory',
|
||||
description: 'Root directory of the GitHub repository.',
|
||||
icon: 'i-lucide-folder',
|
||||
default: '',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
25
app/server/routes/raw/[...slug].md.get.ts
Normal file
25
app/server/routes/raw/[...slug].md.get.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { withLeadingSlash } from 'ufo'
|
||||
import { stringify } from 'minimark/stringify'
|
||||
import { queryCollection } from '@nuxt/content/nitro'
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const slug = getRouterParams(event)['slug.md']
|
||||
if (!slug?.endsWith('.md')) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||
}
|
||||
|
||||
const path = withLeadingSlash(slug.replace('.md', ''))
|
||||
const page = await queryCollection(event, 'docs').path(path).first()
|
||||
if (!page) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||
}
|
||||
|
||||
// Add title and description to the top of the page if missing
|
||||
if (page.body.value[0]?.[0] !== 'h1') {
|
||||
page.body.value.unshift(['blockquote', {}, page.description])
|
||||
page.body.value.unshift(['h1', {}, page.title])
|
||||
}
|
||||
|
||||
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
|
||||
return stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' })
|
||||
})
|
3
app/tsconfig.json
Normal file
3
app/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
Reference in New Issue
Block a user