feat: add posts content layer with gray-matter + compileMDX

This commit is contained in:
2026-06-01 19:44:28 -05:00
parent 5fb57127f4
commit dd68208b3a

77
lib/posts.ts Normal file
View File

@@ -0,0 +1,77 @@
import fs from 'fs/promises'
import path from 'path'
import matter from 'gray-matter'
import { compileMDX } from 'next-mdx-remote/rsc'
import { cache } from 'react'
import { useMDXComponents } from '@/mdx-components'
const postsDirectory = path.join(process.cwd(), 'content/posts')
export interface PostMeta {
slug: string
title: string
date: string
excerpt: string
}
export interface Post extends PostMeta {
source: string
}
const getMdxFiles = cache(async () => {
try {
const files = await fs.readdir(postsDirectory)
return files.filter((f) => f.endsWith('.mdx') || f.endsWith('.md'))
} catch {
return []
}
})
export const getPosts = cache(async (): Promise<PostMeta[]> => {
const files = await getMdxFiles()
return Promise.all(
files.map(async (file) => {
const filePath = path.join(postsDirectory, file)
const raw = await fs.readFile(filePath, 'utf8')
const { data } = matter(raw)
return {
slug: file.replace(/\.(mdx|md)$/, ''),
title: data.title ?? file,
date: data.date ?? 'Unknown',
excerpt: data.excerpt ?? '',
} as PostMeta
})
).then((posts) =>
posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
)
})
export const getPost = async (slug: string): Promise<Post | null> => {
const files = await getMdxFiles()
const file = files.find((f) => f.replace(/\.(mdx|md)$/, '') === slug)
if (!file) return null
const filePath = path.join(postsDirectory, file)
const raw = await fs.readFile(filePath, 'utf8')
const { data, content } = matter(raw)
const components = useMDXComponents({})
const { content: compiledContent } = await compileMDX({
source: content,
components,
options: { parseFrontmatter: true },
})
return {
slug,
title: data.title ?? file,
date: data.date ?? 'Unknown',
excerpt: data.excerpt ?? '',
source: compiledContent,
} as unknown as Post
}
export const getReadingTime = (content: string): number => {
const words = content.replace(/<[^>]*>/g, '').trim().split(/\s+/).filter(Boolean).length
return Math.max(1, Math.ceil(words / 200))
}