Files
Website-2026/js/blog.js
T
2026-07-11 23:34:52 +02:00

206 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// blog.js Loads and renders blog posts with automatic collapse 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('.')) {
// avoid directory links (ending with /)
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();
}
// generate preview: up to 3 sentences, but not exceeding ~300 chars
function getPreview(text, maxLen = 300) {
if (text.length <= maxLen) return text; // not needed, but safe
// try to split on sentence boundaries (. ! ?) followed by space or newline
const sentences = text.match(/[^.!?]+[.!?]+/g) || [];
let preview = '';
let count = 0;
for (const s of sentences) {
if (preview.length + s.length <= maxLen) {
preview += s;
count++;
if (count >= 3) break;
} else {
// if we haven't reached 3 sentences, break and use what we have
break;
}
}
// If we have at least one sentence, use that; otherwise fallback to wordbreak
if (preview.trim().length > 0) {
return preview.trim();
}
// fallback: take first maxLen chars and cut at last space
let truncated = text.slice(0, maxLen);
const lastSpace = truncated.lastIndexOf(' ');
if (lastSpace > 0) truncated = truncated.slice(0, lastSpace);
return truncated + '…';
}
// ---------- rendering ----------
function renderPost(filename, content) {
const lines = content.split('\n');
if (lines.length < 3) {
// malformed: skip or treat as titleonly
return null;
}
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);
// meta
const metaEl = document.createElement('p');
metaEl.className = 'blog-meta';
metaEl.textContent = dateTime;
postDiv.appendChild(metaEl);
const isLong = body.length > 300;
if (!isLong) {
// short post display full body directly
const bodyDiv = document.createElement('div');
bodyDiv.className = 'blog-body';
bodyDiv.style.whiteSpace = 'pre-line';
bodyDiv.textContent = body;
postDiv.appendChild(bodyDiv);
} else {
// long post use <details> with preview
const details = document.createElement('details');
details.className = 'blog-collapse';
const summary = document.createElement('summary');
// preview container
const previewDiv = document.createElement('div');
previewDiv.className = 'blog-preview';
previewDiv.style.whiteSpace = 'pre-line';
const previewText = getPreview(body);
previewDiv.textContent = previewText + (previewText.endsWith('…') ? '' : '…'); // add ellipsis if not already
const readMoreSpan = document.createElement('span');
readMoreSpan.className = 'read-more-text';
readMoreSpan.textContent = 'Read more';
summary.appendChild(previewDiv);
summary.appendChild(readMoreSpan);
details.appendChild(summary);
// full body (hidden until opened)
const fullDiv = document.createElement('div');
fullDiv.className = 'blog-body';
fullDiv.style.whiteSpace = 'pre-line';
fullDiv.textContent = body;
details.appendChild(fullDiv);
// toggle text when details opens/closes
details.addEventListener('toggle', function() {
const span = this.querySelector('.read-more-text');
span.textContent = this.open ? 'Show less' : 'Read more';
});
postDiv.appendChild(details);
}
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;
}
// sort files by name (oldest first? we'll sort by date if embedded, but we'll just alphabetical)
// Typically filenames like 2025-01-01.txt, so alphabetical = chronological
files.sort();
// clear feed
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);
// optionally show error placeholder
const errDiv = document.createElement('div');
errDiv.className = 'blog-post';
errDiv.style.color = '#b91c1c';
errDiv.textContent = `⚠️ Could not load post: ${file}`;
feed.appendChild(errDiv);
}
}
// if nothing was added, show message
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);