diff --git a/apps/server/spec/db/document.db b/apps/server/spec/db/document.db
index 264a9ff0d1..a9340595c7 100644
Binary files a/apps/server/spec/db/document.db and b/apps/server/spec/db/document.db differ
diff --git a/apps/server/spec/etapi/external-blobs.spec.ts b/apps/server/spec/etapi/external-blobs.spec.ts
new file mode 100644
index 0000000000..3665a7a9c9
--- /dev/null
+++ b/apps/server/spec/etapi/external-blobs.spec.ts
@@ -0,0 +1,94 @@
+import { Application } from "express";
+import { beforeAll, describe, expect, it } from "vitest";
+import supertest from "supertest";
+import { login } from "./utils.js";
+import config from "../../src/services/config.js";
+import sql from "../../src/services/sql.js";
+
+let app: Application;
+let token: string;
+
+const USER = "etapi";
+
+describe("etapi/external-blobs", () => {
+ beforeAll(async () => {
+ config.General.noAuthentication = false;
+ config.ExternalBlobStorage.enabled = true;
+ config.ExternalBlobStorage.thresholdBytes = 10;
+
+ const buildApp = (await import("../../src/app.js")).default;
+ app = await buildApp();
+ token = await login(app);
+ });
+
+ it("stores small note content internally", async () => {
+ const payload = "a".repeat(10);
+ const createRes = await supertest(app)
+ .post("/etapi/create-note")
+ .auth(USER, token, { "type": "basic"})
+ .send({
+ "parentNoteId": "root",
+ "title": "Internal Blob Test",
+ "mime": "text/plain",
+ "type": "text",
+ "content": payload
+ })
+ .expect(201);
+
+ const createdNoteId: string = createRes.body.note.noteId;
+ expect(createdNoteId).toBeTruthy();
+
+ const blobId = sql.getValue("SELECT blobId FROM notes WHERE noteId = ?", [createdNoteId]);
+ expect(blobId).toBeTruthy();
+
+ const row = sql.getRow<{ contentLocation: string; content: string | null; contentLength: number }>(
+ "SELECT contentLocation, content, contentLength FROM blobs WHERE blobId = ?",
+ [blobId]
+ );
+
+ expect(row).toBeTruthy();
+ expect(row.contentLength).toEqual(payload.length);
+ expect(row.contentLocation).toEqual("internal");
+ expect(row.content).toEqual(payload);
+ });
+
+ it("stores large note content externally and serves it back", async () => {
+ const payload = "a".repeat(11);
+ const createRes = await supertest(app)
+ .post("/etapi/create-note")
+ .auth(USER, token, { "type": "basic"})
+ .send({
+ "parentNoteId": "root",
+ "title": "External Blob Test",
+ "mime": "application/octet-stream",
+ "type": "file",
+ "content": payload
+ })
+ .expect(201);
+
+ const createdNoteId: string = createRes.body.note.noteId;
+ expect(createdNoteId).toBeTruthy();
+
+ const blobId = sql.getValue("SELECT blobId FROM notes WHERE noteId = ?", [createdNoteId]);
+ expect(blobId).toBeTruthy();
+
+ const row = sql.getRow<{ contentLocation: string; content: string | null; contentLength: number }>(
+ "SELECT contentLocation, content, contentLength FROM blobs WHERE blobId = ?",
+ [blobId]
+ );
+
+ expect(row).toBeTruthy();
+ expect(row.contentLength).toEqual(payload.length);
+ expect(row.contentLocation.startsWith("file://")).toBe(true);
+ expect(row.content).toBeNull();
+
+ const getRes = await supertest(app)
+ .get(`/etapi/notes/${createdNoteId}/content`)
+ .auth(USER, token, { "type": "basic"})
+ .expect(200);
+
+ expect(getRes.body.toString()).toEqual(payload);
+ });
+});
+
+
diff --git a/apps/server/src/assets/config-sample.ini b/apps/server/src/assets/config-sample.ini
index 41eb3d2b62..28c5596aba 100644
--- a/apps/server/src/assets/config-sample.ini
+++ b/apps/server/src/assets/config-sample.ini
@@ -67,3 +67,18 @@ oauthIssuerName=
# Set the issuer icon for OAuth/OpenID authentication
# This is the icon of the service that will be used to verify the user's identity
oauthIssuerIcon=
+
+[ExternalBlobStorage]
+# External blob storage allows large attachments to be stored as separate files
+# instead of being embedded in the SQLite database. This can improve database
+# performance and simplify backup strategies for large files.
+#
+# When enabled=false (default), all attachments are stored in the database
+# When enabled=true, attachments larger than thresholdBytes are stored as files
+# in the external-blobs directory within your data directory
+enabled=false
+
+# Threshold size in bytes - attachments larger than this will be stored externally
+# Default is 100KB (102400 bytes), which balances performance and flexibility
+# See https://www.sqlite.org/intern-v-extern-blob.html for performance considerations
+thresholdBytes=102400
diff --git a/apps/server/src/assets/db/schema.sql b/apps/server/src/assets/db/schema.sql
index 07d924a915..409899e00f 100644
--- a/apps/server/src/assets/db/schema.sql
+++ b/apps/server/src/assets/db/schema.sql
@@ -107,6 +107,8 @@ CREATE TABLE IF NOT EXISTS "recent_notes"
CREATE TABLE IF NOT EXISTS "blobs" (
`blobId` TEXT NOT NULL,
`content` TEXT NULL DEFAULT NULL,
+ `contentLocation` TEXT NOT NULL DEFAULT 'internal',
+ `contentLength` INTEGER NOT NULL DEFAULT 0,
`dateModified` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`blobId`)
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json b/apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
index ef9d57e303..351e71b54f 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
+++ b/apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
@@ -1 +1 @@
-[{"id":"_help_BOCnjTMBCoxW","title":"Feature Highlights","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Feature Highlights"},{"name":"iconClass","value":"bx bx-star","type":"label"}]},{"id":"_help_Otzi9La2YAUX","title":"Installation & Setup","type":"book","attributes":[{"name":"iconClass","value":"bx bx-cog","type":"label"}],"children":[{"id":"_help_poXkQfguuA0U","title":"Desktop Installation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Desktop Installation"},{"name":"iconClass","value":"bx bx-desktop","type":"label"}],"children":[{"id":"_help_nRqcgfTb97uV","title":"Using the desktop application as a server","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Desktop Installation/Using the desktop application "},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_Rp0q8bSP6Ayl","title":"System Requirements","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Desktop Installation/System Requirements"},{"name":"iconClass","value":"bx bx-chip","type":"label"}]},{"id":"_help_Un4wj2Mak2Ky","title":"Nix flake","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Desktop Installation/Nix flake"},{"name":"iconClass","value":"bx bxl-tux","type":"label"}]}]},{"id":"_help_WOcw2SLH6tbX","title":"Server Installation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation"},{"name":"iconClass","value":"bx bx-server","type":"label"}],"children":[{"id":"_help_Dgg7bR3b6K9j","title":"1. Installing the server","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_3tW6mORuTHnB","title":"Packaged version for Linux","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Packaged version for Linux"},{"name":"iconClass","value":"bx bxl-tux","type":"label"}]},{"id":"_help_rWX5eY045zbE","title":"Using Docker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Docker"},{"name":"iconClass","value":"bx bxl-docker","type":"label"}]},{"id":"_help_moVgBcoxE3EK","title":"On NixOS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/On NixOS"},{"name":"iconClass","value":"bx bxl-tux","type":"label"}]},{"id":"_help_J1Bb6lVlwU5T","title":"Manually","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Manually"},{"name":"iconClass","value":"bx bx-code-alt","type":"label"}]},{"id":"_help_DCmT6e7clMoP","title":"Using Kubernetes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Kubernetes"},{"name":"iconClass","value":"bx bxl-kubernetes","type":"label"}]},{"id":"_help_klCWNks3ReaQ","title":"Multiple server instances","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Multiple server instances"},{"name":"iconClass","value":"bx bxs-user-account","type":"label"}]}]},{"id":"_help_vcjrb3VVYPZI","title":"2. Reverse proxy","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_ud6MShXL4WpO","title":"Nginx","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Nginx"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_fDLvzOx29Pfg","title":"Apache using Docker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Apache using Docker"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_LLzSMXACKhUs","title":"Trusted proxy","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Trusted proxy"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_l2VkvOwUNfZj","title":"HTTPS (TLS)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/HTTPS (TLS)"},{"name":"iconClass","value":"bx bx-lock-alt","type":"label"}]},{"id":"_help_0hzsNCP31IAB","title":"Authentication","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/Authentication"},{"name":"iconClass","value":"bx bx-user","type":"label"}]},{"id":"_help_7DAiwaf8Z7Rz","title":"Multi-Factor Authentication","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/Multi-Factor Authentication"},{"name":"iconClass","value":"bx bx-stopwatch","type":"label"}]},{"id":"_help_Un4wj2Mak2Ky","title":"Nix flake","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/Nix flake.clone"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_yeEaYqosGLSh","title":"Third-party cloud hosting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/Third-party cloud hosting"},{"name":"iconClass","value":"bx bx-cloud","type":"label"}]},{"id":"_help_iGTnKjubbXkA","title":"System Requirements","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/System Requirements"},{"name":"iconClass","value":"bx bx-chip","type":"label"}]}]},{"id":"_help_cbkrhQjrkKrh","title":"Synchronization","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Synchronization"},{"name":"iconClass","value":"bx bx-sync","type":"label"}]},{"id":"_help_RDslemsQ6gCp","title":"Mobile Frontend","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Mobile Frontend"},{"name":"iconClass","value":"bx bx-mobile-alt","type":"label"}]},{"id":"_help_MtPxeAWVAzMg","title":"Web Clipper","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Web Clipper"},{"name":"iconClass","value":"bx bx-paperclip","type":"label"}]},{"id":"_help_n1lujUxCwipy","title":"Upgrading TriliumNext","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Upgrading TriliumNext"},{"name":"iconClass","value":"bx bx-up-arrow-alt","type":"label"}]},{"id":"_help_ODY7qQn5m2FT","title":"Backup","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Backup"},{"name":"iconClass","value":"bx bx-hdd","type":"label"}]},{"id":"_help_tAassRL4RSQL","title":"Data directory","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Data directory"},{"name":"iconClass","value":"bx bx-folder-open","type":"label"}]}]},{"id":"_help_gh7bpGYxajRS","title":"Basic Concepts and Features","type":"book","attributes":[{"name":"iconClass","value":"bx bx-help-circle","type":"label"}],"children":[{"id":"_help_Vc8PjrjAGuOp","title":"UI Elements","type":"book","attributes":[{"name":"iconClass","value":"bx bx-window-alt","type":"label"}],"children":[{"id":"_help_x0JgW8UqGXvq","title":"Vertical and horizontal layout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Vertical and horizontal layout"},{"name":"iconClass","value":"bx bxs-layout","type":"label"}]},{"id":"_help_x3i7MxGccDuM","title":"Global menu","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Global menu"},{"name":"iconClass","value":"bx bx-menu","type":"label"}]},{"id":"_help_oPVyFC7WL2Lp","title":"Note Tree","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree"},{"name":"iconClass","value":"bx bxs-tree-alt","type":"label"}],"children":[{"id":"_help_YtSN43OrfzaA","title":"Note tree contextual menu","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu"},{"name":"iconClass","value":"bx bx-menu","type":"label"}]},{"id":"_help_yTjUdsOi4CIE","title":"Multiple selection","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Multiple selection"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]},{"id":"_help_DvdZhoQZY9Yd","title":"Keyboard shortcuts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Keyboard shortcuts"},{"name":"iconClass","value":"bx bxs-keyboard","type":"label"}]}]},{"id":"_help_BlN9DFI679QC","title":"Ribbon","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Ribbon"},{"name":"iconClass","value":"bx bx-dots-horizontal","type":"label"}]},{"id":"_help_3seOhtN8uLIY","title":"Tabs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Tabs"},{"name":"iconClass","value":"bx bx-dock-top","type":"label"}]},{"id":"_help_xYmIYSP6wE3F","title":"Launch Bar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Launch Bar"},{"name":"iconClass","value":"bx bx-sidebar","type":"label"}]},{"id":"_help_8YBEPzcpUgxw","title":"Note buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note buttons"},{"name":"iconClass","value":"bx bx-dots-vertical-rounded","type":"label"}]},{"id":"_help_4TIF1oA4VQRO","title":"Options","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Options"},{"name":"iconClass","value":"bx bx-cog","type":"label"}]},{"id":"_help_luNhaphA37EO","title":"Split View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Split View"},{"name":"iconClass","value":"bx bx-dock-right","type":"label"}]},{"id":"_help_XpOYSgsLkTJy","title":"Floating buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Floating buttons"},{"name":"iconClass","value":"bx bx-rectangle","type":"label"}]},{"id":"_help_RnaPdbciOfeq","title":"Right Sidebar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Right Sidebar"},{"name":"iconClass","value":"bx bxs-dock-right","type":"label"}]},{"id":"_help_r5JGHN99bVKn","title":"Recent Changes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Recent Changes"},{"name":"iconClass","value":"bx bx-history","type":"label"}]},{"id":"_help_ny318J39E5Z0","title":"Zoom","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Zoom"},{"name":"iconClass","value":"bx bx-zoom-in","type":"label"}]},{"id":"_help_ZjLYv08Rp3qC","title":"Quick edit","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Quick edit"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_lgKX7r3aL30x","title":"Note Tooltip","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tooltip"},{"name":"iconClass","value":"bx bx-message-detail","type":"label"}]}]},{"id":"_help_BFs8mudNFgCS","title":"Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes"},{"name":"iconClass","value":"bx bx-notepad","type":"label"}],"children":[{"id":"_help_p9kXRFAkwN4o","title":"Note Icons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note Icons"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]},{"id":"_help_0vhv7lsOLy82","title":"Attachments","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Attachments"},{"name":"iconClass","value":"bx bx-paperclip","type":"label"}]},{"id":"_help_IakOLONlIfGI","title":"Cloning Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Cloning Notes"},{"name":"iconClass","value":"bx bx-duplicate","type":"label"}],"children":[{"id":"_help_TBwsyfadTA18","title":"Branch prefix","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Cloning Notes/Branch prefix"},{"name":"iconClass","value":"bx bx-rename","type":"label"}]}]},{"id":"_help_bwg0e8ewQMak","title":"Protected Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Protected Notes"},{"name":"iconClass","value":"bx bx-lock-alt","type":"label"}]},{"id":"_help_MKmLg5x6xkor","title":"Archived Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Archived Notes"},{"name":"iconClass","value":"bx bx-box","type":"label"}]},{"id":"_help_vZWERwf8U3nx","title":"Note Revisions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note Revisions"},{"name":"iconClass","value":"bx bx-history","type":"label"}]},{"id":"_help_aGlEvb9hyDhS","title":"Sorting Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Sorting Notes"},{"name":"iconClass","value":"bx bx-sort-up","type":"label"}]},{"id":"_help_NRnIZmSMc5sj","title":"Printing & Exporting as PDF","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF"},{"name":"iconClass","value":"bx bx-printer","type":"label"}]},{"id":"_help_CoFPLs3dRlXc","title":"Read-Only Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Read-Only Notes"},{"name":"iconClass","value":"bx bx-edit-alt","type":"label"}]},{"id":"_help_0ESUbbAxVnoK","title":"Note List","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note List"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]}]},{"id":"_help_wArbEsdSae6g","title":"Navigation","type":"book","attributes":[{"name":"iconClass","value":"bx bx-navigation","type":"label"}],"children":[{"id":"_help_kBrnXNG3Hplm","title":"Tree Concepts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Tree Concepts"},{"name":"iconClass","value":"bx bx-pyramid","type":"label"}]},{"id":"_help_MMiBEQljMQh2","title":"Note Navigation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Note Navigation"},{"name":"iconClass","value":"bx bxs-navigation","type":"label"}]},{"id":"_help_Ms1nauBra7gq","title":"Quick search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Quick search"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]},{"id":"_help_F1r9QtzQLZqm","title":"Jump to...","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Jump to"},{"name":"iconClass","value":"bx bx-send","type":"label"}]},{"id":"_help_eIg8jdvaoNNd","title":"Search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Search"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]},{"id":"_help_u3YFHC9tQlpm","title":"Bookmarks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Bookmarks"},{"name":"iconClass","value":"bx bx-bookmarks","type":"label"}]},{"id":"_help_OR8WJ7Iz9K4U","title":"Note Hoisting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Note Hoisting"},{"name":"iconClass","value":"bx bxs-chevrons-up","type":"label"}]},{"id":"_help_ZjLYv08Rp3qC","title":"Quick edit","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Quick edit.clone"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_9sRHySam5fXb","title":"Workspaces","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Workspaces"},{"name":"iconClass","value":"bx bx-door-open","type":"label"}]},{"id":"_help_xWtq5NUHOwql","title":"Similar Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Similar Notes"},{"name":"iconClass","value":"bx bx-bar-chart","type":"label"}]},{"id":"_help_McngOG2jbUWX","title":"Search in note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Search in note"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]}]},{"id":"_help_A9Oc6YKKc65v","title":"Keyboard Shortcuts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Keyboard Shortcuts"},{"name":"iconClass","value":"bx bxs-keyboard","type":"label"}]},{"id":"_help_Wy267RK4M69c","title":"Themes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Themes"},{"name":"iconClass","value":"bx bx-palette","type":"label"}],"children":[{"id":"_help_VbjZvtUek0Ln","title":"Theme Gallery","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Themes/Theme Gallery"},{"name":"iconClass","value":"bx bx-book-reader","type":"label"}]}]},{"id":"_help_mHbBMPDPkVV5","title":"Import & Export","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export"},{"name":"iconClass","value":"bx bx-import","type":"label"}],"children":[{"id":"_help_Oau6X9rCuegd","title":"Markdown","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown"},{"name":"iconClass","value":"bx bxl-markdown","type":"label"}],"children":[{"id":"_help_rJ9grSgoExl9","title":"Supported syntax","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown/Supported syntax"},{"name":"iconClass","value":"bx bx-code-alt","type":"label"}]}]},{"id":"_help_syuSEKf2rUGr","title":"Evernote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/Evernote"},{"name":"iconClass","value":"bx bx-window-open","type":"label"}]},{"id":"_help_GnhlmrATVqcH","title":"OneNote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/OneNote"},{"name":"iconClass","value":"bx bx-window-open","type":"label"}]}]},{"id":"_help_rC3pL2aptaRE","title":"Zen mode","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Zen mode"},{"name":"iconClass","value":"bx bxs-yin-yang","type":"label"}]}]},{"id":"_help_s3YCWHBfmYuM","title":"Quick Start","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Quick Start"},{"name":"iconClass","value":"bx bx-run","type":"label"}]},{"id":"_help_i6dbnitykE5D","title":"FAQ","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/FAQ"},{"name":"iconClass","value":"bx bx-question-mark","type":"label"}]},{"id":"_help_KSZ04uQ2D1St","title":"Note Types","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types"},{"name":"iconClass","value":"bx bx-edit","type":"label"}],"children":[{"id":"_help_iPIMuisry3hd","title":"Text","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text"},{"name":"iconClass","value":"bx bx-note","type":"label"}],"children":[{"id":"_help_NwBbFdNZ9h7O","title":"Block quotes & admonitions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Block quotes & admonitions"},{"name":"iconClass","value":"bx bx-info-circle","type":"label"}]},{"id":"_help_oSuaNgyyKnhu","title":"Bookmarks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Bookmarks"},{"name":"iconClass","value":"bx bx-bookmark","type":"label"}]},{"id":"_help_veGu4faJErEM","title":"Content language & Right-to-left support","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Content language & Right-to-le"},{"name":"iconClass","value":"bx bx-align-right","type":"label"}]},{"id":"_help_2x0ZAX9ePtzV","title":"Cut to subnote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Cut to subnote"},{"name":"iconClass","value":"bx bx-cut","type":"label"}]},{"id":"_help_UYuUB1ZekNQU","title":"Developer-specific formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Developer-specific formatting"},{"name":"iconClass","value":"bx bx-code-alt","type":"label"}],"children":[{"id":"_help_QxEyIjRBizuC","title":"Code blocks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Developer-specific formatting/Code blocks"},{"name":"iconClass","value":"bx bx-code","type":"label"}]}]},{"id":"_help_AgjCISero73a","title":"Footnotes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Footnotes"},{"name":"iconClass","value":"bx bx-bracket","type":"label"}]},{"id":"_help_nRhnJkTT8cPs","title":"Formatting toolbar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Formatting toolbar"},{"name":"iconClass","value":"bx bx-text","type":"label"}]},{"id":"_help_Gr6xFaF6ioJ5","title":"General formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/General formatting"},{"name":"iconClass","value":"bx bx-bold","type":"label"}]},{"id":"_help_AxshuNRegLAv","title":"Highlights list","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Highlights list"},{"name":"iconClass","value":"bx bx-highlight","type":"label"}]},{"id":"_help_mT0HEkOsz6i1","title":"Images","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Images"},{"name":"iconClass","value":"bx bx-image-alt","type":"label"}],"children":[{"id":"_help_0Ofbk1aSuVRu","title":"Image references","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Images/Image references"},{"name":"iconClass","value":"bx bxs-file-image","type":"label"}]}]},{"id":"_help_nBAXQFj20hS1","title":"Include Note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Include Note"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_CohkqWQC1iBv","title":"Insert buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Insert buttons"},{"name":"iconClass","value":"bx bx-plus","type":"label"}]},{"id":"_help_oiVPnW8QfnvS","title":"Keyboard shortcuts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Keyboard shortcuts"},{"name":"iconClass","value":"bx bxs-keyboard","type":"label"}]},{"id":"_help_QEAPj01N5f7w","title":"Links","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Links"},{"name":"iconClass","value":"bx bx-link-alt","type":"label"}],"children":[{"id":"_help_3IDVtesTQ8ds","title":"External links","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Links/External links"},{"name":"iconClass","value":"bx bx-link-external","type":"label"}]},{"id":"_help_hrZ1D00cLbal","title":"Internal (reference) links","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Links/Internal (reference) links"},{"name":"iconClass","value":"bx bx-link","type":"label"}]}]},{"id":"_help_S6Xx8QIWTV66","title":"Lists","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Lists"},{"name":"iconClass","value":"bx bx-list-ul","type":"label"}]},{"id":"_help_QrtTYPmdd1qq","title":"Markdown-like formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Markdown-like formatting"},{"name":"iconClass","value":"bx bxl-markdown","type":"label"}]},{"id":"_help_YfYAtQBcfo5V","title":"Math Equations","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Math Equations"},{"name":"iconClass","value":"bx bx-math","type":"label"}]},{"id":"_help_dEHYtoWWi8ct","title":"Other features","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Other features"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]},{"id":"_help_gLt3vA97tMcp","title":"Premium features","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Premium features"},{"name":"iconClass","value":"bx bx-star","type":"label"}],"children":[{"id":"_help_ZlN4nump6EbW","title":"Slash Commands","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Premium features/Slash Commands"},{"name":"iconClass","value":"bx bx-menu","type":"label"}]},{"id":"_help_pwc194wlRzcH","title":"Text Snippets","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Premium features/Text Snippets"},{"name":"iconClass","value":"bx bx-align-left","type":"label"}]}]},{"id":"_help_BFvAtE74rbP6","title":"Table of contents","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Table of contents"},{"name":"iconClass","value":"bx bx-heading","type":"label"}]},{"id":"_help_NdowYOC1GFKS","title":"Tables","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Tables"},{"name":"iconClass","value":"bx bx-table","type":"label"}]}]},{"id":"_help_6f9hih2hXXZk","title":"Code","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Code"},{"name":"iconClass","value":"bx bx-code","type":"label"}]},{"id":"_help_m523cpzocqaD","title":"Saved Search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Saved Search"},{"name":"iconClass","value":"bx bx-file-find","type":"label"}]},{"id":"_help_iRwzGnHPzonm","title":"Relation Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Relation Map"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_bdUJEHsAPYQR","title":"Note Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Note Map"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_HcABDtFCkbFN","title":"Render Note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Render Note"},{"name":"iconClass","value":"bx bx-extension","type":"label"}]},{"id":"_help_s1aBHPd79XYj","title":"Mermaid Diagrams","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mermaid Diagrams"},{"name":"iconClass","value":"bx bx-selection","type":"label"}],"children":[{"id":"_help_RH6yLjjWJHof","title":"ELK layout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mermaid Diagrams/ELK layout"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_WWgeUaBb7UfC","title":"Syntax reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://mermaid.js.org/intro/syntax-reference.html"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"enforceAttributes":true}]},{"id":"_help_grjYqerjn243","title":"Canvas","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Canvas"},{"name":"iconClass","value":"bx bx-pen","type":"label"}]},{"id":"_help_1vHRoWCEjj0L","title":"Web View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Web View"},{"name":"iconClass","value":"bx bx-globe-alt","type":"label"}]},{"id":"_help_gBbsAeiuUxI5","title":"Mind Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mind Map"},{"name":"iconClass","value":"bx bx-sitemap","type":"label"}]},{"id":"_help_W8vYD3Q1zjCR","title":"File","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/File"},{"name":"iconClass","value":"bx bx-file-blank","type":"label"}]}]},{"id":"_help_GTwFsgaA0lCt","title":"Collections","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections"},{"name":"iconClass","value":"bx bx-book","type":"label"}],"children":[{"id":"_help_xWbu3jpNWapp","title":"Calendar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Calendar"},{"name":"iconClass","value":"bx bx-calendar","type":"label"}]},{"id":"_help_2FvYrpmOXm29","title":"Table","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Table"},{"name":"iconClass","value":"bx bx-table","type":"label"}]},{"id":"_help_CtBQqbwXDx1w","title":"Kanban Board","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Kanban Board"},{"name":"iconClass","value":"bx bx-columns","type":"label"}]},{"id":"_help_81SGnPGMk7Xc","title":"Geo Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Geo Map"},{"name":"iconClass","value":"bx bx-map-alt","type":"label"}]},{"id":"_help_zP3PMqaG71Ct","title":"Presentation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Presentation"},{"name":"iconClass","value":"bx bx-slideshow","type":"label"}]},{"id":"_help_8QqnMzx393bx","title":"Grid View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Grid View"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]},{"id":"_help_mULW0Q3VojwY","title":"List View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/List View"},{"name":"iconClass","value":"bx bx-list-ul","type":"label"}]}]},{"id":"_help_BgmBlOIl72jZ","title":"Troubleshooting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting"},{"name":"iconClass","value":"bx bx-bug","type":"label"}],"children":[{"id":"_help_wy8So3yZZlH9","title":"Reporting issues","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Reporting issues"},{"name":"iconClass","value":"bx bx-bug-alt","type":"label"}]},{"id":"_help_x59R8J8KV5Bp","title":"Anonymized Database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Anonymized Database"},{"name":"iconClass","value":"bx bx-low-vision","type":"label"}]},{"id":"_help_qzNzp9LYQyPT","title":"Error logs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Error logs"},{"name":"iconClass","value":"bx bx-comment-error","type":"label"}],"children":[{"id":"_help_bnyigUA2UK7s","title":"Backend (server) logs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Error logs/Backend (server) logs"},{"name":"iconClass","value":"bx bx-server","type":"label"}]},{"id":"_help_9yEHzMyFirZR","title":"Frontend logs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Error logs/Frontend logs"},{"name":"iconClass","value":"bx bx-window-alt","type":"label"}]}]},{"id":"_help_vdlYGAcpXAgc","title":"Synchronization fails with 504 Gateway Timeout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Synchronization fails with 504"},{"name":"iconClass","value":"bx bx-error","type":"label"}]},{"id":"_help_s8alTXmpFR61","title":"Refreshing the application","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Refreshing the application"},{"name":"iconClass","value":"bx bx-refresh","type":"label"}]}]},{"id":"_help_pKK96zzmvBGf","title":"Theme development","type":"book","attributes":[{"name":"iconClass","value":"bx bx-palette","type":"label"}],"children":[{"id":"_help_7NfNr5pZpVKV","title":"Creating a custom theme","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Creating a custom theme"},{"name":"iconClass","value":"bx bxs-color","type":"label"}]},{"id":"_help_WFGzWeUK6arS","title":"Customize the Next theme","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Customize the Next theme"},{"name":"iconClass","value":"bx bx-news","type":"label"}]},{"id":"_help_WN5z4M8ASACJ","title":"Reference","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Reference"},{"name":"iconClass","value":"bx bx-book-open","type":"label"}]},{"id":"_help_AlhDUqhENtH7","title":"Custom app-wide CSS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Custom app-wide CSS"},{"name":"iconClass","value":"bx bxs-file-css","type":"label"}]}]},{"id":"_help_tC7s2alapj8V","title":"Advanced Usage","type":"book","attributes":[{"name":"iconClass","value":"bx bx-rocket","type":"label"}],"children":[{"id":"_help_zEY4DaJG4YT5","title":"Attributes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes"},{"name":"iconClass","value":"bx bx-list-check","type":"label"}],"children":[{"id":"_help_HI6GBBIduIgv","title":"Labels","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Labels"},{"name":"iconClass","value":"bx bx-hash","type":"label"}]},{"id":"_help_Cq5X6iKQop6R","title":"Relations","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Relations"},{"name":"iconClass","value":"bx bx-transfer","type":"label"}]},{"id":"_help_bwZpz2ajCEwO","title":"Attribute Inheritance","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Attribute Inheritance"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]},{"id":"_help_OFXdgB2nNk1F","title":"Promoted Attributes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Promoted Attributes"},{"name":"iconClass","value":"bx bx-table","type":"label"}]}]},{"id":"_help_KC1HB96bqqHX","title":"Templates","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Templates"},{"name":"iconClass","value":"bx bx-copy","type":"label"}]},{"id":"_help_BCkXAVs63Ttv","title":"Note Map (Link map, Tree map)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note Map (Link map, Tree map)"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_R9pX4DGra2Vt","title":"Sharing","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing"},{"name":"iconClass","value":"bx bx-share-alt","type":"label"}],"children":[{"id":"_help_Qjt68inQ2bRj","title":"Serving directly the content of a note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing/Serving directly the content o"},{"name":"iconClass","value":"bx bx-code","type":"label"}]},{"id":"_help_ycBFjKrrwE9p","title":"Exporting static HTML for web publishing","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing/Exporting static HTML for web "},{"name":"iconClass","value":"bx bxs-file-html","type":"label"}]},{"id":"_help_sLIJ6f1dkJYW","title":"Reverse proxy configuration","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing/Reverse proxy configuration"},{"name":"iconClass","value":"bx bx-world","type":"label"}]}]},{"id":"_help_5668rwcirq1t","title":"Advanced Showcases","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases"},{"name":"iconClass","value":"bx bxs-component","type":"label"}],"children":[{"id":"_help_l0tKav7yLHGF","title":"Day Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes"},{"name":"iconClass","value":"bx bx-calendar","type":"label"}]},{"id":"_help_R7abl2fc6Mxi","title":"Weight Tracker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Weight Tracker"},{"name":"iconClass","value":"bx bx-line-chart","type":"label"}]},{"id":"_help_xYjQUYhpbUEW","title":"Task Manager","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Task Manager"},{"name":"iconClass","value":"bx bx-calendar-check","type":"label"}]}]},{"id":"_help_J5Ex1ZrMbyJ6","title":"Custom Request Handler","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Custom Request Handler"},{"name":"iconClass","value":"bx bx-globe","type":"label"}]},{"id":"_help_d3fAXQ2diepH","title":"Custom Resource Providers","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Custom Resource Providers"},{"name":"iconClass","value":"bx bxs-file-plus","type":"label"}]},{"id":"_help_pgxEVkzLl1OP","title":"ETAPI (REST API)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/ETAPI (REST API)"},{"name":"iconClass","value":"bx bx-extension","type":"label"}],"children":[{"id":"_help_9qPsTWBorUhQ","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://docs.triliumnotes.org/rest-api/etapi/"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"enforceAttributes":true}]},{"id":"_help_47ZrP6FNuoG8","title":"Default Note Title","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Default Note Title"},{"name":"iconClass","value":"bx bx-edit-alt","type":"label"}]},{"id":"_help_wX4HbRucYSDD","title":"Database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database"},{"name":"iconClass","value":"bx bx-data","type":"label"}],"children":[{"id":"_help_oyIAJ9PvvwHX","title":"Manually altering the database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Manually altering the database"},{"name":"iconClass","value":"bx bxs-edit","type":"label"}],"children":[{"id":"_help_YKWqdJhzi2VY","title":"SQL Console","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Manually altering the database/SQL Console"},{"name":"iconClass","value":"bx bx-data","type":"label"}]}]},{"id":"_help_6tZeKvSHEUiB","title":"Demo Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Demo Notes"},{"name":"iconClass","value":"bx bx-package","type":"label"}]}]},{"id":"_help_Gzjqa934BdH4","title":"Configuration (config.ini or environment variables)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or e"},{"name":"iconClass","value":"bx bx-cog","type":"label"}],"children":[{"id":"_help_c5xB8m4g2IY6","title":"Trilium instance","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or environment variables)/Trilium instance"},{"name":"iconClass","value":"bx bx-windows","type":"label"}]},{"id":"_help_LWtBjFej3wX3","title":"Cross-Origin Resource Sharing (CORS)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or environment variables)/Cross-Origin Resource Sharing "},{"name":"iconClass","value":"bx bx-lock","type":"label"}]}]},{"id":"_help_ivYnonVFBxbQ","title":"Bulk Actions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Bulk Actions"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]},{"id":"_help_4FahAwuGTAwC","title":"Note source","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note source"},{"name":"iconClass","value":"bx bx-code","type":"label"}]},{"id":"_help_1YeN2MzFUluU","title":"Technologies used","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used"},{"name":"iconClass","value":"bx bx-pyramid","type":"label"}],"children":[{"id":"_help_MI26XDLSAlCD","title":"CKEditor","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/CKEditor"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_N4IDkixaDG9C","title":"MindElixir","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/MindElixir"},{"name":"iconClass","value":"bx bx-sitemap","type":"label"}]},{"id":"_help_H0mM1lTxF9JI","title":"Excalidraw","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/Excalidraw"},{"name":"iconClass","value":"bx bx-pen","type":"label"}]},{"id":"_help_MQHyy2dIFgxS","title":"Leaflet","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/Leaflet"},{"name":"iconClass","value":"bx bx-map-alt","type":"label"}]}]},{"id":"_help_m1lbrzyKDaRB","title":"Note ID","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note ID"},{"name":"iconClass","value":"bx bx-hash","type":"label"}]},{"id":"_help_0vTSyvhPTAOz","title":"Internal API","type":"book","attributes":[{"name":"iconClass","value":"bx bxs-component","type":"label"}],"children":[{"id":"_help_z8O2VG4ZZJD7","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://docs.triliumnotes.org/rest-api/internal/"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"enforceAttributes":true}]},{"id":"_help_2mUhVmZK8RF3","title":"Hidden Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Hidden Notes"},{"name":"iconClass","value":"bx bx-hide","type":"label"}]},{"id":"_help_uYF7pmepw27K","title":"Metrics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Metrics"},{"name":"iconClass","value":"bx bxs-data","type":"label"}],"children":[{"id":"_help_bOP3TB56fL1V","title":"grafana-dashboard.json","type":"doc","attributes":[{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_64ZTlUPgEPtW","title":"Safe mode","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Safe mode"},{"name":"iconClass","value":"bx bxs-virus-block","type":"label"}]},{"id":"_help_HAIOFBoYIIdO","title":"Nightly release","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Nightly release"},{"name":"iconClass","value":"bx bx-moon","type":"label"}]},{"id":"_help_ZmT9ln8XJX2o","title":"Read-only database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Read-only database"},{"name":"iconClass","value":"bx bx-book-reader","type":"label"}]}]},{"id":"_help_GBBMSlVSOIGP","title":"AI","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI"},{"name":"iconClass","value":"bx bx-bot","type":"label"}],"children":[{"id":"_help_WkM7gsEUyCXs","title":"Providers","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Providers"},{"name":"iconClass","value":"bx bx-select-multiple","type":"label"}],"children":[{"id":"_help_7EdTxPADv95W","title":"Ollama","type":"book","attributes":[{"name":"iconClass","value":"bx bx-message-dots","type":"label"}],"children":[{"id":"_help_vvUCN7FDkq7G","title":"Installing Ollama","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Providers/Ollama/Installing Ollama"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_ZavFigBX9AwP","title":"OpenAI","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Providers/OpenAI"},{"name":"iconClass","value":"bx bx-message-dots","type":"label"}]},{"id":"_help_e0lkirXEiSNc","title":"Anthropic","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Providers/Anthropic"},{"name":"iconClass","value":"bx bx-message-dots","type":"label"}]}]}]},{"id":"_help_CdNpE2pqjmI6","title":"Scripting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting"},{"name":"iconClass","value":"bx bxs-file-js","type":"label"}],"children":[{"id":"_help_yIhgI5H7A2Sm","title":"Frontend Basics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics"},{"name":"iconClass","value":"bx bx-window","type":"label"}],"children":[{"id":"_help_MgibgPcfeuGz","title":"Custom Widgets","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets"},{"name":"iconClass","value":"bx bxs-widget","type":"label"}],"children":[{"id":"_help_YNxAqkI5Kg1M","title":"Word count widget","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/Word count widget"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_SynTBQiBsdYJ","title":"Widget Basics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/Widget Basics"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_M8IppdwVHSjG","title":"Right pane widget","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/Right pane widget"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_VqGQnnPGnqAU","title":"CSS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/CSS"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_es8OU2GuguFU","title":"Examples","type":"book","attributes":[{"name":"iconClass","value":"bx bx-code-alt","type":"label"}],"children":[{"id":"_help_TjLYAo3JMO8X","title":"\"New Task\" launcher button","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Examples/New Task launcher button"},{"name":"iconClass","value":"bx bx-task","type":"label"}]},{"id":"_help_7kZPMD0uFwkH","title":"Downloading responses from Google Forms","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Examples/Downloading responses from Goo"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_DL92EjAaXT26","title":"Using promoted attributes to configure scripts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Examples/Using promoted attributes to c"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]}]},{"id":"_help_SPirpZypehBG","title":"Backend scripts","type":"book","attributes":[{"name":"iconClass","value":"bx bx-server","type":"label"}],"children":[{"id":"_help_fZ2IGYFXjkEy","title":"Server-side imports","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Backend scripts/Server-side imports"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_GPERMystNGTB","title":"Events","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Backend scripts/Events"},{"name":"iconClass","value":"bx bx-rss","type":"label"}]}]},{"id":"_help_GLks18SNjxmC","title":"Script API","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Script API"},{"name":"iconClass","value":"bx bx-code-curly","type":"label"}],"children":[{"id":"_help_Q2z6av6JZVWm","title":"Frontend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://docs.triliumnotes.org/script-api/frontend"},{"name":"iconClass","value":"bx bx-folder","type":"label"}],"enforceAttributes":true,"children":[{"id":"_help_habiZ3HU8Kw8","title":"FNote","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://docs.triliumnotes.org/script-api/frontend/interfaces/FNote.html"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"enforceAttributes":true}]},{"id":"_help_MEtfsqa5VwNi","title":"Backend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://docs.triliumnotes.org/script-api/backend"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"enforceAttributes":true}]},{"id":"_help_vElnKeDNPSVl","title":"Logging","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Logging"},{"name":"iconClass","value":"bx bx-terminal","type":"label"}]}]},{"id":"_help_Fm0j45KqyHpU","title":"Miscellaneous","type":"book","attributes":[{"name":"iconClass","value":"bx bx-info-circle","type":"label"}],"children":[{"id":"_help_WFbFXrgnDyyU","title":"Privacy Policy","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Miscellaneous/Privacy Policy"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_NcsmUYZRWEW4","title":"Patterns of personal knowledge","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Miscellaneous/Patterns of personal knowledge"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]}]
\ No newline at end of file
+[{"id":"_help_BOCnjTMBCoxW","title":"Feature Highlights","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Feature Highlights"},{"name":"iconClass","value":"bx bx-star","type":"label"}]},{"id":"_help_Otzi9La2YAUX","title":"Installation & Setup","type":"book","attributes":[{"name":"iconClass","value":"bx bx-cog","type":"label"}],"children":[{"id":"_help_poXkQfguuA0U","title":"Desktop Installation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Desktop Installation"},{"name":"iconClass","value":"bx bx-desktop","type":"label"}],"children":[{"id":"_help_nRqcgfTb97uV","title":"Using the desktop application as a server","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Desktop Installation/Using the desktop application "},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_Rp0q8bSP6Ayl","title":"System Requirements","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Desktop Installation/System Requirements"},{"name":"iconClass","value":"bx bx-chip","type":"label"}]},{"id":"_help_Un4wj2Mak2Ky","title":"Nix flake","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Desktop Installation/Nix flake"},{"name":"iconClass","value":"bx bxl-tux","type":"label"}]}]},{"id":"_help_WOcw2SLH6tbX","title":"Server Installation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation"},{"name":"iconClass","value":"bx bx-server","type":"label"}],"children":[{"id":"_help_Dgg7bR3b6K9j","title":"1. Installing the server","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_3tW6mORuTHnB","title":"Packaged version for Linux","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Packaged version for Linux"},{"name":"iconClass","value":"bx bxl-tux","type":"label"}]},{"id":"_help_rWX5eY045zbE","title":"Using Docker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Docker"},{"name":"iconClass","value":"bx bxl-docker","type":"label"}]},{"id":"_help_moVgBcoxE3EK","title":"On NixOS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/On NixOS"},{"name":"iconClass","value":"bx bxl-tux","type":"label"}]},{"id":"_help_J1Bb6lVlwU5T","title":"Manually","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Manually"},{"name":"iconClass","value":"bx bx-code-alt","type":"label"}]},{"id":"_help_DCmT6e7clMoP","title":"Using Kubernetes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Kubernetes"},{"name":"iconClass","value":"bx bxl-kubernetes","type":"label"}]},{"id":"_help_klCWNks3ReaQ","title":"Multiple server instances","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Multiple server instances"},{"name":"iconClass","value":"bx bxs-user-account","type":"label"}]}]},{"id":"_help_vcjrb3VVYPZI","title":"2. Reverse proxy","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_ud6MShXL4WpO","title":"Nginx","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Nginx"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_fDLvzOx29Pfg","title":"Apache using Docker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Apache using Docker"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_LLzSMXACKhUs","title":"Trusted proxy","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Trusted proxy"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_l2VkvOwUNfZj","title":"HTTPS (TLS)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/HTTPS (TLS)"},{"name":"iconClass","value":"bx bx-lock-alt","type":"label"}]},{"id":"_help_0hzsNCP31IAB","title":"Authentication","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/Authentication"},{"name":"iconClass","value":"bx bx-user","type":"label"}]},{"id":"_help_7DAiwaf8Z7Rz","title":"Multi-Factor Authentication","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/Multi-Factor Authentication"},{"name":"iconClass","value":"bx bx-stopwatch","type":"label"}]},{"id":"_help_Un4wj2Mak2Ky","title":"Nix flake","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/Nix flake.clone"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_yeEaYqosGLSh","title":"Third-party cloud hosting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/Third-party cloud hosting"},{"name":"iconClass","value":"bx bx-cloud","type":"label"}]},{"id":"_help_iGTnKjubbXkA","title":"System Requirements","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/System Requirements"},{"name":"iconClass","value":"bx bx-chip","type":"label"}]}]},{"id":"_help_cbkrhQjrkKrh","title":"Synchronization","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Synchronization"},{"name":"iconClass","value":"bx bx-sync","type":"label"}]},{"id":"_help_RDslemsQ6gCp","title":"Mobile Frontend","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Mobile Frontend"},{"name":"iconClass","value":"bx bx-mobile-alt","type":"label"}]},{"id":"_help_MtPxeAWVAzMg","title":"Web Clipper","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Web Clipper"},{"name":"iconClass","value":"bx bx-paperclip","type":"label"}]},{"id":"_help_n1lujUxCwipy","title":"Upgrading TriliumNext","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Upgrading TriliumNext"},{"name":"iconClass","value":"bx bx-up-arrow-alt","type":"label"}]},{"id":"_help_ODY7qQn5m2FT","title":"Backup","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Backup"},{"name":"iconClass","value":"bx bx-hdd","type":"label"}]},{"id":"_help_tAassRL4RSQL","title":"Data directory","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Data directory"},{"name":"iconClass","value":"bx bx-folder-open","type":"label"}]}]},{"id":"_help_gh7bpGYxajRS","title":"Basic Concepts and Features","type":"book","attributes":[{"name":"iconClass","value":"bx bx-help-circle","type":"label"}],"children":[{"id":"_help_Vc8PjrjAGuOp","title":"UI Elements","type":"book","attributes":[{"name":"iconClass","value":"bx bx-window-alt","type":"label"}],"children":[{"id":"_help_x0JgW8UqGXvq","title":"Vertical and horizontal layout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Vertical and horizontal layout"},{"name":"iconClass","value":"bx bxs-layout","type":"label"}]},{"id":"_help_x3i7MxGccDuM","title":"Global menu","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Global menu"},{"name":"iconClass","value":"bx bx-menu","type":"label"}]},{"id":"_help_oPVyFC7WL2Lp","title":"Note Tree","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree"},{"name":"iconClass","value":"bx bxs-tree-alt","type":"label"}],"children":[{"id":"_help_YtSN43OrfzaA","title":"Note tree contextual menu","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu"},{"name":"iconClass","value":"bx bx-menu","type":"label"}]},{"id":"_help_yTjUdsOi4CIE","title":"Multiple selection","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Multiple selection"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]},{"id":"_help_DvdZhoQZY9Yd","title":"Keyboard shortcuts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Keyboard shortcuts"},{"name":"iconClass","value":"bx bxs-keyboard","type":"label"}]}]},{"id":"_help_BlN9DFI679QC","title":"Ribbon","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Ribbon"},{"name":"iconClass","value":"bx bx-dots-horizontal","type":"label"}]},{"id":"_help_3seOhtN8uLIY","title":"Tabs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Tabs"},{"name":"iconClass","value":"bx bx-dock-top","type":"label"}]},{"id":"_help_xYmIYSP6wE3F","title":"Launch Bar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Launch Bar"},{"name":"iconClass","value":"bx bx-sidebar","type":"label"}]},{"id":"_help_8YBEPzcpUgxw","title":"Note buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note buttons"},{"name":"iconClass","value":"bx bx-dots-vertical-rounded","type":"label"}]},{"id":"_help_4TIF1oA4VQRO","title":"Options","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Options"},{"name":"iconClass","value":"bx bx-cog","type":"label"}]},{"id":"_help_luNhaphA37EO","title":"Split View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Split View"},{"name":"iconClass","value":"bx bx-dock-right","type":"label"}]},{"id":"_help_XpOYSgsLkTJy","title":"Floating buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Floating buttons"},{"name":"iconClass","value":"bx bx-rectangle","type":"label"}]},{"id":"_help_RnaPdbciOfeq","title":"Right Sidebar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Right Sidebar"},{"name":"iconClass","value":"bx bxs-dock-right","type":"label"}]},{"id":"_help_r5JGHN99bVKn","title":"Recent Changes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Recent Changes"},{"name":"iconClass","value":"bx bx-history","type":"label"}]},{"id":"_help_ny318J39E5Z0","title":"Zoom","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Zoom"},{"name":"iconClass","value":"bx bx-zoom-in","type":"label"}]},{"id":"_help_ZjLYv08Rp3qC","title":"Quick edit","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Quick edit"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_lgKX7r3aL30x","title":"Note Tooltip","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tooltip"},{"name":"iconClass","value":"bx bx-message-detail","type":"label"}]}]},{"id":"_help_BFs8mudNFgCS","title":"Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes"},{"name":"iconClass","value":"bx bx-notepad","type":"label"}],"children":[{"id":"_help_p9kXRFAkwN4o","title":"Note Icons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note Icons"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]},{"id":"_help_0vhv7lsOLy82","title":"Attachments","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Attachments"},{"name":"iconClass","value":"bx bx-paperclip","type":"label"}]},{"id":"_help_IakOLONlIfGI","title":"Cloning Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Cloning Notes"},{"name":"iconClass","value":"bx bx-duplicate","type":"label"}],"children":[{"id":"_help_TBwsyfadTA18","title":"Branch prefix","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Cloning Notes/Branch prefix"},{"name":"iconClass","value":"bx bx-rename","type":"label"}]}]},{"id":"_help_bwg0e8ewQMak","title":"Protected Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Protected Notes"},{"name":"iconClass","value":"bx bx-lock-alt","type":"label"}]},{"id":"_help_MKmLg5x6xkor","title":"Archived Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Archived Notes"},{"name":"iconClass","value":"bx bx-box","type":"label"}]},{"id":"_help_vZWERwf8U3nx","title":"Note Revisions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note Revisions"},{"name":"iconClass","value":"bx bx-history","type":"label"}]},{"id":"_help_aGlEvb9hyDhS","title":"Sorting Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Sorting Notes"},{"name":"iconClass","value":"bx bx-sort-up","type":"label"}]},{"id":"_help_NRnIZmSMc5sj","title":"Printing & Exporting as PDF","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF"},{"name":"iconClass","value":"bx bx-printer","type":"label"}]},{"id":"_help_CoFPLs3dRlXc","title":"Read-Only Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Read-Only Notes"},{"name":"iconClass","value":"bx bx-edit-alt","type":"label"}]},{"id":"_help_0ESUbbAxVnoK","title":"Note List","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note List"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]}]},{"id":"_help_wArbEsdSae6g","title":"Navigation","type":"book","attributes":[{"name":"iconClass","value":"bx bx-navigation","type":"label"}],"children":[{"id":"_help_kBrnXNG3Hplm","title":"Tree Concepts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Tree Concepts"},{"name":"iconClass","value":"bx bx-pyramid","type":"label"}]},{"id":"_help_MMiBEQljMQh2","title":"Note Navigation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Note Navigation"},{"name":"iconClass","value":"bx bxs-navigation","type":"label"}]},{"id":"_help_Ms1nauBra7gq","title":"Quick search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Quick search"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]},{"id":"_help_F1r9QtzQLZqm","title":"Jump to...","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Jump to"},{"name":"iconClass","value":"bx bx-send","type":"label"}]},{"id":"_help_eIg8jdvaoNNd","title":"Search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Search"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]},{"id":"_help_u3YFHC9tQlpm","title":"Bookmarks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Bookmarks"},{"name":"iconClass","value":"bx bx-bookmarks","type":"label"}]},{"id":"_help_OR8WJ7Iz9K4U","title":"Note Hoisting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Note Hoisting"},{"name":"iconClass","value":"bx bxs-chevrons-up","type":"label"}]},{"id":"_help_ZjLYv08Rp3qC","title":"Quick edit","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Quick edit.clone"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_9sRHySam5fXb","title":"Workspaces","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Workspaces"},{"name":"iconClass","value":"bx bx-door-open","type":"label"}]},{"id":"_help_xWtq5NUHOwql","title":"Similar Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Similar Notes"},{"name":"iconClass","value":"bx bx-bar-chart","type":"label"}]},{"id":"_help_McngOG2jbUWX","title":"Search in note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Search in note"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]}]},{"id":"_help_A9Oc6YKKc65v","title":"Keyboard Shortcuts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Keyboard Shortcuts"},{"name":"iconClass","value":"bx bxs-keyboard","type":"label"}]},{"id":"_help_Wy267RK4M69c","title":"Themes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Themes"},{"name":"iconClass","value":"bx bx-palette","type":"label"}],"children":[{"id":"_help_VbjZvtUek0Ln","title":"Theme Gallery","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Themes/Theme Gallery"},{"name":"iconClass","value":"bx bx-book-reader","type":"label"}]}]},{"id":"_help_mHbBMPDPkVV5","title":"Import & Export","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export"},{"name":"iconClass","value":"bx bx-import","type":"label"}],"children":[{"id":"_help_Oau6X9rCuegd","title":"Markdown","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown"},{"name":"iconClass","value":"bx bxl-markdown","type":"label"}],"children":[{"id":"_help_rJ9grSgoExl9","title":"Supported syntax","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown/Supported syntax"},{"name":"iconClass","value":"bx bx-code-alt","type":"label"}]}]},{"id":"_help_syuSEKf2rUGr","title":"Evernote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/Evernote"},{"name":"iconClass","value":"bx bx-window-open","type":"label"}]},{"id":"_help_GnhlmrATVqcH","title":"OneNote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/OneNote"},{"name":"iconClass","value":"bx bx-window-open","type":"label"}]}]},{"id":"_help_rC3pL2aptaRE","title":"Zen mode","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Zen mode"},{"name":"iconClass","value":"bx bxs-yin-yang","type":"label"}]}]},{"id":"_help_s3YCWHBfmYuM","title":"Quick Start","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Quick Start"},{"name":"iconClass","value":"bx bx-run","type":"label"}]},{"id":"_help_i6dbnitykE5D","title":"FAQ","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/FAQ"},{"name":"iconClass","value":"bx bx-question-mark","type":"label"}]},{"id":"_help_KSZ04uQ2D1St","title":"Note Types","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types"},{"name":"iconClass","value":"bx bx-edit","type":"label"}],"children":[{"id":"_help_iPIMuisry3hd","title":"Text","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text"},{"name":"iconClass","value":"bx bx-note","type":"label"}],"children":[{"id":"_help_NwBbFdNZ9h7O","title":"Block quotes & admonitions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Block quotes & admonitions"},{"name":"iconClass","value":"bx bx-info-circle","type":"label"}]},{"id":"_help_oSuaNgyyKnhu","title":"Bookmarks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Bookmarks"},{"name":"iconClass","value":"bx bx-bookmark","type":"label"}]},{"id":"_help_veGu4faJErEM","title":"Content language & Right-to-left support","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Content language & Right-to-le"},{"name":"iconClass","value":"bx bx-align-right","type":"label"}]},{"id":"_help_2x0ZAX9ePtzV","title":"Cut to subnote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Cut to subnote"},{"name":"iconClass","value":"bx bx-cut","type":"label"}]},{"id":"_help_UYuUB1ZekNQU","title":"Developer-specific formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Developer-specific formatting"},{"name":"iconClass","value":"bx bx-code-alt","type":"label"}],"children":[{"id":"_help_QxEyIjRBizuC","title":"Code blocks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Developer-specific formatting/Code blocks"},{"name":"iconClass","value":"bx bx-code","type":"label"}]}]},{"id":"_help_AgjCISero73a","title":"Footnotes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Footnotes"},{"name":"iconClass","value":"bx bx-bracket","type":"label"}]},{"id":"_help_nRhnJkTT8cPs","title":"Formatting toolbar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Formatting toolbar"},{"name":"iconClass","value":"bx bx-text","type":"label"}]},{"id":"_help_Gr6xFaF6ioJ5","title":"General formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/General formatting"},{"name":"iconClass","value":"bx bx-bold","type":"label"}]},{"id":"_help_AxshuNRegLAv","title":"Highlights list","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Highlights list"},{"name":"iconClass","value":"bx bx-highlight","type":"label"}]},{"id":"_help_mT0HEkOsz6i1","title":"Images","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Images"},{"name":"iconClass","value":"bx bx-image-alt","type":"label"}],"children":[{"id":"_help_0Ofbk1aSuVRu","title":"Image references","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Images/Image references"},{"name":"iconClass","value":"bx bxs-file-image","type":"label"}]}]},{"id":"_help_nBAXQFj20hS1","title":"Include Note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Include Note"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_CohkqWQC1iBv","title":"Insert buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Insert buttons"},{"name":"iconClass","value":"bx bx-plus","type":"label"}]},{"id":"_help_oiVPnW8QfnvS","title":"Keyboard shortcuts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Keyboard shortcuts"},{"name":"iconClass","value":"bx bxs-keyboard","type":"label"}]},{"id":"_help_QEAPj01N5f7w","title":"Links","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Links"},{"name":"iconClass","value":"bx bx-link-alt","type":"label"}],"children":[{"id":"_help_3IDVtesTQ8ds","title":"External links","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Links/External links"},{"name":"iconClass","value":"bx bx-link-external","type":"label"}]},{"id":"_help_hrZ1D00cLbal","title":"Internal (reference) links","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Links/Internal (reference) links"},{"name":"iconClass","value":"bx bx-link","type":"label"}]}]},{"id":"_help_S6Xx8QIWTV66","title":"Lists","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Lists"},{"name":"iconClass","value":"bx bx-list-ul","type":"label"}]},{"id":"_help_QrtTYPmdd1qq","title":"Markdown-like formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Markdown-like formatting"},{"name":"iconClass","value":"bx bxl-markdown","type":"label"}]},{"id":"_help_YfYAtQBcfo5V","title":"Math Equations","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Math Equations"},{"name":"iconClass","value":"bx bx-math","type":"label"}]},{"id":"_help_dEHYtoWWi8ct","title":"Other features","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Other features"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]},{"id":"_help_gLt3vA97tMcp","title":"Premium features","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Premium features"},{"name":"iconClass","value":"bx bx-star","type":"label"}],"children":[{"id":"_help_ZlN4nump6EbW","title":"Slash Commands","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Premium features/Slash Commands"},{"name":"iconClass","value":"bx bx-menu","type":"label"}]},{"id":"_help_pwc194wlRzcH","title":"Text Snippets","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Premium features/Text Snippets"},{"name":"iconClass","value":"bx bx-align-left","type":"label"}]}]},{"id":"_help_BFvAtE74rbP6","title":"Table of contents","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Table of contents"},{"name":"iconClass","value":"bx bx-heading","type":"label"}]},{"id":"_help_NdowYOC1GFKS","title":"Tables","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Tables"},{"name":"iconClass","value":"bx bx-table","type":"label"}]}]},{"id":"_help_6f9hih2hXXZk","title":"Code","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Code"},{"name":"iconClass","value":"bx bx-code","type":"label"}]},{"id":"_help_m523cpzocqaD","title":"Saved Search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Saved Search"},{"name":"iconClass","value":"bx bx-file-find","type":"label"}]},{"id":"_help_iRwzGnHPzonm","title":"Relation Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Relation Map"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_bdUJEHsAPYQR","title":"Note Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Note Map"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_HcABDtFCkbFN","title":"Render Note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Render Note"},{"name":"iconClass","value":"bx bx-extension","type":"label"}]},{"id":"_help_s1aBHPd79XYj","title":"Mermaid Diagrams","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mermaid Diagrams"},{"name":"iconClass","value":"bx bx-selection","type":"label"}],"children":[{"id":"_help_RH6yLjjWJHof","title":"ELK layout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mermaid Diagrams/ELK layout"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_WWgeUaBb7UfC","title":"Syntax reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://mermaid.js.org/intro/syntax-reference.html"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"enforceAttributes":true}]},{"id":"_help_grjYqerjn243","title":"Canvas","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Canvas"},{"name":"iconClass","value":"bx bx-pen","type":"label"}]},{"id":"_help_1vHRoWCEjj0L","title":"Web View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Web View"},{"name":"iconClass","value":"bx bx-globe-alt","type":"label"}]},{"id":"_help_gBbsAeiuUxI5","title":"Mind Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mind Map"},{"name":"iconClass","value":"bx bx-sitemap","type":"label"}]},{"id":"_help_W8vYD3Q1zjCR","title":"File","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/File"},{"name":"iconClass","value":"bx bx-file-blank","type":"label"}]}]},{"id":"_help_GTwFsgaA0lCt","title":"Collections","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections"},{"name":"iconClass","value":"bx bx-book","type":"label"}],"children":[{"id":"_help_xWbu3jpNWapp","title":"Calendar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Calendar"},{"name":"iconClass","value":"bx bx-calendar","type":"label"}]},{"id":"_help_2FvYrpmOXm29","title":"Table","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Table"},{"name":"iconClass","value":"bx bx-table","type":"label"}]},{"id":"_help_CtBQqbwXDx1w","title":"Kanban Board","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Kanban Board"},{"name":"iconClass","value":"bx bx-columns","type":"label"}]},{"id":"_help_81SGnPGMk7Xc","title":"Geo Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Geo Map"},{"name":"iconClass","value":"bx bx-map-alt","type":"label"}]},{"id":"_help_zP3PMqaG71Ct","title":"Presentation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Presentation"},{"name":"iconClass","value":"bx bx-slideshow","type":"label"}]},{"id":"_help_8QqnMzx393bx","title":"Grid View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/Grid View"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]},{"id":"_help_mULW0Q3VojwY","title":"List View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Collections/List View"},{"name":"iconClass","value":"bx bx-list-ul","type":"label"}]}]},{"id":"_help_BgmBlOIl72jZ","title":"Troubleshooting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting"},{"name":"iconClass","value":"bx bx-bug","type":"label"}],"children":[{"id":"_help_wy8So3yZZlH9","title":"Reporting issues","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Reporting issues"},{"name":"iconClass","value":"bx bx-bug-alt","type":"label"}]},{"id":"_help_x59R8J8KV5Bp","title":"Anonymized Database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Anonymized Database"},{"name":"iconClass","value":"bx bx-low-vision","type":"label"}]},{"id":"_help_qzNzp9LYQyPT","title":"Error logs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Error logs"},{"name":"iconClass","value":"bx bx-comment-error","type":"label"}],"children":[{"id":"_help_bnyigUA2UK7s","title":"Backend (server) logs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Error logs/Backend (server) logs"},{"name":"iconClass","value":"bx bx-server","type":"label"}]},{"id":"_help_9yEHzMyFirZR","title":"Frontend logs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Error logs/Frontend logs"},{"name":"iconClass","value":"bx bx-window-alt","type":"label"}]}]},{"id":"_help_vdlYGAcpXAgc","title":"Synchronization fails with 504 Gateway Timeout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Synchronization fails with 504"},{"name":"iconClass","value":"bx bx-error","type":"label"}]},{"id":"_help_s8alTXmpFR61","title":"Refreshing the application","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Refreshing the application"},{"name":"iconClass","value":"bx bx-refresh","type":"label"}]}]},{"id":"_help_pKK96zzmvBGf","title":"Theme development","type":"book","attributes":[{"name":"iconClass","value":"bx bx-palette","type":"label"}],"children":[{"id":"_help_7NfNr5pZpVKV","title":"Creating a custom theme","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Creating a custom theme"},{"name":"iconClass","value":"bx bxs-color","type":"label"}]},{"id":"_help_WFGzWeUK6arS","title":"Customize the Next theme","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Customize the Next theme"},{"name":"iconClass","value":"bx bx-news","type":"label"}]},{"id":"_help_WN5z4M8ASACJ","title":"Reference","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Reference"},{"name":"iconClass","value":"bx bx-book-open","type":"label"}]},{"id":"_help_AlhDUqhENtH7","title":"Custom app-wide CSS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Custom app-wide CSS"},{"name":"iconClass","value":"bx bxs-file-css","type":"label"}]}]},{"id":"_help_tC7s2alapj8V","title":"Advanced Usage","type":"book","attributes":[{"name":"iconClass","value":"bx bx-rocket","type":"label"}],"children":[{"id":"_help_zEY4DaJG4YT5","title":"Attributes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes"},{"name":"iconClass","value":"bx bx-list-check","type":"label"}],"children":[{"id":"_help_HI6GBBIduIgv","title":"Labels","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Labels"},{"name":"iconClass","value":"bx bx-hash","type":"label"}]},{"id":"_help_Cq5X6iKQop6R","title":"Relations","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Relations"},{"name":"iconClass","value":"bx bx-transfer","type":"label"}]},{"id":"_help_bwZpz2ajCEwO","title":"Attribute Inheritance","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Attribute Inheritance"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]},{"id":"_help_OFXdgB2nNk1F","title":"Promoted Attributes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Promoted Attributes"},{"name":"iconClass","value":"bx bx-table","type":"label"}]}]},{"id":"_help_KC1HB96bqqHX","title":"Templates","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Templates"},{"name":"iconClass","value":"bx bx-copy","type":"label"}]},{"id":"_help_BCkXAVs63Ttv","title":"Note Map (Link map, Tree map)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note Map (Link map, Tree map)"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_R9pX4DGra2Vt","title":"Sharing","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing"},{"name":"iconClass","value":"bx bx-share-alt","type":"label"}],"children":[{"id":"_help_Qjt68inQ2bRj","title":"Serving directly the content of a note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing/Serving directly the content o"},{"name":"iconClass","value":"bx bx-code","type":"label"}]},{"id":"_help_ycBFjKrrwE9p","title":"Exporting static HTML for web publishing","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing/Exporting static HTML for web "},{"name":"iconClass","value":"bx bxs-file-html","type":"label"}]},{"id":"_help_sLIJ6f1dkJYW","title":"Reverse proxy configuration","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing/Reverse proxy configuration"},{"name":"iconClass","value":"bx bx-world","type":"label"}]}]},{"id":"_help_5668rwcirq1t","title":"Advanced Showcases","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases"},{"name":"iconClass","value":"bx bxs-component","type":"label"}],"children":[{"id":"_help_l0tKav7yLHGF","title":"Day Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes"},{"name":"iconClass","value":"bx bx-calendar","type":"label"}]},{"id":"_help_R7abl2fc6Mxi","title":"Weight Tracker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Weight Tracker"},{"name":"iconClass","value":"bx bx-line-chart","type":"label"}]},{"id":"_help_xYjQUYhpbUEW","title":"Task Manager","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Task Manager"},{"name":"iconClass","value":"bx bx-calendar-check","type":"label"}]}]},{"id":"_help_J5Ex1ZrMbyJ6","title":"Custom Request Handler","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Custom Request Handler"},{"name":"iconClass","value":"bx bx-globe","type":"label"}]},{"id":"_help_d3fAXQ2diepH","title":"Custom Resource Providers","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Custom Resource Providers"},{"name":"iconClass","value":"bx bxs-file-plus","type":"label"}]},{"id":"_help_pgxEVkzLl1OP","title":"ETAPI (REST API)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/ETAPI (REST API)"},{"name":"iconClass","value":"bx bx-extension","type":"label"}],"children":[{"id":"_help_9qPsTWBorUhQ","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://docs.triliumnotes.org/rest-api/etapi/"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"enforceAttributes":true}]},{"id":"_help_xB9eL2mK8vWp","title":"External Blob Storage","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/External Blob Storage"},{"name":"iconClass","value":"bx bx-hdd","type":"label"}]},{"id":"_help_47ZrP6FNuoG8","title":"Default Note Title","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Default Note Title"},{"name":"iconClass","value":"bx bx-edit-alt","type":"label"}]},{"id":"_help_wX4HbRucYSDD","title":"Database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database"},{"name":"iconClass","value":"bx bx-data","type":"label"}],"children":[{"id":"_help_oyIAJ9PvvwHX","title":"Manually altering the database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Manually altering the database"},{"name":"iconClass","value":"bx bxs-edit","type":"label"}],"children":[{"id":"_help_YKWqdJhzi2VY","title":"SQL Console","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Manually altering the database/SQL Console"},{"name":"iconClass","value":"bx bx-data","type":"label"}]}]},{"id":"_help_6tZeKvSHEUiB","title":"Demo Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Demo Notes"},{"name":"iconClass","value":"bx bx-package","type":"label"}]}]},{"id":"_help_Gzjqa934BdH4","title":"Configuration (config.ini or environment variables)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or e"},{"name":"iconClass","value":"bx bx-cog","type":"label"}],"children":[{"id":"_help_c5xB8m4g2IY6","title":"Trilium instance","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or environment variables)/Trilium instance"},{"name":"iconClass","value":"bx bx-windows","type":"label"}]},{"id":"_help_LWtBjFej3wX3","title":"Cross-Origin Resource Sharing (CORS)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or environment variables)/Cross-Origin Resource Sharing "},{"name":"iconClass","value":"bx bx-lock","type":"label"}]}]},{"id":"_help_ivYnonVFBxbQ","title":"Bulk Actions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Bulk Actions"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]},{"id":"_help_4FahAwuGTAwC","title":"Note source","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note source"},{"name":"iconClass","value":"bx bx-code","type":"label"}]},{"id":"_help_1YeN2MzFUluU","title":"Technologies used","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used"},{"name":"iconClass","value":"bx bx-pyramid","type":"label"}],"children":[{"id":"_help_MI26XDLSAlCD","title":"CKEditor","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/CKEditor"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_N4IDkixaDG9C","title":"MindElixir","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/MindElixir"},{"name":"iconClass","value":"bx bx-sitemap","type":"label"}]},{"id":"_help_H0mM1lTxF9JI","title":"Excalidraw","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/Excalidraw"},{"name":"iconClass","value":"bx bx-pen","type":"label"}]},{"id":"_help_MQHyy2dIFgxS","title":"Leaflet","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/Leaflet"},{"name":"iconClass","value":"bx bx-map-alt","type":"label"}]}]},{"id":"_help_m1lbrzyKDaRB","title":"Note ID","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note ID"},{"name":"iconClass","value":"bx bx-hash","type":"label"}]},{"id":"_help_0vTSyvhPTAOz","title":"Internal API","type":"book","attributes":[{"name":"iconClass","value":"bx bxs-component","type":"label"}],"children":[{"id":"_help_z8O2VG4ZZJD7","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://docs.triliumnotes.org/rest-api/internal/"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"enforceAttributes":true}]},{"id":"_help_2mUhVmZK8RF3","title":"Hidden Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Hidden Notes"},{"name":"iconClass","value":"bx bx-hide","type":"label"}]},{"id":"_help_uYF7pmepw27K","title":"Metrics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Metrics"},{"name":"iconClass","value":"bx bxs-data","type":"label"}],"children":[{"id":"_help_bOP3TB56fL1V","title":"grafana-dashboard.json","type":"doc","attributes":[{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_64ZTlUPgEPtW","title":"Safe mode","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Safe mode"},{"name":"iconClass","value":"bx bxs-virus-block","type":"label"}]},{"id":"_help_HAIOFBoYIIdO","title":"Nightly release","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Nightly release"},{"name":"iconClass","value":"bx bx-moon","type":"label"}]},{"id":"_help_ZmT9ln8XJX2o","title":"Read-only database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Read-only database"},{"name":"iconClass","value":"bx bx-book-reader","type":"label"}]}]},{"id":"_help_GBBMSlVSOIGP","title":"AI","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI"},{"name":"iconClass","value":"bx bx-bot","type":"label"}],"children":[{"id":"_help_WkM7gsEUyCXs","title":"Providers","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Providers"},{"name":"iconClass","value":"bx bx-select-multiple","type":"label"}],"children":[{"id":"_help_7EdTxPADv95W","title":"Ollama","type":"book","attributes":[{"name":"iconClass","value":"bx bx-message-dots","type":"label"}],"children":[{"id":"_help_vvUCN7FDkq7G","title":"Installing Ollama","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Providers/Ollama/Installing Ollama"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_ZavFigBX9AwP","title":"OpenAI","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Providers/OpenAI"},{"name":"iconClass","value":"bx bx-message-dots","type":"label"}]},{"id":"_help_e0lkirXEiSNc","title":"Anthropic","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Providers/Anthropic"},{"name":"iconClass","value":"bx bx-message-dots","type":"label"}]}]}]},{"id":"_help_CdNpE2pqjmI6","title":"Scripting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting"},{"name":"iconClass","value":"bx bxs-file-js","type":"label"}],"children":[{"id":"_help_yIhgI5H7A2Sm","title":"Frontend Basics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics"},{"name":"iconClass","value":"bx bx-window","type":"label"}],"children":[{"id":"_help_MgibgPcfeuGz","title":"Custom Widgets","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets"},{"name":"iconClass","value":"bx bxs-widget","type":"label"}],"children":[{"id":"_help_YNxAqkI5Kg1M","title":"Word count widget","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/Word count widget"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_SynTBQiBsdYJ","title":"Widget Basics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/Widget Basics"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_M8IppdwVHSjG","title":"Right pane widget","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/Right pane widget"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_VqGQnnPGnqAU","title":"CSS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/CSS"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_es8OU2GuguFU","title":"Examples","type":"book","attributes":[{"name":"iconClass","value":"bx bx-code-alt","type":"label"}],"children":[{"id":"_help_TjLYAo3JMO8X","title":"\"New Task\" launcher button","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Examples/New Task launcher button"},{"name":"iconClass","value":"bx bx-task","type":"label"}]},{"id":"_help_7kZPMD0uFwkH","title":"Downloading responses from Google Forms","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Examples/Downloading responses from Goo"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_DL92EjAaXT26","title":"Using promoted attributes to configure scripts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics/Examples/Using promoted attributes to c"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]}]},{"id":"_help_SPirpZypehBG","title":"Backend scripts","type":"book","attributes":[{"name":"iconClass","value":"bx bx-server","type":"label"}],"children":[{"id":"_help_fZ2IGYFXjkEy","title":"Server-side imports","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Backend scripts/Server-side imports"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_GPERMystNGTB","title":"Events","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Backend scripts/Events"},{"name":"iconClass","value":"bx bx-rss","type":"label"}]}]},{"id":"_help_GLks18SNjxmC","title":"Script API","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Script API"},{"name":"iconClass","value":"bx bx-code-curly","type":"label"}],"children":[{"id":"_help_Q2z6av6JZVWm","title":"Frontend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://docs.triliumnotes.org/script-api/frontend"},{"name":"iconClass","value":"bx bx-folder","type":"label"}],"enforceAttributes":true,"children":[{"id":"_help_habiZ3HU8Kw8","title":"FNote","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://docs.triliumnotes.org/script-api/frontend/interfaces/FNote.html"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"enforceAttributes":true}]},{"id":"_help_MEtfsqa5VwNi","title":"Backend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://docs.triliumnotes.org/script-api/backend"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"enforceAttributes":true}]},{"id":"_help_vElnKeDNPSVl","title":"Logging","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Logging"},{"name":"iconClass","value":"bx bx-terminal","type":"label"}]}]},{"id":"_help_Fm0j45KqyHpU","title":"Miscellaneous","type":"book","attributes":[{"name":"iconClass","value":"bx bx-info-circle","type":"label"}],"children":[{"id":"_help_WFbFXrgnDyyU","title":"Privacy Policy","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Miscellaneous/Privacy Policy"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_NcsmUYZRWEW4","title":"Patterns of personal knowledge","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Miscellaneous/Patterns of personal knowledge"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]}]
\ No newline at end of file
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.html
index 46523a9cee..de18d335b0 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.html
@@ -288,6 +288,34 @@ Logging Section
+ExternalBlobStorage Section
+
+
+
+ Environment Variable
+ Type
+ Default
+ Description
+
+
+
+
+ TRILIUM_EXTERNAL_BLOB_STORAGE_ENABLED
+
+ boolean
+ false
+ Enable external blob storage for large attachments
+
+
+ TRILIUM_EXTERNAL_BLOB_STORAGE_THRESHOLD
+
+ integer
+ 102400
+ Size threshold in bytes (100KB default) for external storage
+
+
+
+
Alternative Environment Variables
The following alternative environment variable names are also supported
and work identically to their longer counterparts:
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Database.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Database.html
index 6bd4231955..2dd6b9b8f6 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Database.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Database.html
@@ -2,6 +2,10 @@
which contains all notes, tree structure, metadata, and most of the configuration.
The database file is named document.db and is stored in the
application's default Data directory .
+By default, all note and attachment content is stored within the database.
+ However, when external blob storage is enabled,
+ large notes and attachments are stored separately in the external-blobs directory
+ for better performance and backup flexibility.
Demo Notes
When first starting Trilium, it will provide a set of notes to showcase
various features of the application.
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/External Blob Storage.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/External Blob Storage.html
new file mode 100644
index 0000000000..6c7614d0a2
--- /dev/null
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/External Blob Storage.html
@@ -0,0 +1,59 @@
+External blob storage is an optional feature that stores large attachments
+ as separate files on the filesystem instead of embedding them in the SQLite
+ database. This feature is disabled by default to maintain backward compatibility
+ and simplicity for most users.
+When to Enable External Blob Storage
+Benefits of enabling external storage:
+
+ Performance : Large attachments don't bloat the database,
+ which can improve query performance
+ Backup flexibility : Large files can be backed up separately
+ from the database using different strategies or schedules
+ Storage efficiency : Easier to manage and migrate large
+ files independently
+
+When to keep it disabled (default):
+
+ Simplicity : All data in one database file makes backup
+ and migration straightforward
+ Small attachments : If your attachments are mostly small,
+ database storage is more efficient
+ Portability : Single-file database is easier to copy and
+ move between systems
+
+How It Works
+When external blob storage is enabled and a note or attachment exceeds
+ the configured threshold size the content is saved to the file system in
+ the external-blobs directory within your data directory .
+ The files are organized in a partitioned directory structure to prevent
+ excessive files in a single directory. The database stores a reference
+ to the external file and any additional metadata.
+Attachments below the threshold, or when external storage is disabled,
+ continue to be stored in the database.
+When you enable external blob storage, existing attachments in the database
+ remain there. Only new attachments that exceed the threshold will be stored
+ externally. Automatically migrating existing attachments is currently not
+ supported.
+If you disable external blob storage after using it, existing external
+ blobs remain in the external-blobs directory and continue to
+ work. New attachments will be stored in the database regardless of size.
+Configuration
+External blob storage can be configured via config.ini or environment
+ variables. See the Configuration documentation
+ for all available options.
+Threshold Size Recommendations
+The default threshold of 100KB (102400 bytes) is based on SQLite's performance recommendations .
+You can adjust the threshold based on your needs:
+
+ Lower threshold (e.g., 50KB) : More files stored externally,
+ smaller database
+ Higher threshold (e.g., 1MB) : Fewer external files, larger
+ database but simpler backup
+
+Important Backup Considerations
+When external blob storage is enabled, you must backup both the database
+ and the external-blobs directory.
+Without backing up the external-blobs directory, you would
+ lose large attachments if restoring from backup.
+See the Backup documentation for detailed
+ backup strategies with external blob storage.
\ No newline at end of file
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Note source.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Note source.html
index 2292a7cec9..a306420d76 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Note source.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Note source.html
@@ -3,20 +3,20 @@ Understanding the source code of the different notes
on the Note Types .
For example:
- Text notes are
+ Text notes are
represented internally as HTML, using the CKEditor representation. Note that due
to the custom plugins, some HTML elements are specific to Trilium only,
for example the admonitions.
- Code notes are
+ Code notes are
plain text and are represented internally as-is.
- Geo Map notes
+ Geo Map notes
contain only minimal information (viewport, zoom) as a JSON.
- Canvas notes
+ Canvas notes
are represented as JSON, with Trilium's own information alongside with
Excalidraw 's internal JSON representation format.
- Mind Map notes
+ Mind Map notes
are represented as JSON, with the internal format of MindElixir .
@@ -53,13 +53,11 @@ Modifying the source code
via the Note source functionality.
To do so:
- Change the note type from the real note type (e.g. Canvas, Geo Type) to
+ Change the note type from the real note type (e.g. Canvas, Geo Type) to
Code (plain text) or the corresponding format such as JSON or HTML.
- Confirm the warning about changing the note type.
- The source code will appear, make the necessary modifications.
- Change the note type back to the real note type.
+ Confirm the warning about changing the note type.
+ The source code will appear, make the necessary modifications.
+ Change the note type back to the real note type.
Depending on the changes made, there is a risk that the note will not
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Attachments.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Attachments.html
index 537a7c70ca..4a0eb8e813 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Attachments.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Attachments.html
@@ -13,6 +13,11 @@
visuals in notes. It is important to link image attachments within the
text of the owning note; otherwise, they will be automatically deleted
after a configurable timeout period if not referenced.
+Storage
+By default, attachments are stored within the SQLite database. For installations
+ with many large attachments, Trilium supports external blob storage which
+ stores large files separately on the file system. This is optional and
+ disabled by default to maintain simplicity for most users.
Converting notes to attachments
File notes
can be easily converted to attachments of the parent note.
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Backup.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Backup.html
index de085dcc8c..bdc5e3a31c 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Backup.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Backup.html
@@ -17,6 +17,22 @@
Note that Synchronization provides
also some backup capabilities by its nature of distributing the data to
other computers.
+External Blob Storage and Backups
+If you have enabled external blob storage ,
+ you must backup both the database and the external-blobs directory.
+ Without backing up external-blobs separately, you would lose
+ large attachments if restoring from backup.
+By default, external blob storage is disabled, meaning all attachments
+ are stored in the database and the automatic backup described above is
+ sufficient.
+How to Backup with External Blobs
+When external blob storage is enabled, backup the complete data directory which
+ includes both the database and external-blobs. This is the
+ simplest approach and ensures nothing is missed.
+To ensure consistency, either stop Trilium before backing up, or use a
+ filesystem snapshot tool (e.g., LVM snapshots, ZFS snapshots, or volume
+ shadow copy on Windows) to capture both the database and external-blobs directory
+ at the same point in time.
Restoring backup
Let's assume you want to restore the weekly backup, here's how to do it:
@@ -37,6 +53,8 @@ Restoring backup
make sure that the file is writable, e.g. with chmod 600 document.db
+ if you have external blob storage enabled, also restore the external-blobs directory
+ from your backup
start Trilium again
If you have configured sync then you need to do it across all members
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Data directory.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Data directory.html
index 9f2de88530..b1a4875cd6 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Data directory.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Data directory.html
@@ -7,6 +7,8 @@
backup - contains automatically backup of
documents
log - contains application log files
+ external-blobs - contains large attachments when external blob storage is
+ enabled (optional, not present by default)
Location of the data directory
Easy way how to find out which data directory Trilium uses is to look
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Canvas.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Canvas.html
index a5dfa45b11..5be4d8ca0a 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Canvas.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Canvas.html
@@ -7,6 +7,7 @@
text and graphics input.
Interaction
\ No newline at end of file
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/File.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/File.html
index 80dad6f451..3a18e1e239 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/File.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/File.html
@@ -4,9 +4,8 @@ Uploading a file
Since these files come from an external source, it is not possible to
create a File note type directly:
- Drag a file into the Note Tree .
- Right click a note and select Import into note and point it to
+ Drag a file into the Note Tree .
+ Right click a note and select Import into note and point it to
one of the supported files.
Supported file types
@@ -18,20 +17,18 @@ PDFs
PDFs can be browsed directly from Trilium.
Interaction:
- Press the menu icon at the top-left to see a preview (thumbnail) of all
+ Press the menu icon at the top-left to see a preview (thumbnail) of all
the pages, as well as a table of contents (if the PDF has this information).
- See or edit the page number at the top.
- Adjust the zoom using the buttons at the top or manually editing the value.
- Rotate the document if it's in the wrong orientation.
- In the contextual menu:
-
- View two pages at once (great for books).
- Toggle annotations (if present in the document).
- View document properties.
-
-
+ See or edit the page number at the top.
+ Adjust the zoom using the buttons at the top or manually editing the value.
+ Rotate the document if it's in the wrong orientation.
+ In the contextual menu:
+
+ View two pages at once (great for books).
+ Toggle annotations (if present in the document).
+ View document properties.
+
+
Images
@@ -40,14 +37,14 @@ Images
Interaction:
- Copy reference to clipboard , for embedding the image within
+ Copy reference to clipboard , for embedding the image within
Text notes.
@@ -77,11 +74,11 @@ Audio
be used to play it.
Interactions:
- The audio can be played/paused using the dedicated button.
- Dragging the mouse across, or clicking the progress bar will seek through
+ The audio can be played/paused using the dedicated button.
+ Dragging the mouse across, or clicking the progress bar will seek through
the song.
- The volume can be set.
- The playback speed can be adjusted via the contextual menu next to the
+ The volume can be set.
+ The playback speed can be adjusted via the contextual menu next to the
volume.
Text files
@@ -114,35 +111,33 @@ Unknown file types
file externally, but there will be no preview of the content.
Interaction
- Regardless of the file type, a series of buttons will be displayed in
+ Regardless of the file type, a series of buttons will be displayed in
the Image or File tab in the Ribbon .
- Download , which will download the file for local use.
- Open , will will open the file with the system-default application.
- Upload new revision to replace the file with a new one.
+ Download , which will download the file for local use.
+ Open , will will open the file with the system-default application.
+ Upload new revision to replace the file with a new one.
-
- It is not possible to change the note type of a File note.
- Convert into an attachment from the note menu .
+
+ It is not possible to change the note type of a File note.
+ Convert into an attachment from the note menu .
Relation with other notes
-
+
Files are also displayed in the Note List based
on their type:
-
-
-
+
+
+
+ Non-image files can be embedded into text notes as read-only widgets via
+ the Include Note functionality.
+
+
+ Image files can be embedded into text notes like normal images via
+ Image references .
- Non-image files can be embedded into text notes as read-only widgets via
- the Include Note functionality.
- Image files can be embedded into text notes like normal images via
- Image references .
\ No newline at end of file
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Mind Map.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Mind Map.html
index b310466e98..988f1bcacd 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Mind Map.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Mind Map.html
@@ -6,42 +6,35 @@
a hierarchical fashion.
Terminology
- A node is a single idea, represented differently based
+ A node is a single idea, represented differently based
on depth (filled rounded rectangle for the root note, unfilled rectangles
for sub-ideas, lines only for sub-sub-ideas).
- The root node is the top-most node from which all other
+ The root node is the top-most node from which all other
nodes derive, displayed as a filled rectangle. There can only be a single
root node.
Interaction
- To create a new node at the same level as the current one, press Enter ,
+ To create a new node at the same level as the current one, press Enter ,
enter the desired text and then press Enter once again to confirm.
- Similarly, to create a sub-node, press Tab , enter the desired
+ Similarly, to create a sub-node, press Tab , enter the desired
text and then press Enter .
- To create a parent, use Ctrl +Enter instead.
- To remove a node, press Delete .
- To move a node up or down, press Page Up or Page Down .
- To adjust the font size, color of the text or background or to add a link,
- click on a node and use the floating panel that appears to the right.
- To select one or more notes, drag and drop across the map.
- Right click the node to bring a contextual menu with options such as creating
- new nodes, focusing on a particular notes or creating links between them.
- Use the buttons at the top-left to change the positioning of the nodes
- relative to the root node (to the left, to the right, or to both sides).
- In the Floating buttons area:
-
- An image reference can be copied, to paste
- the mind map in a text note.
- The diagram can be exported either as SVG (vectorial) or PNG (raster).
- The note can be togged read-only .
-
-
+ To create a parent, use Ctrl +Enter instead.
+ To remove a node, press Delete .
+ To move a node up or down, press Page Up or Page Down .
+ To adjust the font size, color of the text or background or to add a link,
+ click on a node and use the floating panel that appears to the right.
+ To select one or more notes, drag and drop across the map.
+ Right click the node to bring a contextual menu with options such as creating
+ new nodes, focusing on a particular notes or creating links between them.
+ Use the buttons at the top-left to change the positioning of the nodes
+ relative to the root node (to the left, to the right, or to both sides).
+ In the Floating buttons area:
+
+ An image reference can be copied, to paste
+ the mind map in a text note.
+ The diagram can be exported either as SVG (vectorial) or PNG (raster).
+ The note can be togged read-only .
+
+
\ No newline at end of file
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html
index 14a6cf28c7..5e5895b8d8 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html
@@ -7,13 +7,11 @@
via an attribute.
Creating a render note
- Create a Code note
+ Create a Code note
with the HTML language, with what needs to be displayed (for example <p>Hello world.</p>).
- Create a Render Note .
- Assign the renderNote relation to
- point at the previously created code note.
+ Create a Render Note .
+ Assign the renderNote relation to
+ point at the previously created code note.
Dynamic content
A static HTML is generally not enough for Dynamic content
Refreshing the note
It's possible to refresh the note via:
Examples
\ No newline at end of file
diff --git a/apps/server/src/becca/becca-interface.ts b/apps/server/src/becca/becca-interface.ts
index 005a5cc520..b2a7c760bc 100644
--- a/apps/server/src/becca/becca-interface.ts
+++ b/apps/server/src/becca/becca-interface.ts
@@ -171,7 +171,7 @@ export default class Becca {
opts.includeContentLength = !!opts.includeContentLength;
const query = opts.includeContentLength
- ? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
+ ? /*sql*/`SELECT attachments.*, blobs.contentLength AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE attachmentId = ? AND isDeleted = 0`
@@ -197,7 +197,7 @@ export default class Becca {
return null;
}
- const row = sql.getRow("SELECT *, LENGTH(content) AS contentLength FROM blobs WHERE blobId = ?", [entity.blobId]);
+ const row = sql.getRow("SELECT * FROM blobs WHERE blobId = ?", [entity.blobId]);
return row ? new BBlob(row) : null;
}
diff --git a/apps/server/src/becca/entities/abstract_becca_entity.ts b/apps/server/src/becca/entities/abstract_becca_entity.ts
index 1f3bd0d863..5e1e904c98 100644
--- a/apps/server/src/becca/entities/abstract_becca_entity.ts
+++ b/apps/server/src/becca/entities/abstract_becca_entity.ts
@@ -9,8 +9,11 @@ import cls from "../../services/cls.js";
import log from "../../services/log.js";
import protectedSessionService from "../../services/protected_session.js";
import blobService from "../../services/blob.js";
+import blobStorageService from "../../services/blob-storage.js";
+import type { Blob } from "../../services/blob-interface.js";
import type { default as Becca, ConstructorData } from "../becca-interface.js";
import becca from "../becca.js";
+import type { BlobContentLocation, BlobRow } from "@triliumnext/commons";
interface ContentOpts {
forceSave?: boolean;
@@ -195,6 +198,11 @@ abstract class AbstractBeccaEntity> {
return;
}
+ if (blobStorageService.hasExternalContentColumns()) {
+ const row = sql.getRow<{ contentLocation: string }>("SELECT contentLocation FROM blobs WHERE blobId = ?", [oldBlobId]);
+ blobStorageService.deleteExternal(row);
+ }
+
sql.execute("DELETE FROM blobs WHERE blobId = ?", [oldBlobId]);
// blobs are not marked as erased in entity_changes, they are just purged completely
// this is because technically every keystroke can create a new blob, and there would be just too many
@@ -222,17 +230,60 @@ abstract class AbstractBeccaEntity> {
const blobNeedsInsert = !sql.getValue("SELECT 1 FROM blobs WHERE blobId = ?", [newBlobId]);
if (!blobNeedsInsert) {
- return newBlobId;
+ if (!blobStorageService.hasExternalContentColumns()) {
+ // If no external storage support, safe to reuse blob
+ return newBlobId;
+ }
+
+ // Self recover external blob if the file was deleted externally
+ const existingBlob = sql.getRow<{ contentLocation: BlobContentLocation }>(/*sql*/
+ `SELECT contentLocation FROM blobs WHERE blobId = ?`,
+ [newBlobId]
+ );
+
+ const isInternalBlob = existingBlob?.contentLocation === 'internal';
+ if (isInternalBlob || blobStorageService.externalFileExists(existingBlob.contentLocation)) {
+ // If external blob is still present, safe to reuse
+ return newBlobId;
+ }
+
+ // External file is missing, recreate it
+ log.info(`External file missing for blob ${newBlobId}, recreating...`);
}
- const pojo = {
+ // Check if we should store this blob externally
+ const shouldStoreExternally = blobStorageService.shouldStoreExternally(content);
+ let contentLocation: BlobContentLocation = "internal";
+ if (shouldStoreExternally) {
+ try {
+ contentLocation = blobStorageService.saveExternal(newBlobId, content);
+ } catch (error) {
+ log.error(`Failed to store blob ${newBlobId} externally, falling back to internal storage: ${error}`);
+ contentLocation = "internal";
+ }
+ }
+
+ const contentLength = blobService.getContentLength(content);
+
+ const pojo: BlobRow = {
blobId: newBlobId,
- content: content,
+ content: contentLocation === 'internal' ? content : null,
+ contentLocation,
+ contentLength,
dateModified: dateUtils.localNowDateTime(),
utcDateModified: dateUtils.utcNowDateTime()
};
- sql.upsert("blobs", "blobId", pojo);
+ // external content columns might not be present when applying older migrations
+ const pojoToSave = blobStorageService.hasExternalContentColumns()
+ ? pojo
+ : {
+ blobId: pojo.blobId,
+ content,
+ dateModified: pojo.dateModified,
+ utcDateModified: pojo.utcDateModified
+ };
+ sql.upsert("blobs", "blobId", pojoToSave);
// we can't reuse blobId as an entity_changes hash, because this one has to be calculatable without having
// access to the decrypted content
@@ -259,14 +310,20 @@ abstract class AbstractBeccaEntity> {
}
protected _getContent(): string | Buffer {
- const row = sql.getRow<{ content: string | Buffer }>(/*sql*/`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
+ const query = blobStorageService.hasExternalContentColumns()
+ ? /*sql*/`SELECT content, contentLocation FROM blobs WHERE blobId = ?`
+ : /*sql*/`SELECT content, 'internal' as contentLocation FROM blobs WHERE blobId = ?`;
+
+ const row = sql.getRow<{ content: string | Buffer, contentLocation: string }>(query, [this.blobId]);
if (!row) {
const constructorData = this.constructor as unknown as ConstructorData;
throw new Error(`Cannot find content for ${constructorData.primaryKeyName} '${(this as any)[constructorData.primaryKeyName]}', blobId '${this.blobId}'`);
}
- return blobService.processContent(row.content, this.isProtected || false, this.hasStringContent());
+ const content = blobStorageService.getContent(row);
+
+ return blobService.processContent(content, this.isProtected || false, this.hasStringContent());
}
/**
diff --git a/apps/server/src/becca/entities/bblob.ts b/apps/server/src/becca/entities/bblob.ts
index 2cff185d5c..d21e2ae883 100644
--- a/apps/server/src/becca/entities/bblob.ts
+++ b/apps/server/src/becca/entities/bblob.ts
@@ -1,5 +1,5 @@
import AbstractBeccaEntity from "./abstract_becca_entity.js";
-import type { BlobRow } from "@triliumnext/commons";
+import type { BlobRow, BlobContentLocation } from "@triliumnext/commons";
// TODO: Why this does not extend the abstract becca?
class BBlob extends AbstractBeccaEntity {
@@ -10,11 +10,12 @@ class BBlob extends AbstractBeccaEntity {
return "blobId";
}
static get hashedProperties() {
- return ["blobId", "content"];
+ return ["blobId", "content", "contentLocation"];
}
- content!: string | Buffer;
+ content!: string | Buffer | null;
contentLength!: number;
+ contentLocation!: BlobContentLocation;
constructor(row: BlobRow) {
super();
@@ -25,6 +26,7 @@ class BBlob extends AbstractBeccaEntity {
this.blobId = row.blobId;
this.content = row.content;
this.contentLength = row.contentLength;
+ this.contentLocation = row.contentLocation;
this.dateModified = row.dateModified;
this.utcDateModified = row.utcDateModified;
}
@@ -34,6 +36,7 @@ class BBlob extends AbstractBeccaEntity {
blobId: this.blobId,
content: this.content || null,
contentLength: this.contentLength,
+ contentLocation: this.contentLocation,
dateModified: this.dateModified,
utcDateModified: this.utcDateModified
};
diff --git a/apps/server/src/becca/entities/bnote.ts b/apps/server/src/becca/entities/bnote.ts
index dd05fd9746..f77ff93e26 100644
--- a/apps/server/src/becca/entities/bnote.ts
+++ b/apps/server/src/becca/entities/bnote.ts
@@ -10,6 +10,7 @@ import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import BRevision from "./brevision.js";
import BAttachment from "./battachment.js";
+import blobStorageService from "../../services/blob-storage.js";
import TaskContext from "../../services/task_context.js";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc.js";
@@ -1107,8 +1108,12 @@ class BNote extends AbstractBeccaEntity {
// from testing, it looks like calculating length does not make a difference in performance even on large-ish DB
// given that we're always fetching attachments only for a specific note, we might just do it always
+ const contentLengthColumn = blobStorageService.hasExternalContentColumns()
+ ? "blobs.contentLength"
+ : "LENGTH(COALESCE(blobs.content, ''))";
+
const query = opts.includeContentLength
- ? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
+ ? /*sql*/`SELECT attachments.*, ${contentLengthColumn} AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND isDeleted = 0
@@ -1121,8 +1126,12 @@ class BNote extends AbstractBeccaEntity {
getAttachmentById(attachmentId: string, opts: AttachmentOpts = {}) {
opts.includeContentLength = !!opts.includeContentLength;
+ const contentLengthColumn = blobStorageService.hasExternalContentColumns()
+ ? "blobs.contentLength"
+ : "LENGTH(COALESCE(blobs.content, ''))";
+
const query = opts.includeContentLength
- ? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
+ ? /*sql*/`SELECT attachments.*, ${contentLengthColumn} AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
diff --git a/apps/server/src/becca/entities/brevision.ts b/apps/server/src/becca/entities/brevision.ts
index 88f647db29..4d06f00763 100644
--- a/apps/server/src/becca/entities/brevision.ts
+++ b/apps/server/src/becca/entities/brevision.ts
@@ -6,6 +6,7 @@ import dateUtils from "../../services/date_utils.js";
import becca from "../becca.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import sql from "../../services/sql.js";
+import blobStorageService from "../../services/blob-storage.js";
import BAttachment from "./battachment.js";
import type { AttachmentRow, NoteType, RevisionPojo, RevisionRow } from "@triliumnext/commons";
import eraseService from "../../services/erase.js";
@@ -140,8 +141,12 @@ class BRevision extends AbstractBeccaEntity {
getAttachmentById(attachmentId: String, opts: GetByIdOpts = {}): BAttachment | null {
opts.includeContentLength = !!opts.includeContentLength;
+ const contentLengthColumn = blobStorageService.hasExternalContentColumns()
+ ? "blobs.contentLength"
+ : "LENGTH(COALESCE(blobs.content, ''))";
+
const query = opts.includeContentLength
- ? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
+ ? /*sql*/`SELECT attachments.*, ${contentLengthColumn} AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
diff --git a/apps/server/src/migrations/migrations.ts b/apps/server/src/migrations/migrations.ts
index 2757b4c25a..808a5b2107 100644
--- a/apps/server/src/migrations/migrations.ts
+++ b/apps/server/src/migrations/migrations.ts
@@ -6,6 +6,19 @@
// Migrations should be kept in descending order, so the latest migration is first.
const MIGRATIONS: (SqlMigration | JsMigration)[] = [
+ // Add external blob storage support
+ {
+ version: 234,
+ sql: /*sql*/`
+ -- Add contentLocation column
+ ALTER TABLE blobs ADD contentLocation TEXT DEFAULT 'internal';
+ UPDATE blobs SET contentLocation = 'internal' WHERE contentLocation IS NULL;
+
+ -- Add contentLength column
+ ALTER TABLE blobs ADD contentLength INTEGER DEFAULT 0;
+ UPDATE blobs SET contentLength = CASE WHEN content IS NULL THEN 0 ELSE LENGTH(content) END WHERE contentLength IS NULL;
+ `,
+ },
// Migrate geo map to collection
{
version: 233,
diff --git a/apps/server/src/routes/api/revisions.ts b/apps/server/src/routes/api/revisions.ts
index d9b7b975ed..6f206f4311 100644
--- a/apps/server/src/routes/api/revisions.ts
+++ b/apps/server/src/routes/api/revisions.ts
@@ -7,6 +7,7 @@ import cls from "../../services/cls.js";
import path from "path";
import becca from "../../becca/becca.js";
import blobService from "../../services/blob.js";
+import blobStorageService from "../../services/blob-storage.js";
import eraseService from "../../services/erase.js";
import type { Request, Response } from "express";
import type BRevision from "../../becca/entities/brevision.js";
@@ -33,10 +34,14 @@ function getRevisionBlob(req: Request) {
}
function getRevisions(req: Request) {
+ const contentLengthColumn = blobStorageService.hasExternalContentColumns()
+ ? "blobs.contentLength"
+ : "LENGTH(COALESCE(blobs.content, ''))";
+
return becca.getRevisionsFromQuery(
`
SELECT revisions.*,
- LENGTH(blobs.content) AS contentLength
+ ${contentLengthColumn} AS contentLength
FROM revisions
JOIN blobs ON revisions.blobId = blobs.blobId
WHERE revisions.noteId = ?
diff --git a/apps/server/src/routes/api/stats.ts b/apps/server/src/routes/api/stats.ts
index aebca079e3..1d4b05a14e 100644
--- a/apps/server/src/routes/api/stats.ts
+++ b/apps/server/src/routes/api/stats.ts
@@ -1,14 +1,19 @@
import sql from "../../services/sql.js";
import becca from "../../becca/becca.js";
+import blobStorageService from "../../services/blob-storage.js";
import type { Request } from "express";
import { NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons";
function getNoteSize(req: Request) {
const { noteId } = req.params;
+ const contentLengthColumn = blobStorageService.hasExternalContentColumns()
+ ? "blobs.contentLength"
+ : "LENGTH(COALESCE(blobs.content, ''))";
+
const blobSizes = sql.getMap(
`
- SELECT blobs.blobId, LENGTH(content)
+ SELECT blobs.blobId, ${contentLengthColumn} as contentLength
FROM blobs
LEFT JOIN notes ON notes.blobId = blobs.blobId AND notes.noteId = ? AND notes.isDeleted = 0
LEFT JOIN attachments ON attachments.blobId = blobs.blobId AND attachments.ownerId = ? AND attachments.isDeleted = 0
@@ -33,8 +38,12 @@ function getSubtreeSize(req: Request) {
sql.fillParamList(subTreeNoteIds);
+ const contentLengthColumn = blobStorageService.hasExternalContentColumns()
+ ? "blobs.contentLength"
+ : "LENGTH(COALESCE(blobs.content, ''))";
+
const blobSizes = sql.getMap(`
- SELECT blobs.blobId, LENGTH(content)
+ SELECT blobs.blobId, ${contentLengthColumn} as contentLength
FROM param_list
JOIN notes ON notes.noteId = param_list.paramId AND notes.isDeleted = 0
LEFT JOIN attachments ON attachments.ownerId = param_list.paramId AND attachments.isDeleted = 0
diff --git a/apps/server/src/services/app_info.ts b/apps/server/src/services/app_info.ts
index 2837e8de79..002f9c43b4 100644
--- a/apps/server/src/services/app_info.ts
+++ b/apps/server/src/services/app_info.ts
@@ -4,7 +4,7 @@ import packageJson from "../../package.json" with { type: "json" };
import dataDir from "./data_dir.js";
import { AppInfo } from "@triliumnext/commons";
-const APP_DB_VERSION = 233;
+const APP_DB_VERSION = 234;
const SYNC_VERSION = 36;
const CLIPPER_PROTOCOL_VERSION = "1.0";
diff --git a/apps/server/src/services/blob-interface.ts b/apps/server/src/services/blob-interface.ts
index a0e6052785..8fcdff1906 100644
--- a/apps/server/src/services/blob-interface.ts
+++ b/apps/server/src/services/blob-interface.ts
@@ -1,5 +1,7 @@
export interface Blob {
blobId: string;
- content: string | Buffer;
+ content: string | Buffer | null;
+ contentLocation: string;
+ contentLength: number;
utcDateModified: string;
}
diff --git a/apps/server/src/services/blob-storage.ts b/apps/server/src/services/blob-storage.ts
new file mode 100644
index 0000000000..3878151cb0
--- /dev/null
+++ b/apps/server/src/services/blob-storage.ts
@@ -0,0 +1,141 @@
+import fs from "fs";
+import path from "path";
+import { randomUUID } from "crypto";
+import dataDirs from "./data_dir.js";
+import log from "./log.js";
+import config from "./config.js";
+import blob from "./blob.js";
+import sql from "./sql.js";
+import type { Blob } from "./blob-interface.js";
+import { BlobContentLocation } from "@triliumnext/commons";
+
+const EXTERNAL_BLOB_DIR = "external-blobs";
+
+export class BlobStorageService {
+ private externalBlobPath: string;
+ private _hasExternalContentColumns: boolean | null = null;
+
+ constructor() {
+ this.externalBlobPath = path.join(dataDirs.TRILIUM_DATA_DIR, EXTERNAL_BLOB_DIR);
+ this.ensureExternalBlobDir();
+ }
+
+ /**
+ * Check if the external content columns (contentLocation, contentLength) exist in the blobs table.
+ * This is cached for performance.
+ * Returns false before migration 234 has been applied (aka when applying older migrations).
+ */
+ hasExternalContentColumns(): boolean {
+ if (!this._hasExternalContentColumns) {
+ // In most cases the columns will already exist, so we intentionally recheck
+ // to see if the migration was applied after the first check.
+ this._hasExternalContentColumns = !!sql.getValue(/*sql*/`
+ SELECT 1 FROM pragma_table_info('blobs') WHERE name = 'contentLocation'
+ `);
+ }
+
+ return this._hasExternalContentColumns;
+ }
+
+ private ensureExternalBlobDir(): void {
+ if (!fs.existsSync(this.externalBlobPath)) {
+ try {
+ fs.mkdirSync(this.externalBlobPath, { recursive: true, mode: 0o700 });
+ log.info(`Created external blob directory: ${this.externalBlobPath}`);
+ } catch (error) {
+ log.error(`Failed to create external blob directory: ${error}`);
+ throw error;
+ }
+ }
+ }
+
+ /**
+ * Store blob content externally and return the relative file path
+ */
+ saveExternal(blobId: string, content: string | Buffer): BlobContentLocation {
+ const uuid = randomUUID();
+ const partition = uuid.substring(0, 2);
+ const relativePath = path.join(partition, `${uuid}.blob`);
+ const absolutePath = path.join(this.externalBlobPath, relativePath);
+
+ try {
+ const dir = path.dirname(absolutePath);
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
+ }
+
+ fs.writeFileSync(absolutePath, content, { mode: 0o600 });
+ log.info(`Stored blob ${blobId} externally at: ${absolutePath}`);
+ return `file://${relativePath}`;
+ } catch (error) {
+ log.error(`Failed to store blob ${blobId} externally: ${error}`);
+ throw error;
+ }
+ }
+
+ /**
+ * Retrieve blob content from external storage
+ */
+ getExternal(relativePath: string): Buffer {
+ const filePath = path.join(this.externalBlobPath, relativePath);
+ try {
+ return fs.readFileSync(filePath);
+ } catch (error) {
+ log.error(`Failed to retrieve blob from external storage ${filePath}: ${error}`);
+ throw error;
+ }
+ }
+
+ /**
+ * Get the content of a blob row
+ */
+ getContent(row: Pick): string | Buffer | null {
+ return row.contentLocation === "internal" ? row.content : this.getExternal(row.contentLocation.replace("file://", ""));
+ }
+
+ /**
+ * Delete external blob file asynchronously for cleanup
+ */
+ deleteExternal(row: Pick): void {
+ if (!row.contentLocation || row.contentLocation === "internal") {
+ return;
+ }
+
+ const relativePath = row.contentLocation.replace("file://", "");
+ const filePath = path.join(this.externalBlobPath, relativePath);
+
+ try {
+ fs.unlinkSync(filePath);
+ log.info(`Deleted external blob file: ${filePath}`);
+ } catch (error) {
+ log.error(`Failed to delete external blob file ${filePath}: ${error}`);
+ throw error;
+ }
+ }
+
+ /**
+ * Check if a blob should be stored externally based on size
+ */
+ shouldStoreExternally(content: string | Buffer): boolean {
+ if (!config.ExternalBlobStorage.enabled) {
+ return false;
+ }
+
+ return blob.getContentLength(content) > config.ExternalBlobStorage.thresholdBytes;
+ }
+
+ /**
+ * Check if an external blob file exists
+ */
+ externalFileExists(contentLocation: BlobContentLocation): boolean {
+ if (contentLocation === "internal" || !contentLocation.startsWith("file://")) {
+ return false;
+ }
+
+ const relativePath = contentLocation.replace("file://", "");
+ const filePath = path.join(this.externalBlobPath, relativePath);
+ return fs.existsSync(filePath);
+ }
+}
+
+export default new BlobStorageService();
diff --git a/apps/server/src/services/blob.ts b/apps/server/src/services/blob.ts
index c4684c2ae5..1289c3187e 100644
--- a/apps/server/src/services/blob.ts
+++ b/apps/server/src/services/blob.ts
@@ -2,6 +2,7 @@ import becca from "../becca/becca.js";
import NotFoundError from "../errors/not_found_error.js";
import protectedSessionService from "./protected_session.js";
import { hash } from "./utils.js";
+import blobStorageService from "./blob-storage.js";
import type { Blob } from "./blob-interface.js";
function getBlobPojo(entityName: string, entityId: string, opts?: { preview: boolean }) {
@@ -21,7 +22,8 @@ function getBlobPojo(entityName: string, entityId: string, opts?: { preview: boo
if (!entity.hasStringContent()) {
pojo.content = null;
} else {
- pojo.content = processContent(pojo.content, !!entity.isProtected, true);
+ const content = blobStorageService.getContent(pojo);
+ pojo.content = processContent(content, !!entity.isProtected, true);
}
return pojo;
@@ -50,12 +52,27 @@ function processContent(content: Buffer | string | null, isProtected: boolean, i
}
}
-function calculateContentHash({ blobId, content }: Blob) {
- return hash(`${blobId}|${content.toString()}`);
+function calculateContentHash({ blobId, content, contentLocation }: Blob) {
+ const rawContent = blobStorageService.getContent({ content, contentLocation });
+ const contentString = Buffer.isBuffer(rawContent) ? rawContent.toString('base64') : rawContent;
+ return hash(`${blobId}|${contentLocation}|${contentString}`);
+}
+
+function getContentLength(content: Buffer | string | null) {
+ if (content === null) {
+ return 0;
+ }
+
+ if (Buffer.isBuffer(content)) {
+ return content.length;
+ }
+
+ return Buffer.byteLength(content, "utf8");
}
export default {
getBlobPojo,
processContent,
- calculateContentHash
+ calculateContentHash,
+ getContentLength
};
diff --git a/apps/server/src/services/config.ts b/apps/server/src/services/config.ts
index e08b7264e6..4450237145 100644
--- a/apps/server/src/services/config.ts
+++ b/apps/server/src/services/config.ts
@@ -134,7 +134,11 @@ export interface TriliumConfig {
* log files created by Trilium older than the specified amount of time will be deleted.
*/
retentionDays: number;
- }
+ },
+ ExternalBlobStorage: {
+ enabled: boolean;
+ thresholdBytes: number;
+ };
}
/**
@@ -143,32 +147,38 @@ export interface TriliumConfig {
*/
export const LOGGING_DEFAULT_RETENTION_DAYS = 90;
+/**
+ * Default threshold for external blob storage in bytes.
+ * 100kB by default, see performance impact here: https://www.sqlite.org/intern-v-extern-blob.html
+ */
+export const EXTERNAL_BLOB_STORAGE_DEFAULT_THRESHOLD_BYTES = 100 * 1024;
+
/**
* Configuration value source with precedence handling.
* This interface defines how each configuration value is resolved from multiple sources.
*/
interface ConfigValue {
- /**
+ /**
* Standard environment variable name following TRILIUM_[SECTION]_[KEY] pattern.
* This is the primary way to override configuration via environment.
*/
standardEnvVar?: string;
- /**
+ /**
* Alternative environment variable names for additional flexibility.
* These provide shorter or more intuitive names for common settings.
*/
aliasEnvVars?: string[];
- /**
+ /**
* Function to retrieve the value from the parsed INI configuration.
* Returns undefined if the value is not set in config.ini.
*/
iniGetter: () => IniConfigValue | IniConfigSection;
- /**
+ /**
* Default value used when no environment variable or INI value is found.
* This ensures every configuration has a sensible default.
*/
defaultValue: T;
- /**
+ /**
* Optional transformer function to convert string values to the correct type.
* Common transformers handle boolean and integer conversions.
*/
@@ -177,18 +187,18 @@ interface ConfigValue {
/**
* Core configuration resolution function.
- *
+ *
* Resolves configuration values using a clear precedence order:
* 1. Standard environment variable (highest priority) - Follows TRILIUM_[SECTION]_[KEY] pattern
* 2. Alias environment variables - Alternative names for convenience and compatibility
* 3. INI config file value - User-defined settings in config.ini
* 4. Default value (lowest priority) - Fallback to ensure valid configuration
- *
+ *
* This precedence allows for flexible configuration management:
* - Environment variables for container/cloud deployments
* - config.ini for traditional installations
* - Defaults ensure the application always has valid settings
- *
+ *
* @param config - Configuration value definition with sources and transformers
* @returns The resolved configuration value with appropriate type
*/
@@ -223,7 +233,7 @@ function getConfigValue(config: ConfigValue): T {
* Helper function to safely access INI config sections.
* The ini parser can return either a section object or a primitive value,
* so we need to check the type before accessing nested properties.
- *
+ *
* @param sectionName - The name of the INI section to access
* @returns The section object or undefined if not found or not an object
*/
@@ -237,15 +247,15 @@ function getIniSection(sectionName: string): IniConfigSection | undefined {
/**
* Transform a value to boolean, handling various input formats.
- *
+ *
* This function provides flexible boolean parsing to handle different
* configuration sources (environment variables, INI files, etc.):
* - String "true"/"false" (case-insensitive)
- * - String "1"/"0"
+ * - String "1"/"0"
* - Numeric 1/0
* - Actual boolean values
* - Any other value defaults to false
- *
+ *
* @param value - The value to transform (string, number, boolean, etc.)
* @returns The boolean value or false as default
*/
@@ -253,28 +263,28 @@ function transformBoolean(value: unknown): boolean {
// First try the standard envToBoolean function which handles "true"/"false" strings
const result = envToBoolean(String(value));
if (result !== undefined) return result;
-
+
// Handle numeric boolean values (both string and number types)
if (value === "1" || value === 1) return true;
if (value === "0" || value === 0) return false;
-
+
// Default to false for any other value
return false;
}
/**
* Complete configuration mapping that defines how each setting is resolved.
- *
+ *
* This mapping structure:
* 1. Mirrors the INI file sections for consistency
* 2. Defines multiple sources for each configuration value
* 3. Provides type transformers where needed
* 4. Maintains compatibility with various environment variable formats
- *
+ *
* Environment Variable Patterns:
* - Standard: TRILIUM_[SECTION]_[KEY] (e.g., TRILIUM_GENERAL_INSTANCENAME)
* - Aliases: Shorter alternatives (e.g., TRILIUM_OAUTH_BASE_URL)
- *
+ *
* Both patterns are equally valid and can be used based on preference.
* The standard pattern provides consistency, while aliases offer convenience.
*/
@@ -450,18 +460,32 @@ const configMapping = {
defaultValue: LOGGING_DEFAULT_RETENTION_DAYS,
transformer: (value: unknown) => stringToInt(String(value)) ?? LOGGING_DEFAULT_RETENTION_DAYS
}
+ },
+ ExternalBlobStorage: {
+ enabled: {
+ standardEnvVar: 'TRILIUM_EXTERNAL_BLOB_STORAGE_ENABLED',
+ iniGetter: () => getIniSection("ExternalBlobStorage")?.enabled,
+ defaultValue: false,
+ transformer: transformBoolean
+ },
+ thresholdBytes: {
+ standardEnvVar: 'TRILIUM_EXTERNAL_BLOB_STORAGE_THRESHOLD',
+ iniGetter: () => getIniSection("ExternalBlobStorage")?.thresholdBytes,
+ defaultValue: EXTERNAL_BLOB_STORAGE_DEFAULT_THRESHOLD_BYTES,
+ transformer: (value: unknown) => stringToInt(String(value)) ?? EXTERNAL_BLOB_STORAGE_DEFAULT_THRESHOLD_BYTES
+ }
}
};
/**
* Build the final configuration object by resolving all values through the mapping.
- *
+ *
* This creates the runtime configuration used throughout the application by:
* 1. Iterating through each section and key in the mapping
* 2. Calling getConfigValue() to resolve each setting with proper precedence
* 3. Applying type transformers where needed (booleans, integers)
* 4. Returning a fully typed TriliumConfig object
- *
+ *
* The resulting config object is immutable at runtime and represents
* the complete application configuration.
*/
@@ -502,6 +526,10 @@ const config: TriliumConfig = {
},
Logging: {
retentionDays: getConfigValue(configMapping.Logging.retentionDays)
+ },
+ ExternalBlobStorage: {
+ enabled: getConfigValue(configMapping.ExternalBlobStorage.enabled),
+ thresholdBytes: getConfigValue(configMapping.ExternalBlobStorage.thresholdBytes)
}
};
@@ -509,26 +537,26 @@ const config: TriliumConfig = {
* =====================================================================
* ENVIRONMENT VARIABLE REFERENCE
* =====================================================================
- *
+ *
* Trilium supports flexible environment variable configuration with multiple
* naming patterns. Both formats below are equally valid and can be used
* based on your preference.
- *
+ *
* CONFIGURATION PRECEDENCE:
* 1. Environment variables (highest priority)
* 2. config.ini file values
* 3. Default values (lowest priority)
- *
+ *
* FULL FORMAT VARIABLES (following TRILIUM_[SECTION]_[KEY] pattern):
* ====================================================================
- *
+ *
* General Section:
* - TRILIUM_GENERAL_INSTANCENAME : Custom instance identifier
* - TRILIUM_GENERAL_NOAUTHENTICATION : Disable auth (true/false)
* - TRILIUM_GENERAL_NOBACKUP : Disable backups (true/false)
* - TRILIUM_GENERAL_NODESKTOPICON : No desktop icon (true/false)
* - TRILIUM_GENERAL_READONLY : Read-only mode (true/false)
- *
+ *
* Network Section:
* - TRILIUM_NETWORK_HOST : Bind address (e.g., "0.0.0.0")
* - TRILIUM_NETWORK_PORT : Server port (e.g., "8080")
@@ -539,15 +567,15 @@ const config: TriliumConfig = {
* - TRILIUM_NETWORK_CORSALLOWORIGIN : CORS allowed origins
* - TRILIUM_NETWORK_CORSALLOWMETHODS : CORS allowed HTTP methods
* - TRILIUM_NETWORK_CORSALLOWHEADERS : CORS allowed headers
- *
+ *
* Session Section:
* - TRILIUM_SESSION_COOKIEMAXAGE : Cookie lifetime in seconds
- *
+ *
* Sync Section:
* - TRILIUM_SYNC_SYNCSERVERHOST : Sync server URL
* - TRILIUM_SYNC_SYNCSERVERTIMEOUT : Sync timeout in milliseconds
* - TRILIUM_SYNC_SYNCPROXY : Proxy URL for sync
- *
+ *
* Multi-Factor Authentication Section:
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL : OAuth base URL
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID : OAuth client ID
@@ -555,23 +583,23 @@ const config: TriliumConfig = {
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL : OAuth issuer URL
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME : OAuth provider name
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON : OAuth provider icon
- *
+ *
* Logging Section:
* - TRILIUM_LOGGING_RETENTIONDAYS : Log retention period in days
- *
+ *
* SHORTER ALTERNATIVE VARIABLES (equally valid, for convenience):
* ================================================================
- *
+ *
* Network CORS (with underscores):
* - TRILIUM_NETWORK_CORS_ALLOW_ORIGIN : Same as TRILIUM_NETWORK_CORSALLOWORIGIN
* - TRILIUM_NETWORK_CORS_ALLOW_METHODS : Same as TRILIUM_NETWORK_CORSALLOWMETHODS
* - TRILIUM_NETWORK_CORS_ALLOW_HEADERS : Same as TRILIUM_NETWORK_CORSALLOWHEADERS
- *
+ *
* Sync (with SERVER prefix):
* - TRILIUM_SYNC_SERVER_HOST : Same as TRILIUM_SYNC_SYNCSERVERHOST
* - TRILIUM_SYNC_SERVER_TIMEOUT : Same as TRILIUM_SYNC_SYNCSERVERTIMEOUT
* - TRILIUM_SYNC_SERVER_PROXY : Same as TRILIUM_SYNC_SYNCPROXY
- *
+ *
* OAuth (simplified without section name):
* - TRILIUM_OAUTH_BASE_URL : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL
* - TRILIUM_OAUTH_CLIENT_ID : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID
@@ -579,14 +607,14 @@ const config: TriliumConfig = {
* - TRILIUM_OAUTH_ISSUER_BASE_URL : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL
* - TRILIUM_OAUTH_ISSUER_NAME : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME
* - TRILIUM_OAUTH_ISSUER_ICON : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON
- *
+ *
* Logging (with underscore):
* - TRILIUM_LOGGING_RETENTION_DAYS : Same as TRILIUM_LOGGING_RETENTIONDAYS
- *
+ *
* BOOLEAN VALUES:
* - Accept: "true", "false", "1", "0", 1, 0
* - Default to false for invalid values
- *
+ *
* EXAMPLES:
* export TRILIUM_NETWORK_PORT="8080" # Using full format
* export TRILIUM_OAUTH_CLIENT_ID="my-client-id" # Using shorter alternative
@@ -597,23 +625,23 @@ const config: TriliumConfig = {
/**
* The exported configuration object used throughout the Trilium application.
* This object is resolved once at startup and remains immutable during runtime.
- *
+ *
* To override any setting:
* 1. Set an environment variable (see documentation above)
* 2. Edit config.ini in your data directory
* 3. Defaults will be used if neither is provided
- *
+ *
* @example
* // Accessing configuration in other modules:
* import config from './services/config.js';
- *
+ *
* if (config.General.noAuthentication) {
* // Skip authentication checks
* }
- *
+ *
* const server = https.createServer({
* cert: fs.readFileSync(config.Network.certPath),
* key: fs.readFileSync(config.Network.keyPath)
* });
*/
-export default config;
\ No newline at end of file
+export default config;
diff --git a/apps/server/src/services/consistency_checks.ts b/apps/server/src/services/consistency_checks.ts
index 7b4ba72adf..01b730084c 100644
--- a/apps/server/src/services/consistency_checks.ts
+++ b/apps/server/src/services/consistency_checks.ts
@@ -524,7 +524,8 @@ class ConsistencyChecks {
JOIN blobs USING (blobId)
WHERE isDeleted = 0
AND isProtected = 0
- AND content IS NULL`,
+ AND content IS NULL
+ AND (contentLocation IS NULL OR contentLocation = 'internal')`,
({ noteId, type, mime }) => {
if (this.autoFix) {
const note = becca.getNote(noteId);
diff --git a/apps/server/src/services/entity_changes.ts b/apps/server/src/services/entity_changes.ts
index c0a97c7d6b..582b34251b 100644
--- a/apps/server/src/services/entity_changes.ts
+++ b/apps/server/src/services/entity_changes.ts
@@ -6,6 +6,7 @@ import { randomString } from "./utils.js";
import instanceId from "./instance_id.js";
import becca from "../becca/becca.js";
import blobService from "../services/blob.js";
+import blobStorageService from "./blob-storage.js";
import type { EntityChange } from "@triliumnext/commons";
import type { Blob } from "./blob-interface.js";
import eventService from "./events.js";
@@ -146,7 +147,10 @@ function fillEntityChanges(entityName: string, entityPrimaryKey: string, conditi
};
if (entityName === "blobs") {
- const blob = sql.getRow("SELECT blobId, content, utcDateModified FROM blobs WHERE blobId = ?", [entityId]);
+ const query = blobStorageService.hasExternalContentColumns()
+ ? "SELECT blobId, content, contentLocation, contentLength, utcDateModified FROM blobs WHERE blobId = ?"
+ : "SELECT blobId, content, 'internal' as contentLocation, LENGTH(COALESCE(content, '')) as contentLength, utcDateModified FROM blobs WHERE blobId = ?";
+ const blob = sql.getRow(query, [entityId]);
ec.hash = blobService.calculateContentHash(blob);
ec.utcDateChanged = blob.utcDateModified;
ec.isSynced = true; // blobs are always synced
diff --git a/apps/server/src/services/erase.ts b/apps/server/src/services/erase.ts
index 92b28e5735..4d207cca9d 100644
--- a/apps/server/src/services/erase.ts
+++ b/apps/server/src/services/erase.ts
@@ -5,6 +5,7 @@ import optionService from "./options.js";
import dateUtils from "./date_utils.js";
import sqlInit from "./sql_init.js";
import cls from "./cls.js";
+import blobStorageService from "./blob-storage.js";
import type { EntityChange } from "@triliumnext/commons";
function eraseNotes(noteIdsToErase: string[]) {
@@ -92,26 +93,34 @@ function eraseRevisions(revisionIdsToErase: string[]) {
}
function eraseUnusedBlobs() {
- const unusedBlobIds = sql.getColumn(`
- SELECT blobs.blobId
+ const contentLocationColumn = blobStorageService.hasExternalContentColumns()
+ ? "blobs.contentLocation"
+ : "'internal' as contentLocation";
+
+ const unusedBlobRows = sql.getManyRows<{ blobId: string; contentLocation: string }>(/*sql*/`
+ SELECT blobs.blobId, ${contentLocationColumn}
FROM blobs
LEFT JOIN notes ON notes.blobId = blobs.blobId
LEFT JOIN attachments ON attachments.blobId = blobs.blobId
LEFT JOIN revisions ON revisions.blobId = blobs.blobId
WHERE notes.noteId IS NULL
AND attachments.attachmentId IS NULL
- AND revisions.revisionId IS NULL`);
+ AND revisions.revisionId IS NULL`, []);
- if (unusedBlobIds.length === 0) {
+ if (unusedBlobRows.length === 0) {
return;
}
- sql.executeMany(/*sql*/`DELETE FROM blobs WHERE blobId IN (???)`, unusedBlobIds);
+ // Clean up external blob files before deleting from database
+ unusedBlobRows.forEach(row => blobStorageService.deleteExternal(row));
+
+ const blobIds = unusedBlobRows.map(row => row.blobId);
+ sql.executeMany(/*sql*/`DELETE FROM blobs WHERE blobId IN (???)`, blobIds);
// blobs are not marked as erased in entity_changes, they are just purged completely
// this is because technically every keystroke can create a new blob and there would be just too many
- sql.executeMany(/*sql*/`DELETE FROM entity_changes WHERE entityName = 'blobs' AND entityId IN (???)`, unusedBlobIds);
+ sql.executeMany(/*sql*/`DELETE FROM entity_changes WHERE entityName = 'blobs' AND entityId IN (???)`, blobIds);
- log.info(`Erased unused blobs: ${JSON.stringify(unusedBlobIds)}`);
+ log.info(`Erased unused blobs: ${JSON.stringify(blobIds)}`);
}
function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds: number | null = null) {
diff --git a/apps/server/src/services/search/expressions/note_content_fulltext.ts b/apps/server/src/services/search/expressions/note_content_fulltext.ts
index c36dddd740..878711711f 100644
--- a/apps/server/src/services/search/expressions/note_content_fulltext.ts
+++ b/apps/server/src/services/search/expressions/note_content_fulltext.ts
@@ -11,14 +11,14 @@ import protectedSessionService from "../../protected_session.js";
import striptags from "striptags";
import { normalize } from "../../utils.js";
import sql from "../../sql.js";
-import {
- normalizeSearchText,
- calculateOptimizedEditDistance,
- validateFuzzySearchTokens,
+import {
+ normalizeSearchText,
+ validateFuzzySearchTokens,
validateAndPreprocessContent,
fuzzyMatchWord,
- FUZZY_SEARCH_CONFIG
+ FUZZY_SEARCH_CONFIG
} from "../utils/text_utils.js";
+import blobStorageService from "../../blob-storage.js";
const ALLOWED_OPERATORS = new Set(["=", "!=", "*=*", "*=", "=*", "%=", "~=", "~*"]);
@@ -41,7 +41,7 @@ interface ConstructorOpts {
flatText?: boolean;
}
-type SearchRow = Pick;
+type SearchRow = Pick & { contentLocation: string };
class NoteContentFulltextExp extends Expression {
private operator: string;
@@ -86,7 +86,7 @@ class NoteContentFulltextExp extends Expression {
// Search through notes with content
for (const row of sql.iterateRows(`
- SELECT noteId, type, mime, content, isProtected
+ SELECT noteId, type, mime, content, isProtected, contentLocation
FROM notes JOIN blobs USING (blobId)
WHERE type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
AND isDeleted = 0
@@ -170,50 +170,52 @@ class NoteContentFulltextExp extends Expression {
return false;
}
- findInText({ noteId, isProtected, content, type, mime }: SearchRow, inputNoteSet: NoteSet, resultNoteSet: NoteSet) {
+ findInText({ noteId, isProtected, content, type, mime, contentLocation }: SearchRow, inputNoteSet: NoteSet, resultNoteSet: NoteSet) {
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) {
return;
}
+ let rawContent: string | Buffer | null = blobStorageService.getContent({ content: content ?? null, contentLocation });
+
if (isProtected) {
- if (!protectedSessionService.isProtectedSessionAvailable() || !content || typeof content !== "string") {
+ if (!protectedSessionService.isProtectedSessionAvailable() || !rawContent || typeof rawContent !== "string") {
return;
}
try {
- content = protectedSessionService.decryptString(content) || undefined;
+ rawContent = protectedSessionService.decryptString(rawContent);
} catch (e) {
log.info(`Cannot decrypt content of note ${noteId}`);
return;
}
}
- if (!content) {
+ if (!rawContent) {
return;
}
- content = this.preprocessContent(content, type, mime);
+ rawContent = this.preprocessContent(rawContent, type, mime);
// Apply content size validation and preprocessing
- const processedContent = validateAndPreprocessContent(content, noteId);
+ const processedContent = validateAndPreprocessContent(rawContent, noteId);
if (!processedContent) {
return; // Content too large or invalid
}
- content = processedContent;
+ rawContent = processedContent;
if (this.tokens.length === 1) {
const [token] = this.tokens;
let matches = false;
if (this.operator === "=") {
- matches = this.containsExactWord(token, content);
+ matches = this.containsExactWord(token, rawContent);
// Also check flatText if enabled (includes attributes)
if (!matches && this.flatText) {
const flatText = becca.notes[noteId].getFlatText();
matches = this.containsExactPhrase([token], flatText, true);
}
} else if (this.operator === "!=") {
- matches = !this.containsExactWord(token, content);
+ matches = !this.containsExactWord(token, rawContent);
// For negation, check flatText too
if (matches && this.flatText) {
const flatText = becca.notes[noteId].getFlatText();
@@ -223,12 +225,12 @@ class NoteContentFulltextExp extends Expression {
if (
matches ||
- (this.operator === "*=" && content.endsWith(token)) ||
- (this.operator === "=*" && content.startsWith(token)) ||
- (this.operator === "*=*" && content.includes(token)) ||
- (this.operator === "%=" && getRegex(token).test(content)) ||
- (this.operator === "~=" && this.matchesWithFuzzy(content, noteId)) ||
- (this.operator === "~*" && this.fuzzyMatchToken(normalizeSearchText(token), normalizeSearchText(content)))
+ (this.operator === "*=" && rawContent.endsWith(token)) ||
+ (this.operator === "=*" && rawContent.startsWith(token)) ||
+ (this.operator === "*=*" && rawContent.includes(token)) ||
+ (this.operator === "%=" && getRegex(token).test(rawContent)) ||
+ (this.operator === "~=" && this.matchesWithFuzzy(rawContent, noteId)) ||
+ (this.operator === "~*" && this.fuzzyMatchToken(normalizeSearchText(token), normalizeSearchText(rawContent)))
) {
resultNoteSet.add(becca.notes[noteId]);
}
@@ -236,12 +238,12 @@ class NoteContentFulltextExp extends Expression {
// Multi-token matching with fuzzy support and phrase proximity
if (this.operator === "~=" || this.operator === "~*") {
// Fuzzy phrase matching
- if (this.matchesWithFuzzy(content, noteId)) {
+ if (this.matchesWithFuzzy(rawContent, noteId)) {
resultNoteSet.add(becca.notes[noteId]);
}
} else if (this.operator === "=" || this.operator === "!=") {
// Exact phrase matching for = and !=
- let matches = this.containsExactPhrase(this.tokens, content, false);
+ let matches = this.containsExactPhrase(this.tokens, rawContent, false);
// Also check flatText if enabled (includes attributes)
if (!matches && this.flatText) {
@@ -257,7 +259,7 @@ class NoteContentFulltextExp extends Expression {
// Other operators: check all tokens present (any order)
const nonMatchingToken = this.tokens.find(
(token) =>
- !this.tokenMatchesContent(token, content, noteId)
+ !this.tokenMatchesContent(token, rawContent, noteId)
);
if (!nonMatchingToken) {
@@ -266,7 +268,7 @@ class NoteContentFulltextExp extends Expression {
}
}
- return content;
+ return rawContent;
}
preprocessContent(content: string | Buffer, type: string, mime: string) {
@@ -307,16 +309,16 @@ class NoteContentFulltextExp extends Expression {
private tokenMatchesContent(token: string, content: string, noteId: string): boolean {
const normalizedToken = normalizeSearchText(token);
const normalizedContent = normalizeSearchText(content);
-
+
if (normalizedContent.includes(normalizedToken)) {
return true;
}
-
+
// Check flat text for default fulltext search
if (!this.flatText || !becca.notes[noteId].getFlatText().includes(token)) {
return false;
}
-
+
return true;
}
@@ -327,15 +329,15 @@ class NoteContentFulltextExp extends Expression {
try {
const normalizedContent = normalizeSearchText(content);
const flatText = this.flatText ? normalizeSearchText(becca.notes[noteId].getFlatText()) : "";
-
+
// For phrase matching, check if tokens appear within reasonable proximity
if (this.tokens.length > 1) {
return this.matchesPhrase(normalizedContent, flatText);
}
-
+
// Single token fuzzy matching
const token = normalizeSearchText(this.tokens[0]);
- return this.fuzzyMatchToken(token, normalizedContent) ||
+ return this.fuzzyMatchToken(token, normalizedContent) ||
(this.flatText && this.fuzzyMatchToken(token, flatText));
} catch (error) {
log.error(`Error in fuzzy matching for note ${noteId}: ${error}`);
@@ -348,45 +350,45 @@ class NoteContentFulltextExp extends Expression {
*/
private matchesPhrase(content: string, flatText: string): boolean {
const searchText = this.flatText ? `${content} ${flatText}` : content;
-
+
// Apply content size limits for phrase matching
const limitedText = validateAndPreprocessContent(searchText);
if (!limitedText) {
return false;
}
-
+
const words = limitedText.toLowerCase().split(/\s+/);
-
+
// Only skip phrase matching for truly extreme word counts that could crash the system
if (words.length > FUZZY_SEARCH_CONFIG.ABSOLUTE_MAX_WORD_COUNT) {
console.error(`Phrase matching skipped due to extreme word count that could cause system instability: ${words.length} words`);
return false;
}
-
+
// Warn about large word counts but still attempt matching
if (words.length > FUZZY_SEARCH_CONFIG.PERFORMANCE_WARNING_WORDS) {
console.info(`Large word count for phrase matching: ${words.length} words - may take longer but will attempt full matching`);
}
-
+
// Find positions of each token
const tokenPositions: number[][] = this.tokens.map(token => {
const normalizedToken = normalizeSearchText(token);
const positions: number[] = [];
-
+
words.forEach((word, index) => {
if (this.fuzzyMatchSingle(normalizedToken, word)) {
positions.push(index);
}
});
-
+
return positions;
});
-
+
// Check if we found all tokens
if (tokenPositions.some(positions => positions.length === 0)) {
return false;
}
-
+
// Check for phrase proximity using configurable distance
return this.hasProximityMatch(tokenPositions, FUZZY_SEARCH_CONFIG.MAX_PHRASE_PROXIMITY);
}
@@ -400,18 +402,18 @@ class NoteContentFulltextExp extends Expression {
const [pos1, pos2] = tokenPositions;
return pos1.some(p1 => pos2.some(p2 => Math.abs(p1 - p2) <= maxDistance));
}
-
+
// For more tokens, check if we can find a sequence where all tokens are within range
const findSequence = (remaining: number[][], currentPos: number): boolean => {
if (remaining.length === 0) return true;
-
+
const [nextPositions, ...rest] = remaining;
- return nextPositions.some(pos =>
- Math.abs(pos - currentPos) <= maxDistance &&
+ return nextPositions.some(pos =>
+ Math.abs(pos - currentPos) <= maxDistance &&
findSequence(rest, pos)
);
};
-
+
const [firstPositions, ...rest] = tokenPositions;
return firstPositions.some(startPos => findSequence(rest, startPos));
}
@@ -424,12 +426,12 @@ class NoteContentFulltextExp extends Expression {
// For short tokens, require exact match to avoid too many false positives
return content.includes(token);
}
-
+
const words = content.split(/\s+/);
-
+
// Only limit word processing for truly extreme cases to prevent system instability
const limitedWords = words.slice(0, FUZZY_SEARCH_CONFIG.ABSOLUTE_MAX_WORD_COUNT);
-
+
return limitedWords.some(word => this.fuzzyMatchSingle(token, word));
}
diff --git a/apps/server/src/services/search/services/search.ts b/apps/server/src/services/search/services/search.ts
index 5ca4bda4a1..850af7fe91 100644
--- a/apps/server/src/services/search/services/search.ts
+++ b/apps/server/src/services/search/services/search.ts
@@ -11,6 +11,7 @@ import beccaService from "../../../becca/becca_service.js";
import { normalize, escapeHtml, escapeRegExp } from "../../utils.js";
import log from "../../log.js";
import hoistedNoteService from "../../hoisted_note.js";
+import blobStorageService from "../../blob-storage.js";
import type BNote from "../../../becca/entities/bnote.js";
import type BAttribute from "../../../becca/entities/battribute.js";
import type { SearchParams, TokenStructure } from "./types.js";
@@ -118,6 +119,10 @@ function loadNeededInfoFromDatabase() {
*/
const noteBlobs: Record> = {};
+ const contentLengthColumn = blobStorageService.hasExternalContentColumns()
+ ? "contentLength"
+ : "LENGTH(COALESCE(blobs.content, ''))";
+
type NoteContentLengthsRow = {
noteId: string;
blobId: string;
@@ -127,7 +132,7 @@ function loadNeededInfoFromDatabase() {
SELECT
noteId,
blobId,
- LENGTH(content) AS length
+ ${contentLengthColumn} AS length
FROM notes
JOIN blobs USING(blobId)
WHERE notes.isDeleted = 0`);
@@ -153,7 +158,7 @@ function loadNeededInfoFromDatabase() {
SELECT
ownerId AS noteId,
attachments.blobId,
- LENGTH(content) AS length
+ ${contentLengthColumn} AS length
FROM attachments
JOIN notes ON attachments.ownerId = notes.noteId
JOIN blobs ON attachments.blobId = blobs.blobId
@@ -188,7 +193,7 @@ function loadNeededInfoFromDatabase() {
SELECT
noteId,
revisions.blobId,
- LENGTH(content) AS length,
+ ${contentLengthColumn} AS length,
1 AS isNoteRevision
FROM notes
JOIN revisions USING(noteId)
@@ -198,7 +203,7 @@ function loadNeededInfoFromDatabase() {
SELECT
noteId,
revisions.blobId,
- LENGTH(content) AS length,
+ ${contentLengthColumn} AS length,
0 AS isNoteRevision -- it's attachment not counting towards revision count
FROM notes
JOIN revisions USING(noteId)
@@ -252,21 +257,21 @@ function findResultsWithExpression(expression: Expression, searchContext: Search
// Phase 1: Try exact matches first (without fuzzy matching)
const exactResults = performSearch(expression, searchContext, false);
-
+
// Check if we have sufficient high-quality results
const minResultThreshold = 5;
const minScoreForQuality = 10; // Minimum score to consider a result "high quality"
-
+
const highQualityResults = exactResults.filter(result => result.score >= minScoreForQuality);
-
+
// If we have enough high-quality exact matches, return them
if (highQualityResults.length >= minResultThreshold) {
return exactResults;
}
-
+
// Phase 2: Add fuzzy matching as fallback when exact matches are insufficient
const fuzzyResults = performSearch(expression, searchContext, true);
-
+
// Merge results, ensuring exact matches always rank higher than fuzzy matches
return mergeExactAndFuzzyResults(exactResults, fuzzyResults);
}
@@ -326,10 +331,10 @@ function performSearch(expression: Expression, searchContext: SearchContext, ena
function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: SearchResult[]): SearchResult[] {
// Create a map of exact result note IDs for deduplication
const exactNoteIds = new Set(exactResults.map(result => result.noteId));
-
+
// Add fuzzy results that aren't already in exact results
const additionalFuzzyResults = fuzzyResults.filter(result => !exactNoteIds.has(result.noteId));
-
+
// Sort exact results by score (best exact matches first)
exactResults.sort((a, b) => {
if (a.score > b.score) {
@@ -345,7 +350,7 @@ function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: S
return a.notePathArray.length < b.notePathArray.length ? -1 : 1;
});
-
+
// Sort fuzzy results by score (best fuzzy matches first)
additionalFuzzyResults.sort((a, b) => {
if (a.score > b.score) {
@@ -361,7 +366,7 @@ function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: S
return a.notePathArray.length < b.notePathArray.length ? -1 : 1;
});
-
+
// CRITICAL: Always put exact matches before fuzzy matches, regardless of scores
return [...exactResults, ...additionalFuzzyResults];
}
@@ -417,10 +422,10 @@ function findResultsWithQuery(query: string, searchContext: SearchContext): Sear
}
// If the query starts with '#', it's a pure expression query.
- // Don't use progressive search for these as they may have complex
+ // Don't use progressive search for these as they may have complex
// ordering or other logic that shouldn't be interfered with.
const isPureExpressionQuery = query.trim().startsWith('#');
-
+
if (isPureExpressionQuery) {
// For pure expression queries, use standard search without progressive phases
return performSearch(expression, searchContext, searchContext.enableFuzzyMatching);
@@ -448,7 +453,7 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
try {
let content = note.getContent();
-
+
if (!content || typeof content !== "string") {
return "";
}
@@ -489,7 +494,7 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
for (const token of searchTokens) {
const normalizedToken = normalizeString(token.toLowerCase());
const matchIndex = normalizedContent.indexOf(normalizedToken);
-
+
if (matchIndex !== -1) {
// Center the snippet around the match
snippetStart = Math.max(0, matchIndex - maxLength / 2);
@@ -542,7 +547,7 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
}
snippet = "..." + snippet;
}
-
+
if (snippetStart + maxLength < content.length) {
const lastSpace = snippet.search(/\s[^\s]*$/);
if (lastSpace > snippet.length - 20 && lastSpace > 0) {
@@ -573,19 +578,19 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng
}
let matchingAttributes: Array<{name: string, value: string, type: string}> = [];
-
+
// Look for attributes that match the search tokens
for (const attr of attributes) {
const attrName = attr.name?.toLowerCase() || "";
const attrValue = attr.value?.toLowerCase() || "";
const attrType = attr.type || "";
-
+
// Check if any search token matches the attribute name or value
const hasMatch = searchTokens.some(token => {
const normalizedToken = normalizeString(token.toLowerCase());
return attrName.includes(normalizedToken) || attrValue.includes(normalizedToken);
});
-
+
if (hasMatch) {
matchingAttributes.push({
name: attr.name || "",
@@ -611,20 +616,20 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng
const targetTitle = targetNote ? targetNote.title : attr.value;
line = `~${attr.name}="${targetTitle}"`;
}
-
+
if (line) {
lines.push(line);
}
}
let snippet = lines.join('\n');
-
+
// Apply length limit while preserving line structure
if (snippet.length > maxLength) {
// Try to truncate at word boundaries but keep lines intact
const truncated = snippet.substring(0, maxLength);
const lastNewline = truncated.lastIndexOf('\n');
-
+
if (lastNewline > maxLength / 2) {
// If we can keep most content by truncating to last complete line
snippet = truncated.substring(0, lastNewline);
@@ -698,7 +703,7 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
for (const result of searchResults) {
result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, "");
-
+
// Initialize highlighted content snippet
if (result.contentSnippet) {
// Escape HTML but preserve newlines for later conversion to
@@ -706,7 +711,7 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
// Remove any stray < { } that might interfere with our highlighting markers
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/[<{}]/g, "");
}
-
+
// Initialize highlighted attribute snippet
if (result.attributeSnippet) {
// Escape HTML but preserve newlines for later conversion to
@@ -767,14 +772,14 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
if (result.highlightedNotePathTitle) {
result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/{/g, "").replace(/}/g, " ");
}
-
+
if (result.highlightedContentSnippet) {
// Replace highlighting markers with HTML tags
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/{/g, "").replace(/}/g, " ");
// Convert newlines to tags for HTML display
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/\n/g, " ");
}
-
+
if (result.highlightedAttributeSnippet) {
// Replace highlighting markers with HTML tags
result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/{/g, "").replace(/}/g, " ");
diff --git a/apps/server/src/services/sync.ts b/apps/server/src/services/sync.ts
index ef3bd6cba9..85725cc899 100644
--- a/apps/server/src/services/sync.ts
+++ b/apps/server/src/services/sync.ts
@@ -17,11 +17,12 @@ import ws from "./ws.js";
import entityChangesService from "./entity_changes.js";
import entityConstructor from "../becca/entity_constructor.js";
import becca from "../becca/becca.js";
-import type { EntityChange, EntityChangeRecord, EntityRow } from "@triliumnext/commons";
+import type { EntityChange, EntityChangeRecord, EntityRow, BlobRow } from "@triliumnext/commons";
import type { CookieJar, ExecOpts } from "./request_interface.js";
import setupService from "./setup.js";
import consistency_checks from "./consistency_checks.js";
import becca_loader from "../becca/becca_loader.js";
+import blobStorageService from "./blob-storage.js";
let proxyToggle = true;
@@ -354,13 +355,15 @@ function getEntityChangeRow(entityChange: EntityChange) {
return null;
}
- if (entityName === "blobs" && entityRow.content !== null) {
- if (typeof entityRow.content === "string") {
- entityRow.content = Buffer.from(entityRow.content, "utf-8");
- }
+ if (entityName === "blobs") {
+ const blobRow = entityRow as BlobRow;
+ const rawContent = blobStorageService.getContent(blobRow);
+ if (rawContent) {
+ const buffer = Buffer.isBuffer(rawContent)
+ ? rawContent
+ : Buffer.from(rawContent, "utf-8");
- if (entityRow.content) {
- entityRow.content = entityRow.content.toString("base64");
+ entityRow.content = buffer.toString("base64");
}
}
diff --git a/apps/server/src/services/sync_update.ts b/apps/server/src/services/sync_update.ts
index 9d4ff5c4c0..5ab3b46e5a 100644
--- a/apps/server/src/services/sync_update.ts
+++ b/apps/server/src/services/sync_update.ts
@@ -4,7 +4,8 @@ import entityChangesService from "./entity_changes.js";
import eventService from "./events.js";
import entityConstructor from "../becca/entity_constructor.js";
import ws from "./ws.js";
-import type { EntityChange, EntityChangeRecord, EntityRow } from "@triliumnext/commons";
+import blobStorageService from "./blob-storage.js";
+import type { EntityChange, EntityChangeRecord, EntityRow, BlobRow } from "@triliumnext/commons";
interface UpdateContext {
alreadyErased: number;
@@ -126,6 +127,14 @@ function preProcessContent(remoteEC: EntityChange, remoteEntityRow: EntityRow) {
remoteEntityRow.content = "";
}
}
+
+ // store external blobs on this instance too
+ const blobRow = remoteEntityRow as BlobRow;
+ if (blobRow.contentLocation && blobRow.contentLocation !== "internal" && remoteEntityRow.content) {
+ const newContentLocation = blobStorageService.saveExternal(blobRow.blobId, remoteEntityRow.content);
+ blobRow.contentLocation = newContentLocation;
+ blobRow.content = null;
+ }
}
}
diff --git a/apps/server/src/services/ws.ts b/apps/server/src/services/ws.ts
index 9dfcbc0198..2c79ee99bd 100644
--- a/apps/server/src/services/ws.ts
+++ b/apps/server/src/services/ws.ts
@@ -7,6 +7,7 @@ import config from "./config.js";
import syncMutexService from "./sync_mutex.js";
import protectedSessionService from "./protected_session.js";
import becca from "../becca/becca.js";
+import blobStorageService from "./blob-storage.js";
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import type { IncomingMessage, Server as HttpServer } from "http";
@@ -153,8 +154,12 @@ function fillInAdditionalProperties(entityChange: EntityChange) {
entityChange.entity = becca.getAttachment(entityChange.entityId);
if (!entityChange.entity) {
+ const contentLengthColumn = blobStorageService.hasExternalContentColumns()
+ ? "blobs.contentLength"
+ : "LENGTH(COALESCE(blobs.content, ''))";
+
entityChange.entity = sql.getRow(
- /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
+ /*sql*/`SELECT attachments.*, ${contentLengthColumn} AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE attachmentId = ?`,
diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts
index 338ba07aef..e420c7e9c7 100644
--- a/apps/server/src/share/content_renderer.ts
+++ b/apps/server/src/share/content_renderer.ts
@@ -28,7 +28,7 @@ const templateCache: Map = new Map();
*/
export interface Result {
header: string;
- content: string | Buffer | undefined;
+ content: string | Buffer | null | undefined;
/** Set to `true` if the provided content should be rendered as empty. */
isEmpty?: boolean;
}
diff --git a/apps/server/src/share/shaca/entities/sattachment.ts b/apps/server/src/share/shaca/entities/sattachment.ts
index 11d3af0969..17873ea90b 100644
--- a/apps/server/src/share/shaca/entities/sattachment.ts
+++ b/apps/server/src/share/shaca/entities/sattachment.ts
@@ -2,6 +2,7 @@
import sql from "../../sql.js";
import utils from "../../../services/utils.js";
+import blobStorageService from "../../../services/blob-storage.js";
import AbstractShacaEntity from "./abstract_shaca_entity.js";
import type SNote from "./snote.js";
import type { Blob } from "../../../services/blob-interface.js";
@@ -37,7 +38,11 @@ class SAttachment extends AbstractShacaEntity {
}
getContent(silentNotFoundError = false) {
- const row = sql.getRow>(/*sql*/`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
+ const query = blobStorageService.hasExternalContentColumns()
+ ? /*sql*/`SELECT content, contentLocation FROM blobs WHERE blobId = ?`
+ : /*sql*/`SELECT content, 'internal' as contentLocation FROM blobs WHERE blobId = ?`;
+
+ const row = sql.getRow>(query, [this.blobId]);
if (!row) {
if (silentNotFoundError) {
@@ -47,7 +52,7 @@ class SAttachment extends AbstractShacaEntity {
}
}
- const content = row.content;
+ const content = blobStorageService.getContent(row);
if (this.hasStringContent()) {
return content === null ? "" : content.toString("utf-8");
diff --git a/apps/server/src/share/shaca/entities/snote.ts b/apps/server/src/share/shaca/entities/snote.ts
index 19dbd463e2..a729213952 100644
--- a/apps/server/src/share/shaca/entities/snote.ts
+++ b/apps/server/src/share/shaca/entities/snote.ts
@@ -5,6 +5,7 @@ import utils from "../../../services/utils.js";
import AbstractShacaEntity from "./abstract_shaca_entity.js";
import escape from "escape-html";
import type { Blob } from "../../../services/blob-interface.js";
+import blobStorageService from "../../../services/blob-storage.js";
import type SAttachment from "./sattachment.js";
import type SAttribute from "./sattribute.js";
import type SBranch from "./sbranch.js";
@@ -96,7 +97,11 @@ class SNote extends AbstractShacaEntity {
}
getContent(silentNotFoundError = false) {
- const row = sql.getRow>(/*sql*/`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
+ const query = blobStorageService.hasExternalContentColumns()
+ ? /*sql*/`SELECT content, contentLocation FROM blobs WHERE blobId = ?`
+ : /*sql*/`SELECT content, 'internal' as contentLocation FROM blobs WHERE blobId = ?`;
+
+ const row = sql.getRow>(query, [this.blobId]);
if (!row) {
if (silentNotFoundError) {
@@ -106,7 +111,7 @@ class SNote extends AbstractShacaEntity {
}
}
- const content = row.content;
+ const content = blobStorageService.getContent(row);
if (this.hasStringContent()) {
return content === null ? "" : content.toString("utf-8");
diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts
index 991d370bc5..14c67c823b 100644
--- a/apps/server/vite.config.mts
+++ b/apps/server/vite.config.mts
@@ -27,6 +27,7 @@ export default defineConfig(() => ({
provider: 'v8' as const,
reporter: [ "text", "html" ]
},
- pool: "vmForks"
+ pool: "vmForks",
+ hookTimeout: 30000
},
}));
diff --git a/docs/Developer Guide/Developer Guide/Architecture/Database/Database structure/blobs.md b/docs/Developer Guide/Developer Guide/Architecture/Database/Database structure/blobs.md
index 63bf8c24d9..3dc7f7ddee 100644
--- a/docs/Developer Guide/Developer Guide/Architecture/Database/Database structure/blobs.md
+++ b/docs/Developer Guide/Developer Guide/Architecture/Database/Database structure/blobs.md
@@ -1,2 +1,2 @@
# blobs
-Column Name Data Type Nullity Default value Description blobIdText Non-null The unique ID of the blob (e.g. XXbfAJXqWrYnSXcelLFA).
The ID is actually a hash of the content, see AbstractBeccaEntity#saveBlob! It is a logic error to modify an existing blob.
contentText Nullable nullThe content of the blob, can be either:
text (for plain text notes or HTML notes). binary (for images and other types of attachments) dateModifiedText Non-null Creation date with timezone offset (e.g. 2023-11-08 18:43:44.204+0200) utcDateModifiedText Non-null Creation date in UTC format (e.g. 2023-11-08 16:43:44.204Z).
Blobs cannot be modified, so this timestamp specifies when the blob was created.
\ No newline at end of file
+Column Name Data Type Nullity Default value Description blobIdText Non-null The unique ID of the blob (e.g. XXbfAJXqWrYnSXcelLFA).
The ID is actually a hash of the content, see AbstractBeccaEntity#saveBlob! It is a logic error to modify an existing blob.
contentText Nullable nullThe content of the blob, can be either:
text (for plain text notes or HTML notes) binary (for images and other types of attachments) NULL when the content is stored externally (see contentLocation) contentLocationText Non-null internalLocation of the blob content (added in migration 234):
internal - content is stored in the content columnfile://<path> - content is stored externally in the file system at the specified path relative to external-blobs directorycontentLengthInteger Non-null 0Size of the blob content in bytes (added in migration 234). dateModifiedText Non-null Creation date with timezone offset (e.g. 2023-11-08 18:43:44.204+0200) utcDateModifiedText Non-null Creation date in UTC format (e.g. 2023-11-08 16:43:44.204Z).
Blobs cannot be modified, so this timestamp specifies when the blob was created.
\ No newline at end of file
diff --git a/docs/User Guide/!!!meta.json b/docs/User Guide/!!!meta.json
index b05f2ed21b..9d0912761c 100644
--- a/docs/User Guide/!!!meta.json
+++ b/docs/User Guide/!!!meta.json
@@ -1632,10 +1632,17 @@
{
"type": "relation",
"name": "internalLink",
- "value": "Gzjqa934BdH4",
+ "value": "xB9eL2mK8vWp",
"isInheritable": false,
"position": 40
},
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "Gzjqa934BdH4",
+ "isInheritable": false,
+ "position": 50
+ },
{
"type": "label",
"name": "shareAlias",
@@ -1687,24 +1694,31 @@
{
"type": "relation",
"name": "internalLink",
- "value": "bnyigUA2UK7s",
+ "value": "xB9eL2mK8vWp",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
- "value": "x59R8J8KV5Bp",
+ "value": "bnyigUA2UK7s",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
- "value": "Gzjqa934BdH4",
+ "value": "x59R8J8KV5Bp",
"isInheritable": false,
"position": 50
},
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "Gzjqa934BdH4",
+ "isInheritable": false,
+ "position": 60
+ },
{
"type": "label",
"name": "shareAlias",
@@ -3615,24 +3629,31 @@
{
"type": "relation",
"name": "internalLink",
- "value": "W8vYD3Q1zjCR",
+ "value": "xB9eL2mK8vWp",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
- "value": "8YBEPzcpUgxw",
+ "value": "W8vYD3Q1zjCR",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
- "value": "oPVyFC7WL2Lp",
+ "value": "8YBEPzcpUgxw",
"isInheritable": false,
"position": 60
},
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "oPVyFC7WL2Lp",
+ "isInheritable": false,
+ "position": 70
+ },
{
"type": "label",
"name": "shareAlias",
@@ -8864,17 +8885,31 @@
{
"type": "relation",
"name": "internalLink",
- "value": "R7abl2fc6Mxi",
+ "value": "XpOYSgsLkTJy",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
- "value": "6tZeKvSHEUiB",
+ "value": "A9Oc6YKKc65v",
"isInheritable": false,
"position": 60
},
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "R7abl2fc6Mxi",
+ "isInheritable": false,
+ "position": 70
+ },
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "6tZeKvSHEUiB",
+ "isInheritable": false,
+ "position": 80
+ },
{
"type": "label",
"name": "iconClass",
@@ -8888,20 +8923,6 @@
"value": "render-note",
"isInheritable": false,
"position": 70
- },
- {
- "type": "relation",
- "name": "internalLink",
- "value": "XpOYSgsLkTJy",
- "isInheritable": false,
- "position": 80
- },
- {
- "type": "relation",
- "name": "internalLink",
- "value": "A9Oc6YKKc65v",
- "isInheritable": false,
- "position": 90
}
],
"format": "markdown",
@@ -9095,32 +9116,32 @@
"mime": "text/html",
"attributes": [
{
- "type": "label",
- "name": "iconClass",
- "value": "bx bx-pen",
+ "type": "relation",
+ "name": "internalLink",
+ "value": "CoFPLs3dRlXc",
"isInheritable": false,
"position": 10
},
{
- "type": "label",
- "name": "shareAlias",
- "value": "canvas",
+ "type": "relation",
+ "name": "internalLink",
+ "value": "XpOYSgsLkTJy",
"isInheritable": false,
"position": 20
},
{
- "type": "relation",
- "name": "internalLink",
- "value": "CoFPLs3dRlXc",
+ "type": "label",
+ "name": "iconClass",
+ "value": "bx bx-pen",
"isInheritable": false,
- "position": 30
+ "position": 10
},
{
- "type": "relation",
- "name": "internalLink",
- "value": "XpOYSgsLkTJy",
+ "type": "label",
+ "name": "shareAlias",
+ "value": "canvas",
"isInheritable": false,
- "position": 40
+ "position": 20
}
],
"format": "markdown",
@@ -9206,6 +9227,13 @@
"isInheritable": false,
"position": 20
},
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "CoFPLs3dRlXc",
+ "isInheritable": false,
+ "position": 30
+ },
{
"type": "label",
"name": "iconClass",
@@ -9219,13 +9247,6 @@
"value": "mindmap",
"isInheritable": false,
"position": 30
- },
- {
- "type": "relation",
- "name": "internalLink",
- "value": "CoFPLs3dRlXc",
- "isInheritable": false,
- "position": 40
}
],
"format": "markdown",
@@ -9280,66 +9301,73 @@
{
"type": "relation",
"name": "internalLink",
- "value": "wX4HbRucYSDD",
+ "value": "XpOYSgsLkTJy",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
- "value": "ODY7qQn5m2FT",
+ "value": "wX4HbRucYSDD",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
- "value": "mHbBMPDPkVV5",
+ "value": "ODY7qQn5m2FT",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
- "value": "6f9hih2hXXZk",
+ "value": "mHbBMPDPkVV5",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
- "value": "BlN9DFI679QC",
+ "value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
- "value": "0vhv7lsOLy82",
+ "value": "BlN9DFI679QC",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
- "value": "8YBEPzcpUgxw",
+ "value": "0vhv7lsOLy82",
"isInheritable": false,
"position": 100
},
{
"type": "relation",
"name": "internalLink",
- "value": "0ESUbbAxVnoK",
+ "value": "8YBEPzcpUgxw",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
- "value": "nBAXQFj20hS1",
+ "value": "0ESUbbAxVnoK",
"isInheritable": false,
"position": 120
},
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "nBAXQFj20hS1",
+ "isInheritable": false,
+ "position": 130
+ },
{
"type": "label",
"name": "shareAlias",
@@ -9353,13 +9381,6 @@
"value": "bx bx-file-blank",
"isInheritable": false,
"position": 140
- },
- {
- "type": "relation",
- "name": "internalLink",
- "value": "XpOYSgsLkTJy",
- "isInheritable": false,
- "position": 150
}
],
"format": "markdown",
@@ -12808,6 +12829,61 @@
}
]
},
+ {
+ "isClone": false,
+ "noteId": "xB9eL2mK8vWp",
+ "notePath": [
+ "pOsGYCXsbNQG",
+ "tC7s2alapj8V",
+ "xB9eL2mK8vWp"
+ ],
+ "title": "External Blob Storage",
+ "notePosition": 135,
+ "prefix": null,
+ "isExpanded": false,
+ "type": "text",
+ "mime": "text/markdown",
+ "attributes": [
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "tAassRL4RSQL",
+ "isInheritable": false,
+ "position": 10
+ },
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "Gzjqa934BdH4",
+ "isInheritable": false,
+ "position": 20
+ },
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "ODY7qQn5m2FT",
+ "isInheritable": false,
+ "position": 30
+ },
+ {
+ "type": "label",
+ "name": "shareAlias",
+ "value": "external-blob-storage",
+ "isInheritable": false,
+ "position": 10
+ },
+ {
+ "type": "label",
+ "name": "iconClass",
+ "value": "bx bx-hdd",
+ "isInheritable": false,
+ "position": 20
+ }
+ ],
+ "format": "markdown",
+ "dataFileName": "External Blob Storage.md",
+ "attachments": []
+ },
{
"isClone": false,
"noteId": "47ZrP6FNuoG8",
@@ -12888,24 +12964,31 @@
{
"type": "relation",
"name": "internalLink",
- "value": "6tZeKvSHEUiB",
+ "value": "xB9eL2mK8vWp",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
- "value": "oyIAJ9PvvwHX",
+ "value": "6tZeKvSHEUiB",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
- "value": "Gzjqa934BdH4",
+ "value": "oyIAJ9PvvwHX",
"isInheritable": false,
"position": 40
},
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "Gzjqa934BdH4",
+ "isInheritable": false,
+ "position": 50
+ },
{
"type": "label",
"name": "shareAlias",
diff --git a/docs/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.md b/docs/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.md
index 936cc3c4e4..df997d97c3 100644
--- a/docs/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.md
+++ b/docs/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.md
@@ -81,6 +81,13 @@ Additionally, shorter aliases are available for common configurations (see Alter
| --- | --- | --- | --- |
| `TRILIUM_LOGGING_RETENTIONDAYS` | integer | 90 | Number of days to retain log files |
+### ExternalBlobStorage Section
+
+| Environment Variable | Type | Default | Description |
+| --- | --- | --- | --- |
+| `TRILIUM_EXTERNAL_BLOB_STORAGE_ENABLED` | boolean | false | Enable external blob storage for large attachments |
+| `TRILIUM_EXTERNAL_BLOB_STORAGE_THRESHOLD` | integer | 102400 | Size threshold in bytes (100KB default) for external storage |
+
## Alternative Environment Variables
The following alternative environment variable names are also supported and work identically to their longer counterparts:
diff --git a/docs/User Guide/User Guide/Advanced Usage/Database.md b/docs/User Guide/User Guide/Advanced Usage/Database.md
index a0a99228e0..1bed53c843 100644
--- a/docs/User Guide/User Guide/Advanced Usage/Database.md
+++ b/docs/User Guide/User Guide/Advanced Usage/Database.md
@@ -1,6 +1,8 @@
# Database
Your Trilium data is stored in a [SQLite](https://www.sqlite.org) database which contains all notes, tree structure, metadata, and most of the configuration. The database file is named `document.db` and is stored in the application's default [Data directory](../Installation%20%26%20Setup/Data%20directory.md).
+By default, all note and attachment content is stored within the database. However, when [external blob storage](External%20Blob%20Storage.md) is enabled, large notes and attachments are stored separately in the `external-blobs` directory for better performance and backup flexibility.
+
## Demo Notes
When first starting Trilium, it will provide a set of notes to showcase various features of the application.
diff --git a/docs/User Guide/User Guide/Advanced Usage/External Blob Storage.md b/docs/User Guide/User Guide/Advanced Usage/External Blob Storage.md
new file mode 100644
index 0000000000..a46ca84a86
--- /dev/null
+++ b/docs/User Guide/User Guide/Advanced Usage/External Blob Storage.md
@@ -0,0 +1,47 @@
+# External Blob Storage
+External blob storage is an optional feature that stores large attachments as separate files on the filesystem instead of embedding them in the SQLite database. This feature is disabled by default to maintain backward compatibility and simplicity for most users.
+
+## When to Enable External Blob Storage
+
+### Benefits of enabling external storage:
+
+* **Performance**: Large attachments don't bloat the database, which can improve query performance
+* **Backup flexibility**: Large files can be backed up separately from the database using different strategies or schedules
+* **Storage efficiency**: Easier to manage and migrate large files independently
+
+### When to keep it disabled (default):
+
+* **Simplicity**: All data in one database file makes backup and migration straightforward
+* **Small attachments**: If your attachments are mostly small, database storage is more efficient
+* **Portability**: Single-file database is easier to copy and move between systems
+
+## How It Works
+
+When external blob storage is enabled and a note or attachment exceeds the configured threshold size the content is saved to the file system in the `external-blobs` directory within your [data directory](../Installation%20%26%20Setup/Data%20directory.md). The files are organized in a partitioned directory structure to prevent excessive files in a single directory. The database stores a reference to the external file and any additional metadata.
+
+Attachments below the threshold, or when external storage is disabled, continue to be stored in the database.
+
+When you enable external blob storage, existing attachments in the database remain there. Only new attachments that exceed the threshold will be stored externally. Automatically migrating existing attachments is currently not supported.
+
+If you disable external blob storage after using it, existing external blobs remain in the `external-blobs` directory and continue to work. New attachments will be stored in the database regardless of size.
+
+## Configuration
+
+External blob storage can be configured via `config.ini` or environment variables. See the [Configuration](Configuration%20\(config.ini%20or%20e.md) documentation for all available options.
+
+## Threshold Size Recommendations
+
+The default threshold of 100KB (102400 bytes) is based on [SQLite's performance recommendations](https://www.sqlite.org/intern-v-extern-blob.html).
+
+You can adjust the threshold based on your needs:
+
+* **Lower threshold (e.g., 50KB)**: More files stored externally, smaller database
+* **Higher threshold (e.g., 1MB)**: Fewer external files, larger database but simpler backup
+
+## Important Backup Considerations
+
+When external blob storage is enabled, you must backup both the database and the `external-blobs` directory.
+
+Without backing up the `external-blobs` directory, you would lose large attachments if restoring from backup.
+
+See the [Backup](../Installation%20%26%20Setup/Backup.md) documentation for detailed backup strategies with external blob storage.
\ No newline at end of file
diff --git a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Attachments.md b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Attachments.md
index bb38e72f90..5c7dae9d3f 100644
--- a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Attachments.md
+++ b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Attachments.md
@@ -7,6 +7,10 @@ Each note exclusively owns its attachments, meaning attachments cannot be shared
Attachments, especially image files, are the recommended method for embedding visuals in notes. It is important to link image attachments within the text of the owning note; otherwise, they will be automatically deleted after a configurable timeout period if not referenced.
+## Storage
+
+By default, attachments are stored within the SQLite database. For installations with many large attachments, Trilium supports [external blob storage](../../Advanced%20Usage/External%20Blob%20Storage.md) which stores large files separately on the file system. This is optional and disabled by default to maintain simplicity for most users.
+
## Converting notes to attachments
File notes can be easily converted to attachments of the parent note.
diff --git a/docs/User Guide/User Guide/Installation & Setup/Backup.md b/docs/User Guide/User Guide/Installation & Setup/Backup.md
index 699ca0e009..42c437f435 100644
--- a/docs/User Guide/User Guide/Installation & Setup/Backup.md
+++ b/docs/User Guide/User Guide/Installation & Setup/Backup.md
@@ -12,6 +12,18 @@ This is only very basic backup solution, and you're encouraged to add some bette
Note that Synchronization provides also some backup capabilities by its nature of distributing the data to other computers.
+## External Blob Storage and Backups
+
+If you have enabled external blob storage , you must backup both the database and the `external-blobs` directory. Without backing up `external-blobs` separately, you would lose large attachments if restoring from backup.
+
+By default, external blob storage is disabled, meaning all attachments are stored in the database and the automatic backup described above is sufficient.
+
+### How to Backup with External Blobs
+
+When external blob storage is enabled, backup the complete [data directory](Data%20directory.md) which includes both the database and `external-blobs`. This is the simplest approach and ensures nothing is missed.
+
+To ensure consistency, either stop Trilium before backing up, or use a filesystem snapshot tool (e.g., LVM snapshots, ZFS snapshots, or volume shadow copy on Windows) to capture both the database and `external-blobs` directory at the same point in time.
+
## Restoring backup
Let's assume you want to restore the weekly backup, here's how to do it:
@@ -23,6 +35,7 @@ Let's assume you want to restore the weekly backup, here's how to do it:
* delete `~/trilium-data/document.db`, `~/trilium-data/document.db-wal` and `~/trilium-data/document.db-shm` (latter two files are auto generated)
* copy and rename this `~/trilium-data/backup/backup-weekly.db` to `~/trilium-data/document.db`
* make sure that the file is writable, e.g. with `chmod 600 document.db`
+* if you have external blob storage enabled, also restore the `external-blobs` directory from your backup
* start Trilium again
If you have configured sync then you need to do it across all members of the sync cluster, otherwise older version (restored backup) of the document will be detected and synced to the newer version.
diff --git a/docs/User Guide/User Guide/Installation & Setup/Data directory.md b/docs/User Guide/User Guide/Installation & Setup/Data directory.md
index 3d66bafe90..684657bf7c 100644
--- a/docs/User Guide/User Guide/Installation & Setup/Data directory.md
+++ b/docs/User Guide/User Guide/Installation & Setup/Data directory.md
@@ -5,6 +5,7 @@ Data directory contains:
* `config.ini` - instance level settings like port on which the Trilium application runs
* `backup` - contains automatically [backup](Backup.md) of documents
* `log` - contains application log files
+* `external-blobs` - contains large attachments when [external blob storage](../Advanced%20Usage/External%20Blob%20Storage.md) is enabled (optional, not present by default)
## Location of the data directory
diff --git a/packages/commons/src/lib/rows.ts b/packages/commons/src/lib/rows.ts
index 5710cf84f7..4f914515aa 100644
--- a/packages/commons/src/lib/rows.ts
+++ b/packages/commons/src/lib/rows.ts
@@ -66,10 +66,13 @@ export interface EtapiTokenRow {
isDeleted?: boolean;
}
+export type BlobContentLocation = 'internal' | `file://${string}`;
+
export interface BlobRow {
blobId: string;
- content: string | Buffer;
+ content: string | Buffer | null;
contentLength: number;
+ contentLocation: BlobContentLocation;
dateModified: string;
utcDateModified: string;
}