// blog.js – Loads and renders blog posts with simple "Read more" toggle for long entries const BLOG_DIR = 'blog/'; const MANIFEST = BLOG_DIR + 'manifest.json'; // ---------- helpers ---------- // crude HTML parser for directory listings (when manifest is missing) function parseIndexHtmlList(html) { const hrefs = []; const re = /href="([^"]+)"/g; let m; while ((m = re.exec(html)) !== null) { const name = m[1]; // accept typical text file extensions (and no extension) if (name.match(/\.(txt|md|text)$/i) || !name.includes('.')) { if (!name.endsWith('/')) hrefs.push(name); } } return hrefs; } // fetch list of blog post filenames (manifest first, fallback to directory listing) async function loadPostList() { try { const r = await fetch(MANIFEST, { cache: 'no-cache' }); if (r.ok) return await r.json(); } catch (_) { /* ignore */ } try { const r = await fetch(BLOG_DIR, { cache: 'no-cache' }); if (r.ok) { const html = await r.text(); return parseIndexHtmlList(html); } } catch (e) { console.warn('Could not read blog directory listing', e); } return []; } // fetch a single post file async function fetchPost(filename) { const res = await fetch(BLOG_DIR + filename, { cache: 'no-cache' }); if (!res.ok) throw new Error(`Failed to fetch ${filename}`); return await res.text(); } // ---------- rendering ---------- function renderPost(filename, content) { const lines = content.split('\n'); if (lines.length < 3) { return null; // malformed } const dateTime = lines[0].trim(); const title = lines[1].trim(); const bodyLines = lines.slice(2); const body = bodyLines.join('\n').trim(); const postDiv = document.createElement('div'); postDiv.className = 'blog-post'; // Title const titleEl = document.createElement('h3'); titleEl.className = 'blog-title'; titleEl.textContent = title; postDiv.appendChild(titleEl); // Date const metaEl = document.createElement('p'); metaEl.className = 'blog-meta'; metaEl.textContent = dateTime; postDiv.appendChild(metaEl); const isLong = body.length > 300; // Body container (always created, but hidden for long posts) const bodyDiv = document.createElement('div'); bodyDiv.className = 'blog-body'; bodyDiv.style.whiteSpace = 'pre-line'; bodyDiv.textContent = body; if (!isLong) { // Short post – show body directly postDiv.appendChild(bodyDiv); } else { // Long post – hide body initially, add toggle button bodyDiv.style.display = 'none'; postDiv.appendChild(bodyDiv); // Toggle button const toggleBtn = document.createElement('button'); toggleBtn.className = 'blog-toggle'; toggleBtn.textContent = 'Read more'; postDiv.appendChild(toggleBtn); toggleBtn.addEventListener('click', function() { const isHidden = bodyDiv.style.display === 'none'; bodyDiv.style.display = isHidden ? 'block' : 'none'; this.textContent = isHidden ? 'Show less' : 'Read more'; }); } return postDiv; } // ---------- main ---------- async function initBlog() { const feed = document.querySelector('.blog-feed'); if (!feed) return; feed.textContent = 'Loading blog posts…'; try { const files = await loadPostList(); if (!files.length) { feed.textContent = 'No blog posts found.'; return; } files.sort(); // alphabetical -> chronological feed.innerHTML = ''; for (const file of files) { try { const content = await fetchPost(file); const el = renderPost(file, content); if (el) feed.appendChild(el); } catch (err) { console.warn(`Failed to load ${file}:`, err); const errDiv = document.createElement('div'); errDiv.className = 'blog-post'; errDiv.style.color = '#b91c1c'; errDiv.textContent = `⚠️ Could not load post: ${file}`; feed.appendChild(errDiv); } } if (feed.children.length === 0) { feed.textContent = 'No valid blog posts found.'; } } catch (err) { console.error('Blog init error:', err); feed.textContent = 'Error loading blog posts.'; } } document.addEventListener('DOMContentLoaded', initBlog);