263 lines
13 KiB
Markdown
263 lines
13 KiB
Markdown
# Trilium Share – Gruvbox Blog System
|
||
|
||
[](LICENSE)
|
||
[](https://richard.familie-zink.org)
|
||
[](https://github.com/TriliumNext/Trilium)
|
||
[](https://github.com/morhetz/gruvbox)
|
||
[](https://opencode.ai)
|
||
[](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 + Gruvbox 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/`).
|
||
|
||

|
||
|
||
*Palette from the [morhetz/gruvbox](https://github.com/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.
|
||
- **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**.
|
||
- **Widget rendering** for Mermaid, Excalidraw (canvas) and MindElixir (mind map) notes, with a
|
||
collapsible, highlighted "Source" block and copy button.
|
||
- 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.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/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 |
|
||
| `templates/blog_article.html` | article template with promoted labels | text |
|
||
| `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` |
|
||
| `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) |
|
||
|
||
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 |
|
||
| `~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` | 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` |
|
||
| `.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.
|
||
|
||
### 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` |
|
||
| `__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 and category – used by the header search and the
|
||
category-scoped prev/next).
|
||
- Sets `shareDescription` on every published note from its `summary`.
|
||
|
||
## Customisation
|
||
|
||
- **Theme**: edit `theme/blog_share_theme.css` (Gruvbox light/dark via `html.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.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), `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. |