86 lines
2.5 KiB
TypeScript
86 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 {}
|
|
|
|
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 } = 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))
|
|
}
|