# Trilium Share – Coffee Blog System

[![License](https://img.shields.io/badge/License-GPL--3.0-fb4934)](LICENSE)
[![Live demo](https://img.shields.io/badge/Live%20demo-richard.familie--zink.org-8ec07c)](https://richard.familie-zink.org)
[![TriliumNext](https://img.shields.io/badge/TriliumNext-Public%20Share-83a598)](https://github.com/TriliumNext/Trilium)
[![Theme](https://img.shields.io/badge/Theme-Coffee-e0a15c)](https://github.com/megabyte0x/omarchy-coffee-theme)
[![Built with](https://img.shields.io/badge/Built%20with-Opencode-fe8019)](https://opencode.ai)
[![Model](https://img.shields.io/badge/Model-Deepseek%20V4%20Flash-d3869b)](https://www.deepseek.com/)

> **Note:** This theme and blog system was created with the help of
> [**Opencode**](https://opencode.ai) and **Deepseek V4 Flash**.

A complete **public-share blog system + Coffee theme** for [TriliumNext](https://github.com/TriliumNext/Trilium).
It turns a normal Trilium note tree into a themed, searchable blog with categories, an RSS feed,
a sitemap, a digital garden (growth states) and client-side rendering of Mermaid, Excalidraw and
mind-map notes.

Everything is driven by Trilium's built-in **Public Share** feature plus a few frontend/backend scripts –
no external dependencies (except `highlight.js`, vendored in `lib/`).

## Themes

The share ships with **three themes**, each with a dark and a light variant (toggled via the header
switch / `html.theme-light` + `html.theme-dark`). They live in `theme/` and are set on the blog by
pointing the `shareCss` relation at the matching note – see
[`docs/switch-blog-theme.md`](docs/switch-blog-theme.md) for the step-by-step guide.

| Theme | File | Palette (dark / light) | Source |
|---|---|---|---|
| **Coffee** (active by default) | `theme/blog_share_theme_coffee.css` | espresso `#1d150f` / latte-cream `#f0e2d0`, caramel `#e0a15c` · cream `#E8E3C3` / ink `#2D1A1D`, berry `#8e2d53` | [megabyte0x/omarchy-coffee-theme](https://github.com/megabyte0x/omarchy-coffee-theme) (MIT) |
| **Harbor** | `theme/blog_share_theme_harbor.css` | near-black `#1b1b1b` / cream `#efebdc`, coral `#e75a50` · paper `#dfe4c4` / ink `#1c2d28`, nord-blue `#5e81ac` | [HANCORE-linux/omarchy-harbor-theme](https://github.com/HANCORE-linux/omarchy-harbor-theme) (MIT) |
| **Gruvbox** | `theme/blog_share_theme_gruvbox.css` | bg `#282828` / fg `#ebdbb2`, accents red/green/yellow/blue | [morhetz/gruvbox](https://github.com/morhetz/gruvbox) (MIT) |

![Coffee palette](assets/palette-coffee.png)
![Harbor palette](assets/palette-harbor.png)
![Gruvbox palette](assets/palette-gruvbox.png)

## Features

- Coffee dark / Coffee Latte light theme for the share, with a theme toggle in the header
  (Gruvbox and Harbor are available as well – see the [Themes](#themes) section).
- Blog index, category pages, sub-categories, tags and tag overview.
- **Menu sorting**: articles in the left menu are sorted by publication date (newest first)
  and labelled `Fri 2026-08-28 <shortened title>` automatically. All date displays
  (post cards, garden table, menu) include the weekday (`dateText` from the generator).
- **Article thumbnails**: each post card shows the first image of the article on the left
  (bounded by the teaser height), summary on the right, lazy-loaded. If an article has a child
  note named `thumb.<ext>` **or** `*_thumb.<ext>` (e.g. `thumb.png`, `ngc281_thumb.jpg`), that
  image is used instead – ideal for serving small pre-made thumbnails. Thumb child notes are
  auto-hidden from the blog menu (`shareHiddenFromTree`).
- **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.
- **Digital Garden**: `state` labels (Seed → Evergreen, Draft, Review) rendered as badges and
  colour-coded growth tables.
- **Search** (client-side over a generated index), **RSS feed** and **sitemap.xml**.
- **SEO**: per-page canonical + Open Graph (`og:url`, `og:image`, article/website type),
  Twitter cards, JSON-LD structured data (WebSite / BlogPosting / CollectionPage),
  `robots.txt` and `index,follow` meta. Provided by a custom share template
  (`templates/share/page.ejs`).
- **Widget rendering** for Mermaid, Excalidraw (canvas) and MindElixir (mind map) notes, with a
  collapsible, highlighted "Source" block and copy button.
- **3D models**: `.step` / `.stp` / `.stl` files (as file notes, as inline attachments, or linked
  from text notes) render in an interactive three.js viewer (STEP via **occt-wasm**, OpenCASCADE
  V8, parsed in a non-blocking **module worker**). The viewer is **full-bleed** (~`100vw`) with a
  viewport-based height, drag to rotate / scroll to zoom, rounded 8px box, a loading progress bar
  and a caption showing `name · size · triangle count`. Large meshes are
  split into `Uint16` chunks so mobile GPUs render them too (≈480k triangles for the demo
  battery module). See [docs/embed-3d-model.md](docs/embed-3d-model.md).
- **3D viewer inside Trilium (app widget)**: the same viewer runs in the Trilium app itself
  (independent of the blog) via a `note-detail-pane` widget – see `app/`.
- Code highlighting (highlight.js + line numbers) and copy buttons on all code boxes.
- Optional "Buy me a coffee" button with locally-hosted Cookie font.
- `blog_generator` (backend) builds index/feed/sitemap/search-data; `blog_publish_manager` moves
  notes between an `Unpublished` staging area and the live blog.

## Repository layout

| Path | Content | Trilium note type / mime |
|---|---|---|
| `blog_generator.js` | backend script that generates index, feed, sitemap, search data, category & tag pages | code, `application/javascript;env=backend` |
| `blog_publish_manager.js` | backend script that mirrors notes between `Unpublished` and the blog | code, `application/javascript;env=backend` |
| `theme/blog_share_theme_coffee.css` | Coffee / Coffee Latte share theme (active by default) | code, `text/css` |
| `theme/blog_share_theme_harbor.css` | Harbor / Harbordark share theme | code, `text/css` |
| `theme/blog_share_theme_gruvbox.css` | Gruvbox light/dark share theme | code, `text/css` |
| `theme/favicon.svg` | default site favicon | code (SVG) |
| `theme/cookie.woff2` | "Cookie" font for the Buy me a coffee button | file, `font/woff2` |
| `lib/highlight.js` | vendored highlight.js | code, `application/javascript` |
| `lib/3d/*` | three.js + OrbitControls + STLLoader + occt-wasm (OCCT V8 bundle + WASM) for the 3D viewer | code / file |
| `lib/hljs_line_numbers.js` | highlight.js line-numbers plugin | code |
| `scripts/*.js` | frontend share snippets (content is pasted **verbatim** into a code note, including the `<script>`/`<style>` wrapper) | code |
| `app/3d-model-viewer-app.jsx` | JSX widget that renders the 3D viewer for model file notes inside the Trilium app (label `#widget=3dModelViewer`) | code, `text/jsx` |
| `app/3d-worker.js` | Web Worker (STEP parsing) loaded by the app widget from a code note (CSP-safe) | code, `text/javascript` |
| `templates/blog_article.html` | article template with promoted labels | text |
| `templates/share/*.ejs` | custom share template + partials (canonical, OG, JSON-LD, robots) | code, `application/x-ejs` |
| `assets/og-banner.png` | default Open Graph image (1200×630, replace with your own) | file |
| `docs/embed-3d-model.md` | how to embed STEP/STL in a note | – |
| `examples/*` | sample category logo + menu icon script (ignis8) | code / SVG |
| `generated/*` | reference output, **regenerated automatically** by `blog_generator.js` | code / xml |

> The file content of a `scripts/*` note already contains the `<script>` / `<style>` tags. When you
> create the code note, paste the **whole file** into the note content.

## Prerequisites

- TriliumNext server with **Public Share** enabled (Options → Public access).
- A public hostname (optional but recommended, e.g. for `sitemap.xml` / OG tags).
- Basic familiarity with Trilium attributes (labels & relations).

---

## Installation

### 1. Create the note structure

```
root
└── Blog                    (book)
    ├── Richard's Blog      (book)  ← blog root, the shared note
    │   ├── .blog           (book)  ← system notes (scripts, css, data)
    │   ├── Astrophotography (book) ← example category
    │   ├── Electronic      (book)
    │   ├── Leathercraft    (book)
    │   ├── Digital Garden  (book)  ← growth-state garden category
    │   ├── Tags            (text)  ← auto-filled tag overview
    │   ├── About Me        (text)
    │   └── Impressum       (text)
    ├── Unpublished         (book)  ← staging area
    ├── blog_generator      (code, backend)
    └── blog_publish_manager (code, backend)
```

You can use any titles; the generator finds notes by the constants at the top of
`blog_generator.js` (`BLOG_ROOT_NOTE_ID`, `BLOG_SYSTEM_NOTE_ID`, `TAGS_SECTION_TITLE`).

### 2. Create the code notes

Under **`.blog`** create one code note per file, using the file name as the note title
(optional, but matches the generator):

| Note title | File | mime |
|---|---|---|
| `blog_share_theme_coffee.css` | `theme/blog_share_theme_coffee.css` | `text/css` |
| `blog_share_theme_harbor.css` | `theme/blog_share_theme_harbor.css` | `text/css` |
| `blog_share_theme_gruvbox.css` | `theme/blog_share_theme_gruvbox.css` | `text/css` |
| `favicon` | `theme/favicon.svg` | – (SVG) |
| `Cookie` | `theme/cookie.woff2` | `font/woff2` (file note) |
| `highlight.js` | `lib/highlight.js` | `application/javascript` |
| `hljs line numbers plugin` | `lib/hljs_line_numbers.js` | `text/javascript` |
| `hide home title` | `scripts/hide_home_title.js` | `text/html` |
| `garden h1 green` | `scripts/garden_h1_green.js` | `text/html` |
| `table column hover` | `scripts/table_column_hover.js` | `text/html` |
| `blog card click` | `scripts/blog_card_click.js` | `text/html` |
| `garden row click` | `scripts/garden_row_click.js` | `text/html` |
| `code note highlight` | `scripts/code_note_highlight.js` | `text/html` |
| `code copy & lines` | `scripts/code_copy_lines.js` | `text/html` |
| `title state badge` | `scripts/title_state_badge.js` | `text/html` |
| `bmc button` | `scripts/bmc_button.js` | `text/html` |
| `logo share alias` | `scripts/logo_share_alias.js` | `text/html` |
| `prev next` | `scripts/prev_next.js` | `text/html` |
| `reading time` | `scripts/reading_time.js` | `text/html` |
| `search header` | `scripts/search_header.js` | `text/html` |
| `note fallback` | `scripts/note_fallback.js` | `text/html` |
| `menu date sort` | `scripts/menu_date_sort.js` | `text/html` |
| `3d model viewer` | `scripts/3d_model_viewer.js` | `text/html` |
| `three.js` | `lib/3d/three.min.js` | `application/javascript` |
| `three orbit controls` | `lib/3d/OrbitControls.js` | `application/javascript` |
| `three stl loader` | `lib/3d/STLLoader.js` | `application/javascript` |
| `occt-import-js` | `lib/3d/occt-import-js.js` | `application/javascript` |
| `occt-import-js.wasm` | `lib/3d/occt-import-js.wasm` | `application/wasm` (file note) |
| `feed.xml` | `generated/feed.xml` | code (regenerated) |
| `sitemap.xml` | `generated/sitemap.xml` | code (regenerated) |
| `state_map` | `generated/state_map.js` | `text/html` (regenerated) |
| `blog_data` | `generated/blog_data.js` | `text/html` (regenerated) |
| `robots.txt` | `generated/robots.txt` | code (static) |
| `share template` | `templates/share/page.ejs` | `application/x-ejs` |
| `tree_item`, `prev_next`, `toc_item` (children of `share template`) | `templates/share/tree_item.ejs` etc. | `application/x-ejs` |
| `og-banner` | `assets/og-banner.png` | image note |

Also create, under the `Blog` book: `blog_generator` (from `blog_generator.js`,
mime `application/javascript;env=backend`) and `blog_publish_manager` (from
`blog_publish_manager.js`, mime `application/javascript;env=backend`).

### 3. Wire the attributes

**On the blog root note** (`Richard's Blog`), add:

| Attribute | Value | Inheritable |
|---|---|---|
| `shareRoot` | `true` | – |
| `shareAlias` | `blog` | – |
| `shareIndex` | (empty) | – |
| `shareDescription` | e.g. `Project documentation & technical blog` | – |
| `shareOpenGraphURL` | `https://YOUR-INSTANCE.example.com/` | – |
| `shareOpenGraphColor` | `#83a598` | – |
| `iconClass` | `bx bx-edit-alt` | – |
| `~shareCss` | → `blog_share_theme_coffee.css` (the active theme) | yes |
| `~shareFavicon` | → `favicon` | yes |
| `~shareTemplate` | → `share template` | yes |
| `~shareOpenGraphImage` | → `og-banner` | – |
| `~shareHtml` | → `table column hover`, `blog card click`, `garden row click`, `code note highlight`, `code copy & lines`, `state_map`, `title state badge`, `bmc button`, `logo share alias`, `blog_data`, `prev next`, `reading time`, `search header`, `ignis8 menu icon` (optional), `note fallback`, `menu date sort`, `3d model viewer` | yes |
| `~shareHtml` | → `hide home title` | **no** (index only) |
| `~runOnNoteContentChange`, `~runOnAttributeChange`, `~runOnNoteCreation`, `~runOnNoteTitleChange`, `~runOnAttributeCreation`, `~runOnBranchDeletion`, `~runOnChildNoteCreation` | → `blog_generator` | yes |
| `~runOnAttributeChange`, `~runOnAttributeCreation` | → `blog_publish_manager` | yes |

**On the script notes** (under `.blog`), add:

| Note | Attribute |
|---|---|
| `hide home title` | `shareHtmlLocation=head:end`, `shareHiddenFromTree` |
| `garden h1 green` | `shareHtmlLocation=head:end`, `shareHiddenFromTree` |
| `state_map` | `shareHtmlLocation=body:start`, `shareHiddenFromTree` |
| `title state badge` | `shareHtmlLocation=content:start`, `shareHiddenFromTree` |
| every other script | `shareHtmlLocation=body:end`, `shareHiddenFromTree` |
| `highlight.js` | `shareHiddenFromTree` |
| `feed.xml` | `shareAlias=feed`, `shareRaw`, `shareHiddenFromTree` |
| `sitemap.xml` | `shareAlias=sitemap.xml`, `shareRaw`, `shareHiddenFromTree` |
| `robots.txt` | `shareAlias=robots.txt`, `shareRaw`, `shareHiddenFromTree` |
| `.blog` (book) | `shareHiddenFromTree`, `viewType=list` |

**On the Digital Garden category note** (and any other garden category), add:

| Attribute | Value | Inheritable |
|---|---|---|
| `blogGarden` | (empty) | – |
| `shareAlias` | `digital-garden` | – |
| `iconClass` | `bx bx-leaf` | – |
| `label:state` | `promoted,alias=Growth Condition,single,select,options=Seed;Seedling;Sprout;Sprouting;Bud;Evergreen;Draft;Review` | yes |
| `~shareHtml` | → `garden h1 green` | – |

> Categories: any top-level note under the blog root (that is not `Tags`, `.blog`, `About Me`,
> `Impressum` or `Search`) is treated as a category. `blogGarden` marks it as a growth-state garden.
>
> **Project landing pages**: to turn a category into a pure project description page instead of an
> article collector, add `blogNoArticles` (empty label) to the category note. The generator then keeps
> only the user text above the `<!-- blog:articles -->` marker and does not append the article list;
> the articles stay reachable via the tree and the "Subpages" child links.

### 4. Replace the placeholder note IDs

Trilium assigns random note IDs on creation, so the scripts that reference notes by ID contain
placeholder tokens. Replace every `__TOKEN__` with the real note ID of the corresponding note:

| Token | Replace with note |
|---|---|
| `__BLOG_ROOT_ID__` | `Richard's Blog` |
| `__BLOG_SYSTEM_ID__` | `.blog` |
| `__UNPUBLISHED_ROOT_ID__` | `Unpublished` |
| `__BLOG_GENERATOR_ID__` | `blog_generator` |
| `__HIDE_HOME_TITLE_ID__` | `hide home title` |
| `__HLJS_ID__` | `highlight.js` |
| `__HLJS_LN_ID__` | `hljs line numbers plugin` |
| `__COOKIE_FONT_ID__` | `Cookie` |
| `__THREE_ID__` | `three.js` |
| `__THREE_ORBIT_ID__` | `three orbit controls` |
| `__THREE_STL_ID__` | `three stl loader` |
| `__OCCT_JS_ID__` | `occt-import-js` |
| `__OCCT_WASM_ID__` | `occt-import-js.wasm` |
| `__TITLE_ICON_ID__` | (only needed for the `ignis8` example) |
| `__CATEGORY_NOTE_ID__` | (only needed for the `ignis8` example) |

Then edit the constants at the top of `blog_generator.js` and `blog_publish_manager.js`:
- `BLOG_ROOT_NOTE_ID` / `BLOG_ROOT_ID` → the blog root note ID
- `BLOG_SYSTEM_NOTE_ID` → the `.blog` note ID
- `UNPUBLISHED_ROOT_ID` → the `Unpublished` note ID
- `BLOG_GENERATOR_ID` → the generator note ID
- `SHARE_BASE` → your public base URL, e.g. `https://YOUR-INSTANCE.example.com`
- `SITE_TITLE`, `SITE_DESCRIPTION` → your blog name/tagline

### 5. Enable Public Share and generate

1. On `Richard's Blog` right-click → *Share* → enable sharing.
2. Run the generator once: execute the `blog_generator` script (e.g. press play in the script
   editor, or trigger any attribute/content change). It is also wired to run automatically on
   every relevant change (`runOn*`), and you can add `run=daily` to it for a nightly run.
3. Open the share (options → Public access → URL) – the blog index is generated at the share root.

### 6. Publish an article

1. Create a note from the `templates/blog_article.html` content (or clone the template note).
   It promotes the `publish`, `date`, `category`, `summary`, `shareAlias` and `tags` labels.
2. Put it under a category inside `Richard's Blog` and set `publish=true`.
3. To keep it private, put it under `Unpublished` instead – `blog_publish_manager` clones it
   into the matching category (and back) when you flip `publish`.

### 7. Optional: clean hostname via nginx

If you want a clean domain instead of `/share`, proxy it and rewrite `/` to `/share/`:

```nginx
server {
    server_name blog.example.com;
    location / {
        proxy_pass https://notes.example.com/share/;
        proxy_set_header Host notes.example.com;
    }
}
```

---

## How the generator works

- Walks the blog root and collects notes with `publish=true`.
- Writes the **index** (first 10 posts) plus page notes (`blog-2`, `blog-3`, …) with
  prev/next navigation between the pages.
- 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`
  (search index with title, summary, tags, category and `YYYY-MM-DD` date – used by the
  header search, the category-scoped prev/next and the date-sorted menu).
- Sets `shareDescription` on every published note from its `summary`.

## Customisation

- **Theme**: pick one of `theme/blog_share_theme_{coffee,harbor,gruvbox}.css` and point the blog's
  `shareCss` relation at it – see [`docs/switch-blog-theme.md`](docs/switch-blog-theme.md). Each theme
  ships dark + light via `html.theme-light/dark`. The 3D viewer and Mermaid rendering read the theme's
  CSS variables, so they adapt automatically to whichever theme is active.
- **Buy me a coffee**: edit `scripts/bmc_button.js` (link, text, copyright) – the font is
  referenced via `__COOKIE_FONT_ID__`.
- **Category logos**: copy the `examples/` icon + `ignis8_menu_icon.js` pattern to give a
  category a custom menu icon and title logo.

## License

This project is licensed under the **GNU General Public License v3.0** – see [`LICENSE`](LICENSE).

The repository also contains third-party assets that keep their own licenses:
`lib/highlight.js` (BSD-3-Clause), `lib/hljs_line_numbers.js` (MIT),
`theme/cookie.woff2` (SIL OFL 1.1), the three palette images `assets/palette-*.png` (MIT) and the embedded
Trilium logo in `scripts/bmc_button.js` (AGPL-3.0). See
[`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md) for details.