This commit is contained in:
2026-07-12 10:15:22 +02:00
parent 9d7a3f6660
commit 9d3d33251a
2 changed files with 54 additions and 24 deletions
+26 -23
View File
@@ -1,18 +1,16 @@
// blog.js Loads and renders blog posts with simple "Read more" toggle for long entries // blog.js Loads blog posts and uses a gradient fade for long entries
const BLOG_DIR = 'blog/'; const BLOG_DIR = 'blog/';
const MANIFEST = BLOG_DIR + 'manifest.json'; const MANIFEST = BLOG_DIR + 'manifest.json';
// ---------- helpers ---------- // ---------- helpers ----------
// crude HTML parser for directory listings (when manifest is missing)
function parseIndexHtmlList(html) { function parseIndexHtmlList(html) {
const hrefs = []; const hrefs = [];
const re = /href="([^"]+)"/g; const re = /href="([^"]+)"/g;
let m; let m;
while ((m = re.exec(html)) !== null) { while ((m = re.exec(html)) !== null) {
const name = m[1]; const name = m[1];
// accept typical text file extensions (and no extension)
if (name.match(/\.(txt|md|text)$/i) || !name.includes('.')) { if (name.match(/\.(txt|md|text)$/i) || !name.includes('.')) {
if (!name.endsWith('/')) hrefs.push(name); if (!name.endsWith('/')) hrefs.push(name);
} }
@@ -20,7 +18,6 @@ function parseIndexHtmlList(html) {
return hrefs; return hrefs;
} }
// fetch list of blog post filenames (manifest first, fallback to directory listing)
async function loadPostList() { async function loadPostList() {
try { try {
const r = await fetch(MANIFEST, { cache: 'no-cache' }); const r = await fetch(MANIFEST, { cache: 'no-cache' });
@@ -39,7 +36,6 @@ async function loadPostList() {
return []; return [];
} }
// fetch a single post file
async function fetchPost(filename) { async function fetchPost(filename) {
const res = await fetch(BLOG_DIR + filename, { cache: 'no-cache' }); const res = await fetch(BLOG_DIR + filename, { cache: 'no-cache' });
if (!res.ok) throw new Error(`Failed to fetch ${filename}`); if (!res.ok) throw new Error(`Failed to fetch ${filename}`);
@@ -50,9 +46,7 @@ async function fetchPost(filename) {
function renderPost(filename, content) { function renderPost(filename, content) {
const lines = content.split('\n'); const lines = content.split('\n');
if (lines.length < 3) { if (lines.length < 3) return null;
return null; // malformed
}
const dateTime = lines[0].trim(); const dateTime = lines[0].trim();
const title = lines[1].trim(); const title = lines[1].trim();
const bodyLines = lines.slice(2); const bodyLines = lines.slice(2);
@@ -75,31 +69,41 @@ function renderPost(filename, content) {
const isLong = body.length > 300; const isLong = body.length > 300;
// Body container (always created, but hidden for long posts) // Container for the body (with optional fade)
const wrapper = document.createElement('div');
wrapper.className = 'blog-body-wrapper';
// The actual text
const bodyDiv = document.createElement('div'); const bodyDiv = document.createElement('div');
bodyDiv.className = 'blog-body'; bodyDiv.className = 'blog-body';
bodyDiv.style.whiteSpace = 'pre-line'; bodyDiv.style.whiteSpace = 'pre-line';
bodyDiv.textContent = body; bodyDiv.textContent = body;
wrapper.appendChild(bodyDiv);
if (!isLong) { // If long, add collapsed class and a toggle button
// Short post show body directly if (isLong) {
postDiv.appendChild(bodyDiv); wrapper.classList.add('collapsed');
} else {
// Long post hide body initially, add toggle button
bodyDiv.style.display = 'none';
postDiv.appendChild(bodyDiv);
// Toggle button
const toggleBtn = document.createElement('button'); const toggleBtn = document.createElement('button');
toggleBtn.className = 'blog-toggle'; toggleBtn.className = 'blog-toggle';
toggleBtn.textContent = 'Read more'; toggleBtn.textContent = 'Read more';
postDiv.appendChild(toggleBtn);
toggleBtn.addEventListener('click', function() { toggleBtn.addEventListener('click', function() {
const isHidden = bodyDiv.style.display === 'none'; const isCollapsed = wrapper.classList.contains('collapsed');
bodyDiv.style.display = isHidden ? 'block' : 'none'; if (isCollapsed) {
this.textContent = isHidden ? 'Show less' : 'Read more'; wrapper.classList.remove('collapsed');
this.textContent = 'Show less';
} else {
wrapper.classList.add('collapsed');
this.textContent = 'Read more';
}
}); });
postDiv.appendChild(wrapper);
postDiv.appendChild(toggleBtn);
} else {
// Short post no collapse, no button
postDiv.appendChild(wrapper);
} }
return postDiv; return postDiv;
@@ -120,8 +124,7 @@ async function initBlog() {
return; return;
} }
files.sort(); // alphabetical -> chronological files.sort();
feed.innerHTML = ''; feed.innerHTML = '';
for (const file of files) { for (const file of files) {
+28 -1
View File
@@ -31,13 +31,39 @@ a:hover { color:#991b1b; text-decoration:underline; }
.blog-title { margin:0 0 8px 0; font-size:1.5rem; } .blog-title { margin:0 0 8px 0; font-size:1.5rem; }
.blog-meta { margin:0 0 16px 0; color:#7f1d1d; font-size:0.95rem; } .blog-meta { margin:0 0 16px 0; color:#7f1d1d; font-size:0.95rem; }
/* ----- Blog body with fade-out ----- */
.blog-body-wrapper {
position: relative;
margin-bottom: 0.5rem;
/* No max-height by default (full) */
}
.blog-body { .blog-body {
white-space: pre-line; white-space: pre-line;
margin: 0 0 1em 0; margin: 0;
} }
.blog-body p { margin:0 0 1em 0; } .blog-body p { margin:0 0 1em 0; }
.blog-body p:last-child { margin-bottom:0; } .blog-body p:last-child { margin-bottom:0; }
/* When collapsed, limit height and show gradient fade */
.blog-body-wrapper.collapsed {
max-height: 300px; /* ~6 lines, adjust as needed */
overflow: hidden;
/* The gradient overlay is done via ::after */
}
.blog-body-wrapper.collapsed::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 80px; /* fade height */
background: linear-gradient(to bottom, transparent, #fff);
pointer-events: none; /* allow clicking through to the button */
/* If your blog background is not white, change #fff to match */
}
/* Toggle button */ /* Toggle button */
.blog-toggle { .blog-toggle {
background: none; background: none;
@@ -53,6 +79,7 @@ a:hover { color:#991b1b; text-decoration:underline; }
text-decoration: underline; text-decoration: underline;
} }
/* Other existing styles (photography, modal, etc.) */
.blog-attachment-list { margin:16px 0 0 0; padding:0; list-style:none; } .blog-attachment-list { margin:16px 0 0 0; padding:0; list-style:none; }
.blog-attachment-list li { margin:0.5em 0; } .blog-attachment-list li { margin:0.5em 0; }
.blog-attachment-list a { color:#dc2626; text-decoration:none; } .blog-attachment-list a { color:#dc2626; text-decoration:none; }