// 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
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);