diff --git a/README.md b/README.md index 323e733..4aba072 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Trilium Share – Gruvbox Blog System [](LICENSE) +[](https://richard.familie-zink.org) [](https://github.com/TriliumNext/Trilium) [](https://github.com/morhetz/gruvbox) [](https://opencode.ai) @@ -25,6 +26,8 @@ no external dependencies (except `highlight.js`, vendored in `lib/`). - Gruvbox light/dark theme for the share, with a theme toggle in the header. - Blog index, category pages, sub-categories, tags and tag overview. +- **Pagination**: 10 posts per page on the index and category pages, with prev/next + navigation between the pages; article prev/next links stay within the article's category. - Article metadata: date, category, summary, tags, prev/next navigation, reading time. - **Digital Garden**: `state` labels (Seed → Evergreen, Draft, Review) rendered as badges and colour-coded growth tables. @@ -230,11 +233,15 @@ server { ## How the generator works - Walks the blog root and collects notes with `publish=true`. -- Writes the **index** (article cards), **category pages** (sub-category chips + articles, - respecting a `` marker so your hand-written intro text is kept), - **tag pages** and the **tags overview**. +- Writes the **index** (first 10 posts) plus page notes (`blog-2`, `blog-3`, …) with + prev/next navigation between the pages. +- Writes **category pages** (sub-category chips + articles, respecting a + `` marker so your hand-written intro text is kept), paginated the + same way; page notes live in the `.blog` system note and are tagged with `blogPagedFor`. +- Writes **tag pages** and the **tags overview**. - Regenerates `feed.xml` (RSS), `sitemap.xml`, `state_map` (garden states) and `blog_data` - (search index used by the header search + prev/next). + (search index with title, summary, tags and category – used by the header search and the + category-scoped prev/next). - Sets `shareDescription` on every published note from its `summary`. ## Customisation diff --git a/blog_generator.js b/blog_generator.js index dcde490..8d94d52 100644 --- a/blog_generator.js +++ b/blog_generator.js @@ -6,6 +6,8 @@ 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 { @@ -22,6 +24,8 @@ function isInSubtree(note, targetId) { 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"); @@ -160,8 +164,77 @@ function run() { return html; } - function buildIndexHtml() { - return renderArticles(articles) || '
No articles published yet.
'; + 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() { @@ -286,7 +359,8 @@ function run() { title: a.title, url: SHARE_BASE + "/" + encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId), summary: a.getLabelValue("summary") || "", - tags: tagList(a) + tags: tagList(a), + category: a.getLabelValue("category") || "" })); const html = ``; if (dataNote.getContent() !== html) { @@ -330,8 +404,8 @@ function run() { note.setContent(html); note.save(); } - if (!note.hasRelation("shareHtml", "__HIDE_HOME_TITLE_ID__")) { - note.setRelation("shareHtml", "__HIDE_HOME_TITLE_ID__"); + if (!note.hasRelation("shareHtml", HIDE_HOME_TITLE_NOTE_ID)) { + note.setRelation("shareHtml", HIDE_HOME_TITLE_NOTE_ID); note.save(); } const alias = "tag-" + slugifyTag(tagName); @@ -440,17 +514,21 @@ function run() { } 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); + html += buildGardenTable(catNote, articlesList.slice(0, POSTS_PER_PAGE)); } else { - html += renderArticles(articlesList) || 'No articles yet.
'; + 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); + api.log("blog_generator: category page updated: " + catNote.title + " (" + pageCount + " pages)"); } + syncPageNotes("cat:" + catNote.title, aliasBase, pageCount, articlesList, catNote.hasLabel("blogGarden"), false); } } @@ -475,14 +553,7 @@ function run() { } } - const indexHtml = buildIndexHtml(); - if (blogRoot.getContent() !== indexHtml) { - blogRoot.setContent(indexHtml); - blogRoot.save(); - api.log("blog_generator: index updated (" + articles.length + " articles)"); - } else { - api.log("blog_generator: index unchanged"); - } + buildIndex(); const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID); const feedNote = systemNote.getChildNotes().find(n => n.title === "feed.xml"); diff --git a/scripts/prev_next.js b/scripts/prev_next.js index 8de7570..5143c96 100644 --- a/scripts/prev_next.js +++ b/scripts/prev_next.js @@ -3,10 +3,12 @@ var data = window.blogData; if (!data || !data.articles || !data.articles.length) return; var id = document.body.getAttribute("data-note-id"); - var list = data.articles; - var idx = -1; - for (var i = 0; i < list.length; i++) { if (list[i].id === id) { idx = i; break; } } - if (idx < 0) return; + var all = data.articles; + var me = null; + for (var i = 0; i < all.length; i++) { if (all[i].id === id) { me = all[i]; break; } } + if (!me) return; + var list = me.category ? all.filter(function (a) { return a.category === me.category; }) : all; + var idx = list.indexOf(me); var builtin = document.querySelector("#content-footer .navigation"); if (builtin) builtin.style.display = "none"; var content = document.getElementById("content"); diff --git a/theme/blog_share_theme.css b/theme/blog_share_theme.css index e8139a9..e1097dc 100644 --- a/theme/blog_share_theme.css +++ b/theme/blog_share_theme.css @@ -197,6 +197,27 @@ body { margin: -0.9em 0 1.25em; } +/* ---------- Blog pagination ---------- */ +.blog-pagination { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin: 1.5em 0 0; + padding-top: 1em; + border-top: 1px solid var(--background-highlight); +} + +.blog-pagination .blog-page-num { + font-size: 0.85em; + color: var(--text-menu); +} + +/* ---------- Share footer navigation (built-in prev/next) ausblenden ---------- */ +#content-footer .navigation { + display: none; +} + /* ---------- Prev/Next ---------- */ .blog-prevnext { display: flex; @@ -713,6 +734,10 @@ html.theme-light { display: none; } +#index { + display: none; +} + /* ---------- Code blocks - Gruvbox Dark (always, also in light mode) ---------- */ .ck-content pre, #content pre {