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

224 lines
6.6 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;
// 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 show preview with toggle
const container = document.createElement('div');
container.className = 'blog-collapsible';
// preview
const previewDiv = document.createElement('div');
previewDiv.className = 'blog-preview';
previewDiv.style.whiteSpace = 'pre-line';
let previewText = getPreview(body);
// ensure ellipsis if preview is truncated
if (previewText.length < body.length && !previewText.endsWith('…')) {
previewText += '…';
}
previewDiv.textContent = previewText;
container.appendChild(previewDiv);
// Read more button
const readMoreBtn = document.createElement('button');
readMoreBtn.className = 'blog-toggle read-more';
readMoreBtn.textContent = 'Read more';
container.appendChild(readMoreBtn);
// Full content (hidden initially)
const fullDiv = document.createElement('div');
fullDiv.className = 'blog-full';
fullDiv.style.whiteSpace = 'pre-line';
fullDiv.textContent = body;
fullDiv.style.display = 'none';
container.appendChild(fullDiv);
// Show less button (hidden initially) appears at the bottom
const showLessBtn = document.createElement('button');
showLessBtn.className = 'blog-toggle show-less';
showLessBtn.textContent = '✕ Show less';
showLessBtn.style.display = 'none';
container.appendChild(showLessBtn);
// Toggle logic
function expand() {
fullDiv.style.display = 'block';
readMoreBtn.style.display = 'none';
showLessBtn.style.display = 'inline-block';
}
function collapse() {
fullDiv.style.display = 'none';
readMoreBtn.style.display = 'inline-block';
showLessBtn.style.display = 'none';
}
readMoreBtn.addEventListener('click', expand);
showLessBtn.addEventListener('click', collapse);
postDiv.appendChild(container);
}
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);