// blog.js — Load and render blog posts from content/projects/ // Journal format: one post.md per project with ## YYYY-MM-DD HH:MM:SS entry headers. // Image insertion into post.md is handled at build time by build-posts.js. const Blog = (() => { const BASE = 'content/projects'; let cachedProjects = null; async function fetchProjects() { if (cachedProjects) return cachedProjects; try { const res = await fetch(`${BASE}/projects.json`); if (!res.ok) throw new Error(`HTTP ${res.status}`); cachedProjects = await res.json(); return cachedProjects; } catch (e) { console.error('Blog: failed to load projects.json', e.message); return []; } } // Fetch and parse a project's post.md into entry objects, oldest-first. async function fetchPostsByProject(projectSlug) { try { const postRes = await fetch(`${BASE}/${projectSlug}/post.md`); if (!postRes.ok) throw new Error(`HTTP ${postRes.status}`); return parseJournal(projectSlug, await postRes.text()); } catch (e) { console.error('Blog: failed to load', projectSlug, e.message); return []; } } // Parse a journal post.md into entry objects, oldest-first. function parseJournal(projectSlug, raw) { const headerRe = /^## (\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}(?::\d{2})?)?)\s*$/gm; const headers = [...raw.matchAll(headerRe)]; const entries = headers.map((match, i) => { const rawDate = match[1]; const dateStr = rawDate.slice(0, 10); const start = match.index + match[0].length; const end = i + 1 < headers.length ? headers[i + 1].index : raw.length; const body = raw.slice(start, end).trim(); const titleMatch = body.match(/^###\s+(.+)/); const title = titleMatch ? titleMatch[1].trim() : dateStr; const bodyText = titleMatch ? body.slice(titleMatch[0].length).trim() : body; return { slug: `${projectSlug}#${i}`, projectSlug, date: dateStr, title, body: bodyText, }; }); return entries.sort((a, b) => a.date.localeCompare(b.date)); } // Fetch a single entry by slug ("projectSlug#index") async function fetchPostContent(slug) { const hashIdx = slug.lastIndexOf('#'); const projectSlug = hashIdx >= 0 ? slug.slice(0, hashIdx) : slug; const entries = await fetchPostsByProject(projectSlug); const entry = entries.find(e => e.slug === slug) || entries[0]; if (!entry) return { meta: {}, body: 'Post not found.' }; return { meta: { title: entry.title, date: entry.date }, body: entry.body }; } // Simple markdown-to-segments parser for BBS display function parseMarkdown(md) { const lines = md.split('\n'); const result = []; for (const line of lines) { if (line.startsWith('## ')) { result.push({ type: 'h2', text: line.slice(3) }); } else if (line.startsWith('### ')) { result.push({ type: 'h3', text: line.slice(4) }); } else if (line.startsWith('# ')) { result.push({ type: 'h1', text: line.slice(2) }); } else if (/^\d+\.\s/.test(line)) { result.push({ type: 'li', text: line }); } else if (line.startsWith('- ') || line.startsWith('* ')) { result.push({ type: 'li', text: line }); } else if (/^!\[.*\]\(.*\)/.test(line)) { const alt = line.match(/!\[(.*?)\]/)[1]; const src = line.match(/\((.*?)\)/)[1]; result.push({ type: 'img', text: alt, src }); } else if (line.trim() === '') { result.push({ type: 'blank', text: '' }); } else { result.push({ type: 'p', text: line }); } } return result; } function formatDate(dateStr) { const d = new Date(dateStr + 'T00:00:00'); const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`; } return { fetchProjects, fetchPostsByProject, fetchPostContent, parseMarkdown, formatDate, }; })();