import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; import { config } from '@/lib/config'; const BLOG_DIR = path.join(process.cwd(), 'content', 'blog'); export interface PostFrontmatter { title: string; date: string; excerpt?: string; featuredImage?: string | null; focalX?: number; focalY?: number; category?: string; public?: boolean; } export interface PostData { slug: string; frontmatter: PostFrontmatter; content: string; } export function isPostVisible(post: { frontmatter: { date: string; public?: boolean } }) { if (post.frontmatter.public === false && config.isProduction) { return false; } const postDate = new Date(post.frontmatter.date); const now = new Date(); return !(postDate > now && config.isProduction); } export async function getPostBySlug(slug: string, locale: string): Promise { // Check locale-specific directory first const localeDir = path.join(process.cwd(), 'content', locale, 'blog'); const filePath = path.join(localeDir, `${slug}.mdx`); if (!fs.existsSync(filePath)) return null; const source = fs.readFileSync(filePath, 'utf8'); const { content, data } = matter(source); return { slug, frontmatter: data as PostFrontmatter, content, }; } export async function getAllPosts(locale: string): Promise { const localeDir = path.join(process.cwd(), 'content', locale, 'blog'); if (!fs.existsSync(localeDir)) return []; const files = fs.readdirSync(localeDir).filter(f => f.endsWith('.mdx')); const posts = await Promise.all( files.map(async f => { const slug = f.replace('.mdx', ''); return await getPostBySlug(slug, locale); }) ); return posts .filter((p): p is PostData => p !== null) .filter(isPostVisible) .sort((a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()); } export async function getAllPostsMetadata(locale: string): Promise[]> { const posts = await getAllPosts(locale); return posts.map((p) => ({ slug: p.slug, frontmatter: p.frontmatter, })); } export async function getAdjacentPosts( slug: string, locale: string, ): Promise<{ prev: PostData | null; next: PostData | null; isPrevRandom?: boolean; isNextRandom?: boolean; }> { const posts = await getAllPosts(locale); const currentIndex = posts.findIndex((post) => post.slug === slug); if (currentIndex === -1) { return { prev: null, next: null }; } let next = currentIndex > 0 ? posts[currentIndex - 1] : null; let prev = currentIndex < posts.length - 1 ? posts[currentIndex + 1] : null; return { prev, next }; } export function getReadingTime(content: string): number { const wordsPerMinute = 200; const noOfWords = content.split(/\s/g).length; const minutes = noOfWords / wordsPerMinute; return Math.ceil(minutes); } export function generateHeadingId(text: string): string { let id = text.toLowerCase(); id = id.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss'); id = id.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, ''); return id || 'heading'; } export function getHeadings(content: string): { id: string; text: string; level: number }[] { const headingLines = content.split('\n').filter((line) => line.match(/^#{2,4}\s/)); return headingLines.map((line) => { const level = line.match(/^#+/)?.[0].length || 2; const text = line.replace(/^#+\s/, '').trim(); const id = generateHeadingId(text); return { id, text, level }; }); }