import fs from 'fs' import path from 'path' import matter from 'gray-matter' import { cache } from 'react' const postsDirectory = path.join(process.cwd(), 'content/posts') export interface PostMeta { slug: string title: string date: string excerpt: string tags: string[] author: string | null coverImage: string | null readingTime: number } export interface Post extends PostMeta {} const getMdxFiles = cache(async () => { try { const files = await fs.promises.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.promises.readFile(filePath, 'utf-8') const { data } = matter(raw) const slug = file.replace(/\.(mdx|md)$/, '') // Compute reading time from content (everything after frontmatter) const content = raw.split(/---\n*\n*/).slice(2).join('\n') const readingTime = Math.max(1, Math.ceil(content.split(/\s+/).length / 200)) return { slug, title: data.title ?? slug, date: data.date ?? 'Unknown', excerpt: data.excerpt ?? '', tags: data.tags ?? [], author: data.author ?? null, coverImage: data.coverImage ?? null, readingTime, } }) ).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.promises.readFile(filePath, 'utf8') const { data } = matter(raw) const contentForReadingTime = raw.replace(/^---[\s\S]*?---\s*?/, '') const readingTime = Math.max(1, Math.ceil(contentForReadingTime.split(/\s+/).filter(Boolean).length / 200)) return { slug, title: data.title ?? slug, date: data.date ?? 'Unknown', excerpt: data.excerpt ?? '', tags: data.tags ?? [], author: data.author ?? null, coverImage: data.coverImage ?? null, readingTime, } satisfies 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)) }