const BLOG_ROOT_NOTE_ID = "__BLOG_ROOT_ID__"; const BLOG_SYSTEM_NOTE_ID = "__BLOG_SYSTEM_ID__"; const BLOG_ALIAS = "blog"; const SHARE_BASE = "https://YOUR-INSTANCE.example.com"; const BASE_URL = SHARE_BASE + "/" + BLOG_ALIAS; const SITE_TITLE = "Your Blog"; const SITE_DESCRIPTION = "Project documentation & technical blog by the author"; const TAGS_SECTION_TITLE = "Tags"; const HIDE_HOME_TITLE_NOTE_ID = "__HIDE_HOME_TITLE_ID__"; const POSTS_PER_PAGE = 10; function isInSubtree(note, targetId) { try { if (note.noteId === targetId) return true; for (const child of note.getChildNotes()) { if (isInSubtree(child, targetId)) return true; } } catch (e) { api.log("blog_generator: isInSubtree error: " + e); } return false; } function isGeneratedNoteId(id) { try { if (id === BLOG_ROOT_NOTE_ID) return true; const n = api.getNote(id); if (n && n.hasLabel("blogPagedFor")) return true; const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID); if (systemNote) { const feedNote = systemNote.getChildNotes().find(n => n.title === "feed.xml"); if (feedNote && id === feedNote.noteId) return true; } const blogRoot = api.getNote(BLOG_ROOT_NOTE_ID); if (blogRoot) { const tagsNote = blogRoot.getChildNotes().find(n => n.title === TAGS_SECTION_TITLE); if (tagsNote && isInSubtree(tagsNote, id)) return true; } } catch (e) { api.log("blog_generator: isGeneratedNoteId error: " + e); } return false; } if (api.originEntity && isGeneratedNoteId(api.originEntity.noteId)) { api.log("blog_generator: ignoring self-trigger from generated note"); return; } // ---- P2: only rebuild on events that can affect the generated pages ---- // (avoids full rebuild on every snippet/CSS edit, image upload, self-set shareAlias, ...) const GEN_RELEVANT_LABELS = new Set([ "publish", "date", "category", "tags", "summary", "state", "blogGarden", "blogCategory", "blogNoArticles", "blogArticleList", "iconClass", "geolocation", "geo:position", "geoShape", "viewType", "map:style", "map:scale" ]); const GEN_SKIP_RELATIONS = new Set([ "shareHtml", "shareCss", "shareTemplate", "shareFavicon", "shareOpenGraphImage", "runOnNoteContentChange", "runOnAttributeChange", "runOnAttributeCreation", "runOnNoteTitleChange", "runOnNoteCreation", "runOnChildNoteCreation", "runOnBranchDeletion", "runOnEntityChange" ]); function shouldRebuild() { const o = api.originEntity; const ev = api.originEvent || ""; if (!o) return true; if (/contentChange/i.test(ev)) { // Article body edits don't change the index/feed/tag pages (only summary labels do). // Keep the refresh only for thumbnail image replacements (index shows the thumb). const title = o.title || ""; if (!/^(?:thumb|.+_thumb)\.[A-Za-z0-9]+$/i.test(title)) return false; } if (o.type === "label" && !GEN_RELEVANT_LABELS.has(o.name)) return false; if (o.type === "relation" && GEN_SKIP_RELATIONS.has(o.name)) return false; if (o.type === "branch") { // Reordering route points (drag in tree) must refresh geo_data try { const bn = o.getNote ? o.getNote() : null; if (bn && (bn.hasLabel("geolocation") || bn.hasLabel("geo:position") || bn.hasLabel("geoShape"))) return true; } catch (e) {} return false; // publish/unpublish covers clone+move via label events } return true; } if (!shouldRebuild()) { api.log("blog_generator: event skipped (no index impact)"); return; } try { run(); } catch (e) { api.log("blog_generator: ERROR " + (e && e.stack ? e.stack : e)); } function run() { const blogRoot = api.getNote(BLOG_ROOT_NOTE_ID); if (!blogRoot) { api.log("blog_generator: root note not found"); return; } const articles = []; function visit(note, depth) { if (depth > 20) return; if (note.noteId !== BLOG_ROOT_NOTE_ID && note.getLabelValue("blogGarden") !== null) { return; } const publish = note.getLabelValue("publish"); if (note.noteId !== BLOG_ROOT_NOTE_ID && publish === "true") { articles.push(note); return; } for (const child of note.getChildNotes()) { visit(child, depth + 1); } } visit(blogRoot, 0); articles.sort((a, b) => { const da = a.getLabelValue("date") || a.dateCreated; const db = b.getLabelValue("date") || b.dateCreated; return (db < da) ? -1 : (db > da ? 1 : 0); }); function escapeHtml(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function tagList(a) { return (a.getLabelValue("tags") || "").split(/\s+/).filter(Boolean).map(t => t.replace(/^!+/, "")); } function tagHref(t) { return SHARE_BASE + "/" + encodeURIComponent("tag-" + slugifyTag(t)); } function thumbSrc(a) { try { const thumbs = (a.getChildNotes() || []).filter(n => /^(?:thumb|.+_thumb)\.[A-Za-z0-9]+$/i.test(n.title)); if (thumbs.length) return "api/notes/" + thumbs[0].noteId + "/download"; } catch (e) {} try { const m = String(a.getContent() || "").match(/]+src="([^"]+)"/); return m ? m[1] : ""; } catch (e) { return ""; } } const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; function dateText(raw) { const iso = String(raw || "").slice(0, 10); if (!iso) return ""; const d = new Date(iso); if (isNaN(d)) return iso; return WEEKDAYS[d.getUTCDay()] + " " + iso; } function renderArticles(list) { const lines = []; for (const a of list) { const date = dateText(a.getLabelValue("date") || a.dateCreated); const cat = a.getLabelValue("category") || ""; const summary = a.getLabelValue("summary") || ""; const link = `${SHARE_BASE}/${encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId)}`; const tags = tagList(a); const thumb = thumbSrc(a); lines.push('
'); lines.push('
'); lines.push(`

