169 lines
12 KiB
Markdown
169 lines
12 KiB
Markdown
# AGENTS.md – Trilium Share Gruvbox Blog System
|
||
|
||
Public repository for the **Gruvbox public-share theme + blog system** that runs on a
|
||
[TriliumNext](https://github.com/TriliumNext/Trilium) 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.*`
|
||
(e.g. `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. **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.xml` → `shareAlias=feed` + `shareRaw`; `sitemap.xml` → `shareAlias=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, a
|
||
caption that shows `name · size · triangle count`, and a **debug line** under the box that
|
||
logs the step (`loading`/`parsing`/`done`/`error`) for on-device diagnosis. 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.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 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.org` → `familie--zink.org`), otherwise shields renders a "404 badge not
|
||
found" error badge. |