#!/usr/bin/env node /** * fetch-reddit-posts.mjs * * Fetches Reddit posts and writes them as JSON files into * src/content/blog/{slug}.json. Run before `pnpm build` so SvelteKit * can prerender the blog routes at build time. * * Strategy: * Fetch raw HTML directly from Reddit (www.reddit.com first, then old.reddit.com) * and parse it to extract post data — * no API key or Reddit credentials required. * * Usage: node scripts/fetch-reddit-posts.mjs */ import { writeFileSync, mkdirSync, readFileSync, existsSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const OUTPUT_DIR = join(__dirname, "../src/content/blog"); mkdirSync(OUTPUT_DIR, { recursive: true }); /** All Reddit posts to mirror, in chronological order. */ const POSTS = [ { slug: "no-you-didnt-get-banned-from-the-discord-server", subreddit: "suyu", id: "1c0vlvj", }, { slug: "the-death-of-suyu", subreddit: "suyu", id: "1c1hs3l", }, { slug: "bus-has-moved-suyus-server-from-discord-to-matrix", subreddit: "suyu", id: "1c1sfen", }, { slug: "founder-speaking-future", subreddit: "suyu", id: "1c1tnh5", }, { slug: "official-suyu-website-and-download", subreddit: "suyu", id: "1dyaokm", }, { slug: "fix-echoes-of-wisdom-not-running-correctly", subreddit: "suyu", id: "1frtdjy", }, { slug: "suyuzudachi-my-account-and-our-group-dm-is-down", subreddit: "suyu", id: "1eilofq", }, { slug: "suyu-has-officially-stated-their-project-is-eol", subreddit: "suyu", id: "1j7e9u6", }, { slug: "re-suyu-exposed", subreddit: "EmulationOnAndroid", id: "1jy39lo", }, { slug: "fixed-the-old-website", subreddit: "suyu", id: "1k6xa9x", }, { slug: "project-work-continuing-on-github", subreddit: "suyu", id: "1kblsih", }, { slug: "suyu-is-now-on-issuehunt-bountysource-esque", subreddit: "suyu", id: "1kqd3gq", }, { slug: "public-suyu-dev-recruitment-post", subreddit: "EmuDev", id: "1m4vcnc", }, { slug: "suyu-update-final", subreddit: "EmuDev", id: "1svi23y", }, ]; // ── Enhanced browser-like headers to avoid HTTP 403 ────────────────────────── const BROWSER_HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "Cache-Control": "no-cache", "Accept-Encoding": "gzip, deflate, br", "Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24"', "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": '"Windows"', "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", Pragma: "no-cache", }; async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } // ── HTML helpers (no external deps) ───────────────────────────────────────── /** Decode the most common HTML entities. */ function decodeHtmlEntities(str) { return str .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(///g, "/") .replace(/(\d+);/g, (_, code) => String.fromCharCode(Number(code))); } /** * Extract image URLs from Reddit post data (JSON API format). * Handles preview images, galleries, and direct image URLs. */ function extractImages(postData) { const images = []; // 1. Preview image (most common) if (postData?.preview?.images?.[0]?.source?.url) { images.push(decodeHtmlEntities(postData.preview.images[0].source.url)); } // 2. Gallery images if (postData?.gallery_data?.items && postData?.media_metadata) { for (const item of postData.gallery_data.items) { const mediaId = item.media_id; const media = postData.media_metadata[mediaId]; if (media?.s?.u) { images.push(decodeHtmlEntities(media.s.u)); } } } // 3. Direct image URL (i.redd.it, imgur, etc.) if (postData?.url && /\.(jpg|jpeg|png|gif|webp)$/i.test(postData.url)) { if (!images.includes(postData.url)) { images.push(postData.url); } } return images; } /** Strip all HTML tags and collapse whitespace to plain text. */ function stripHtml(html) { return html .replace(/<[^>]+>/g, " ") .replace(/\s+/g, " ") .trim(); } /** * Extract the inner HTML of the first