// blog.js const BLOG_DIR = 'blog/'; const MANIFEST = BLOG_DIR + 'manifest.json'; // ------------------------------------------------------------ // 1. Fetch the list of blog post files // ------------------------------------------------------------ async function getPostFiles() { // Try manifest first try { const r = await fetch(MANIFEST, { cache: 'no-cache' }); if (r.ok) { const data = await r.json(); return data.filter(f => f.endsWith('.txt')); } } catch (_) { /* ignore */ } // Fallback: parse directory listing (requires autoindex) try { const r = await fetch(BLOG_DIR, { cache: 'no-cache' }); if (r.ok) { const text = await r.text(); const hrefs = []; const re = /href="([^"]+)"/g; let m; while ((m = re.exec(text)) !== null) { const name = m[1]; if (name.endsWith('.txt')) hrefs.push(name); } return hrefs; } } catch (e) { console.warn('Could not read blog directory listing', e); } return []; } // ------------------------------------------------------------ // 2. Parse a single blog post file // ------------------------------------------------------------ async function fetchPost(filename) { const url = BLOG_DIR + filename; const resp = await fetch(url); if (!resp.ok) throw new Error(`Failed to fetch ${filename}`); const text = await resp.text(); const lines = text.split('\n'); // First line: date/time, second: title, rest: content const dateTime = lines[0]?.trim() || 'Unknown date'; const title = lines[1]?.trim() || 'Untitled'; const content = lines.slice(2).join('\n').trim(); return { filename, dateTime, title, content }; } // ------------------------------------------------------------ // 3. Generate a preview for long posts // ------------------------------------------------------------ function createPreview(content, maxLen = 300) { // Return { preview: string, truncated: boolean } if (content.length <= maxLen) { return { preview: content, truncated: false }; } // Try to get at least 3 sentences const sentences = content.match(/[^.!?]*[.!?](\s+|$)/g) || []; if (sentences.length >= 3) { let preview = sentences.slice(0, 3).join(''); // If preview is still > maxLen, trim to nearest space if (preview.length > maxLen) { const trimmed = preview.slice(0, maxLen); const lastSpace = trimmed.lastIndexOf(' '); if (lastSpace > 0) preview = trimmed.slice(0, lastSpace) + '…'; else preview = trimmed + '…'; } else { preview = preview.trim() + '…'; } return { preview, truncated: true }; } // Fallback: at least 3 lines (by newline) const lines = content.split('\n').filter(l => l.trim() !== ''); if (lines.length >= 3) { let preview = lines.slice(0, 3).join('\n'); if (preview.length > maxLen) { const trimmed = preview.slice(0, maxLen); const lastSpace = trimmed.lastIndexOf(' '); if (lastSpace > 0) preview = trimmed.slice(0, lastSpace) + '…'; else preview = trimmed + '…'; } else { preview = preview.trim() + '…'; } return { preview, truncated: true }; } // Last resort: take first maxLen chars, break at word boundary if (content.length > maxLen) { const trimmed = content.slice(0, maxLen); const lastSpace = trimmed.lastIndexOf(' '); const preview = (lastSpace > 0 ? trimmed.slice(0, lastSpace) : trimmed) + '…'; return { preview, truncated: true }; } return { preview: content, truncated: false }; } // ------------------------------------------------------------ // 4. Render a single blog post // ------------------------------------------------------------ function renderPost(post) { const { dateTime, title, content } = post; const article = document.createElement('article'); article.className = 'blog-post'; // Title const h3 = document.createElement('h3'); h3.className = 'blog-title'; h3.textContent = title; article.appendChild(h3); // Meta (date) const meta = document.createElement('div'); meta.className = 'blog-meta'; meta.textContent = dateTime; article.appendChild(meta); // Body content: split paragraphs by double newline, wrap in

const bodyDiv = document.createElement('div'); bodyDiv.className = 'blog-body'; const paragraphs = content.split(/\n\s*\n/); paragraphs.forEach(para => { const p = document.createElement('p'); p.textContent = para.trim(); bodyDiv.appendChild(p); }); // Decide if we need collapse const { preview, truncated } = createPreview(content); if (!truncated) { // Short post – show full content directly article.appendChild(bodyDiv); } else { // Long post – wrap in

const details = document.createElement('details'); details.className = 'blog-collapse'; const summary = document.createElement('summary'); // Preview text const previewDiv = document.createElement('div'); previewDiv.className = 'blog-preview'; // Split preview by newline and create lines, but we can just set innerHTML with
previewDiv.innerHTML = preview.replace(/\n/g, '
'); summary.appendChild(previewDiv); // "Read more" text const moreSpan = document.createElement('span'); moreSpan.className = 'read-more-text'; moreSpan.textContent = 'Read more'; summary.appendChild(moreSpan); details.appendChild(summary); // Full content inside the details details.appendChild(bodyDiv); article.appendChild(details); } return article; } // ------------------------------------------------------------ // 5. Main: load posts and render // ------------------------------------------------------------ async function initBlog() { const feed = document.querySelector('.blog-feed'); if (!feed) return; feed.innerHTML = 'Loading blog posts…'; try { const files = await getPostFiles(); if (!files.length) { feed.innerHTML = 'No blog posts found.'; return; } // Fetch and parse all posts const posts = await Promise.all(files.map(f => fetchPost(f))); // Sort by date descending (assuming ISO-like date in first line) posts.sort((a, b) => { const dA = new Date(a.dateTime); const dB = new Date(b.dateTime); return dB - dA; }); // Render each post feed.innerHTML = ''; posts.forEach(post => { const el = renderPost(post); feed.appendChild(el); }); } catch (err) { console.error(err); feed.innerHTML = 'Error loading blog posts.'; } } document.addEventListener('DOMContentLoaded', initBlog);