17 KiB
Trilium Share – Gruvbox 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 + Gruvbox 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/).
Palette from the morhetz/gruvbox repository.
Features
- Gruvbox light/dark theme for the share, with a theme toggle in the header.
- 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.*(e.g.ngc281_thumb.jpg), that image is used instead – ideal for serving small pre-made thumbnails. - 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-import-js, OpenCascade WASM, parsed in a Web Worker so the page stays responsive). 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 that always showsname · size · triangle count. STEP tessellation is dense (≈0.1–1 M+ triangles for assemblies). 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.css |
the Gruvbox 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-import-js (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.css |
theme/blog_share_theme.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.css |
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.
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: edit
theme/blog_share_theme.css(Gruvbox light/dark viahtml.theme-light/dark). - 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), assets/palette.png (MIT) and the embedded
Trilium logo in scripts/bmc_button.js (AGPL-3.0). See
THIRD_PARTY_LICENSES.md for details.
