# 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.<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".
- **Inline images in article content**: pasted image attachments render as
  `<img src="api/attachments/<attachmentId>/image/<filename>">`. The share only serves
  attachments through that `/image/` path segment – without it the request 404s. Use image
  **attachments** for inline images (no child note, no menu clutter) and keep the `thumb.<ext>`
  child note as the only image child.
- 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 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.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.