20 KiB
Trilium Share – Coffee Blog System
Note: This theme and blog system was created with the help of Opencode and Deepseek V4 Flash.
A complete public-share blog system + Coffee theme for TriliumNext. 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 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 (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 (MIT) |
| Gruvbox | theme/blog_share_theme_gruvbox.css |
bg #282828 / fg #ebdbb2, accents red/green/yellow/blue |
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 (MIT) |
| Tokyo Night | theme/blog_share_theme_tokyonight.css |
storm #24283b / fg #c0caf5, blue #7aa2f7 · day #e1e2e7 / fg #3760bf |
tokyonight.nvim (Apache-2.0) |
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 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 (dateTextfrom 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:
statelabels (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.txtandindex,followmeta. 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/.stlfiles (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 showingname · size · triangle count. Large meshes are split intoUint16chunks so mobile GPUs render them too (≈480k triangles for the demo battery module). See 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-panewidget – seeapp/. - 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_managermoves notes between anUnpublishedstaging 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,ImpressumorSearch) is treated as a category.blogGardenmarks 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 IDBLOG_SYSTEM_NOTE_ID→ the.blognote IDUNPUBLISHED_ROOT_ID→ theUnpublishednote IDBLOG_GENERATOR_ID→ the generator note IDSHARE_BASE→ your public base URL, e.g.https://YOUR-INSTANCE.example.comSITE_TITLE,SITE_DESCRIPTION→ your blog name/tagline
5. Enable Public Share and generate
- On
Richard's Blogright-click → Share → enable sharing. - Run the generator once: execute the
blog_generatorscript (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 addrun=dailyto it for a nightly run. - Open the share (options → Public access → URL) – the blog index is generated at the share root.
6. Publish an article
- Create a note from the
templates/blog_article.htmlcontent (or clone the template note). It promotes thepublish,date,category,summary,shareAliasandtagslabels. - Put it under a category inside
Richard's Blogand setpublish=true. - To keep it private, put it under
Unpublishedinstead –blog_publish_managerclones it into the matching category (and back) when you flippublish.
7. Optional: clean hostname via nginx
If you want a clean domain instead of /share, proxy it and rewrite / to /share/:
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.blogsystem note and are tagged withblogPagedFor. - Writes tag pages and the tags overview.
- Regenerates
feed.xml(RSS),sitemap.xml,state_map(garden states) andblog_data(search index with title, summary, tags, category andYYYY-MM-DDdate – used by the header search, the category-scoped prev/next and the date-sorted menu). - Sets
shareDescriptionon every published note from itssummary.
Customisation
- Theme: pick one of
theme/blog_share_theme_{coffee,harbor,gruvbox,solarized,tokyonight}.cssand point the blog'sshareCssrelation at it – seedocs/switch-blog-theme.md. Each theme ships dark + light viahtml.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.jspattern 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.
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 for details.




