Add pagination (10 posts/page), category-scoped prev/next, hide share footer nav; demo link in README
This commit is contained in:
@@ -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 `<!-- 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
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user