${escapeHtml(a.title)}

`); lines.push(' '); lines.push('
'); if (summary || thumb) { lines.push('
'); if (thumb) { lines.push(` '); } lines.push('
'); if (summary) lines.push(`

${escapeHtml(summary)}

`); if (tags.length) { lines.push(' '); } lines.push('
'); lines.push('
'); } else if (tags.length) { lines.push(' '); } lines.push('
'); } return lines.join("\n"); } function stateIcon(state) { const s = String(state).toLowerCase(); const icons = { "seed": "bx bxs-coffee-bean", "seedling": "bx bxs-coffee-bean", "sprout": "bx bxs-leaf", "sprouting": "bx bxs-leaf", "bud": "bx bxs-leaf", "evergreen": "bx bxs-tree-alt", "draft": "bx bx-edit", "review": "bx bx-search-alt" }; return icons[s] || "bx bxs-circle"; } function stateClass(state) { const s = String(state).toLowerCase(); const growth = ["seed", "seedling", "sprout", "sprouting", "bud", "evergreen"]; if (growth.includes(s)) return "state-growth"; if (s === "draft") return "state-draft"; if (s === "review") return "state-review"; return "state-unknown"; } function buildGardenTable(catNote, list) { let html = '\n\n'; for (const a of list) { const link = SHARE_BASE + "/" + encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId); const type = a.type || "text"; const state = a.getLabelValue("state") || ""; const date = dateText(a.getLabelValue("date") || a.dateCreated); html += ``; } html += '
TitleTypeStateDate
${escapeHtml(a.title)}${escapeHtml(type)}${state ? `${escapeHtml(state)}` : ""}${escapeHtml(date)}
'; return html; } function pageUrl(aliasBase, p) { return SHARE_BASE + "/" + (p <= 1 ? aliasBase : aliasBase + "-" + p); } function paginationHtml(page, totalPages, aliasBase) { if (totalPages <= 1) return ""; let html = ''; return html; } function syncPageNotes(pageLabel, aliasBase, totalPages, list, garden, hideTitle) { const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID); if (!systemNote) return; const existing = systemNote.getChildNotes().filter(n => n.getLabelValue("blogPagedFor") === pageLabel); for (let p = 2; p <= totalPages; p++) { let note = existing.find(n => n.getLabelValue("blogPagedNumber") === String(p)); const created = !note; if (!note) { const res = api.createTextNote(systemNote.noteId, "Blog page " + p + " (" + pageLabel + ")", ""); note = res.note ? res.note : res; } const slice = list.slice((p - 1) * POSTS_PER_PAGE, p * POSTS_PER_PAGE); let body = garden ? buildGardenTable(note, slice) : (renderArticles(slice) || '

No articles.

'); body += paginationHtml(p, totalPages, aliasBase); if (note.getContent() !== body) { note.setContent(body); note.save(); } note.setLabel("blogPagedFor", pageLabel); note.setLabel("blogPagedNumber", String(p)); note.setLabel("shareAlias", aliasBase + "-" + p); note.setLabel("shareHiddenFromTree", ""); if (hideTitle && !note.hasRelation("shareHtml", HIDE_HOME_TITLE_NOTE_ID)) { note.setRelation("shareHtml", HIDE_HOME_TITLE_NOTE_ID); } note.save(); api.log("blog_generator: page " + pageLabel + " #" + p + (created ? " created" : " updated")); } for (const n of existing) { const num = parseInt(n.getLabelValue("blogPagedNumber") || "999", 10); if (num > totalPages) { api.log("blog_generator: removing obsolete page note " + n.title); n.deleteNote(); } } } function buildIndex() { const pageCount = Math.max(1, Math.ceil(articles.length / POSTS_PER_PAGE)); const body = (renderArticles(articles.slice(0, POSTS_PER_PAGE)) || '

No articles published yet.

') + paginationHtml(1, pageCount, BLOG_ALIAS); if (blogRoot.getContent() !== body) { blogRoot.setContent(body); blogRoot.save(); api.log("blog_generator: index updated (" + articles.length + " articles, " + pageCount + " pages)"); } else { api.log("blog_generator: index unchanged"); } syncPageNotes("index", BLOG_ALIAS, pageCount, articles, false, true); } function buildFeedXml() { const items = articles.map(a => { const date = a.getLabelValue("date") || a.dateCreated; const alias = a.getLabelValue("shareAlias") || a.noteId; const summary = a.getLabelValue("summary") || ""; const pubDate = new Date(date).toUTCString(); const link = `${BASE_URL}/${encodeURIComponent(alias)}`; return ` \n ${escapeHtml(a.title)}\n ${link}\n ${escapeHtml(a.noteId)}\n ${pubDate}\n ${escapeHtml(summary)}\n `; }).join("\n"); const now = new Date().toUTCString(); return `\n\n \n ${escapeHtml(SITE_TITLE)}\n ${BASE_URL}\n \n ${escapeHtml(SITE_DESCRIPTION)}\n en\n ${now}\n${items}\n \n`; } function slugifyTag(t) { return t.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); } function buildStateMap() { const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID); if (!systemNote) return; const mapNote = systemNote.getChildNotes().find(n => n.title === "state_map"); if (!mapNote) return; const map = {}; const seen = new Set(); (function walk(note, depth) { if (depth > 20) return; for (const child of note.getChildNotes()) { if (seen.has(child.noteId)) continue; seen.add(child.noteId); const state = child.getLabelValue("state"); if (child.getLabelValue("publish") === "true" && state) { map[child.noteId] = state; } walk(child, depth + 1); } })(blogRoot, 0); const html = ``; if (mapNote.getContent() !== html) { mapNote.setContent(html); mapNote.save(); api.log("blog_generator: state map updated"); } } function collectAllPublished() { const out = []; const seen = new Set(); (function walk(note, depth) { if (depth > 20) return; for (const child of note.getChildNotes()) { if (seen.has(child.noteId)) continue; seen.add(child.noteId); if (child.getLabelValue("publish") === "true") out.push(child); walk(child, depth + 1); } })(blogRoot, 0); out.sort((a, b) => { const da = a.getLabelValue("date") || a.dateCreated; const db = b.getLabelValue("date") || b.dateCreated; return (db < da) ? -1 : (db > da ? 1 : 0); }); return out; } function buildSitemap() { const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID); if (!systemNote) return; const sitemapNote = systemNote.getChildNotes().find(n => n.title === "sitemap.xml"); if (!sitemapNote) return; const urls = [SHARE_BASE + "/" + BLOG_ALIAS]; for (const a of collectAllPublished()) { urls.push(SHARE_BASE + "/" + encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId)); } for (const cat of collectCategoryNotes()) { urls.push(SHARE_BASE + "/" + encodeURIComponent(cat.getLabelValue("shareAlias") || cat.noteId)); } const tagsNote = blogRoot.getChildNotes().find(n => n.title === TAGS_SECTION_TITLE); if (tagsNote) { for (const t of tagsNote.getChildNotes()) { urls.push(SHARE_BASE + "/" + encodeURIComponent(t.getLabelValue("shareAlias") || t.noteId)); } } const body = urls.map(u => ` ${escapeHtml(u)}`).join("\n"); const xml = `\n\n${body}\n`; if (sitemapNote.getContent() !== xml) { sitemapNote.setContent(xml); sitemapNote.save(); api.log("blog_generator: sitemap updated (" + urls.length + " urls)"); } } function ensureSeoMeta() { const seen = new Set(); (function walk(note, depth) { if (depth > 20) return; for (const child of note.getChildNotes()) { if (seen.has(child.noteId)) continue; seen.add(child.noteId); const published = child.getLabelValue("publish") === "true"; const isCat = isCategoryNote(child); if (published || isCat) { const desc = child.getLabelValue("summary") || (isCat ? "Articles in " + child.title : SITE_DESCRIPTION); if (child.getLabelValue("shareDescription") !== desc) { child.setLabel("shareDescription", desc); child.save(); } const url = SHARE_BASE + "/" + encodeURIComponent(child.getLabelValue("shareAlias") || child.noteId); if (child.getLabelValue("shareOpenGraphURL") !== url) { child.setLabel("shareOpenGraphURL", url); child.save(); api.log("blog_generator: set shareOpenGraphURL on " + child.title); } } walk(child, depth + 1); } })(blogRoot, 0); } function plainText(a) { try { return String(a.getContent() || "") .replace(//gi, " ") .replace(//gi, " ") .replace(/<[^>]+>/g, " ") .replace(/&[a-z#0-9]+;/gi, " ") .replace(/\s+/g, " ") .trim() .slice(0, 1500); } catch (e) { return ""; } } function isInGardenTree(note) { try { for (const p of note.getParentNotes()) { let cur = p; for (let d = 0; cur && d < 8; d++) { if (cur.hasLabel && cur.hasLabel("blogGarden")) return true; cur = cur.getParentNotes()[0]; } } } catch (e) {} return false; } function buildBlogData() { const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID); if (!systemNote) return; const dataNote = systemNote.getChildNotes().find(n => n.title === "blog_data"); if (!dataNote) return; const list = collectAllPublished().map(a => ({ id: a.noteId, title: a.title, url: SHARE_BASE + "/" + encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId), summary: a.getLabelValue("summary") || "", tags: tagList(a), category: a.getLabelValue("category") || (isInGardenTree(a) ? "Digital Garden" : ""), date: (a.getLabelValue("date") || a.dateCreated).slice(0, 10), created: (a.dateCreated || "").slice(0, 10), dateText: dateText(a.getLabelValue("date") || a.dateCreated), text: plainText(a) })); const html = ``; if (dataNote.getContent() !== html) { dataNote.setContent(html); dataNote.save(); api.log("blog_generator: blog_data updated (" + list.length + " articles)"); } } // Published notes with viewType=geoMap get their child geolocations exported // as window.geoData; the "geo map render" snippet turns them into Leaflet maps. function buildGeoData() { const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID); if (!systemNote) return; const books = []; const seen = new Set(); (function walk(note, depth) { if (depth > 20) return; for (const child of note.getChildNotes()) { if (seen.has(child.noteId)) continue; seen.add(child.noteId); if (child.getLabelValue("viewType") === "geoMap" && child.getLabelValue("publish") === "true") books.push(child); walk(child, depth + 1); } })(blogRoot, 0); let dataNote = systemNote.getChildNotes().find(n => n.title === "geo_data"); if (!books.length) { if (dataNote && dataNote.getContent() !== "") { dataNote.setContent(""); dataNote.save(); api.log("blog_generator: geo_data cleared"); } return; } if (!dataNote) { const created = api.createTextNote(systemNote.noteId, "geo_data", ""); dataNote = created.note ? created.note : created; } if (!blogRoot.hasRelation("shareHtml", dataNote.noteId)) { blogRoot.setInheritableRelation("shareHtml", dataNote.noteId); blogRoot.save(); api.log("blog_generator: geo_data attached to blog root"); } const maps = books.map(b => { const pts = []; let kids = b.getChildNotes(); try { if (typeof api.getBranchFromChildAndParent === "function") { kids = kids.slice().sort((c1, c2) => { const br1 = api.getBranchFromChildAndParent(c1.noteId, b.noteId); const br2 = api.getBranchFromChildAndParent(c2.noteId, b.noteId); return (br1 ? br1.position : 0) - (br2 ? br2.position : 0); }); } else { api.log("blog_generator: getBranchFromChildAndParent unavailable, using natural order"); } } catch (e) { api.log("blog_generator: geo sort failed (" + e + "), using natural order"); } function parsePairs(s) { const out = []; for (const pair of String(s).split(/\s+/)) { const pm = pair.match(/^(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)$/); if (pm) out.push({ lat: parseFloat(pm[1]), lng: parseFloat(pm[2]) }); } return out; } const tracks = []; for (const c of kids) { const published = c.getLabelValue("publish") === "true"; const purl = published ? SHARE_BASE + "/" + encodeURIComponent(c.getLabelValue("shareAlias") || c.noteId) : ""; const summary = c.getLabelValue("summary") || ""; const markerCoords = []; let shapes = []; try { shapes = c.getLabels("geoShape"); } catch (e) {} for (const s of shapes) { const sm = String(s.value).match(/^\s*(point|line|polygon)\s*:\s*([\s\S]*)$/i); if (!sm) continue; const cs = parsePairs(sm[2]); if (!cs.length) continue; if (sm[1].toLowerCase() === "point") markerCoords.push(...cs); else tracks.push({ title: c.title, coords: cs.map(p => [p.lng, p.lat]) }); } const g = c.getLabelValue("geolocation") || c.getLabelValue("geo:position") || ""; markerCoords.push(...parsePairs(g)); markerCoords.forEach((p, i) => { pts.push({ title: markerCoords.length > 1 ? c.title + " (" + (i + 1) + ")" : c.title, lat: p.lat, lng: p.lng, url: purl, summary: summary }); }); if (c.type === "file" && (/\.gpx$/i.test(c.title || "") || /\.gpx$/i.test(c.getLabelValue("originalFileName") || ""))) { tracks.push({ title: c.title.replace(/\.gpx$/i, ""), url: "api/notes/" + c.noteId + "/download" }); } } return { id: b.noteId, title: b.title, style: b.getLabelValue("map:style") || "", points: pts, tracks: tracks }; }); const gpxIndex = []; (function walkGpx(note, depth) { if (depth > 20) return; for (const child of note.getChildNotes()) { if (child.type === "file" && (/\.gpx$/i.test(child.title || "") || /\.gpx$/i.test(child.getLabelValue("originalFileName") || ""))) { gpxIndex.push({ id: child.noteId, title: child.title.replace(/\.gpx$/i, "") }); } walkGpx(child, depth + 1); } })(blogRoot, 0); const cadIndex = []; (function walkCad(note, depth) { if (depth > 20) return; for (const child of note.getChildNotes()) { if (child.type === "file") { const cm = String(child.title || "").match(/\.(stl|step|stp)$/i) || String(child.getLabelValue("originalFileName") || "").match(/\.(stl|step|stp)$/i); if (cm) { cadIndex.push({ id: child.noteId, title: child.title.replace(/\.(stl|step|stp)$/i, ""), format: cm[1].toLowerCase() }); } } walkCad(child, depth + 1); } })(blogRoot, 0); const mediaIndex = []; (function walkMedia(note, depth) { if (depth > 20) return; for (const child of note.getChildNotes()) { if (child.type === "file") { const titleStr = String(child.title || ""); const origStr = String(child.getLabelValue("originalFileName") || ""); const wm = titleStr.match(/\.(mp4|webm|mov|m4v|mp3|m4a|ogg|oga|wav|flac|aac|opus)$/i) || origStr.match(/\.(mp4|webm|mov|m4v|mp3|m4a|ogg|oga|wav|flac|aac|opus)$/i); if (wm) { const fmt = wm[1].toLowerCase(); mediaIndex.push({ id: child.noteId, title: titleStr.replace(/\.[a-z0-9]+$/i, "") || origStr.replace(/\.[a-z0-9]+$/i, ""), format: fmt, kind: ["mp4", "webm", "mov", "m4v"].indexOf(fmt) >= 0 ? "video" : "audio" }); } } walkMedia(child, depth + 1); } })(blogRoot, 0); const html = ``; if (dataNote.getContent() !== html) { dataNote.setContent(html); dataNote.save(); api.log("blog_generator: geo_data updated (" + maps.length + " maps)"); } } function buildTags() { const tagsNote = blogRoot.getChildNotes().find(n => n.title === TAGS_SECTION_TITLE); if (!tagsNote) return; const map = new Map(); for (const a of articles) { for (const t of tagList(a)) { if (!map.has(t)) map.set(t, []); map.get(t).push(a); } } const tagNames = Array.from(map.keys()).sort(); const existing = tagsNote.getChildNotes(); const byTitle = {}; for (const n of existing) byTitle[n.title] = n; for (const tagName of tagNames) { let note = byTitle[tagName]; if (!note) { const created = api.createTextNote(tagsNote.noteId, tagName, ""); note = created.note ? created.note : created; } const tagged = map.get(tagName).slice(); tagged.sort((a, b) => { const da = a.getLabelValue("date") || a.dateCreated; const db = b.getLabelValue("date") || b.dateCreated; return (db < da) ? -1 : (db > da ? 1 : 0); }); const html = `

