|
| 1 | +const fs = require("fs"); |
| 2 | +const path = require("path"); |
| 3 | + |
| 4 | +const CONTENT_DIR = path.join(__dirname, "..", "src", "content", "blog"); |
| 5 | +const OUTPUT_DIR = path.join(__dirname, "..", "public", "blog"); |
| 6 | +const SITE_URL = "https://typia.io"; |
| 7 | + |
| 8 | +const escapeXml = (value) => |
| 9 | + value |
| 10 | + .replaceAll("&", "&") |
| 11 | + .replaceAll("<", "<") |
| 12 | + .replaceAll(">", ">") |
| 13 | + .replaceAll('"', """) |
| 14 | + .replaceAll("'", "'"); |
| 15 | + |
| 16 | +const parseFrontmatter = (content) => { |
| 17 | + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); |
| 18 | + if (!match) return {}; |
| 19 | + |
| 20 | + const metadata = {}; |
| 21 | + let currentKey = null; |
| 22 | + for (const rawLine of match[1].split(/\r?\n/)) { |
| 23 | + if (!rawLine.trim()) continue; |
| 24 | + |
| 25 | + const listMatch = rawLine.match(/^\s*-\s+(.*)$/); |
| 26 | + if (listMatch && currentKey) { |
| 27 | + metadata[currentKey].push(listMatch[1].trim().replace(/^"|"$/g, "")); |
| 28 | + continue; |
| 29 | + } |
| 30 | + |
| 31 | + const keyValueMatch = rawLine.match(/^\s*([A-Za-z0-9_]+):\s*(.*)$/); |
| 32 | + if (!keyValueMatch) continue; |
| 33 | + |
| 34 | + const [, key, value] = keyValueMatch; |
| 35 | + if (value === "") { |
| 36 | + metadata[key] = []; |
| 37 | + currentKey = key; |
| 38 | + } else { |
| 39 | + metadata[key] = value.trim().replace(/^"|"$/g, ""); |
| 40 | + currentKey = key; |
| 41 | + } |
| 42 | + } |
| 43 | + return metadata; |
| 44 | +}; |
| 45 | + |
| 46 | +const posts = fs |
| 47 | + .readdirSync(CONTENT_DIR) |
| 48 | + .filter((file) => file.endsWith(".md") || file.endsWith(".mdx")) |
| 49 | + .map((file) => { |
| 50 | + const fullPath = path.join(CONTENT_DIR, file); |
| 51 | + const source = fs.readFileSync(fullPath, "utf8"); |
| 52 | + const metadata = parseFrontmatter(source); |
| 53 | + const slug = file.replace(/\.mdx?$/, ""); |
| 54 | + return { |
| 55 | + title: metadata.title ?? slug, |
| 56 | + description: metadata.description ?? "", |
| 57 | + date: metadata.date ?? new Date().toISOString().slice(0, 10), |
| 58 | + slug, |
| 59 | + }; |
| 60 | + }) |
| 61 | + .sort((a, b) => new Date(b.date) - new Date(a.date)); |
| 62 | + |
| 63 | +const xml = `<?xml version="1.0" encoding="UTF-8"?> |
| 64 | +<rss version="2.0"> |
| 65 | + <channel> |
| 66 | + <title>Typia Blog</title> |
| 67 | + <link>${SITE_URL}/blog/</link> |
| 68 | + <description>Engineering notes, releases, and deep dives from Typia.</description> |
| 69 | + <language>en</language> |
| 70 | +${posts |
| 71 | + .map( |
| 72 | + (post) => ` <item> |
| 73 | + <title>${escapeXml(post.title)}</title> |
| 74 | + <link>${SITE_URL}/blog/${post.slug}/</link> |
| 75 | + <guid>${SITE_URL}/blog/${post.slug}/</guid> |
| 76 | + <pubDate>${new Date(post.date).toUTCString()}</pubDate> |
| 77 | + <description>${escapeXml(post.description)}</description> |
| 78 | + </item>`, |
| 79 | + ) |
| 80 | + .join("\n")} |
| 81 | + </channel> |
| 82 | +</rss> |
| 83 | +`; |
| 84 | + |
| 85 | +fs.mkdirSync(OUTPUT_DIR, { recursive: true }); |
| 86 | +fs.writeFileSync(path.join(OUTPUT_DIR, "rss.xml"), xml); |
0 commit comments