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
|
# Trilium Share – Gruvbox Blog System
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
[](https://richard.familie-zink.org)
|
||||||
[](https://github.com/TriliumNext/Trilium)
|
[](https://github.com/TriliumNext/Trilium)
|
||||||
[](https://github.com/morhetz/gruvbox)
|
[](https://github.com/morhetz/gruvbox)
|
||||||
[](https://opencode.ai)
|
[](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.
|
- Gruvbox light/dark theme for the share, with a theme toggle in the header.
|
||||||
- Blog index, category pages, sub-categories, tags and tag overview.
|
- 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.
|
- Article metadata: date, category, summary, tags, prev/next navigation, reading time.
|
||||||
- **Digital Garden**: `state` labels (Seed → Evergreen, Draft, Review) rendered as badges and
|
- **Digital Garden**: `state` labels (Seed → Evergreen, Draft, Review) rendered as badges and
|
||||||
colour-coded growth tables.
|
colour-coded growth tables.
|
||||||
@@ -230,11 +233,15 @@ server {
|
|||||||
## How the generator works
|
## How the generator works
|
||||||
|
|
||||||
- Walks the blog root and collects notes with `publish=true`.
|
- Walks the blog root and collects notes with `publish=true`.
|
||||||
- Writes the **index** (article cards), **category pages** (sub-category chips + articles,
|
- Writes the **index** (first 10 posts) plus page notes (`blog-2`, `blog-3`, …) with
|
||||||
respecting a `<!-- blog:articles -->` marker so your hand-written intro text is kept),
|
prev/next navigation between the pages.
|
||||||
**tag pages** and the **tags overview**.
|
- 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`
|
- 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`.
|
- Sets `shareDescription` on every published note from its `summary`.
|
||||||
|
|
||||||
## Customisation
|
## Customisation
|
||||||
|
|||||||
+87
-16
@@ -6,6 +6,8 @@ const BASE_URL = SHARE_BASE + "/" + BLOG_ALIAS;
|
|||||||
const SITE_TITLE = "Your Blog";
|
const SITE_TITLE = "Your Blog";
|
||||||
const SITE_DESCRIPTION = "Project documentation & technical blog by the author";
|
const SITE_DESCRIPTION = "Project documentation & technical blog by the author";
|
||||||
const TAGS_SECTION_TITLE = "Tags";
|
const TAGS_SECTION_TITLE = "Tags";
|
||||||
|
const HIDE_HOME_TITLE_NOTE_ID = "__HIDE_HOME_TITLE_ID__";
|
||||||
|
const POSTS_PER_PAGE = 10;
|
||||||
|
|
||||||
function isInSubtree(note, targetId) {
|
function isInSubtree(note, targetId) {
|
||||||
try {
|
try {
|
||||||
@@ -22,6 +24,8 @@ function isInSubtree(note, targetId) {
|
|||||||
function isGeneratedNoteId(id) {
|
function isGeneratedNoteId(id) {
|
||||||
try {
|
try {
|
||||||
if (id === BLOG_ROOT_NOTE_ID) return true;
|
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);
|
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
|
||||||
if (systemNote) {
|
if (systemNote) {
|
||||||
const feedNote = systemNote.getChildNotes().find(n => n.title === "feed.xml");
|
const feedNote = systemNote.getChildNotes().find(n => n.title === "feed.xml");
|
||||||
@@ -160,8 +164,77 @@ function run() {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildIndexHtml() {
|
function pageUrl(aliasBase, p) {
|
||||||
return renderArticles(articles) || '<p>No articles published yet.</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() {
|
function buildFeedXml() {
|
||||||
@@ -286,7 +359,8 @@ function run() {
|
|||||||
title: a.title,
|
title: a.title,
|
||||||
url: SHARE_BASE + "/" + encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId),
|
url: SHARE_BASE + "/" + encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId),
|
||||||
summary: a.getLabelValue("summary") || "",
|
summary: a.getLabelValue("summary") || "",
|
||||||
tags: tagList(a)
|
tags: tagList(a),
|
||||||
|
category: a.getLabelValue("category") || ""
|
||||||
}));
|
}));
|
||||||
const html = `<script>window.blogData = ${JSON.stringify({ articles: list })};</script>`;
|
const html = `<script>window.blogData = ${JSON.stringify({ articles: list })};</script>`;
|
||||||
if (dataNote.getContent() !== html) {
|
if (dataNote.getContent() !== html) {
|
||||||
@@ -330,8 +404,8 @@ function run() {
|
|||||||
note.setContent(html);
|
note.setContent(html);
|
||||||
note.save();
|
note.save();
|
||||||
}
|
}
|
||||||
if (!note.hasRelation("shareHtml", "__HIDE_HOME_TITLE_ID__")) {
|
if (!note.hasRelation("shareHtml", HIDE_HOME_TITLE_NOTE_ID)) {
|
||||||
note.setRelation("shareHtml", "__HIDE_HOME_TITLE_ID__");
|
note.setRelation("shareHtml", HIDE_HOME_TITLE_NOTE_ID);
|
||||||
note.save();
|
note.save();
|
||||||
}
|
}
|
||||||
const alias = "tag-" + slugifyTag(tagName);
|
const alias = "tag-" + slugifyTag(tagName);
|
||||||
@@ -440,17 +514,21 @@ function run() {
|
|||||||
}
|
}
|
||||||
html += "</div>\n";
|
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")) {
|
if (catNote.hasLabel("blogGarden")) {
|
||||||
html += buildGardenTable(catNote, articlesList);
|
html += buildGardenTable(catNote, articlesList.slice(0, POSTS_PER_PAGE));
|
||||||
} else {
|
} 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) {
|
if (catNote.getContent() !== html) {
|
||||||
catNote.setContent(html);
|
catNote.setContent(html);
|
||||||
catNote.save();
|
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();
|
buildIndex();
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
|
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
|
||||||
const feedNote = systemNote.getChildNotes().find(n => n.title === "feed.xml");
|
const feedNote = systemNote.getChildNotes().find(n => n.title === "feed.xml");
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
var data = window.blogData;
|
var data = window.blogData;
|
||||||
if (!data || !data.articles || !data.articles.length) return;
|
if (!data || !data.articles || !data.articles.length) return;
|
||||||
var id = document.body.getAttribute("data-note-id");
|
var id = document.body.getAttribute("data-note-id");
|
||||||
var list = data.articles;
|
var all = data.articles;
|
||||||
var idx = -1;
|
var me = null;
|
||||||
for (var i = 0; i < list.length; i++) { if (list[i].id === id) { idx = i; break; } }
|
for (var i = 0; i < all.length; i++) { if (all[i].id === id) { me = all[i]; break; } }
|
||||||
if (idx < 0) return;
|
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");
|
var builtin = document.querySelector("#content-footer .navigation");
|
||||||
if (builtin) builtin.style.display = "none";
|
if (builtin) builtin.style.display = "none";
|
||||||
var content = document.getElementById("content");
|
var content = document.getElementById("content");
|
||||||
|
|||||||
@@ -197,6 +197,27 @@ body {
|
|||||||
margin: -0.9em 0 1.25em;
|
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 ---------- */
|
/* ---------- Prev/Next ---------- */
|
||||||
.blog-prevnext {
|
.blog-prevnext {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -713,6 +734,10 @@ html.theme-light {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#index {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Code blocks - Gruvbox Dark (always, also in light mode) ---------- */
|
/* ---------- Code blocks - Gruvbox Dark (always, also in light mode) ---------- */
|
||||||
.ck-content pre,
|
.ck-content pre,
|
||||||
#content pre {
|
#content pre {
|
||||||
|
|||||||
Reference in New Issue
Block a user