Tag: ${escapeHtml(tagName)}

\n` + (renderArticles(tagged) || '

No articles.

'); if (note.getContent() !== html) { note.setContent(html); note.save(); } if (!note.hasRelation("shareHtml", HIDE_HOME_TITLE_NOTE_ID)) { note.setRelation("shareHtml", HIDE_HOME_TITLE_NOTE_ID); note.save(); } const alias = "tag-" + slugifyTag(tagName); if (note.getLabelValue("shareAlias") !== alias) { note.setLabel("shareAlias", alias); note.save(); } if (!note.hasLabel("shareHiddenFromTree")) { note.setLabel("shareHiddenFromTree", ""); note.save(); } } // Tags overview page (list all tags as clickable chips) const tagChildren = tagsNote.getChildNotes().slice().sort((a, b) => a.title.localeCompare(b.title)); let idxHtml = '
'; for (const t of tagChildren) { const url = SHARE_BASE + "/" + encodeURIComponent(t.getLabelValue("shareAlias") || t.noteId); idxHtml += `#${escapeHtml(t.title)}`; } idxHtml += '
'; if (tagsNote.getContent() !== idxHtml) { tagsNote.setContent(idxHtml); tagsNote.save(); api.log("blog_generator: tags overview updated"); } for (const n of existing) { if (!map.has(n.title)) { api.log("blog_generator: removing obsolete tag note " + n.title); n.deleteNote(); } } } function isCategoryNote(note) { if (note.hasLabel("blogCategory")) return true; const parents = note.getParentNotes(); const isTopLevel = parents.some(p => p.noteId === BLOG_ROOT_NOTE_ID); if (!isTopLevel) return false; const sysTitles = ["Tags", ".blog", "Impressum", "About Me", "Search"]; if (sysTitles.includes(note.title)) return false; return true; } function collectCategoryNotes() { const out = []; (function walk(note, depth) { if (depth > 20) return; if (note.noteId !== BLOG_ROOT_NOTE_ID && isCategoryNote(note)) { out.push(note); } for (const child of note.getChildNotes()) { walk(child, depth + 1); } })(blogRoot, 0); return out; } function buildCategories() { const byCategoryLabel = new Map(); for (const a of articles) { const c = a.getLabelValue("category") || ""; if (!c) continue; if (!byCategoryLabel.has(c)) byCategoryLabel.set(c, []); byCategoryLabel.get(c).push(a); } const marker = ""; for (const catNote of collectCategoryNotes()) { const current = catNote.getContent() || ""; let userText = ""; const markerIdx = current.indexOf(marker); if (markerIdx >= 0) { userText = current.slice(0, markerIdx); } else if (!current.trim().startsWith(" c.getLabelValue("publish") !== "true"); const merged = new Map(); for (const c of children) { if (c.getLabelValue("publish") === "true") merged.set(c.noteId, c); } for (const a of (byCategoryLabel.get(catNote.title) || [])) { merged.set(a.noteId, a); } const articlesList = Array.from(merged.values()).sort((a, b) => { const da = a.getLabelValue("date") || a.dateCreated; const db = b.getLabelValue("date") || b.dateCreated; return (db < da) ? -1 : (db > da ? 1 : 0); }); let html = userText; if (html && !html.endsWith("\n")) html += "\n"; html += marker + "\n"; if (subs.length) { html += '
'; for (const s of subs) { const url = SHARE_BASE + "/" + encodeURIComponent(s.getLabelValue("shareAlias") || s.noteId); const icon = s.getLabelValue("iconClass"); html += ``; if (icon) html += ``; html += `${escapeHtml(s.title)}`; } html += "
\n"; } const pageCount = Math.max(1, Math.ceil(articlesList.length / POSTS_PER_PAGE)); const aliasBase = catNote.getLabelValue("shareAlias") || catNote.noteId; if (catNote.hasLabel("blogGarden")) { html += buildGardenTable(catNote, articlesList.slice(0, POSTS_PER_PAGE)); } else { html += renderArticles(articlesList.slice(0, POSTS_PER_PAGE)) || '

