1+ import { join } from 'node:path'
12import Markdown from 'unplugin-vue-markdown/vite'
2- import { addVitePlugin , defineNuxtModule , useNuxt } from 'nuxt/kit'
3+ import { addTemplate , addVitePlugin , defineNuxtModule , useNuxt , createResolver } from 'nuxt/kit'
34import shiki from '@shikijs/markdown-it'
45import { defu } from 'defu'
6+ import { read } from 'gray-matter'
7+ import { safeParse } from 'valibot'
8+ import { BlogPostSchema , type BlogPostFrontmatter } from '../shared/schemas/blog'
9+ import { globSync } from 'tinyglobby'
10+ import { isProduction } from '../config/env'
11+
12+ /**
13+ * Scans the blog directory for .md files and extracts validated frontmatter.
14+ * Returns only non-draft posts sorted by date descending.
15+ */
16+ function loadBlogPosts ( blogDir : string ) : BlogPostFrontmatter [ ] {
17+ const files : string [ ] = globSync ( join ( blogDir , '*.md' ) )
18+
19+ const posts : BlogPostFrontmatter [ ] = [ ]
20+
21+ for ( const file of files ) {
22+ const { data : frontmatter } = read ( file )
23+
24+ // Normalise slug → path (same logic as standard-site-sync)
25+ if ( typeof frontmatter . slug === 'string' && ! frontmatter . path ) {
26+ frontmatter . path = `/blog/${ frontmatter . slug } `
27+ }
28+ // Normalise date to ISO string
29+ if ( frontmatter . date ) {
30+ const raw = frontmatter . date
31+ frontmatter . date = new Date ( raw instanceof Date ? raw : String ( raw ) ) . toISOString ( )
32+ }
33+
34+ const result = safeParse ( BlogPostSchema , frontmatter )
35+ if ( ! result . success ) continue
36+
37+ if ( result . output . draft ) continue
38+
39+ posts . push ( result . output )
40+ }
41+
42+ // Sort newest first
43+ posts . sort ( ( a , b ) => new Date ( b . date ) . getTime ( ) - new Date ( a . date ) . getTime ( ) )
44+ return posts
45+ }
546
647export default defineNuxtModule ( {
748 meta : {
849 name : 'blog' ,
950 } ,
1051 setup ( ) {
1152 const nuxt = useNuxt ( )
53+ const resolver = createResolver ( import . meta. url )
54+ const blogDir = resolver . resolve ( '../app/pages/blog' )
1255
1356 nuxt . options . extensions . push ( '.md' )
1457 nuxt . options . vite . vue = defu ( nuxt . options . vite . vue , {
@@ -32,5 +75,43 @@ export default defineNuxtModule({
3275 } ,
3376 } ) ,
3477 )
78+
79+ // Expose frontmatter for published posts to avoid bundling the full content
80+ // of all posts in `/blog` page.
81+ addTemplate ( {
82+ filename : 'blog/posts.ts' ,
83+ write : true ,
84+ getContents : ( ) => {
85+ const posts = loadBlogPosts ( blogDir )
86+ return [
87+ `import type { BlogPostFrontmatter } from '#shared/schemas/blog'` ,
88+ `` ,
89+ `export const posts: BlogPostFrontmatter[] = ${ JSON . stringify ( posts , null , 2 ) } ` ,
90+ ] . join ( '\n' )
91+ } ,
92+ } )
93+
94+ nuxt . options . alias [ '#blog/posts' ] = join ( nuxt . options . buildDir , 'blog/posts' )
95+
96+ // In production, remove page routes for draft posts
97+ if ( ! nuxt . options . dev && isProduction ) {
98+ const publishedPosts = loadBlogPosts ( blogDir )
99+ const publishedSlugs = new Set ( publishedPosts . map ( p => p . slug ) )
100+
101+ nuxt . hook ( 'pages:extend' , pages => {
102+ // Walk the pages tree and remove draft blog post pages
103+ for ( let i = pages . length - 1 ; i >= 0 ; i -- ) {
104+ const page = pages [ i ] !
105+ // Blog post pages are at /blog/<slug> — the file is blog/<slug>.md
106+ if ( page . file ?. endsWith ( '.md' ) && page . file ?. includes ( '/blog/' ) ) {
107+ // Extract the slug from the filename
108+ const filename = page . file . split ( '/' ) . pop ( ) ?. replace ( '.md' , '' )
109+ if ( filename && filename !== 'index' && ! publishedSlugs . has ( filename ) ) {
110+ pages . splice ( i , 1 )
111+ }
112+ }
113+ }
114+ } )
115+ }
35116 } ,
36117} )
0 commit comments