Skip to content

Commit c5fcd2b

Browse files
committed
feat: generate blog posts content via markdown
1 parent 651fe07 commit c5fcd2b

10 files changed

Lines changed: 128 additions & 77 deletions

File tree

app/pages/blog/[...path].vue

Lines changed: 0 additions & 62 deletions
This file was deleted.

app/pages/blog/[slug].vue

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script setup lang="ts">
2+
const route = useRoute()
3+
const { data: post } = await useAsyncData(route.path, () => {
4+
return queryCollection('blog').path(route.path).first()
5+
})
6+
7+
definePageMeta({
8+
name: `blog-post`,
9+
// alias: ['/:path(.*)*'],
10+
})
11+
12+
useSeoMeta({
13+
title: () => post.value?.title || 'Blog', // TODO: How does i18n deal with dynamic values? $t('blog.post.title'),
14+
description: () => (post.value?.description ? `Blog Article ${post.value?.description}` : ''),
15+
})
16+
</script>
17+
18+
<template>
19+
<main class="container py-8 sm:py-12 w-full">
20+
<!-- Header -->
21+
<header class="mb-8 pb-8 border-b border-border">
22+
<div class="">I AM A WEAK HEADER</div>
23+
</header>
24+
25+
<article v-if="post">
26+
<ContentRenderer v-if="post" :value="post" />
27+
</article>
28+
29+
<article v-else>
30+
<h1>Post Not Found</h1>
31+
<p>We couldn't find a post at /blog/{{ route.path }}</p>
32+
</article>
33+
</main>
34+
</template>
35+
36+
<!-- TODO: styles -->
37+
<style>
38+
h1 {
39+
@apply text-4xl font-bold text-gray-900 dark:text-white mb-2;
40+
}
41+
</style>

app/pages/blog/index.vue

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
<script setup lang="ts">
2-
const route = useRoute('blog')
3-
const router = useRouter()
4-
5-
// 1. Define your list of posts (This would eventually come from useAsyncData)
6-
const posts = [
7-
{ slug: 'hello-world', title: 'Hello World', excerpt: 'My first post' },
8-
{ slug: 'server-components', title: 'Server Components', excerpt: 'Zero JS' },
9-
]
2+
const { data: posts } = await useAsyncData('blog-posts', () =>
3+
queryCollection('blog').where('draft', '<>', true).order('date', 'DESC').all(),
4+
)
105
116
definePageMeta({
127
name: 'blog',

content.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineContentConfig, defineCollection } from '@nuxt/content'
2+
import { BlogPostSchema } from './shared/schemas/blog'
3+
4+
export default defineContentConfig({
5+
collections: {
6+
blog: defineCollection({
7+
type: 'page',
8+
source: 'blog/**/*.md',
9+
schema: BlogPostSchema,
10+
}),
11+
},
12+
})

content/blog/first-post.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: 'Hello World'
3+
date: '2026-01-28'
4+
slug: 'first-post'
5+
description: 'My first post on the blog'
6+
excerpt: 'My first post'
7+
draft: false
8+
---
9+
10+
# My First Page
11+
12+
Here is some content.

content/blog/server-components.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: 'Server Components'
3+
date: '2026-01-28'
4+
slug: 'server-components'
5+
description: 'My first post on the blog'
6+
excerpt: 'Zero JS'
7+
draft: false
8+
---
9+
10+
# Server components
11+
12+
Here is some server component razzle dazzle.

nuxt.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export default defineNuxtConfig({
3434
'@vueuse/nuxt',
3535
'@nuxtjs/i18n',
3636
'@nuxtjs/color-mode',
37+
'@nuxt/content',
3738
],
3839

3940
colorMode: {

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@nuxt/scripts": "0.13.2",
4545
"@nuxtjs/color-mode": "4.0.0",
4646
"@nuxtjs/html-validator": "2.1.0",
47+
"@nuxt/content": "3.11.0",
4748
"@nuxtjs/i18n": "10.2.1",
4849
"@shikijs/langs": "3.21.0",
4950
"@shikijs/themes": "3.21.0",
@@ -77,6 +78,7 @@
7778
"@types/validate-npm-package-name": "4.0.2",
7879
"@unocss/nuxt": "66.6.0",
7980
"@unocss/preset-wind4": "66.6.0",
81+
"@valibot/to-json-schema": "^1.5.0",
8082
"@vite-pwa/assets-generator": "1.0.2",
8183
"@vite-pwa/nuxt": "1.1.0",
8284
"@vitest/browser-playwright": "4.0.18",

pnpm-lock.yaml

Lines changed: 23 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shared/schemas/blog.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { object, string, boolean, array, optional } from 'valibot'
2+
import type { InferOutput } from 'valibot'
3+
4+
/**
5+
* Schema for blog post frontmatter
6+
* Uses simple Valibot primitives (required by Nuxt Content's JSON Schema conversion)
7+
*/
8+
export const BlogPostSchema = object({
9+
title: string(),
10+
date: string(),
11+
description: string(),
12+
slug: string(),
13+
excerpt: optional(string()),
14+
author: optional(string()),
15+
tags: optional(array(string())),
16+
draft: optional(boolean()),
17+
})
18+
19+
/**
20+
* Inferred type for blog post frontmatter
21+
*/
22+
export type BlogPostFrontmatter = InferOutput<typeof BlogPostSchema>

0 commit comments

Comments
 (0)