diff --git a/blog/manifest.json b/blog/manifest.json
index d6fc449..187f310 100644
--- a/blog/manifest.json
+++ b/blog/manifest.json
@@ -1,5 +1,4 @@
[
- "2026-06-06.text",
- "2026-07-09-welcome.txt",
- "2026-07-10-attachment-test.txt"
+ "10072026.text",
+ "the first post.text"
]
\ No newline at end of file
diff --git a/images/manifest.json b/images/manifest.json
new file mode 100644
index 0000000..7d0ee00
--- /dev/null
+++ b/images/manifest.json
@@ -0,0 +1,159 @@
+[
+ "_DSC2592.jpg",
+ "_DSC2563.jpg",
+ "_DSC2536.jpg",
+ "_DSC2534.jpg",
+ "_DSC2532.jpg",
+ "_DSC2527.jpg",
+ "_DSC2627.jpg",
+ "_DSC2600.jpg",
+ "_DSC5088.jpg",
+ "_DSC5082.jpg",
+ "_DSC5070.jpg",
+ "_DSC5060.jpg",
+ "_DSC5058.jpg",
+ "_DSC5037.jpg",
+ "_DSC5031.jpg",
+ "_DSC5014.jpg",
+ "_DSC5005.jpg",
+ "_DSC4996.jpg",
+ "_DSC4984.jpg",
+ "_DSC4935.jpg",
+ "_DSC4930.jpg",
+ "_DSC4927.jpg",
+ "_DSC4907.jpg",
+ "_DSC4859.jpg",
+ "_DSC4850.jpg",
+ "_DSC4824.jpg",
+ "_DSC4814.jpg",
+ "_DSC4784.jpg",
+ "_DSC4774.jpg",
+ "_DSC4760.jpg",
+ "_DSC4689.jpg",
+ "_DSC4686.jpg",
+ "_DSC4670.jpg",
+ "_DSC4640.jpg",
+ "_DSC4565.jpg",
+ "_DSC4561.jpg",
+ "_DSC4558.jpg",
+ "_DSC4537.jpg",
+ "_DSC4532.jpg",
+ "_DSC4523.jpg",
+ "_DSC4522.jpg",
+ "_DSC4478.jpg",
+ "_DSC4447.jpg",
+ "_DSC4416.jpg",
+ "_DSC4408.jpg",
+ "_DSC4373.jpg",
+ "_DSC4343.jpg",
+ "_DSC4332.jpg",
+ "_DSC4322.jpg",
+ "_DSC5220.jpg",
+ "_DSC5112.jpg",
+ "_DSC5109.jpg",
+ "_DSC5096.jpg",
+ "_DSC1049.jpg",
+ "_DSC1042.jpg",
+ "_DSC1037.jpg",
+ "_DSC1030.jpg",
+ "_DSC1028.jpg",
+ "_DSC1021.jpg",
+ "_DSC1016.jpg",
+ "_DSC1015.jpg",
+ "_DSC1009.jpg",
+ "_DSC1007.jpg",
+ "_DSC1102.jpg",
+ "_DSC1098.jpg",
+ "_DSC1052.jpg",
+ "_DSC1468.jpg",
+ "_DSC1464.jpg",
+ "_DSC1559.jpg",
+ "_DSC1550.jpg",
+ "_DSC1543.jpg",
+ "_DSC1542.jpg",
+ "_DSC1539.jpg",
+ "_DSC1527.jpg",
+ "_DSC1516.jpg",
+ "_DSC1498.jpg",
+ "_DSC1495.jpg",
+ "_DSC1487.jpg",
+ "_DSC1471.jpg",
+ "_DSC1152.jpg",
+ "_DSC1150.jpg",
+ "_DSC1396.jpg",
+ "_DSC1341.jpg",
+ "_DSC1330.jpg",
+ "_DSC1315.jpg",
+ "_DSC1282.jpg",
+ "_DSC1174.jpg",
+ "_DSC1164.jpg",
+ "_DSC0779.jpg",
+ "_DSC0762.jpg",
+ "_DSC0732.jpg",
+ "_DSC0707.jpg",
+ "_DSC0701.jpg",
+ "_DSC0699.jpg",
+ "_DSC0697.jpg",
+ "_DSC0683.jpg",
+ "_DSC0678.jpg",
+ "_DSC0677.jpg",
+ "_DSC0671.jpg",
+ "_DSC0669.jpg",
+ "_DSC0665.jpg",
+ "_DSC0649.jpg",
+ "_DSC0646.jpg",
+ "_DSC0624.jpg",
+ "_DSC0621.jpg",
+ "_DSC0601.jpg",
+ "_DSC0590.jpg",
+ "_DSC0582.jpg",
+ "_DSC0866.jpg",
+ "_DSC0861.jpg",
+ "_DSC0858.jpg",
+ "_DSC0854.jpg",
+ "_DSC0850.jpg",
+ "_DSC0846.jpg",
+ "_DSC0841.jpg",
+ "_DSC0837.jpg",
+ "_DSC0833.jpg",
+ "_DSC0823.jpg",
+ "_DSC0817.jpg",
+ "_DSC0784.jpg",
+ "_DSC0782.jpg",
+ "DSC_0337.jpg",
+ "DSC_0335.jpg",
+ "DSC_0319.jpg",
+ "DSC_0297.jpg",
+ "DSC_0278.jpg",
+ "DSC_0257.jpg",
+ "DSC_0234.jpg",
+ "DSC_0224.jpg",
+ "DSC_0214.jpg",
+ "DSC_0207.jpg",
+ "DSC_0499.jpg",
+ "DSC_0470.jpg",
+ "DSC_0455.jpg",
+ "DSC_0450.jpg",
+ "DSC_0446.jpg",
+ "DSC_0439.jpg",
+ "DSC_0430.jpg",
+ "DSC_0404.jpg",
+ "DSC_0402.jpg",
+ "DSC_0389.jpg",
+ "DSC_0387.jpg",
+ "DSC_0378.jpg",
+ "DSC_0368.jpg",
+ "DSC_0036.jpg",
+ "DSC_0122.jpg",
+ "DSC_0114.jpg",
+ "DSC_0089.jpg",
+ "DSC_0087.jpg",
+ "DSC_0084.jpg",
+ "DSC_0083.jpg",
+ "DSC_0081.jpg",
+ "DSC_0068.jpg",
+ "DSC_0054.jpg",
+ "DSC_0048.jpg",
+ "DSC_0045.jpg",
+ "DSC_0042.jpg"
+]
\ No newline at end of file
diff --git a/js/blog.js b/js/blog.js
index 930ae75..5817b6a 100644
--- a/js/blog.js
+++ b/js/blog.js
@@ -1,127 +1,3 @@
-const BLOG_DIR = 'blog/';
-const ATTACHMENT_DIR = BLOG_DIR + 'files/';
-const MANIFEST = BLOG_DIR + 'manifest.json';
-const TXT_EXT = /\.(txt|text)$/i;
-const ATTACHMENT_RE = /\[\[(?:file|attachment):\s*([^\]]+)\]\]/gi;
-
-function escapeHtml(value) {
- return value
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
-}
-
-function parseIndexHtmlList(text) {
- const hrefs = [];
- const re = /href="([^"]+)"/g;
- let m;
- while ((m = re.exec(text)) !== null) {
- const name = m[1];
- if (TXT_EXT.test(name)) hrefs.push(name);
- }
- return hrefs;
-}
-
-async function loadBlogFiles() {
- try {
- const response = await fetch(MANIFEST, { cache: 'no-cache' });
- if (response.ok) {
- return await response.json();
- }
- } catch (e) {
- // manifest missing or unreadable
- }
-
- try {
- const response = await fetch(BLOG_DIR, { cache: 'no-cache' });
- if (response.ok) {
- const text = await response.text();
- return parseIndexHtmlList(text);
- }
- } catch (e) {
- console.warn('Could not read blog directory listing', e);
- }
-
- return [];
-}
-
-function parseDateLine(rawDate) {
- if (!rawDate) return null;
- let value = rawDate.trim();
- if (value.toLowerCase().startsWith('date:')) {
- value = value.slice(5).trim();
- }
- if (!value) return null;
- const normalized = value.replace(' ', 'T');
- const date = new Date(normalized);
- return Number.isNaN(date.getTime()) ? null : date;
-}
-
-function extractAttachments(body) {
- const attachments = new Set();
- body.replace(ATTACHMENT_RE, (_, filename) => {
- const safeName = filename.trim();
- if (safeName) attachments.add(safeName);
- return '';
- });
- return Array.from(attachments);
-}
-
-function parseBlogText(text, filename) {
- const lines = text.replace(/\r/g, '').split('\n');
- let index = 0;
- while (index < lines.length && !lines[index].trim()) index += 1;
-
- const rawDate = index < lines.length ? lines[index].trim() : '';
- if (rawDate.toLowerCase().startsWith('date:')) {
- index += 1;
- } else {
- index += 1;
- }
-
- while (index < lines.length && !lines[index].trim()) index += 1;
- const title = index < lines.length ? lines[index].trim() : 'Untitled';
- index += 1;
-
- while (index < lines.length && !lines[index].trim()) index += 1;
- const body = lines.slice(index).join('\n').trim();
-
- return {
- filename,
- title: title || 'Untitled',
- date: parseDateLine(rawDate) || null,
- dateLabel: rawDate || '',
- body: body || '',
- attachments: extractAttachments(body),
- };
-}
-
-function formatDate(date) {
- if (!date) return 'Unknown date';
- return new Intl.DateTimeFormat('en-US', {
- year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit'
- }).format(date);
-}
-
-function createParagraphs(body) {
- return body
- .split(/\n{2,}/)
- .filter(Boolean)
- .map((paragraph) => {
- const p = document.createElement('p');
- const text = escapeHtml(paragraph).replace(ATTACHMENT_RE, (match, filename) => {
- const safeName = filename.trim();
- const url = ATTACHMENT_DIR + encodeURI(safeName);
- return `${escapeHtml(safeName)}`;
- });
- const lines = text.split('\n');
- p.innerHTML = lines.join('
');
- return p;
- });
-}
-
function renderBlogEntries(entries) {
const container = document.querySelector('.blog-feed');
container.innerHTML = '';
@@ -141,78 +17,111 @@ function renderBlogEntries(entries) {
const meta = document.createElement('div');
meta.className = 'blog-meta';
- meta.textContent = entry.date ? formatDate(entry.date) : entry.dateLabel || 'Date not provided';
+ meta.textContent = entry.date
+ ? formatDate(entry.date)
+ : entry.dateLabel || 'Date not provided';
+
+
+ const createBody = () => {
+ const body = document.createElement('div');
+ body.className = 'blog-body';
+
+ const paragraphs = createParagraphs(entry.body);
+
+ if (!paragraphs.length) {
+ const empty = document.createElement('p');
+ empty.textContent = 'No content yet.';
+ body.appendChild(empty);
+ } else {
+ paragraphs.forEach((p) => body.appendChild(p));
+ }
+
+ return body;
+ };
+
+
+ let bodyContainer;
+
+ if (entry.body.length > BLOG_COLLAPSE_THRESHOLD) {
+ const details = document.createElement('details');
+ details.className = 'blog-collapse';
+
+ const summary = document.createElement('summary');
+
+ // Create preview
+ const preview = document.createElement('div');
+ preview.className = 'blog-preview';
+
+ const previewText = entry.body
+ .split('\n')
+ .filter(line => line.trim())
+ .slice(0, BLOG_PREVIEW_LINES)
+ .join('\n');
+
+ preview.textContent = previewText;
+
+
+ summary.appendChild(preview);
+
+ const moreText = document.createElement('span');
+ moreText.className = 'read-more-text';
+ moreText.textContent = 'Read more';
+
+ summary.appendChild(moreText);
+
+
+ details.appendChild(summary);
+ details.appendChild(createBody());
+
+
+ details.addEventListener('toggle', () => {
+ moreText.textContent = details.open
+ ? 'Show less'
+ : 'Read more';
+
+ preview.style.display = details.open
+ ? 'none'
+ : 'block';
+ });
+
+
+ bodyContainer = details;
- const body = document.createElement('div');
- body.className = 'blog-body';
- const paragraphs = createParagraphs(entry.body);
- if (!paragraphs.length) {
- const empty = document.createElement('p');
- empty.textContent = 'No content yet.';
- body.appendChild(empty);
} else {
- paragraphs.forEach((p) => body.appendChild(p));
+ bodyContainer = createBody();
}
+
if (entry.attachments && entry.attachments.length) {
const attachTitle = document.createElement('div');
attachTitle.className = 'blog-meta';
attachTitle.textContent = 'Attachments:';
+
const list = document.createElement('ul');
list.className = 'blog-attachment-list';
+
entry.attachments.forEach((attachment) => {
const item = document.createElement('li');
+
const link = document.createElement('a');
link.href = ATTACHMENT_DIR + encodeURI(attachment);
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.textContent = attachment;
+
item.appendChild(link);
list.appendChild(item);
});
+
article.appendChild(attachTitle);
article.appendChild(list);
}
+
article.appendChild(title);
article.appendChild(meta);
- article.appendChild(body);
+ article.appendChild(bodyContainer);
+
container.appendChild(article);
});
-}
-
-function sortEntries(entries) {
- return entries.slice().sort((a, b) => {
- if (a.date && b.date) return b.date - a.date;
- if (a.date) return -1;
- if (b.date) return 1;
- return a.filename.localeCompare(b.filename);
- });
-}
-
-async function initBlog() {
- const fileNames = await loadBlogFiles();
- if (!fileNames.length) {
- renderBlogEntries([]);
- return;
- }
-
- const promises = fileNames
- .filter((name) => TXT_EXT.test(name))
- .map(async (name) => {
- try {
- const response = await fetch(BLOG_DIR + name, { cache: 'no-cache' });
- if (!response.ok) throw new Error('Failed to load ' + name);
- const text = await response.text();
- return parseBlogText(text, name);
- } catch (error) {
- console.warn(error);
- return null;
- }
- });
-
- const entries = (await Promise.all(promises)).filter(Boolean);
- renderBlogEntries(sortEntries(entries));
-}
-
-document.addEventListener('DOMContentLoaded', initBlog);
+}
\ No newline at end of file