No articles yet.

'; } html += paginationHtml(1, pageCount, aliasBase); if (catNote.getContent() !== html) { catNote.setContent(html); catNote.save(); api.log("blog_generator: category page updated: " + catNote.title + " (" + pageCount + " pages)"); } syncPageNotes("cat:" + catNote.title, aliasBase, pageCount, articlesList, catNote.hasLabel("blogGarden"), false); } } // Opt-in (label "blogArticleList"): append a generated list of the note's // published child notes after the user's own content. Idempotent via marker; // placing the marker yourself controls where the list appears. The label // value (if any, not "true") becomes the list heading. function buildChildArticleLists() { const marker = ""; const seen = new Set(); (function walk(note, depth) { if (depth > 20) return; for (const child of note.getChildNotes()) { if (seen.has(child.noteId)) continue; seen.add(child.noteId); if (child.hasLabel("blogArticleList")) { const kids = child.getChildNotes().filter(c => c.getLabelValue("publish") === "true"); kids.sort((a, b) => { const da = a.getLabelValue("date") || a.dateCreated; const db = b.getLabelValue("date") || b.dateCreated; return (db < da) ? -1 : (db > da ? 1 : 0); }); const current = child.getContent() || ""; const idx = current.indexOf(marker); const userText = idx >= 0 ? current.slice(0, idx) : current; let out; if (kids.length) { out = userText; if (out && !out.endsWith("\n")) out += "\n"; out += marker + "\n"; const heading = child.getLabelValue("blogArticleList") || ""; if (heading && heading !== "true") out += `

