151 lines
4.1 KiB
JavaScript
151 lines
4.1 KiB
JavaScript
// 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); |