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

208 lines
6.5 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
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 <p>
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 <details>
const details = document.createElement('details');
details.className = 'blog-collapse';
const summary = document.createElement('summary');
// Preview text
const previewDiv = document.createElement('div');
previewDiv.className = 'blog-preview';
// Split preview by newline and create lines, but we can just set innerHTML with <br>
previewDiv.innerHTML = preview.replace(/\n/g, '<br>');
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);