Openstatus
www.openstatus.dev
1import fs from "node:fs";
2import path from "node:path";
3import { z } from "zod";
4
5const metadataSchema = z.object({
6 title: z.string(),
7 publishedAt: z.coerce.date(),
8 description: z.string(),
9 category: z.string(),
10 author: z.string(),
11 image: z.string().optional(),
12});
13
14export type Metadata = z.infer<typeof metadataSchema>;
15
16function parseFrontmatter(fileContent: string) {
17 const frontmatterRegex = /---\s*([\s\S]*?)\s*---/;
18 const match = frontmatterRegex.exec(fileContent);
19 const frontMatterBlock = match?.[1];
20 const content = fileContent.replace(frontmatterRegex, "").trim();
21 const frontMatterLines = frontMatterBlock?.trim().split("\n");
22 const metadata: Record<string, string> = {};
23
24 frontMatterLines?.forEach((line) => {
25 const [key, ...valueArr] = line.split(": ");
26 let value = valueArr.join(": ").trim();
27 value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes
28 metadata[key.trim()] = value;
29 });
30
31 const validatedMetadata = metadataSchema.safeParse(metadata);
32
33 if (!validatedMetadata.success) {
34 console.error(validatedMetadata.error);
35 throw new Error(`Invalid metadata ${fileContent}`);
36 }
37
38 return { metadata: validatedMetadata.data, content };
39}
40
41function getMDXFiles(dir: string) {
42 return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx");
43}
44
45function readMDXFile(filePath: string) {
46 const rawContent = fs.readFileSync(filePath, "utf-8");
47 return parseFrontmatter(rawContent);
48}
49
50function getMDXDataFromDir(dir: string) {
51 const mdxFiles = getMDXFiles(dir);
52 return mdxFiles.map((file) => {
53 return getMDXDataFromFile(path.join(dir, file));
54 });
55}
56
57function getMDXDataFromFile(filePath: string) {
58 const { metadata, content } = readMDXFile(filePath);
59 const slug = path.basename(filePath, path.extname(filePath));
60 return {
61 metadata,
62 slug,
63 content,
64 };
65}
66
67export type MDXData = ReturnType<typeof getMDXDataFromFile>;
68
69export function getBlogPosts(): MDXData[] {
70 return getMDXDataFromDir(
71 path.join(process.cwd(), "src", "content", "pages", "blog"),
72 );
73}
74
75export function getChangelogPosts(): MDXData[] {
76 return getMDXDataFromDir(
77 path.join(process.cwd(), "src", "content", "pages", "changelog"),
78 );
79}
80
81export function getProductPages(): MDXData[] {
82 return getMDXDataFromDir(
83 path.join(process.cwd(), "src", "content", "pages", "product"),
84 );
85}
86
87export function getUnrelatedPages(): MDXData[] {
88 return getMDXDataFromDir(
89 path.join(process.cwd(), "src", "content", "pages", "unrelated"),
90 );
91}
92
93export function getUnrelatedPage(slug: string): MDXData {
94 return getMDXDataFromFile(
95 path.join(
96 process.cwd(),
97 "src",
98 "content",
99 "pages",
100 "unrelated",
101 `${slug}.mdx`,
102 ),
103 );
104}
105
106export function getMainPages(): MDXData[] {
107 return [...getUnrelatedPages(), ...getProductPages()];
108}
109
110export function getComparePages(): MDXData[] {
111 return getMDXDataFromDir(
112 path.join(process.cwd(), "src", "content", "pages", "compare"),
113 );
114}
115
116export function getHomePage(): MDXData {
117 return getMDXDataFromFile(
118 path.join(process.cwd(), "src", "content", "pages", "home.mdx"),
119 );
120}
121
122export function getToolsPages(): MDXData[] {
123 return getMDXDataFromDir(
124 path.join(process.cwd(), "src", "content", "pages", "tools"),
125 );
126}
127
128export function getToolsPage(slug: string): MDXData {
129 return getMDXDataFromFile(
130 path.join(process.cwd(), "src", "content", "pages", "tools", `${slug}.mdx`),
131 );
132}
133
134export function formatDate(targetDate: Date, includeRelative = false) {
135 const currentDate = new Date();
136
137 const yearsAgo = currentDate.getFullYear() - targetDate.getFullYear();
138 const monthsAgo = currentDate.getMonth() - targetDate.getMonth();
139 const daysAgo = currentDate.getDate() - targetDate.getDate();
140
141 let formattedDate = "";
142
143 if (yearsAgo > 0) {
144 formattedDate = `${yearsAgo}y ago`;
145 } else if (monthsAgo > 0) {
146 formattedDate = `${monthsAgo}mo ago`;
147 } else if (daysAgo > 0) {
148 formattedDate = `${daysAgo}d ago`;
149 } else {
150 formattedDate = "Today";
151 }
152
153 const fullDate = targetDate.toLocaleString("en-us", {
154 month: "short",
155 day: "2-digit",
156 year: "numeric",
157 });
158
159 if (!includeRelative) {
160 return fullDate;
161 }
162
163 return `${fullDate} (${formattedDate})`;
164}