diff --git a/lib/posts.ts b/lib/posts.ts new file mode 100644 index 0000000..8eca9df --- /dev/null +++ b/lib/posts.ts @@ -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 => { + 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 => { + 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)) +}