X Tutup
import { unstable_cache } from "next/cache"; // ==================== Types ==================== export interface GitHubRelease { id: number; tag_name: string; name: string; body: string; draft: boolean; prerelease: boolean; created_at: string; published_at: string; author: { login: string; avatar_url: string; }; } export interface ChangelogEntry { version: string; date: string; body: string; draft: boolean; prerelease: boolean; author: { name: string; avatar: string; }; } // ==================== Constants ==================== const GITHUB_OWNER = "javaistic"; const GITHUB_REPO = "javaistic"; const GITHUB_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`; // Cache releases and contributors for 24 hours (86400 seconds) const CACHE_DURATION = 86400; // ==================== Releases ==================== /** * Fetch releases from GitHub with caching * Cached for 24 hours to minimize API hits */ export const fetchGitHubReleases = unstable_cache( async (): Promise => { try { const response = await fetch(`${GITHUB_API_URL}/releases`, { headers: { Accept: "application/vnd.github.v3+json", ...(process.env.GITHUB_TOKEN && { Authorization: `token ${process.env.GITHUB_TOKEN}`, }), }, // Add timeout to prevent hanging requests signal: AbortSignal.timeout(10000), }); if (!response.ok) { console.error( `GitHub API error: ${response.status} ${response.statusText}`, ); return []; } const releases: GitHubRelease[] = await response.json(); // Convert GitHub releases to changelog entries return releases .filter((release) => !release.draft) // Exclude drafts .map((release) => ({ version: release.tag_name, date: new Date(release.published_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }), body: release.body || "No description provided", draft: release.draft, prerelease: release.prerelease, author: { name: release.author.login, avatar: release.author.avatar_url, }, })); } catch (error) { console.error("Failed to fetch GitHub releases:", error); return []; } }, [`github-releases-${GITHUB_OWNER}-${GITHUB_REPO}`], { revalidate: CACHE_DURATION, tags: ["github-releases"], }, ); /** * Extracts and formats the "What’s New" section from MDX content * while preserving links, inline code, bold/italic, emojis, and nested lists. */ export function extractWhatsNew(body: string): string { if (!body) return ""; const lines = body.split("\n"); const whatsNew: string[] = []; let insideSection = false; for (const line of lines) { const trimmed = line.trim(); // Detect start of "What’s New" section (supports both apostrophe types) if (/^##\s*what[’']?s new/i.test(trimmed)) { insideSection = true; continue; } // Stop at the next section header if (insideSection && /^##\s+/.test(trimmed)) break; if (insideSection) { // Collect bullet points or nested bullets if (/^(\*|-|\d+\.)\s+/.test(trimmed)) { // Remove bullet prefix but keep content formatting const content = trimmed.replace(/^(\*|-|\d+\.)\s+/, ""); whatsNew.push(content); } // Collect indented nested lists else if (/^\s{2,}(\*|-|\d+\.)\s+/.test(line)) { const content = trimmed.replace(/^(\*|-|\d+\.)\s+/, " - "); // preserve nesting whatsNew.push(content); } } } if (whatsNew.length === 0) return ""; // Number top-level items and keep nested lists indented let counter = 1; const formatted = whatsNew.map((line) => { if (/^\s{2}- /.test(line)) { return line; // nested item } else { return `${counter++}. ${line}`; } }); return formatted.join("\n"); }
X Tutup