Files
public-blog/lib/posts.ts

88 lines
2.5 KiB
TypeScript

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 {
source: string // raw MDX string, NOT compiled
}
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<PostMeta[]> => {
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<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.promises.readFile(filePath, 'utf8')
const { data, content } = matter(raw)
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,
source: content,
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))
}