import fs from "node:fs"; import path from "node:path"; import { z } from "zod"; const metadataSchema = z.object({ title: z.string(), publishedAt: z.coerce.date(), description: z.string(), category: z.string(), author: z.string(), image: z.string().optional(), }); export type Metadata = z.infer; function parseFrontmatter(fileContent: string) { const frontmatterRegex = /---\s*([\s\S]*?)\s*---/; const match = frontmatterRegex.exec(fileContent); const frontMatterBlock = match?.[1]; const content = fileContent.replace(frontmatterRegex, "").trim(); const frontMatterLines = frontMatterBlock?.trim().split("\n"); const metadata: Record = {}; frontMatterLines?.forEach((line) => { const [key, ...valueArr] = line.split(": "); let value = valueArr.join(": ").trim(); value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes metadata[key.trim()] = value; }); const validatedMetadata = metadataSchema.safeParse(metadata); if (!validatedMetadata.success) { console.error(validatedMetadata.error); throw new Error(`Invalid metadata ${fileContent}`); } return { metadata: validatedMetadata.data, content }; } function getMDXFiles(dir: string) { return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx"); } function readMDXFile(filePath: string) { const rawContent = fs.readFileSync(filePath, "utf-8"); return parseFrontmatter(rawContent); } function getMDXDataFromDir(dir: string) { const mdxFiles = getMDXFiles(dir); return mdxFiles.map((file) => { return getMDXDataFromFile(path.join(dir, file)); }); } function getMDXDataFromFile(filePath: string) { const { metadata, content } = readMDXFile(filePath); const slug = path.basename(filePath, path.extname(filePath)); return { metadata, slug, content, }; } export type MDXData = ReturnType; export function getBlogPosts(): MDXData[] { return getMDXDataFromDir( path.join(process.cwd(), "src", "content", "pages", "blog"), ); } export function getChangelogPosts(): MDXData[] { return getMDXDataFromDir( path.join(process.cwd(), "src", "content", "pages", "changelog"), ); } export function getProductPages(): MDXData[] { return getMDXDataFromDir( path.join(process.cwd(), "src", "content", "pages", "product"), ); } export function getUnrelatedPages(): MDXData[] { return getMDXDataFromDir( path.join(process.cwd(), "src", "content", "pages", "unrelated"), ); } export function getUnrelatedPage(slug: string): MDXData { return getMDXDataFromFile( path.join( process.cwd(), "src", "content", "pages", "unrelated", `${slug}.mdx`, ), ); } export function getMainPages(): MDXData[] { return [...getUnrelatedPages(), ...getProductPages()]; } export function getComparePages(): MDXData[] { return getMDXDataFromDir( path.join(process.cwd(), "src", "content", "pages", "compare"), ); } export function getHomePage(): MDXData { return getMDXDataFromFile( path.join(process.cwd(), "src", "content", "pages", "home.mdx"), ); } export function getToolsPages(): MDXData[] { return getMDXDataFromDir( path.join(process.cwd(), "src", "content", "pages", "tools"), ); } export function getToolsPage(slug: string): MDXData { return getMDXDataFromFile( path.join(process.cwd(), "src", "content", "pages", "tools", `${slug}.mdx`), ); } export function formatDate(targetDate: Date, includeRelative = false) { const currentDate = new Date(); const yearsAgo = currentDate.getFullYear() - targetDate.getFullYear(); const monthsAgo = currentDate.getMonth() - targetDate.getMonth(); const daysAgo = currentDate.getDate() - targetDate.getDate(); let formattedDate = ""; if (yearsAgo > 0) { formattedDate = `${yearsAgo}y ago`; } else if (monthsAgo > 0) { formattedDate = `${monthsAgo}mo ago`; } else if (daysAgo > 0) { formattedDate = `${daysAgo}d ago`; } else { formattedDate = "Today"; } const fullDate = targetDate.toLocaleString("en-us", { month: "short", day: "2-digit", year: "numeric", }); if (!includeRelative) { return fullDate; } return `${fullDate} (${formattedDate})`; }