604 lines
25 KiB
JavaScript
604 lines
25 KiB
JavaScript
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;
|
|
}
|
|
|
|
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, ">")
|
|
.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\.[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(/<img[^>]+src="([^"]+)"/);
|
|
return m ? m[1] : "";
|
|
} catch (e) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function renderArticles(list) {
|
|
const lines = [];
|
|
for (const a of list) {
|
|
const date = (a.getLabelValue("date") || a.dateCreated).slice(0, 10);
|
|
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('<article class="blog-post">');
|
|
lines.push(' <header class="blog-post-header">');
|
|
lines.push(` <h2 class="blog-post-title"><a href="${escapeHtml(link)}">${escapeHtml(a.title)}</a></h2>`);
|
|
lines.push(' <div class="blog-post-meta">');
|
|
if (date) lines.push(` <span class="blog-post-date">${escapeHtml(date)}</span>`);
|
|
if (cat) lines.push(` <span class="blog-post-category">${escapeHtml(cat)}</span>`);
|
|
lines.push(' </div>');
|
|
lines.push(' </header>');
|
|
if (summary || thumb) {
|
|
lines.push(' <div class="blog-post-inner">');
|
|
if (thumb) {
|
|
lines.push(` <a class="blog-thumb-link" href="${escapeHtml(link)}" tabindex="-1" aria-hidden="true">`);
|
|
lines.push(` <img class="blog-thumb" src="${escapeHtml(thumb)}" alt="" loading="lazy">`);
|
|
lines.push(' </a>');
|
|
}
|
|
lines.push(' <div class="blog-post-body">');
|
|
if (summary) lines.push(` <p class="blog-post-summary">${escapeHtml(summary)}</p>`);
|
|
if (tags.length) {
|
|
lines.push(' <div class="blog-post-tags">' + tags.map(t => `<a class="tag" href="${tagHref(t)}">#${escapeHtml(t)}</a>`).join(" ") + '</div>');
|
|
}
|
|
lines.push(' </div>');
|
|
lines.push(' </div>');
|
|
} else if (tags.length) {
|
|
lines.push(' <div class="blog-post-tags">' + tags.map(t => `<a class="tag" href="${tagHref(t)}">#${escapeHtml(t)}</a>`).join(" ") + '</div>');
|
|
}
|
|
lines.push('</article>');
|
|
}
|
|
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 = '<table class="blog-garden-table">\n<thead><tr><th>Title</th><th>Type</th><th>State</th><th>Date</th></tr></thead>\n<tbody>';
|
|
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 = (a.getLabelValue("date") || a.dateCreated).slice(0, 10);
|
|
html += `<tr><td><a href="${escapeHtml(link)}">${escapeHtml(a.title)}</a></td><td><span class="note-type">${escapeHtml(type)}</span></td><td>${state ? `<span class="note-state ${stateClass(state)}"><i class="${stateIcon(state)}"></i>${escapeHtml(state)}</span>` : ""}</td><td>${escapeHtml(date)}</td></tr>`;
|
|
}
|
|
html += '</tbody></table>';
|
|
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 = '<nav class="blog-pagination">';
|
|
if (page > 1) {
|
|
html += '<a class="blog-pn prev" href="' + pageUrl(aliasBase, page - 1) + '"><span class="blog-pn-label">Newer</span></a>';
|
|
} else {
|
|
html += '<span class="blog-pn empty"></span>';
|
|
}
|
|
html += '<span class="blog-page-num">Page ' + page + ' of ' + totalPages + '</span>';
|
|
if (page < totalPages) {
|
|
html += '<a class="blog-pn next" href="' + pageUrl(aliasBase, page + 1) + '"><span class="blog-pn-label">Older</span></a>';
|
|
} else {
|
|
html += '<span class="blog-pn empty"></span>';
|
|
}
|
|
html += '</nav>';
|
|
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) || '<p>No articles.</p>');
|
|
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)) || '<p>No articles published yet.</p>')
|
|
+ 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 ` <item>\n <title>${escapeHtml(a.title)}</title>\n <link>${link}</link>\n <guid isPermaLink="false">${escapeHtml(a.noteId)}</guid>\n <pubDate>${pubDate}</pubDate>\n <description>${escapeHtml(summary)}</description>\n </item>`;
|
|
}).join("\n");
|
|
|
|
const now = new Date().toUTCString();
|
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n <channel>\n <title>${escapeHtml(SITE_TITLE)}</title>\n <link>${BASE_URL}</link>\n <atom:link href="${SHARE_BASE}/feed" rel="self" type="application/rss+xml"/>\n <description>${escapeHtml(SITE_DESCRIPTION)}</description>\n <language>en</language>\n <lastBuildDate>${now}</lastBuildDate>\n${items}\n </channel>\n</rss>`;
|
|
}
|
|
|
|
function slugifyTag(t) {
|
|
return t.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 = `<script>window.blogStates = ${JSON.stringify(map)};</script>`;
|
|
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 => ` <url><loc>${escapeHtml(u)}</loc></url>`).join("\n");
|
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>`;
|
|
if (sitemapNote.getContent() !== xml) {
|
|
sitemapNote.setContent(xml);
|
|
sitemapNote.save();
|
|
api.log("blog_generator: sitemap updated (" + urls.length + " urls)");
|
|
}
|
|
}
|
|
|
|
function ensureShareDescription() {
|
|
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") {
|
|
const desc = child.getLabelValue("summary") || SITE_DESCRIPTION;
|
|
if (child.getLabelValue("shareDescription") !== desc) {
|
|
child.setLabel("shareDescription", desc);
|
|
child.save();
|
|
api.log("blog_generator: set shareDescription on " + child.title);
|
|
}
|
|
}
|
|
walk(child, depth + 1);
|
|
}
|
|
})(blogRoot, 0);
|
|
}
|
|
|
|
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") || "",
|
|
date: (a.getLabelValue("date") || a.dateCreated).slice(0, 10)
|
|
}));
|
|
const html = `<script>window.blogData = ${JSON.stringify({ articles: list })};</script>`;
|
|
if (dataNote.getContent() !== html) {
|
|
dataNote.setContent(html);
|
|
dataNote.save();
|
|
api.log("blog_generator: blog_data updated (" + list.length + " articles)");
|
|
}
|
|
}
|
|
|
|
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 = `<h1>Tag: ${escapeHtml(tagName)}</h1>\n` + (renderArticles(tagged) || '<p>No articles.</p>');
|
|
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 = '<div class="blog-subcategories">';
|
|
for (const t of tagChildren) {
|
|
const url = SHARE_BASE + "/" + encodeURIComponent(t.getLabelValue("shareAlias") || t.noteId);
|
|
idxHtml += `<a class="tag" href="${escapeHtml(url)}">#${escapeHtml(t.title)}</a>`;
|
|
}
|
|
idxHtml += '</div>';
|
|
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 = "<!-- blog:articles -->";
|
|
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("<article") && !current.trim().startsWith(marker)) {
|
|
userText = current;
|
|
}
|
|
|
|
const children = catNote.getChildNotes();
|
|
const subs = children.filter(c => 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 += '<div class="blog-subcategories">';
|
|
for (const s of subs) {
|
|
const url = SHARE_BASE + "/" + encodeURIComponent(s.getLabelValue("shareAlias") || s.noteId);
|
|
const icon = s.getLabelValue("iconClass");
|
|
html += `<a class="blog-subcategory" href="${escapeHtml(url)}">`;
|
|
if (icon) html += `<i class="${escapeHtml(icon)}"></i>`;
|
|
html += `<span>${escapeHtml(s.title)}</span></a>`;
|
|
}
|
|
html += "</div>\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)) || '<p>No articles yet.</p>';
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
buildStateMap();
|
|
buildSitemap();
|
|
ensureShareDescription();
|
|
buildBlogData();
|
|
removeEmptyCategories();
|
|
} |