${escapeHtml(heading)}

\n`; out += renderArticles(kids); } else { out = idx >= 0 ? current.replace(marker + "\n", "").replace(marker, "") : current; } if (out !== current) { child.setContent(out); child.save(); api.log("blog_generator: child article list updated: " + child.title + " (" + kids.length + ")"); } } walk(child, depth + 1); } })(blogRoot, 0); } function removeEmptyCategories() { const blogRoot = api.getNote(BLOG_ROOT_NOTE_ID); if (!blogRoot) return; const toCheck = []; (function collect(note, depth) { if (depth > 20) return; if (note.noteId !== blogRoot.noteId && note.hasLabel("blogCategory")) { toCheck.push(note); } for (const child of note.getChildNotes()) { collect(child, depth + 1); } })(blogRoot, 0); for (const note of toCheck) { if (note.getChildNotes().length === 0) { api.log("blog_generator: removing empty category " + note.title); note.deleteNote(); } } } function hideThumbChildren() { for (const a of articles) { try { for (const c of a.getChildNotes()) { if (/^(?:thumb|.+_thumb)\.[A-Za-z0-9]+$/i.test(c.title) && !c.hasLabel("shareHiddenFromTree")) { c.setLabel("shareHiddenFromTree", ""); c.save(); } } } catch (e) {} } } buildIndex(); const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID); const feedNote = systemNote.getChildNotes().find(n => n.title === "feed.xml"); if (feedNote) { const xml = buildFeedXml(); if (feedNote.getContent() !== xml) { feedNote.setContent(xml); feedNote.save(); api.log("blog_generator: feed updated"); } } buildTags(); buildCategories(); buildChildArticleLists(); buildStateMap(); buildSitemap(); ensureSeoMeta(); buildBlogData(); buildGeoData(); hideThumbChildren(); removeEmptyCategories(); }