12 KiB
AGENTS.md – Trilium Share Gruvbox Blog System
Public repository for the Gruvbox public-share theme + blog system that runs on a
TriliumNext instance (notes.familie-zink.org,
live blog: richard.familie-zink.org). This file captures the requirements and design
decisions so future work stays consistent.
What this is
A blog built on Trilium's Public Share: a backend blog generator builds index/category/tag
pages, an RSS feed, a sitemap, a search index and a garden-state map from notes that carry a
publish=true label. Frontend shareHtml scripts add search, prev/next, reading time,
widget rendering (Mermaid/Excalidraw/MindMap) and a themed footer. All styling lives in one
CSS file (Gruvbox, light + dark, theme toggle without reload).
Repository layout
| Path | Note in Trilium | Purpose |
|---|---|---|
blog_generator.js |
blog_generator (backend script) |
builds index, categories, tags, feed, sitemap, blog_data, state_map |
blog_publish_manager.js |
blog_publish_manager (backend script) |
mirrors notes between Unpublished and the blog on publish label changes |
theme/blog_share_theme.css |
blog_share_theme.css (shareCss) |
the entire Gruvbox theme |
theme/favicon.svg, theme/cookie.woff2 |
favicon, Cookie |
site icon + BMC font |
lib/highlight.js, lib/hljs_line_numbers.js |
highlight.js, hljs line numbers plugin |
code highlighting |
lib/3d/* |
three.js, OrbitControls, STLLoader, occt-import-js (WASM) | local libraries for the 3D model viewer |
scripts/*.js |
one code note each (.blog), attached via ~shareHtml |
frontend snippets (verbatim content incl. <script>) |
templates/blog_article.html |
Blog Article |
article template with promoted labels |
examples/* |
ignis8 icons/script | sample category logo |
generated/* |
feed.xml, sitemap.xml, blog_data, state_map |
regenerated by the generator (reference only) |
assets/palette.png |
– | Gruvbox palette (from morhetz/gruvbox, MIT) |
How the generator works (keep this intact)
- Collects notes with
publish=trueunder the blog root (skipsblogGardensubtrees). - Index: page 1 = blog root, further pages as notes under
.blognamed byblogPagedFor+blogPagedNumberlabels,shareAlias = <alias>-<n>(blog-2, …). - Categories: any top-level note under the blog root that is not a system note
(
Tags,.blog,Impressum,About Me,Search) is a category;blogGardenmarks a growth-state garden. Category pages are paginated the same way. - Sorting: index, categories, tags and
blog_dataare all sorted by publication date descending (datelabel, fallbackdateCreated). Never change this. - Pagination:
POSTS_PER_PAGE = 10. Prev/next on index/category pages link only between that section's pages (server-side.blog-pagination), not globally. - Article prev/next (
scripts/prev_next.js) is scoped to the article's category (blog_data.articles[].category); falls back to the global list when uncategorized. - Menu sorting (
scripts/menu_date_sort.js,shareHtmlLocation=body:end): readsblog_data.articles[].date(ISO, used for sorting viadata-pubdate) and displaysdateText(Fri 2026-08-28), re-sorts each category's menu entries by date descending and prefixes the label withFri 2026-08-28 <shortened title>(icons preserved). Non-article entries are left in place. - Dates: all visible dates (post cards, garden table) are rendered by the generator's
dateText()helper asFri 2026-08-28(English weekday + ISO date);blog_data.datestays ISO for sorting,blog_data.dateTextis added for display. JSON-LD keeps ISO dates. - Thumbnails (
thumbSrc()): prefer a child note of the article titledthumb.<ext>or*_thumb.<ext>(e.g.thumb.png,ngc281_thumb.jpg) →api/notes/<id>/download; otherwise use the first<img>from the note content. Layout: image left, summary right (.blog-post-inner),max-height:256px, lazy-loaded. The generator auto-setsshareHiddenFromTreeon matching thumb child notes so they never appear in the blog menu. There is no server-side resizing – see "Limitations". - Regenerates
feed.xml,sitemap.xml,state_map(garden states) andblog_data(search index: id/title/url/summary/tags/category). SetsshareDescriptionfromsummary.
Publishing workflow (blog_publish_manager)
New notes are created under Unpublished/<Category> (the Unpublished root mirrors the blog
categories). Setting publish=true makes the manager clone the note into the matching blog
category (ensureNoteIsPresentInParent) and run the generator; setting it back to false
moves the clone back out of the blog. So: create the note under Unpublished/<Category>,
set the other labels, and set publish=true last.
Wiring / attributes (the ground truth)
- All inheritable relations live on the blog root:
~shareCss → blog_share_theme.css,~shareFavicon → favicon,~shareHtml → <each script>,~runOnNoteContentChange/… → blog_generator,~runOnAttributeChange/Creation → blog_publish_manager. - Every script note carries
shareHtmlLocation(head:end,body:end,content:start,body:start) andshareHiddenFromTree. feed.xml→shareAlias=feed+shareRaw;sitemap.xml→shareAlias=sitemap.xml+shareRaw.- Generator + publish manager are
application/javascript;env=backend; generator hasrun=daily. - Digital Garden category:
blogGarden,label:statedefinition (promoted,alias=Growth Condition,single,select,options=Seed;Seedling;Sprout;Sprouting;Bud;Evergreen;Draft;Review, inheritable),~shareHtml → garden h1 green. - On import the note IDs differ → replace the
__X_ID__tokens in scripts/generator (see README).
Theme / design rules (keep consistent)
- Gruvbox palette only: bg
#282828/#1d2021, fg#ebdbb2, red#fb4934, green#b8bb26, yellow#fabd2f, blue#83a598, purple#d3869b, aqua#8ec07c, orange#fe8019; light-mode fades from Gruvbox Light. - Radius system (user decision, 2026-08-28):
8pxfor code blocks/code notes, inline code, embedded images, diagrams (mermaid/mindmap/excalidraw) and thumbnails;10pxfor blog cards;6pxfor chips/tags/buttons/inputs;4pxfor tiny menu icons. Embedded images and inline code use the same 8px radius. - Inline code:
#content code { border-radius:8px; padding:.1em .35em }(dark bg in both themes). - Embedded images:
#content img:not(.blog-thumb) { border-radius:8px }. - Figure captions:
#content figcaptionis a Gruvbox chip (secondary bg, highlight border, 8px radius, muted italic text, centered). - 3D models (
scripts/3d_model_viewer.js): renders.step/.stp/.stlas file notes (title ends in the extension), inline attachment links, or links to published model file notes from text notes (resolved viawindow.blogData). The viewer is full-bleed (~100vw,height: min(70vh, 900px), ResizeObserver), with a loading progress bar and a caption that showsname · size · triangle count. STEP is parsed by occt-wasm (OpenCASCADE V8, MIT/Apache-2.0) in a module worker (new Worker(url, {type:"module"}), non-blocking) withlinearDeflection: 0.4, angularDeflection: 0.2. Libraries hosted as local notes via__THREE_ID__/__THREE_ORBIT_ID__/__THREE_STL_ID__/__OCCT_JS_ID__/__OCCT_WASM_ID__. User guide:docs/embed-3d-model.md.- Mobile gotchas (both were real bugs): (1) mesh >65535 vertices must be split into
chunks with
Uint16indices (chunkGeometry) or mobile GPUs withoutOES_element_index_uintrender nothing; (2)geometry.setIndex(rawTypedArray)in three r128 leavesgeometry.indexWITHOUT.count→ always wrap:setIndex(new THREE.BufferAttribute(index, 1)); (3) tighten cameranear/farfrom the model size afterfit()(near=d·0.05, far=d·10) for 16-bit depth buffers.
- Mobile gotchas (both were real bugs): (1) mesh >65535 vertices must be split into
chunks with
- 3D viewer in the Trilium app (independent of the blog):
app/3d-model-viewer-app.jsx(widget,#widget=3dModelViewer,parent: note-detail-pane) +app/3d-worker.js(STEP module worker that imports the occt-wasm bundle from a code note, so it works under the app's CSP). The widget renders the viewer for file notes whose title ends in.step/.stp/.stl. - Images in the share are served full-size only (no resize endpoint) → keep the
_thumbchild-note convention for small thumbnails. - Built-in share footer navigation and book children list are hidden
(
#content-footer .navigation,#index). - SEO: a custom share template (
templates/share/page.ejs, noteshare template,application/x-ejs, wired via~shareTemplateinheritable on the blog root, withtree_item/prev_next/toc_itempartials as children) adds per-page canonical,og:url/og:image/og:type, Twitter cards, JSON-LD (WebSite/BlogPosting/CollectionPage) andindex,follow. The generator'sensureSeoMeta()setsshareDescription+shareOpenGraphURLon every published note and category.robots.txt(shareAlias=robots.txt,shareRaw) points at the sitemap;~shareOpenGraphImageon the blog root sets the default OG image.
Limitations / gotchas
- Backend script module whitelist is hardcoded (
dayjs,marked,turndown,cheerio,axios,xml2js,escape-html,sanitize-html,lodash);sharp,fs,child_process,httpetc. are blocked. Therefore image resizing inside a backend script is impossible without patching Trilium source. Do not attempt to add sharp-based code to the generator. occt-import-js/ OpenCASCADE (used by the 3D viewer) is LGPL-2.1 (seeTHIRD_PARTY_LICENSES.md); three.js is MIT. Both are hosted locally underlib/3d/.- Triggering the generator via an attribute change on the blog root itself is ignored
by the self-trigger guard (
isGeneratedNoteId(BLOG_ROOT_ID) → true). To force a full regeneration, change an attribute/content on a published article instead. - The share never resizes images; the
_thumbchild-note convention is the intended path for bandwidth-friendly thumbnails. - Comments/commits in the repo are English; communication with the owner is German.
Releasing changes (do this after every change set)
- Edit the corresponding Trilium note (live) and verify on
richard.familie-zink.org. - Mirror the anonymized file(s) into this repo: replace
richard.familie-zink.org→https://YOUR-INSTANCE.example.com,Richard's Blog→Your Blog,by fam.zink→by the author, and every note-ID token →__…_ID__(mappings are the tokens already used inblog_generator.js/scripts/*.js). New component notes (e.g. the app widget) get a__…_ID__token in the repo reference file. - Check licenses:
THIRD_PARTY_LICENSES.mdmust list every vendored third-party file; original code stays GPL-3.0. Update it if new libraries were added. - Update docs if behaviour changed:
docs/embed-3d-model.md(3D viewer),README.md(features/layout) and this file. - Commit (English message) and push to
mainwith the neutral identitytrilium-share-gruvbox <trilium-share-gruvbox@users.noreply.git.familie-zink.org>.
Never commit real note IDs, the ETAPI token, or personal data (name/domain are allowed in the README demo badge only).
Repo gotchas
- shields.io badges: a literal
-in label/message must be escaped as--in the badge URL (e.g.familie-zink.org→familie--zink.org), otherwise shields renders a "404 badge not found" error badge.