Skip to content

docs(blog): init #4088

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: v3
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/app/composables/useLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ export function useLinks() {
to: 'https://github.com/Justineo/tempad-dev-plugin-nuxt-ui',
target: '_blank'
}]
}, {
label: 'Blog',
icon: 'i-lucide-file-text',
to: '/blog'
}, {
label: 'Releases',
icon: 'i-lucide-rocket',
Expand Down
4 changes: 4 additions & 0 deletions docs/app/composables/useSearchLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export function useSearchLinks() {
description: 'Meet the team behind Nuxt UI.',
icon: 'i-lucide-users',
to: '/team'
}, {
label: 'Blog',
icon: 'i-lucide-file-text',
to: '/blog'
}, {
label: 'Releases',
icon: 'i-lucide-rocket',
Expand Down
7 changes: 7 additions & 0 deletions docs/app/pages/blog/.blog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
seo:
title: Nuxt UI Blog
description: Read the latest news, tutorials, and updates about Nuxt UI.
title: Nuxt [UI]{.text-primary} Blog
navigation.title: Blog
description: Read the latest news, tutorials, and updates about Nuxt UI.
navigation.icon: i-lucide-newspaper
98 changes: 98 additions & 0 deletions docs/app/pages/blog/[...slug].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script setup lang="ts">
import { kebabCase } from 'scule'

const route = useRoute()

const [{ data: page }, { data: surround }] = await Promise.all([
useAsyncData(kebabCase(route.path), () => queryCollection('blog').path(route.path).first()),
useAsyncData(`${kebabCase(route.path)}-surround`, () => {
return queryCollectionItemSurroundings('blog', route.path, {
fields: ['description']
}).order('date', 'DESC')
})
])
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Article not found', fatal: true })
}

const title = page.value.seo?.title || page.value.title
const description = page.value.seo?.description || page.value.description

useSeoMeta({
title,
description,
ogDescription: description,
ogTitle: title
})

if (page.value.image) {
defineOgImage({ url: page.value.image })
} else {
defineOgImageComponent('Docs', {
headline: 'Blog',
title,
description
})
}

const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
</script>

<template>
<UMain class="mt-20 px-2">
<UContainer class="relative min-h-screen">
<ULink
to="/blog"
class="text-sm flex items-center gap-1 w-fit"
>
<UIcon name="lucide:chevron-left" />
Blog
</ULink>
<UPage v-if="page">
<div class="flex flex-col gap-3 mt-8">
<div class="flex text-xs text-muted items-center justify-center gap-2">
<span v-if="page.date">
{{ formatDate(page.date) }}
</span>
<span v-if="page.date && page.minRead">
-
</span>
<span v-if="page.minRead">
{{ page.minRead }} min read
</span>
</div>
<NuxtImg
:src="page.image"
:alt="page.title"
class="rounded-lg w-full h-[400px] object-cover object-center max-w-5xl mx-auto"
/>
<h1 class="text-4xl text-center font-medium max-w-3xl mx-auto mt-4">
{{ page.title }}
</h1>
<p class="text-muted text-center max-w-2xl mx-auto">
{{ page.description }}
</p>
<div class="mt-4 flex justify-center flex-wrap items-center gap-6">
<UUser v-for="(author, index) in page.authors" :key="index" v-bind="author" :description="author.to ? `@${author.to.split('/').pop()}` : undefined" />
</div>
</div>
<UPageBody class="max-w-3xl mx-auto">
<ContentRenderer
v-if="page.body"
:value="page"
/>

<USeparator v-if="surround?.length" />

<UContentSurround :surround="surround" />
</UPageBody>
</UPage>
</UContainer>
</UMain>
</template>
74 changes: 74 additions & 0 deletions docs/app/pages/blog/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script setup lang="ts">
// @ts-expect-error yaml is not typed
import page from '.blog.yml'

const { data: posts } = await useAsyncData('blogs', () =>
queryCollection('blog').order('date', 'DESC').all()
)

const title = page.seo?.title || page.title
const description = page.seo?.description || page.description

useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description
})

/* defineOgImageComponent('Docs', {
headline: 'Blog',
title: page.title,
description: page.description
}) */
</script>

<template>
<div v-if="page" class="relative">
<UPageHero :links="page.links" :ui="{ container: 'relative' }">
<LazyStarsBg />

<div aria-hidden="true" class="absolute z-[-1] border-x border-default inset-0 mx-4 sm:mx-6 lg:mx-8" />

<template #title>
<MDC :value="page.title" unwrap="p" cache-key="pro-templates-hero-title" />
</template>

<template #description>
<MDC :value="page.description" unwrap="p" cache-key="pro-templates-hero-description" />
</template>
</UPageHero>

<UPageBody class="!my-0 !py-0 border-y border-default">
<UContainer>
<UBlogPosts orientation="vertical" class="border-x border-default !gap-0">
<Motion
v-for="(post, index) in posts"
:key="index"
:initial="{ opacity: 0, transform: 'translateY(10px)' }"
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
:transition="{ delay: 0.2 * index }"
:in-view-options="{ once: true }"
class="group"
>
<UBlogPost
variant="naked"
orientation="horizontal"
:to="post.path"
v-bind="post"
:ui="{
root: 'md:grid md:grid-cols-2 group overflow-visible transition-all duration-300',
image: 'rounded-lg group-hover/blog-post:scale-none shadow-lg border-4 border-muted ring-2 ring-default',
header: 'scale-90 md:scale-85 lg:scale-80 overflow-visible'
}"
/>
<div class="group-last:hidden border-b border-default" />
</Motion>
</UBlogPosts>
</UContainer>
</UPageBody>
<UContainer class="relative h-24">
<div aria-hidden="true" class="absolute z-[-1] border-x border-default inset-0 mx-4 sm:mx-6 lg:mx-8" />
</UContainer>
</div>
</template>
29 changes: 29 additions & 0 deletions docs/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ const Button = z.object({
target: z.enum(['_blank', '_self']).optional()
})

const Image = z.object({
src: z.string(),
alt: z.string(),
width: z.number().optional(),
height: z.number().optional()
})

const Author = z.object({
name: z.string(),
description: z.string().optional(),
username: z.string().optional(),
twitter: z.string().optional(),
to: z.string().optional(),
avatar: Image.optional()
})

const schema = z.object({
category: z.enum(['layout', 'form', 'element', 'navigation', 'data', 'overlay']).optional(),
framework: z.string().optional(),
Expand Down Expand Up @@ -75,5 +91,18 @@ export const collections = {
})
}))
})
}),
blog: defineCollection({
type: 'page',
source: 'blog/*',
schema: z.object({
image: z.string().editor({ input: 'media' }),
authors: z.array(Author),
date: z.string().date(),
minRead: z.number(),
draft: z.boolean().optional(),
category: z.enum(['Release', 'Tutorial', 'Announcement', 'Article']),
tags: z.array(z.string())
})
})
}
Loading