Files
trilium_share_gruvbox/AGENTS.md
T

12 KiB
Raw Blame History

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=true under the blog root (skips blogGarden subtrees).
  • Index: page 1 = blog root, further pages as notes under .blog named by blogPagedFor + blogPagedNumber labels, 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; blogGarden marks a growth-state garden. Category pages are paginated the same way.
  • Sorting: index, categories, tags and blog_data are all sorted by publication date descending (date label, fallback dateCreated). 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): reads blog_data.articles[].date (ISO, used for sorting via data-pubdate) and displays dateText (Fri 2026-08-28), re-sorts each category's menu entries by date descending and prefixes the label with Fri 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 as Fri 2026-08-28 (English weekday + ISO date); blog_data.date stays ISO for sorting, blog_data.dateText is added for display. JSON-LD keeps ISO dates.
  • Thumbnails (thumbSrc()): prefer a child note of the article titled thumb.<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-sets shareHiddenFromTree on 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) and blog_data (search index: id/title/url/summary/tags/category). Sets shareDescription from summary.

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) and shareHiddenFromTree.
  • feed.xmlshareAlias=feed + shareRaw; sitemap.xmlshareAlias=sitemap.xml + shareRaw.
  • Generator + publish manager are application/javascript;env=backend; generator has run=daily.
  • Digital Garden category: blogGarden, label:state definition (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): 8px for code blocks/code notes, inline code, embedded images, diagrams (mermaid/mindmap/excalidraw) and thumbnails; 10px for blog cards; 6px for chips/tags/buttons/inputs; 4px for 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 figcaption is a Gruvbox chip (secondary bg, highlight border, 8px radius, muted italic text, centered).
  • 3D models (scripts/3d_model_viewer.js): renders .step/.stp/.stl as file notes (title ends in the extension), inline attachment links, or links to published model file notes from text notes (resolved via window.blogData). The viewer is full-bleed (~100vw, height: min(70vh, 900px), ResizeObserver), with a loading progress bar and a caption that shows name · 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) with linearDeflection: 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 Uint16 indices (chunkGeometry) or mobile GPUs without OES_element_index_uint render nothing; (2) geometry.setIndex(rawTypedArray) in three r128 leaves geometry.index WITHOUT .count → always wrap: setIndex(new THREE.BufferAttribute(index, 1)); (3) tighten camera near/far from the model size after fit() (near=d·0.05, far=d·10) for 16-bit depth buffers.
  • 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 _thumb child-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, note share template, application/x-ejs, wired via ~shareTemplate inheritable on the blog root, with tree_item/prev_next/toc_item partials as children) adds per-page canonical, og:url/og:image/og:type, Twitter cards, JSON-LD (WebSite/BlogPosting/CollectionPage) and index,follow. The generator's ensureSeoMeta() sets shareDescription + shareOpenGraphURL on every published note and category. robots.txt (shareAlias=robots.txt, shareRaw) points at the sitemap; ~shareOpenGraphImage on 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, http etc. 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 (see THIRD_PARTY_LICENSES.md); three.js is MIT. Both are hosted locally under lib/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 _thumb child-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)

  1. Edit the corresponding Trilium note (live) and verify on richard.familie-zink.org.
  2. Mirror the anonymized file(s) into this repo: replace richard.familie-zink.orghttps://YOUR-INSTANCE.example.com, Richard's BlogYour Blog, by fam.zinkby the author, and every note-ID token → __…_ID__ (mappings are the tokens already used in blog_generator.js / scripts/*.js). New component notes (e.g. the app widget) get a __…_ID__ token in the repo reference file.
  3. Check licenses: THIRD_PARTY_LICENSES.md must list every vendored third-party file; original code stays GPL-3.0. Update it if new libraries were added.
  4. Update docs if behaviour changed: docs/embed-3d-model.md (3D viewer), README.md (features/layout) and this file.
  5. Commit (English message) and push to main with the neutral identity trilium-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.orgfamilie--zink.org), otherwise shields renders a "404 badge not found" error badge.