Add pagination (10 posts/page), category-scoped prev/next, hide share footer nav; demo link in README

This commit is contained in:
trilium-share-gruvbox
2026-08-28 13:55:54 +02:00
parent 74cf0a2b3b
commit 1664e55e4e
4 changed files with 129 additions and 24 deletions
+11 -4
View File
@@ -1,6 +1,7 @@
# Trilium Share Gruvbox Blog System
[![License](https://img.shields.io/badge/License-GPL--3.0-fb4934)](LICENSE)
[![Live demo](https://img.shields.io/badge/Live%20demo-richard.familie-zink.org-8ec07c)](https://richard.familie-zink.org)
[![TriliumNext](https://img.shields.io/badge/TriliumNext-Public%20Share-83a598)](https://github.com/TriliumNext/Trilium)
[![Theme](https://img.shields.io/badge/Theme-Gruvbox-98971a)](https://github.com/morhetz/gruvbox)
[![Built with](https://img.shields.io/badge/Built%20with-Opencode-fe8019)](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 `<!-- blog:articles -->` 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
`<!-- blog:articles -->` 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
+87 -16
View File
@@ -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) || '<p>No articles published yet.</p>';
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() {
@@ -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 = `<script>window.blogData = ${JSON.stringify({ articles: list })};</script>`;
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 += "</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);
html += buildGardenTable(catNote, articlesList.slice(0, POSTS_PER_PAGE));
} else {
html += renderArticles(articlesList) || '<p>No articles yet.</p>';
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);
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");
+6 -4
View File
@@ -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");
+25
View File
@@ -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 {