Skip to content

Commit 6ac567b

Browse files
committed
perf: inject frontmatter via virtual file (+ strip drafts from prod)
1 parent 130c5d5 commit 6ac567b

File tree

2 files changed

+83
-14
lines changed

2 files changed

+83
-14
lines changed

app/pages/blog/index.vue

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,5 @@
11
<script setup lang="ts">
2-
import type { BlogPostFrontmatter } from '#shared/schemas/blog'
3-
4-
const blogModules = import.meta.glob<BlogPostFrontmatter>('./*.md', { eager: true })
5-
6-
const posts: BlogPostFrontmatter[] = []
7-
8-
for (const [_, module] of Object.entries(blogModules)) {
9-
if (module.draft) continue
10-
11-
posts.push({ ...module })
12-
}
13-
14-
posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
2+
import { posts } from '#blog/posts'
153
164
const placeHolder = ['atproto', 'nuxt']
175

modules/blog.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,57 @@
1+
import { join } from 'node:path'
12
import Markdown from 'unplugin-vue-markdown/vite'
2-
import { addVitePlugin, defineNuxtModule, useNuxt } from 'nuxt/kit'
3+
import { addTemplate, addVitePlugin, defineNuxtModule, useNuxt, createResolver } from 'nuxt/kit'
34
import shiki from '@shikijs/markdown-it'
45
import { 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

647
export 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

Comments
 (0)