Openstatus www.openstatus.dev
at 4c0f4c00a38753a5d0dfd7e7b7b7706dec6f1503 230 lines 5.9 kB view raw
1import fs from "node:fs"; 2import path from "node:path"; 3import slugify from "slugify"; 4import { z } from "zod"; 5 6const metadataSchema = z.object({ 7 title: z.string(), 8 publishedAt: z.coerce.date(), 9 description: z.string(), 10 category: z.string(), 11 author: z.string(), 12 image: z.string().optional(), 13}); 14 15export type Metadata = z.infer<typeof metadataSchema>; 16 17function parseFrontmatter(fileContent: string) { 18 const frontmatterRegex = /---\s*([\s\S]*?)\s*---/; 19 const match = frontmatterRegex.exec(fileContent); 20 const frontMatterBlock = match?.[1]; 21 const content = fileContent.replace(frontmatterRegex, "").trim(); 22 const frontMatterLines = frontMatterBlock?.trim().split("\n"); 23 const metadata: Record<string, string> = {}; 24 25 frontMatterLines?.forEach((line) => { 26 const [key, ...valueArr] = line.split(": "); 27 let value = valueArr.join(": ").trim(); 28 value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes 29 metadata[key.trim()] = value; 30 }); 31 32 const validatedMetadata = metadataSchema.safeParse(metadata); 33 34 if (!validatedMetadata.success) { 35 console.error(validatedMetadata.error); 36 throw new Error(`Invalid metadata ${fileContent}`); 37 } 38 39 return { metadata: validatedMetadata.data, content }; 40} 41 42function getMDXFiles(dir: string) { 43 return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx"); 44} 45 46function readMDXFile(filePath: string) { 47 const rawContent = fs.readFileSync(filePath, "utf-8"); 48 return parseFrontmatter(rawContent); 49} 50 51function getMDXDataFromDir(dir: string, prefix = "") { 52 const mdxFiles = getMDXFiles(dir); 53 return mdxFiles.map((file) => { 54 return getMDXDataFromFile(path.join(dir, file), prefix); 55 }); 56} 57 58function getMDXDataFromFile(filePath: string, prefix = "") { 59 const { metadata, content } = readMDXFile(filePath); 60 const slugRaw = path.basename(filePath, path.extname(filePath)); 61 const slug = slugify(slugRaw, { lower: true, strict: true }); 62 const href = prefix ? `${prefix}/${slug}` : `/${slug}`; 63 return { 64 metadata, 65 slug, 66 content, 67 href, 68 }; 69} 70 71export type MDXData = ReturnType<typeof getMDXDataFromFile>; 72 73export function getBlogPosts(): MDXData[] { 74 return getMDXDataFromDir( 75 path.join(process.cwd(), "src", "content", "pages", "blog"), 76 "/blog", 77 ); 78} 79 80export function getChangelogPosts(): MDXData[] { 81 return getMDXDataFromDir( 82 path.join(process.cwd(), "src", "content", "pages", "changelog"), 83 "/changelog", 84 ); 85} 86 87export function getProductPages(): MDXData[] { 88 return getMDXDataFromDir( 89 path.join(process.cwd(), "src", "content", "pages", "product"), 90 "", 91 ); 92} 93 94export function getUnrelatedPages(): MDXData[] { 95 return getMDXDataFromDir( 96 path.join(process.cwd(), "src", "content", "pages", "unrelated"), 97 "", 98 ); 99} 100 101export function getUnrelatedPage(slug: string): MDXData { 102 return getMDXDataFromFile( 103 path.join( 104 process.cwd(), 105 "src", 106 "content", 107 "pages", 108 "unrelated", 109 `${slug}.mdx`, 110 ), 111 "", 112 ); 113} 114 115export function getMainPages(): MDXData[] { 116 return [...getUnrelatedPages(), ...getProductPages()]; 117} 118 119export function getComparePages(): MDXData[] { 120 return getMDXDataFromDir( 121 path.join(process.cwd(), "src", "content", "pages", "compare"), 122 "/compare", 123 ); 124} 125 126export function getHomePage(): MDXData { 127 return getMDXDataFromFile( 128 path.join(process.cwd(), "src", "content", "pages", "home.mdx"), 129 "", 130 ); 131} 132 133export function getToolsPages(): MDXData[] { 134 return getMDXDataFromDir( 135 path.join(process.cwd(), "src", "content", "pages", "tools"), 136 "/play", 137 ); 138} 139 140export function getToolsPage(slug: string): MDXData { 141 return getMDXDataFromFile( 142 path.join(process.cwd(), "src", "content", "pages", "tools", `${slug}.mdx`), 143 "/play", 144 ); 145} 146 147export const PAGE_TYPES = [ 148 "blog", 149 "changelog", 150 "product", 151 "unrelated", 152 "compare", 153 "tools", 154 "all", 155] as const; 156 157export type PageType = (typeof PAGE_TYPES)[number]; 158 159export function getPages(type: PageType) { 160 switch (type) { 161 case "blog": 162 return getBlogPosts(); 163 case "changelog": 164 return getChangelogPosts(); 165 case "product": 166 return getProductPages(); 167 case "unrelated": 168 return getUnrelatedPages(); 169 case "compare": 170 return getComparePages(); 171 case "tools": 172 return getToolsPages(); 173 case "all": 174 return [ 175 ...getBlogPosts(), 176 ...getChangelogPosts(), 177 ...getProductPages(), 178 ...getUnrelatedPages(), 179 ...getComparePages(), 180 ...getToolsPages(), 181 ]; 182 default: 183 throw new Error(`Unknown page type: ${type}`); 184 } 185} 186 187export function getCategories() { 188 return [ 189 ...new Set([ 190 ...getBlogPosts().map((post) => post.metadata.category), 191 ...getChangelogPosts().map((post) => post.metadata.category), 192 ...getProductPages().map((post) => post.metadata.category), 193 ...getUnrelatedPages().map((post) => post.metadata.category), 194 ...getComparePages().map((post) => post.metadata.category), 195 ...getToolsPages().map((post) => post.metadata.category), 196 ]), 197 ] as const; 198} 199 200export function formatDate(targetDate: Date, includeRelative = false) { 201 const currentDate = new Date(); 202 203 const yearsAgo = currentDate.getFullYear() - targetDate.getFullYear(); 204 const monthsAgo = currentDate.getMonth() - targetDate.getMonth(); 205 const daysAgo = currentDate.getDate() - targetDate.getDate(); 206 207 let formattedDate = ""; 208 209 if (yearsAgo > 0) { 210 formattedDate = `${yearsAgo}y ago`; 211 } else if (monthsAgo > 0) { 212 formattedDate = `${monthsAgo}mo ago`; 213 } else if (daysAgo > 0) { 214 formattedDate = `${daysAgo}d ago`; 215 } else { 216 formattedDate = "Today"; 217 } 218 219 const fullDate = targetDate.toLocaleString("en-us", { 220 month: "short", 221 day: "2-digit", 222 year: "numeric", 223 }); 224 225 if (!includeRelative) { 226 return fullDate; 227 } 228 229 return `${fullDate} (${formattedDate})`; 230}