Files
trilium_share_gruvbox/README.md
T

343 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-harbordark-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) |
| **Solarized** | `theme/blog_share_theme_solarized.css` | bg `#002b36` / fg `#839496`, blue `#268bd2` cyan `#2aa198` · bg `#fdf6e3` / fg `#657b83` | [Solarized by Ethan Schoonover](https://ethanschoonover.com/solarized/) (MIT) |
| **Tokyo Night** | `theme/blog_share_theme_tokyonight.css` | storm `#24283b` / fg `#c0caf5`, blue `#7aa2f7` · day `#e1e2e7` / fg `#3760bf` | [tokyonight.nvim](https://github.com/folke/tokyonight.nvim) (Apache-2.0) |
![Coffee palette](assets/palette-coffee.png)
![Harbor palette](assets/palette-harbor.png)
![Gruvbox palette](assets/palette-gruvbox.png)
![Solarized palette](assets/palette-solarized.png)
![Tokyo Night palette](assets/palette-tokyonight.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/blog_share_theme_solarized.css` | Solarized dark/light share theme | code, `text/css` |
| `theme/blog_share_theme_tokyonight.css` | Tokyo Night (storm/day) 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` |
| `blog_share_theme_solarized.css` | `theme/blog_share_theme_solarized.css` | `text/css` |
| `blog_share_theme_tokyonight.css` | `theme/blog_share_theme_tokyonight.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,solarized,tokyonight}.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 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.