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 bd6d49aabe..da62e509d8 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-file","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-file","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","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Apache"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_l2VkvOwUNfZj","title":"TLS Configuration","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/TLS Configuration"},{"name":"iconClass","value":"bx bx-file","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-lock-alt","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_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-file","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-file","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-file","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":"Export as PDF","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Export as PDF"},{"name":"iconClass","value":"bx bxs-file-pdf","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":"book","attributes":[{"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-file","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-file","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_GTwFsgaA0lCt","title":"Collections","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Collections"},{"name":"iconClass","value":"bx bx-book","type":"label"}],"children":[{"id":"_help_xWbu3jpNWapp","title":"Calendar View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Collections/Calendar View"},{"name":"iconClass","value":"bx bx-calendar","type":"label"}]},{"id":"_help_81SGnPGMk7Xc","title":"Geo Map View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Collections/Geo Map View"},{"name":"iconClass","value":"bx bx-map-alt","type":"label"}]},{"id":"_help_8QqnMzx393bx","title":"Grid View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/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/Note Types/Collections/List View"},{"name":"iconClass","value":"bx bx-list-ul","type":"label"}]},{"id":"_help_2FvYrpmOXm29","title":"Table View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Collections/Table View"},{"name":"iconClass","value":"bx bx-table","type":"label"}]},{"id":"_help_CtBQqbwXDx1w","title":"Board View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Collections/Board View"},{"name":"iconClass","value":"bx bx-columns","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_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","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-file","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-file","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-file","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-file","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 bx-file","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-file","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-file","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 bx-file","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-file","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 bx-file","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-file","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-file","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 bx-file","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-file","type":"label"}],"children":[{"id":"_help_9qPsTWBorUhQ","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"/etapi/docs"},{"name":"iconClass","value":"bx bx-file","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-file","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 bx-file","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-file","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-file","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-file","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 bxs-component","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-file","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-file","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-file","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 bx-folder","type":"label"}],"children":[{"id":"_help_z8O2VG4ZZJD7","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"/api/docs"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"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_LMAv4Uy3Wk6J","title":"AI","type":"book","attributes":[{"name":"iconClass","value":"bx bx-bot","type":"label"}],"children":[{"id":"_help_GBBMSlVSOIGP","title":"Introduction","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Introduction"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WkM7gsEUyCXs","title":"AI Provider Information","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_7EdTxPADv95W","title":"Ollama","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_vvUCN7FDkq7G","title":"Installing Ollama","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information/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/AI Provider Information/OpenAI"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_e0lkirXEiSNc","title":"Anthropic","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information/Anthropic"},{"name":"iconClass","value":"bx bx-file","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-file","type":"label"}]},{"id":"_help_es8OU2GuguFU","title":"Examples","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_TjLYAo3JMO8X","title":"\"New Task\" launcher button","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/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/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/Examples/Using promoted attributes to c"},{"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/Events"},{"name":"iconClass","value":"bx bx-rss","type":"label"}]},{"id":"_help_MgibgPcfeuGz","title":"Custom Widgets","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Custom Widgets"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_YNxAqkI5Kg1M","title":"Word count widget","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/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/Custom Widgets/Widget Basics"},{"name":"iconClass","value":"bx bx-file","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-file","type":"label"}],"children":[{"id":"_help_Q2z6av6JZVWm","title":"Frontend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/interfaces/Frontend_Script_API.Api.html"},{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_habiZ3HU8Kw8","title":"FNote","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/classes/Frontend_Script_API.FNote.html"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_MEtfsqa5VwNi","title":"Backend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/interfaces/Backend_Script_API.Api.html"},{"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-file","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-file","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","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Apache"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_l2VkvOwUNfZj","title":"TLS Configuration","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/TLS Configuration"},{"name":"iconClass","value":"bx bx-file","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-lock-alt","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_YXZ2zPDYrWbn","title":"Security","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_im3dphtmpQ46","title":"Protected Notes and Encryption","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_U9HWXPbFIRW3","title":"Server Installation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_2L6zxH5z1QMw","title":"1. Installing the server","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_baqzSqQefEpE","title":"Manually","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Manually"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_Ycyax5BCWgUb","title":"Multiple server instances","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Multiple server instances"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_6YVZgQtfkxkS","title":"On NixOS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/On NixOS"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_Q5jqLlwsnIBb","title":"Packaged version for Linux","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Packaged version for Linux"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_FNVzcT2FwEjq","title":"Using Docker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Using Docker"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_9ErAobCLD1jl","title":"Using Kubernetes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Using Kubernetes"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_pIaUHjns1S3p","title":"2. Reverse proxy","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_Eu2zkbyE2QPK","title":"Apache","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Apache"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_vFhVZ4ZRNxLY","title":"Nginx","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Nginx"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_CpWc4lepON8y","title":"Authentication","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/Authentication"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_iYzJELZlnPVk","title":"Multi-Factor Authentication","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/Multi-Factor Authentication"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_1G2n2zCStCir","title":"TLS Configuration","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/1_Server Installation/TLS Configuration"},{"name":"iconClass","value":"bx bx-file","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-file","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-file","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-file","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":"Export as PDF","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Export as PDF"},{"name":"iconClass","value":"bx bxs-file-pdf","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":"book","attributes":[{"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-file","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-file","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_GTwFsgaA0lCt","title":"Collections","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Collections"},{"name":"iconClass","value":"bx bx-book","type":"label"}],"children":[{"id":"_help_xWbu3jpNWapp","title":"Calendar View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Collections/Calendar View"},{"name":"iconClass","value":"bx bx-calendar","type":"label"}]},{"id":"_help_81SGnPGMk7Xc","title":"Geo Map View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Collections/Geo Map View"},{"name":"iconClass","value":"bx bx-map-alt","type":"label"}]},{"id":"_help_8QqnMzx393bx","title":"Grid View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/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/Note Types/Collections/List View"},{"name":"iconClass","value":"bx bx-list-ul","type":"label"}]},{"id":"_help_2FvYrpmOXm29","title":"Table View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Collections/Table View"},{"name":"iconClass","value":"bx bx-table","type":"label"}]},{"id":"_help_CtBQqbwXDx1w","title":"Board View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Collections/Board View"},{"name":"iconClass","value":"bx bx-columns","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_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","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-file","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-file","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-file","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-file","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 bx-file","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-file","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-file","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 bx-file","type":"label"}]}]},{"id":"_help_tC7s2alapj8V","title":"Advanced Usage","type":"book","attributes":[{"name":"iconClass","value":"bx bx-rocket","type":"label"}],"children":[{"id":"_help_ZuNbtBacBZ6Z","title":"Search","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_ey9TMFyD8SHR","title":"Advanced-Search-Expressions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_6SpIgsGqzwNI","title":"README","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Search/README"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_UUBStSxWzjgA","title":"Saved-Searches","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Search/Saved-Searches"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_yAFfA1SAYlr7","title":"Search-Examples-and-Use-Cases","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Search/Search-Examples-and-Use-Cases"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WcwMZ2tDZmXK","title":"Search-Fundamentals","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Search/Search-Fundamentals"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_ttFz520bcNLf","title":"Technical-Search-Details","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Search/Technical-Search-Details"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"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-file","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 bx-file","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-file","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-file","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 bx-file","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-file","type":"label"}],"children":[{"id":"_help_9qPsTWBorUhQ","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"/etapi/docs"},{"name":"iconClass","value":"bx bx-file","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-file","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 bx-file","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-file","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-file","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-file","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 bxs-component","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-file","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-file","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-file","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 bx-folder","type":"label"}],"children":[{"id":"_help_z8O2VG4ZZJD7","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"/api/docs"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"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_LMAv4Uy3Wk6J","title":"AI","type":"book","attributes":[{"name":"iconClass","value":"bx bx-bot","type":"label"}],"children":[{"id":"_help_GBBMSlVSOIGP","title":"Introduction","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Introduction"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WkM7gsEUyCXs","title":"AI Provider Information","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_7EdTxPADv95W","title":"Ollama","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_vvUCN7FDkq7G","title":"Installing Ollama","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information/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/AI Provider Information/OpenAI"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_e0lkirXEiSNc","title":"Anthropic","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information/Anthropic"},{"name":"iconClass","value":"bx bx-file","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-file","type":"label"}]},{"id":"_help_es8OU2GuguFU","title":"Examples","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_TjLYAo3JMO8X","title":"\"New Task\" launcher button","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/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/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/Examples/Using promoted attributes to c"},{"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/Events"},{"name":"iconClass","value":"bx bx-rss","type":"label"}]},{"id":"_help_MgibgPcfeuGz","title":"Custom Widgets","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Custom Widgets"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_YNxAqkI5Kg1M","title":"Word count widget","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/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/Custom Widgets/Widget Basics"},{"name":"iconClass","value":"bx bx-file","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-file","type":"label"}],"children":[{"id":"_help_Q2z6av6JZVWm","title":"Frontend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/interfaces/Frontend_Script_API.Api.html"},{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_habiZ3HU8Kw8","title":"FNote","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/classes/Frontend_Script_API.FNote.html"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_MEtfsqa5VwNi","title":"Backend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/interfaces/Backend_Script_API.Api.html"},{"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/Search/Advanced-Search-Expressions.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions.html new file mode 100644 index 0000000000..a96ab75e1a --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions.html @@ -0,0 +1,145 @@ +
This guide covers complex search expressions that combine multiple criteria, + use advanced operators, and leverage Trilium's relationship system for + sophisticated queries.
+Use parentheses to group expressions and control evaluation order:
(#book OR #article) AND #author=Tolkien
+Finds notes that are either books or articles, written by Tolkien.
#project AND (#status=active OR #status=pending)
+Finds active or pending projects.
meeting AND (#priority=high OR #urgent) AND note.dateCreated >= TODAY-7
+Finds recent high-priority or urgent meetings.
+Use NOT
or the not()
function to exclude certain
+ criteria:
#book AND not(#genre=fiction)
+Finds non-fiction books.
project AND not(note.isArchived=true)
+Finds non-archived notes containing "project".
#!completed
+Short syntax for notes without the "completed" label.
+Combine full-text, attribute, and property searches:
development #category=work note.type=text note.dateModified >= TODAY-30
+Finds text notes about development, categorized as work, modified in the + last 30 days.
+When fuzzy attribute search is enabled, you can use partial matches:
#lang
+Matches labels like "language", "languages", "programming-lang", etc.
#category=prog
+Matches categories like "programming", "progress", "program", etc.
+#book #author=Tolkien #publicationYear>=1950 #publicationYear<1960
+Finds Tolkien's books published in the 1950s.
#task #priority=high #status!=completed
+Finds high-priority incomplete tasks.
+Use various operators for sophisticated label matching:
#isbn %= '978-[0-9-]+'
+Finds notes with ISBN labels matching the pattern (regex).
#email *=* @company.com
+Finds notes with email labels containing "@company.com".
#version >= 2.0
+Finds notes with version labels of 2.0 or higher (numeric comparison).
+~author.title *=* Tolkien
+Finds notes with an "author" relation to notes containing "Tolkien" in + the title.
~project.labels.status = active
+Finds notes related to projects with active status.
+~author.relations.publisher.title = "Penguin Books"
+Finds notes authored by someone published by Penguin Books.
~project.children.title *=* documentation
+Finds notes related to projects that have child notes about documentation.
+note.children.title = "Chapter 1"
+Finds parent notes that have a child titled "Chapter 1".
note.parents.labels.category = book
+Finds notes whose parents are categorized as books.
note.ancestors.title = "Literature"
+Finds notes with "Literature" anywhere in their ancestor chain.
+note.type=code note.mime=text/javascript note.dateCreated >= MONTH
+Finds JavaScript code notes created this month.
note.isProtected=true note.contentSize > 1000
+Finds large protected notes.
note.childrenCount >= 10 note.type=text
+Finds text notes with many children.
+note.parentCount > 1 #template
+Finds template notes that are cloned in multiple places.
note.attributeCount > 5 note.type=text note.contentSize < 500
+Finds small text notes with many attributes (heavily tagged short notes).
note.revisionCount > 10 note.dateModified >= TODAY-7
+Finds frequently edited notes modified recently.
+#dueDate <= TODAY+7 #dueDate >= TODAY
+Finds tasks due in the next week.
note.dateCreated >= MONTH-2 note.dateCreated < MONTH
+Finds notes created in the past two months.
#eventDate = YEAR note.dateCreated >= YEAR-1
+Finds events scheduled for this year that were planned last year.
+(#startDate <= TODAY AND #endDate >= TODAY) OR #status=ongoing
+Finds current events or ongoing items.
#reminderDate <= NOW+3600 #reminderDate > NOW
+Finds reminders due in the next hour (using seconds offset).
+#title ~= managment
+Finds notes with titles like "management" even with typos.
~category.title ~= progaming
+Finds notes related to categories like "programming" with misspellings.
+note.content ~* algoritm
+Finds notes containing words like "algorithm" with spelling variations.
#description ~* recieve
+Finds notes with descriptions containing "receive" despite the common + misspelling.
+By default, Trilium uses exact matching first, then fuzzy as fallback:
development project
+First finds exact matches for "development" and "project", then adds fuzzy + matches if needed.
+To force fuzzy behavior:
#title ~= development #category ~= projet
+#book orderBy #publicationYear desc, note.title asc limit 20
+Orders books by publication year (newest first), then by title alphabetically, + limited to 20 results.
#task orderBy #priority desc, #dueDate asc
+Orders tasks by priority (high first), then by due date (earliest first).
+#meeting note.dateCreated >= TODAY-30 orderBy note.dateModified desc
+Finds recent meetings ordered by last modification.
#project #status=active orderBy note.childrenCount desc limit 10
+Finds the 10 most complex active projects (by number of sub-notes).
+Start with the most selective criteria:
#book #author=Tolkien note.dateCreated >= 1950-01-01
+Better than:
note.dateCreated >= 1950-01-01 #book #author=Tolkien
+#category=project #status=active
+With fast search enabled, this searches only attributes, not content.
+note.content *=* "complex search term" limit 50
+Limits content search to prevent performance issues.
+Invalid syntax produces helpful error messages:
#book AND OR #author=Tolkien
+Error: "Mixed usage of AND/OR - always use parentheses to group AND/OR + expressions."
+Enable debug mode to see how queries are parsed:
#book #author=Tolkien
+With debug enabled, shows the internal expression tree structure.
+note.title
not title
+ Full syntax:
note.labels.category = book
+Shortcut:
#category = book
+Full syntax:
note.relations.author.title *=* Tolkien
+Shortcut:
~author.title *=* Tolkien
+Some properties have convenient shortcuts:
note.text *=* content
+Searches both title and content for "content".
+(#project OR #task) AND #status!=completed AND
+(#priority=high OR #dueDate <= TODAY+7) AND
+not(note.isArchived=true)
+orderBy #priority desc, #dueDate asc
+(#paper OR #article OR #book) AND
+~author.title *=* smith AND
+#topic *=* "machine learning" AND
+note.dateCreated >= YEAR-2
+orderBy #citationCount desc limit 25
+note.type=text AND note.contentSize > 5000 AND
+#category=documentation AND note.childrenCount >= 3 AND
+note.dateModified >= MONTH-1
+orderBy note.dateModified desc
+note.attributeCount = 0 AND note.childrenCount = 0 AND
+note.parentCount = 1 AND note.contentSize < 100 AND
+note.dateModified < TODAY-90
+Finds potential cleanup candidates: small, untagged, isolated notes not + modified in 90 days.
+Welcome to the comprehensive guide for Trilium's powerful search capabilities. + This documentation covers everything from basic text searches to advanced + query expressions and performance optimization.
+New to Trilium search? Start here:
+#tag
, #category=book
+ ~author
, ~author.title=Tolkien
+ note.type=text
, note.dateCreated >= TODAY-7
+ note.parents.title=Books
+ note.children.labels.status=active
+ %=
operator=
- Exact match!=
- Not equal*=*
- Contains=*
- Starts with*=
- Ends with%=
- Regular expression~=
- Fuzzy exact match~*
- Fuzzy contains match=
, !=
, >
, >=
, <
, <=
+ AND
, OR
, NOT
+ #labelName
- Label exists#labelName=value
- Label equals value~relationName
- Relation exists~relationName.property
- Relation target propertynote.property
- Note property access"exact phrase"
- Quoted phrase searchhello world # Find notes containing both words
+"project management" # Find exact phrase
+#task # Find notes with "task" label
+~author # Find notes with "author" relation
+#book #author=Tolkien # Books by Tolkien
+#task #priority=high #status!=completed # High-priority incomplete tasks
+~project.title *=* alpha # Notes related to projects with "alpha" in title
+note.dateCreated >= TODAY-7 # Notes created in last week
+#dueDate <= TODAY+30 # Items due in next 30 days
+#eventDate = YEAR # Events scheduled for this year
+(#book OR #article) AND #topic=programming AND note.dateModified >= MONTH
+#project AND (#status=active OR #status=pending) AND not(note.isArchived=true)
+=
, *=*
,
+ etc.)#
for labels and ~
for
+ relationsnote.
prefix for metadata
+ searchesTrilium's search integrates seamlessly with your note-taking workflow:
+Start with the fundamentals and gradually explore advanced features as + your needs grow. Trilium's search system is designed to scale from simple + text queries to sophisticated knowledge management systems.
+Happy searching! 🔍
\ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.html new file mode 100644 index 0000000000..f25effb5b1 --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.html @@ -0,0 +1,293 @@ +Saved searches in Trilium allow you to create dynamic collections of notes + that automatically update based on search criteria. They appear as special + notes in your tree and provide a powerful way to organize and access related + content.
+A saved search is a special note type that:
+#searchString
- The search query#fastSearch
- Enable fast search mode#includeArchivedNotes
- Include archived notes#orderBy
- Sort field#orderDirection
- "asc" or "desc"#limit
- Maximum number of resultsFor complex logic, create a JavaScript note and link it:
+~searchScript
- Relation pointing to a backend script note#searchString=project management
+Finds all notes containing "project management".
+#searchString=#book #author=Tolkien
+#orderBy=publicationYear
+#orderDirection=desc
+Creates a collection of Tolkien's books ordered by publication year.
+#searchString=#task #status!=completed #assignee=me
+#orderBy=priority
+#orderDirection=desc
+#limit=20
+Shows your top 20 incomplete tasks by priority.
+#searchString=note.dateModified >= TODAY-7
+#orderBy=dateModified
+#orderDirection=desc
+#limit=50
+Shows the 50 most recently modified notes from the last week.
+#searchString=note.dateCreated >= TODAY-7 note.dateCreated < TODAY
+#orderBy=dateCreated
+#orderDirection=desc
+#searchString=#reviewed=false note.dateCreated >= MONTH note.dateCreated < MONTH+1
+#orderBy=dateCreated
+#searchString=#dueDate >= TODAY #dueDate <= TODAY+14 #status!=completed
+#orderBy=dueDate
+#orderDirection=asc
+#searchString=#project=alpha (#task OR #milestone OR #document)
+#orderBy=priority
+#orderDirection=desc
+#searchString=#project=alpha #status=blocked OR (#dueDate < TODAY #status!=completed)
+#orderBy=dueDate
+#orderDirection=asc
+#searchString=(#documentation OR #guide OR #manual) #product=api
+#orderBy=dateModified
+#orderDirection=desc
+#searchString=#course #level=beginner #topic=programming
+#orderBy=difficulty
+#orderDirection=asc
+For complex logic that can't be expressed in search strings, use JavaScript:
+// Find notes that need attention based on complex criteria
+const api = require('api');
+
+const cutoffDate = new Date();
+cutoffDate.setDate(cutoffDate.getDate() - 30);
+
+const results = [];
+
+// Find high-priority tasks overdue by more than a week
+const overdueTasks = api.searchForNotes(`
+ #task #priority=high #dueDate < TODAY-7 #status!=completed
+`);
+
+// Find projects with no recent activity
+const staleProjets = api.searchForNotes(`
+ #project #status=active note.dateModified < TODAY-30
+`);
+
+// Find notes with many attributes but no content
+const overlabeledNotes = api.searchForNotes(`
+ note.attributeCount > 5 note.contentSize < 100
+`);
+
+return [...overdueTasks, ...staleProjects, ...overlabeledNotes]
+ .map(note => note.noteId);
+// Group notes by quarter based on creation date
+const api = require('api');
+
+const currentYear = new Date().getFullYear();
+const results = [];
+
+for (let quarter = 1; quarter <= 4; quarter++) {
+ const startMonth = (quarter - 1) * 3 + 1;
+ const endMonth = quarter * 3;
+
+ const quarterNotes = api.searchForNotes(`
+ note.dateCreated >= "${currentYear}-${String(startMonth).padStart(2, '0')}-01"
+ note.dateCreated < "${currentYear}-${String(endMonth + 1).padStart(2, '0')}-01"
+ #project
+ `);
+
+ results.push(...quarterNotes.map(note => note.noteId));
+}
+
+return results;
+// Smart dashboard that changes based on day of week
+const api = require('api');
+
+const today = new Date();
+const dayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc.
+
+let searchQuery;
+
+if (dayOfWeek === 1) { // Monday - weekly planning
+ searchQuery = '#task #status=planned #week=' + getWeekNumber(today);
+} else if (dayOfWeek === 5) { // Friday - weekly review
+ searchQuery = '#task #completed=true #week=' + getWeekNumber(today);
+} else { // Regular days - focus on today's work
+ searchQuery = '#task #dueDate=TODAY #status!=completed';
+}
+
+const notes = api.searchForNotes(searchQuery);
+return notes.map(note => note.noteId);
+
+function getWeekNumber(date) {
+ const firstDay = new Date(date.getFullYear(), 0, 1);
+ const pastDays = Math.floor((date - firstDay) / 86400000);
+ return Math.ceil((pastDays + firstDay.getDay() + 1) / 7);
+}
+For collections that don't need content search:
#searchString=#category=reference #type=article
+#fastSearch=true
+#limit=100
+Use indexed properties for better performance:
#orderBy=dateCreated
+#orderBy=title
+#orderBy=noteId
+Avoid complex calculated orderings in large collections.
+Always set reasonable limits for large collections:
#limit=50
+For very large result sets, consider breaking into multiple saved searches.
+Create a folder structure for saved searches:
📁 Searches
+├── 📁 Projects
+│ ├── 🔍 Active Projects
+│ ├── 🔍 Overdue Tasks
+│ └── 🔍 Project Archive
+├── 📁 Content
+│ ├── 🔍 Recent Drafts
+│ ├── 🔍 Published Articles
+│ └── 🔍 Review Queue
+└── 📁 Maintenance
+ ├── 🔍 Untagged Notes
+ ├── 🔍 Cleanup Candidates
+ └── 🔍 Orphaned Notes
+Use clear, descriptive names:
+Tag saved searches for organization:
#searchType=dashboard
+#searchType=maintenance
+#searchType=archive
+#frequency=daily
+#frequency=weekly
+Combine multiple saved searches in a parent note:
📋 My Dashboard
+├── 🔍 Today's Tasks
+├── 🔍 Urgent Items
+├── 🔍 Recent Notes
+├── 🔍 Upcoming Deadlines
+└── 🔍 Weekly Review Items
+📋 Project Alpha Dashboard
+├── 🔍 Active Tasks
+├── 🔍 Blocked Items
+├── 🔍 Recent Updates
+├── 🔍 Milestones
+└── 🔍 Team Notes
+📋 Content Management
+├── 🔍 Draft Articles
+├── 🔍 Review Queue
+├── 🔍 Published This Month
+├── 🔍 High-Engagement Posts
+└── 🔍 Content Ideas
+Periodically review saved searches for:
+As your note-taking evolves, update searches:
+Watch for performance issues:
+#fastSearch=true
for attribute-only searchesThis guide provides practical examples of how to use Trilium's search + capabilities for common organizational patterns and workflows.
+Track your learning progress and find related materials:
#topic=javascript #status=learning
+Find all JavaScript materials you're currently learning.
#course #completed=false note.dateCreated >= MONTH-1
+Find courses started in the last month that aren't completed.
#book #topic *=* programming #rating >= 4
+Find highly-rated programming books.
#paper ~author.title *=* "Andrew Ng" #field=machine-learning
+Find machine learning papers by Andrew Ng.
+Organize meetings, notes, and follow-ups:
#meeting note.dateCreated >= TODAY-7 #attendee *=* smith
+Find this week's meetings with Smith.
#meeting #actionItems #status!=completed
+Find meetings with outstanding action items.
#event #date >= TODAY #date <= TODAY+30
+Find upcoming events in the next 30 days.
#meeting #project=alpha note.dateCreated >= MONTH
+Find this month's meetings about project alpha.
+Maintain and organize your note structure:
note.childrenCount = 0 note.parentCount = 1 note.contentSize < 50 note.dateModified < TODAY-180
+Find small, isolated notes not modified in 6 months (cleanup candidates).
note.attributeCount = 0 note.type=text note.contentSize > 1000
+Find large text notes without any labels (might need categorization).
#draft note.dateCreated < TODAY-30
+Find old draft notes that might need attention.
note.parentCount > 3 note.type=text
+Find notes that are heavily cloned (might indicate important content).
+Manage tasks and project progress:
#task #priority=high #status!=completed #assignee=me
+Find your high-priority incomplete tasks.
#task #dueDate <= TODAY+3 #dueDate >= TODAY #status!=completed
+Find tasks due in the next 3 days.
#project=website #task #status=blocked
+Find blocked tasks in the website project.
#task #estimatedHours > 0 #actualHours > 0 orderBy note.dateModified desc
+Find tasks with time tracking data, sorted by recent updates.
+Monitor project health and progress:
#project #status=active note.children.labels.status = blocked
+Find active projects with blocked tasks.
#project #startDate <= TODAY-90 #status!=completed
+Find projects that started over 90 days ago but aren't completed.
#milestone #targetDate <= TODAY #status!=achieved
+Find overdue milestones.
#project orderBy note.childrenCount desc limit 10
+Find the 10 largest projects by number of sub-notes.
+Track resources and dependencies:
#resource #type=person #availability < 50
+Find people with low availability.
#dependency #status=pending #project=mobile-app
+Find pending dependencies for the mobile app project.
#budget #project #spent > #allocated
+Find projects over budget.
+Manage articles, books, and documentation:
#article #status=draft #wordCount >= 1000
+Find substantial draft articles.
#chapter #book=novel #status=outline
+Find novel chapters still in outline stage.
#blog-post #published=false #topic=technology
+Find unpublished technology blog posts.
#documentation #lastReviewed < TODAY-90 #product=api
+Find API documentation not reviewed in 90 days.
+Track editing and publication status:
#article #editor=jane #status=review
+Find articles assigned to Jane for review.
#manuscript #submissionDate >= TODAY-30 #status=pending
+Find manuscripts submitted in the last 30 days still pending.
#publication #acceptanceDate >= YEAR #status=accepted
+Find accepted publications this year.
+Organize research materials and sources:
#source #reliability >= 8 #topic *=* climate
+Find reliable sources about climate topics.
#quote #author *=* Einstein #verified=true
+Find verified Einstein quotes.
#citation #used=false #relevance=high
+Find high-relevance citations not yet used.
+Track client relationships and projects:
#client=acme #project #status=active
+Find active projects for ACME client.
#meeting #client #date >= MONTH #followUp=required
+Find client meetings this month requiring follow-up.
#contract #renewalDate <= TODAY+60 #renewalDate >= TODAY
+Find contracts expiring in the next 60 days.
#invoice #status=unpaid #dueDate < TODAY
+Find overdue unpaid invoices.
+Maintain procedures and workflows:
#procedure #department=engineering #lastUpdated < TODAY-365
+Find engineering procedures not updated in a year.
#workflow #status=active #automation=possible
+Find active workflows that could be automated.
#checklist #process=onboarding #role=developer
+Find onboarding checklists for developers.
+Track compliance requirements and audits:
#compliance #standard=sox #nextReview <= TODAY+30
+Find SOX compliance items due for review soon.
#audit #finding #severity=high #status!=resolved
+Find unresolved high-severity audit findings.
#policy #department=hr #effectiveDate >= YEAR
+Find HR policies that became effective this year.
+Organize courses and educational content:
#course #semester=fall-2024 #assignment #dueDate >= TODAY
+Find upcoming assignments for fall 2024 courses.
#lecture #course=physics #topic *=* quantum
+Find physics lectures about quantum topics.
#student #grade < 70 #course=mathematics
+Find students struggling in mathematics.
#syllabus #course #lastUpdated < TODAY-180
+Find syllabi not updated in 6 months.
+Track research projects and publications:
#experiment #status=running #endDate <= TODAY+7
+Find experiments ending in the next week.
#dataset #size > 1000000 #cleaned=true #public=false
+Find large, cleaned, private datasets.
#hypothesis #tested=false #priority=high
+Find high-priority untested hypotheses.
#collaboration #institution *=* stanford #status=active
+Find active collaborations with Stanford.
+Manage funding applications and requirements:
#grant #deadline <= TODAY+30 #deadline >= TODAY #status=in-progress
+Find grant applications due in the next 30 days.
#funding #amount >= 100000 #status=awarded #startDate >= YEAR
+Find large grants awarded this year.
#report #funding #dueDate <= TODAY+14 #status!=submitted
+Find funding reports due in 2 weeks.
+Track code-related notes and documentation:
#bug #severity=critical #status!=fixed #product=webapp
+Find critical unfixed bugs in the web app.
#feature #version=2.0 #status=implemented #tested=false
+Find version 2.0 features that are implemented but not tested.
#api #endpoint #deprecated=true #removalDate <= TODAY+90
+Find deprecated API endpoints scheduled for removal soon.
#architecture #component=database #lastReviewed < TODAY-180
+Find database architecture documentation not reviewed in 6 months.
+Manage infrastructure and operations:
#server #status=maintenance #scheduledDate >= TODAY #scheduledDate <= TODAY+7
+Find servers scheduled for maintenance this week.
#backup #status=failed #date >= TODAY-7
+Find backup failures in the last week.
#security #vulnerability #severity=high #patched=false
+Find unpatched high-severity vulnerabilities.
#monitoring #alert #frequency > 10 #period=week
+Find alerts triggering more than 10 times per week.
+Monitor metrics and KPIs:
#metric #kpi=true #trend=declining #period=month
+Find declining monthly KPIs.
#report #frequency=weekly #lastGenerated < TODAY-10
+Find weekly reports that haven't been generated in 10 days.
#dashboard #stakeholder=executive #lastUpdated < TODAY-7
+Find executive dashboards not updated this week.
+Track patterns and changes over time:
#data #source=sales #period=quarter #analyzed=false
+Find unanalyzed quarterly sales data.
#trend #direction=up #significance=high #period=month
+Find significant positive monthly trends.
#forecast #accuracy < 80 #model=linear #period=quarter
+Find inaccurate quarterly linear forecasts.
+#
and ~
shortcuts
+ for efficiencyRegular queries for note maintenance:
# Weekly cleanup check
+note.attributeCount = 0 note.type=text note.contentSize < 100 note.dateModified < TODAY-30
+
+# Monthly project review
+#project #status=active note.dateModified < TODAY-30
+
+# Quarterly archive review
+note.isArchived=false note.dateModified < TODAY-90 note.childrenCount = 0
+Trilium's search system is a powerful tool for finding and organizing + notes. It supports multiple search modes, from simple text queries to complex + expressions using attributes, relationships, and note properties.
+Trilium provides three main search approaches:
+These can be combined in powerful ways to create precise queries.
+hello world
+Finds notes containing both "hello" and "world" anywhere in the title + or content.
+"hello world"
+Finds notes containing the exact phrase "hello world".
+#tag
+Finds notes with the label "tag".
#category=book
+Finds notes with label "category" set to "book".
+~author
+Finds notes with a relation named "author".
~author.title=Tolkien
+Finds notes with an "author" relation pointing to a note titled "Tolkien".
+=
- Exact match!=
- Not equal*=*
- Contains (substring)=*
- Starts with*=
- Ends with%=
- Regular expression match~=
- Fuzzy exact match~*
- Fuzzy contains match=
- Equal!=
- Not equal>
- Greater than>=
- Greater than or equal<
- Less than<=
- Less than or equalAND
- Both conditions must be trueOR
- Either condition must be trueNOT
or not()
- Condition must be falseBy default, search covers:
+When enabled, fast search:
+These characters have special meaning in search queries:
+#
- Label indicator~
- Relation indicator()
- Grouping"
'
`
- Quotes for exact phrasesUse backslash to search for literal special characters:
\#hashtag
+Searches for the literal text "#hashtag" instead of a label.
+Use quotes to include special characters in phrases:
"note.txt file"
+Searches for the exact phrase including the dot.
+TODAY
- Current dateNOW
- Current date and timeMONTH
- Current monthYEAR
- Current year#dateCreated >= TODAY-30
+Finds notes created in the last 30 days.
#eventDate = YEAR+1
+Finds notes with eventDate set to next year.
+Results are ordered by:
+This guide provides technical information about Trilium's search implementation, + performance characteristics, and optimization strategies for power users + and administrators.
+Trilium's search operates across three cache layers:
+The lexer breaks down search queries into components:
// Input: 'project #status=active note.dateCreated >= TODAY-7'
+// Output:
+{
+ fulltextTokens: ['project'],
+ expressionTokens: ['#status', '=', 'active', 'note', '.', 'dateCreated', '>=', 'TODAY-7']
+}
+Tokens are converted into executable expression trees:
// Expression tree for: #book AND #author=Tolkien
+AndExp([
+ AttributeExistsExp('label', 'book'),
+ LabelComparisonExp('label', 'author', equals('tolkien'))
+])
+AndExp
, OrExp
, NotExp
: Boolean logicAttributeExistsExp
: Label/relation existenceLabelComparisonExp
: Label value comparisonRelationWhereExp
: Relation target queriesPropertyComparisonExp
: Note property filteringNoteContentFulltextExp
: Content searchOrderByAndLimitExp
: Result ordering and limiting// Fast exact matching
+const exactResults = performSearch(expression, searchContext, false);
+Characteristics:
+// Activated when exact results < 5 high-quality matches
+if (highQualityResults.length < 5) {
+ const fuzzyResults = performSearch(expression, searchContext, true);
+ return mergeExactAndFuzzyResults(exactResults, fuzzyResults);
+}
+Characteristics:
+Parameter | +Value | +Purpose | +
---|---|---|
MAX_SEARCH_CONTENT_SIZE
+ |
+ 2MB | +Database-level content filtering | +
MIN_FUZZY_TOKEN_LENGTH
+ |
+ 3 chars | +Minimum length for fuzzy matching | +
MAX_EDIT_DISTANCE
+ |
+ 2 chars | +Maximum character changes for fuzzy | +
MAX_PHRASE_PROXIMITY
+ |
+ 10 words | +Maximum distance for phrase matching | +
RESULT_SUFFICIENCY_THRESHOLD
+ |
+ 5 results | +Threshold for fuzzy activation | +
ABSOLUTE_MAX_CONTENT_SIZE
+ |
+ 100MB | +Hard limit to prevent system crash | +
ABSOLUTE_MAX_WORD_COUNT
+ |
+ 2M words | +Hard limit for word processing | +
-- Content size filtering at database level
+SELECT noteId, type, mime, content, isProtected
+FROM notes JOIN blobs USING (blobId)
+WHERE type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
+ AND isDeleted = 0
+ AND LENGTH(content) < 2097152 -- 2MB limit
+// Efficient search context configuration
+const searchContext = new SearchContext({
+ fastSearch: true, // Skip content search
+ limit: 50, // Reasonable result limit
+ orderBy: 'dateCreated', // Use indexed property
+ includeArchivedNotes: false // Reduce search space
+});
+Trilium uses an optimized Levenshtein distance calculation:
// Optimized single-array implementation
+function calculateOptimizedEditDistance(str1, str2, maxDistance) {
+ // Early termination checks
+ if (Math.abs(str1.length - str2.length) > maxDistance) {
+ return maxDistance + 1;
+ }
+
+ // Single array optimization
+ let previousRow = Array.from({ length: str2.length + 1 }, (_, i) => i);
+ let currentRow = new Array(str2.length + 1);
+
+ for (let i = 1; i <= str1.length; i++) {
+ currentRow[0] = i;
+ let minInRow = i;
+
+ for (let j = 1; j <= str2.length; j++) {
+ const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
+ currentRow[j] = Math.min(
+ previousRow[j] + 1, // deletion
+ currentRow[j - 1] + 1, // insertion
+ previousRow[j - 1] + cost // substitution
+ );
+ minInRow = Math.min(minInRow, currentRow[j]);
+ }
+
+ // Early termination if row minimum exceeds threshold
+ if (minInRow > maxDistance) return maxDistance + 1;
+
+ [previousRow, currentRow] = [currentRow, previousRow];
+ }
+
+ return previousRow[str2.length];
+}
+For multi-token fuzzy searches:
// Check if tokens appear within reasonable proximity
+function hasProximityMatch(tokenPositions, maxDistance = 10) {
+ // For 2 tokens, simple distance check
+ if (tokenPositions.length === 2) {
+ const [pos1, pos2] = tokenPositions;
+ return pos1.some(p1 => pos2.some(p2 => Math.abs(p1 - p2) <= maxDistance));
+ }
+
+ // For multiple tokens, find sequence within range
+ const findSequence = (remaining, currentPos) => {
+ if (remaining.length === 0) return true;
+ const [nextPositions, ...rest] = remaining;
+ return nextPositions.some(pos =>
+ Math.abs(pos - currentPos) <= maxDistance &&
+ findSequence(rest, pos)
+ );
+ };
+
+ const [firstPositions, ...rest] = tokenPositions;
+ return firstPositions.some(startPos => findSequence(rest, startPos));
+}
+-- Relevant indexes for search performance
+CREATE INDEX idx_notes_type ON notes(type);
+CREATE INDEX idx_notes_isDeleted ON notes(isDeleted);
+CREATE INDEX idx_notes_dateCreated ON notes(dateCreated);
+CREATE INDEX idx_notes_dateModified ON notes(dateModified);
+CREATE INDEX idx_attributes_name ON attributes(name);
+CREATE INDEX idx_attributes_type ON attributes(type);
+CREATE INDEX idx_attributes_value ON attributes(value);
+Notes are processed differently based on type:
// Content preprocessing by note type
+function preprocessContent(content, type, mime) {
+ content = normalize(content.toString());
+
+ if (type === "text" && mime === "text/html") {
+ content = stripTags(content);
+ content = content.replace(/ /g, " ");
+ } else if (type === "mindMap" && mime === "application/json") {
+ content = processMindmapContent(content);
+ } else if (type === "canvas" && mime === "application/json") {
+ const canvasData = JSON.parse(content);
+ const textElements = canvasData.elements
+ .filter(el => el.type === "text" && el.text)
+ .map(el => el.text);
+ content = normalize(textElements.join(" "));
+ }
+
+ return content.trim();
+}
+Results are scored based on multiple factors:
function computeScore(fulltextQuery, highlightedTokens, enableFuzzyMatching) {
+ let score = 0;
+
+ // Title matches get higher score
+ if (this.noteTitle.toLowerCase().includes(fulltextQuery.toLowerCase())) {
+ score += 10;
+ }
+
+ // Path matches (hierarchical context)
+ const pathMatch = this.notePathArray.some(pathNote =>
+ pathNote.title.toLowerCase().includes(fulltextQuery.toLowerCase())
+ );
+ if (pathMatch) score += 5;
+
+ // Attribute matches
+ score += this.attributeMatches * 3;
+
+ // Content snippet quality
+ if (this.contentSnippet && this.contentSnippet.length > 0) {
+ score += 2;
+ }
+
+ // Fuzzy match penalty
+ if (enableFuzzyMatching && this.isFuzzyMatch) {
+ score *= 0.8; // 20% penalty for fuzzy matches
+ }
+
+ return score;
+}
+Exact and fuzzy results are carefully merged:
function mergeExactAndFuzzyResults(exactResults, fuzzyResults) {
+ // Deduplicate - exact results take precedence
+ const exactNoteIds = new Set(exactResults.map(r => r.noteId));
+ const additionalFuzzyResults = fuzzyResults.filter(r =>
+ !exactNoteIds.has(r.noteId)
+ );
+
+ // Sort within each category
+ exactResults.sort(byScoreAndDepth);
+ additionalFuzzyResults.sort(byScoreAndDepth);
+
+ // CRITICAL: Exact matches always come first
+ return [...exactResults, ...additionalFuzzyResults];
+}
+Monitor these performance indicators:
// Performance tracking
+const searchMetrics = {
+ totalQueries: 0,
+ exactSearchTime: 0,
+ fuzzySearchTime: 0,
+ resultCount: 0,
+ cacheHitRate: 0,
+ slowQueries: [] // queries taking > 1 second
+};
+Track memory consumption:
// Memory monitoring
+const memoryMetrics = {
+ searchCacheSize: 0,
+ activeSearchContexts: 0,
+ largeContentNotes: 0, // notes > 1MB
+ indexSize: 0
+};
+Identify expensive queries:
// Query complexity factors
+const complexityFactors = {
+ tokenCount: query.split(' ').length,
+ hasRegex: query.includes('%='),
+ hasFuzzy: query.includes('~=') || query.includes('~*'),
+ hasRelationTraversal: query.includes('.relations.'),
+ hasNestedProperties: (query.match(/\./g) || []).length > 2,
+ hasOrderBy: query.includes('orderBy'),
+ estimatedResultSize: 'unknown'
+};
+// Diagnosis
+- Check note content sizes
+- Verify content type filtering
+- Monitor regex usage
+- Review fuzzy search activation
+
+// Solutions
+- Enable fast search for attribute-only queries
+- Add content size limits
+- Optimize regex patterns
+- Tune fuzzy search thresholds
+// Diagnosis
+- Monitor result set sizes
+- Check for large content processing
+- Review search context caching
+- Identify memory leaks
+
+// Solutions
+- Add result limits
+- Implement progressive loading
+- Clear unused search contexts
+- Optimize content preprocessing
+// Diagnosis
+- Profile fuzzy search operations
+- Check edit distance calculations
+- Monitor regex compilation
+- Review phrase proximity matching
+
+// Solutions
+- Increase minimum fuzzy token length
+- Reduce maximum edit distance
+- Cache compiled regexes
+- Limit phrase proximity distance
+Enable search debugging:
// Search context with debugging
+const searchContext = new SearchContext({
+ debug: true // Logs expression parsing and execution
+});
+Output includes:
+// Manual performance measurement
+const startTime = Date.now();
+const results = searchService.findResultsWithQuery(query, searchContext);
+const endTime = Date.now();
+console.log(`Search took ${endTime - startTime}ms for ${results.length} results`);
+// Analyze query complexity
+function analyzeQuery(query) {
+ return {
+ tokenCount: query.split(/\s+/).length,
+ hasAttributes: /#|\~/.test(query),
+ hasProperties: /note\./.test(query),
+ hasRegex: /%=/.test(query),
+ hasFuzzy: /~[=*]/.test(query),
+ complexity: calculateComplexityScore(query)
+ };
+}
+Relevant settings in config.ini
:
# Search-related settings
+[Search]
+maxContentSize=2097152 # 2MB content limit
+minFuzzyTokenLength=3 # Minimum chars for fuzzy
+maxEditDistance=2 # Edit distance limit
+resultSufficiencyThreshold=5 # Fuzzy activation threshold
+enableProgressiveSearch=true # Enable progressive strategy
+cacheSearchResults=true # Cache frequent searches
+
+# Performance settings
+[Performance]
+searchTimeoutMs=30000 # 30 second search timeout
+maxSearchResults=1000 # Hard limit on results
+enableSearchProfiling=false # Performance logging
+Adjust search behavior programmatically:
// Dynamic configuration
+const searchConfig = {
+ maxContentSize: 1024 * 1024, // 1MB for faster processing
+ enableFuzzySearch: false, // Exact only for speed
+ resultLimit: 50, // Smaller result sets
+ useIndexedPropertiesOnly: true // Skip expensive calculations
+};
+Extend search functionality with custom expressions:
// Custom expression example
+class CustomDateRangeExp extends Expression {
+ constructor(dateField, startDate, endDate) {
+ super();
+ this.dateField = dateField;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ }
+
+ execute(inputNoteSet, executionContext, searchContext) {
+ // Custom logic for date range filtering
+ // with optimized performance characteristics
+ }
+}
+Implement result caching for frequent queries:
// Simple LRU cache for search results
+class SearchResultCache {
+ constructor(maxSize = 100) {
+ this.cache = new Map();
+ this.maxSize = maxSize;
+ }
+
+ get(queryKey) {
+ if (this.cache.has(queryKey)) {
+ // Move to end (most recently used)
+ const value = this.cache.get(queryKey);
+ this.cache.delete(queryKey);
+ this.cache.set(queryKey, value);
+ return value;
+ }
+ return null;
+ }
+
+ set(queryKey, results) {
+ if (this.cache.size >= this.maxSize) {
+ // Remove least recently used
+ const firstKey = this.cache.keys().next().value;
+ this.cache.delete(firstKey);
+ }
+ this.cache.set(queryKey, results);
+ }
+}
+Running Trilium on a server lets you access your notes from any device + through a web browser and enables synchronization between multiple Trilium + instances. This guide covers the different ways to install and configure + Trilium on your server.
+The easiest way to get started is with Docker, which works on most systems + and architectures. If you prefer not to manage your own server, PikaPods + offers managed hosting.
+Recommended approaches: +
+Advanced options: +
+All server installations include both desktop and mobile web interfaces.
+Trilium stores its configuration in a config.ini
file located
+ in the data directory. To customize
+ your installation, copy the sample configuration file and modify it:
cp config-sample.ini config.ini
+You can also use environment variables instead of the config file. This + is particularly useful for Docker deployments. See the configuration guide for + all available options.
+To store Trilium's data (database, config, backups) in a custom location,
+ set the TRILIUM_DATA_DIR
environment variable:
export TRILIUM_DATA_DIR=/path/to/your/trilium-data
+By default, Trilium limits file uploads to 250MB. You can adjust this + limit based on your needs:
# Increase limit to 450MB
+export MAX_ALLOWED_FILE_SIZE_MB=450
+
+# Remove limit entirely (use with caution)
+export TRILIUM_NO_UPLOAD_LIMIT=true
+See Authentication.
+If you want to access Trilium through a domain name or alongside other + web services, you'll need to configure a reverse proxy. Here's a basic + nginx configuration:
location /trilium/ {
+ proxy_pass http://127.0.0.1:8080/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+}
+
+# Allow larger file uploads (in server block)
+client_max_body_size 0; # 0 = unlimited
+For Apache configuration, see the Apache proxy setup guide.
\ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Manually.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Manually.html new file mode 100644 index 0000000000..dbf209758c --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Manually.html @@ -0,0 +1,43 @@ + +Trilium is a node.js application. Supported (tested) version of node.js + is latest 14.X.X and 16.X.X. Trilium might work with older versions as + well.
+You can check your node version with this command (node.js needs to be + installed):
node --version
+If your Linux distribution has only an outdated version of node.js, you + can take a look at the installation instruction on node.js website, which + covers most popular distributions.
+There are some dependencies required. You can see command for Debian and + its derivatives (like Ubuntu) below:
sudo apt install libpng16-16 libpng-dev pkg-config autoconf libtool build-essential nasm libx11-dev libxkbfile-dev
+You can either download source code zip/tar from https://github.com/TriliumNext/Trilium/releases/latest.
+For the latest version including betas, clone Git repository from main
branch with:
git clone -b main https://github.com/triliumnext/trilium.git
+cd trilium
+
+# download all node dependencies
+npm install
+
+# make sure the better-sqlite3 binary is there
+npm rebuild
+
+# bundles & minifies frontend JavaScript
+npm run webpack
+cd trilium
+
+# using nohup to make sure trilium keeps running after user logs out
+nohup TRILIUM_ENV=dev node src/www &
+The application by default starts up on port 8080, so you can open your + browser and navigate to http://localhost:8080 to + access Trilium (replace "localhost" with your hostname).
+Don't forget to configure TLS which + is required for secure usage!
\ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Multiple server instances.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Multiple server instances.html new file mode 100644 index 0000000000..242bbd7328 --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Multiple server instances.html @@ -0,0 +1,25 @@ +Trilium does not support multiple users. In order to have two or more + persons with their own set of notes, multiple server instances must be + set up. It is also not possible to use multiple sync servers.
+To allow multiple server instances on a single physical server:
+For Packaged version for Linux or + Manually, if starting the server manually just specify a different + port and data directory per instance:
TRILIUM_NETWORK_PORT=8080 TRILIUM_DATA_DIR=/path/to/your/data-dir-A /opt/trilium/trilium.sh
+ For a second instance:
TRILIUM_NETWORK_PORT=8081 TRILIUM_DATA_DIR=/path/to/your/data-dir-B /opt/trilium/trilium.sh
+ If using systemd
, then set the environment variables in the service configuration.
For Using Docker, + simply use two different containers, each with their own port binding and + data directory.
+For On NixOS, + the only possible way is to use Docker OCI containers or at least one NixOS + container with its own service definition.
+For support or additional context, see the related GitHub Discussion.
\ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/On NixOS.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/On NixOS.html new file mode 100644 index 0000000000..b965f62fc6 --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/On NixOS.html @@ -0,0 +1,15 @@ +This page describes configuring the Trilium module included in NixOS.
+NixOS installation.
+Add this to your configuration.nix
:
services.trilium-server.enable = true;
+
+# default data directory: /var/lib/trilium
+#services.trilium-server.dataDir = "/var/lib/trilium-sync-server";
+
+# default bind address: 127.0.0.1, port 8080
+#services.trilium-server.host = "0.0.0.0";
+#services.trilium-server.port = 12783;
+Uncomment any option you would like to change.
+See the NixOS options list for + more options (including nginx reverse proxy configuration).
\ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Packaged version for Linux.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Packaged version for Linux.html new file mode 100644 index 0000000000..cd726e251f --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Packaged version for Linux.html @@ -0,0 +1,167 @@ +This is essentially Trilium sources + node modules + node.js runtime packaged + into one 7z file.
+wget
(or curl
) to download latest TriliumNotes-Server-[VERSION]-linux-x64.tar.xz
(copy
+ link from release page,
+ notice -Server
suffix) on your server.tar -xf -d TriliumNotes-Server-[VERSION]-linux-x64.tar.xz
+ cd trilium-linux-x64-server
+ ./trilium.sh
+ The problem with above steps is that once you close the SSH connection, + the Trilium process is terminated. To avoid that, you have two options:
+nohup ./trilium.sh &
.
+ (nohup keeps the process running in the background, &
runs
+ it in the background)tar -xvf TriliumNotes-Server-[VERSION]-linux-x64.tar.xz
+sudo mv trilium-linux-x64-server /opt/trilium
+sudo nano /etc/systemd/system/trilium.service
+[Unit]
+Description=Trilium Daemon
+After=syslog.target network.target
+
+[Service]
+User=xxx
+Group=xxx
+Type=simple
+ExecStart=/opt/trilium/trilium.sh
+WorkingDirectory=/opt/trilium/
+
+TimeoutStopSec=20
+# KillMode=process leads to error, according to https://www.freedesktop.org/software/systemd/man/systemd.kill.html
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+sudo systemctl enable --now -q trilium
+Run as the same User Trilium runs
+if you run as root please remove 'sudo' from the commands
+requires "jq" apt install jq
+
It will stop the service above, overwrite everything (i expect no config.ini), + and start service It also creates a version file in the Trilium directory + so it updates only with a newer Version
#!/bin/bash
+
+# Configuration
+REPO="TriliumNext/Trilium"
+PATTERN="TriliumNotes-Server-.*-linux-x64.tar.xz"
+DOWNLOAD_DIR="/var/tmp/trilium_download"
+OUTPUT_DIR="/opt/trilium"
+SERVICE_NAME="trilium"
+VERSION_FILE="$OUTPUT_DIR/version.txt"
+
+# Ensure dependencies are installed
+command -v curl >/dev/null 2>&1 || { echo "Error: curl is required"; exit 1; }
+command -v jq >/dev/null 2>&1 || { echo "Error: jq is required"; exit 1; }
+command -v tar >/dev/null 2>&1 || { echo "Error: tar is required"; exit 1; }
+
+# Create download directory
+mkdir -p "$DOWNLOAD_DIR" || { echo "Error: Cannot create $DOWNLOAD_DIR"; exit 1; }
+
+# Get the latest release version
+LATEST_VERSION=$(curl -sL https://api.github.com/repos/$REPO/releases/latest | jq -r '.tag_name')
+if [ -z "$LATEST_VERSION" ]; then
+ echo "Error: Could not fetch latest release version"
+ exit 1
+fi
+
+# Check current installed version (from version.txt or existing tarball)
+CURRENT_VERSION=""
+if [ -f "$VERSION_FILE" ]; then
+ CURRENT_VERSION=$(cat "$VERSION_FILE")
+elif [ -f "$DOWNLOAD_DIR/TriliumNotes-Server-$LATEST_VERSION-linux-x64.tar.xz" ]; then
+ CURRENT_VERSION="$LATEST_VERSION"
+fi
+
+# Compare versions
+if [ "$CURRENT_VERSION" = "$LATEST_VERSION" ]; then
+ echo "Latest version ($LATEST_VERSION) is already installed"
+ exit 0
+fi
+
+# Download the latest release
+LATEST_URL=$(curl -sL https://api.github.com/repos/$REPO/releases/latest | jq -r ".assets[] | select(.name | test(\"$PATTERN\")) | .browser_download_url")
+if [ -z "$LATEST_URL" ]; then
+ echo "Error: No asset found matching pattern '$PATTERN'"
+ exit 1
+fi
+
+FILE_NAME=$(basename "$LATEST_URL")
+FILE_PATH="$DOWNLOAD_DIR/$FILE_NAME"
+
+# Download if not already present
+if [ -f "$FILE_PATH" ]; then
+ echo "Latest release $FILE_NAME already downloaded"
+else
+ curl -LO --output-dir "$DOWNLOAD_DIR" "$LATEST_URL" || { echo "Error: Download failed"; exit 1; }
+ echo "Downloaded $FILE_NAME to $DOWNLOAD_DIR"
+fi
+
+# Extract the tarball
+EXTRACT_DIR="$DOWNLOAD_DIR/extracted"
+mkdir -p "$EXTRACT_DIR"
+tar -xJf "$FILE_PATH" -C "$EXTRACT_DIR" || { echo "Error: Extraction failed"; exit 1; }
+
+# Find the extracted directory (e.g., TriliumNotes-Server-0.97.2-linux-x64)
+INNER_DIR=$(find "$EXTRACT_DIR" -maxdepth 1 -type d -name "TriliumNotes-Server-*-linux-x64" | head -n 1)
+if [ -z "$INNER_DIR" ]; then
+ echo "Error: Could not find extracted directory matching TriliumNotes-Server-*-linux-x64"
+ exit 1
+fi
+
+# Stop the trilium-server service
+if systemctl is-active --quiet "$SERVICE_NAME"; then
+ echo "Stopping $SERVICE_NAME service..."
+ sudo systemctl stop "$SERVICE_NAME" || { echo "Error: Failed to stop $SERVICE_NAME"; exit 1; }
+fi
+
+# Copy contents to /opt/trilium, overwriting existing files
+echo "Copying contents from $INNER_DIR to $OUTPUT_DIR..."
+sudo mkdir -p "$OUTPUT_DIR"
+sudo cp -r "$INNER_DIR"/* "$OUTPUT_DIR"/ || { echo "Error: Copy failed"; exit 1; }
+echo "$LATEST_VERSION" | sudo tee "$VERSION_FILE" >/dev/null
+echo "Files copied to $OUTPUT_DIR"
+
+# Start the trilium-server service
+echo "Starting $SERVICE_NAME service..."
+sudo systemctl start "$SERVICE_NAME" || { echo "Error: Failed to start $SERVICE_NAME"; exit 1; }
+
+# Clean up
+rm -rf "$EXTRACT_DIR"
+echo "Cleanup complete. Trilium updated to $LATEST_VERSION."
+Error: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by /var/www/virtual/.../node_modules/@mlink/scrypt/build/Release/scrypt.node)
+ at Object.Module._extensions..node (module.js:681:18)
+ at Module.load (module.js:565:32)
+ at tryModuleLoad (module.js:505:12)
+If you get an error like this, you need to either upgrade your glibc (typically + by upgrading to up-to-date distribution version) or use some other server installation method.
+Don't forget to configure TLS, which + is required for secure usage!
\ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Using Docker.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Using Docker.html new file mode 100644 index 0000000000..0b76d32dd2 --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/1. Installing the server/Using Docker.html @@ -0,0 +1,192 @@ +Official docker images are published on docker hub for AMD64, ARMv7 and ARM64/v8: + https://hub.docker.com/r/triliumnext/trilium/ +
+Ensure Docker is installed on your system.
+If you need help installing Docker, reference the Docker Installation Docs +
+Note: Trilium's Docker container requires root privileges + to operate correctly.
+ +wget https://raw.githubusercontent.com/TriliumNext/Trilium/master/docker-compose.yml
+Optionally, edit the docker-compose.yml
file to configure the
+ container settings prior to starting it. Unless configured otherwise, the
+ data directory will be ~/trilium-data
and the container will
+ be accessible at port 8080.
Run the following command to start the container in the background:
docker compose up -d
+To pull the image, use the following command, replacing [VERSION]
with
+ the desired version or tag, such as v0.91.6
or just latest
.
+ (See published tag names at https://hub.docker.com/r/triliumnext/trilium/tags.):
docker pull triliumnext/trilium:v0.91.6
+Warning: Avoid using the "latest" tag, as it may automatically + upgrade your instance to a new minor version, potentially disrupting sync + setups or causing other issues.
+Trilium requires a directory on the host system to store its data. This + directory must be mounted into the Docker container with write permissions.
+Run the container to make it accessible only from the localhost. This + setup is suitable for testing or when using a proxy server like Nginx or + Apache.
sudo docker run -t -i -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:[VERSION]
+docker ps
.127.0.0.1:8080
.To make the container accessible only on your local network, first create + a new Docker network:
docker network create -d macvlan -o parent=eth0 --subnet 192.168.2.0/24 --gateway 192.168.2.254 --ip-range 192.168.2.252/27 mynet
+Then, run the container with the network settings:
docker run --net=mynet -d -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:-latest
+To set a different user ID (UID) and group ID (GID) for the saved data,
+ use the USER_UID
and USER_GID
environment variables:
docker run --net=mynet -d -p 127.0.0.1:8080:8080 -e "USER_UID=1001" -e "USER_GID=1001" -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:-latest
+Find the local IP address using docker inspect [container_name]
and
+ access the service from devices on the local network.
docker ps
+docker inspect [container_name]
+To allow access from any IP address, run the container as follows:
docker run -d -p 0.0.0.0:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:[VERSION]
+Stop the container with docker stop <CONTAINER ID>
,
+ where the container ID is obtained from docker ps
.
For a custom data directory, use:
-v ~/YourOwnDirectory:/home/node/trilium-data triliumnext/trilium:[VERSION]
+If you want to run your instance in a non-default way, please use the
+ volume switch as follows: -v ~/YourOwnDirectory:/home/node/trilium-data triliumnext/trilium:<VERSION>
.
+ It is important to be aware of how Docker works for volumes, with the first
+ path being your own and the second the one to virtually bind to. https://docs.docker.com/storage/volumes/ The
+ path before the colon is the host directory, and the path after the colon
+ is the container's path. More details can be found in the Docker Volumes Documentation.
The --user
directive is unsupported. Instead, use the USER_UID
and USER_GID
environment
+ variables to set the appropriate user and group IDs.
If you are having timezone issues and you are not using docker-compose,
+ you may need to add a TZ
environment variable with the TZ identifier of
+ your local timezone.
If you would prefer to run Trilium without having to run the Docker container
+ as root
, you can use either of the provided Debian (default)
+ and Alpine-based images with the rootless
tag.
If you're unsure, stick to the “rootful” Docker image referenced above. +
+Below are some commands to pull the rootless images:
# For Debian-based image
+docker pull triliumnext/trilium:rootless
+
+# For Alpine-based image
+docker pull triliumnext/trilium:rootless-alpine
+Running containers as non-root is a security best practice that reduces + the potential impact of container breakouts. If an attacker manages to + escape the container, they'll only have the permissions of the non-root + user instead of full root access to the host.
+The rootless Trilium image:
+trilium
) during build time--user
flagentrypoint
script# Run with default UID/GID (1000:1000)
+docker-compose -f docker-compose.rootless.yml up -d
+
+# Run with custom UID/GID (e.g., match your host user)
+TRILIUM_UID=$(id -u) TRILIUM_GID=$(id -g) docker-compose -f docker-compose.rootless.yml up -d
+
+# Specify a custom data directory
+TRILIUM_DATA_DIR=/path/to/your/data TRILIUM_UID=$(id -u) TRILIUM_GID=$(id -g) docker-compose -f docker-compose.rootless.yml up -d
+
+# Build the image
+docker build -t triliumnext/trilium:rootless -f apps/server/Dockerfile.rootless .
+
+# Run with default UID/GID (1000:1000)
+docker run -d --name trilium -p 8080:8080 -v ~/trilium-data:/home/trilium/trilium-data triliumnext/trilium:rootless
+
+# Run with custom UID/GID
+docker run -d --name trilium -p 8080:8080 --user $(id -u):$(id -g) -v ~/trilium-data:/home/trilium/trilium-data triliumnext/trilium:rootless
+
+TRILIUM_UID
: UID to use for the container process (passed
+ to Docker's --user
flag)TRILIUM_GID
: GID to use for the container process (passed
+ to Docker's --user
flag)TRILIUM_DATA_DIR
: Path to the data directory inside the container
+ (default: /home/node/trilium-data
)For a complete list of configuration environment variables (network settings, + authentication, sync, etc.), see Configuration (config.ini or environment variables).
+If you encounter permission issues with the data volume, ensure that:
+TRILIUM_UID
and TRILIUM_GID
to
+ match the owner of the host directory# For example, if your data directory is owned by UID 1001 and GID 1001:
+TRILIUM_UID=1001 TRILIUM_GID=1001 docker-compose -f docker-compose.rootless.yml up -d
+
+usermod
/groupmod
commandsTwo rootless variants are provided:
+apps/server/Dockerfile.rootless
+ apps/server/Dockerfile.alpine.rootless
+ If you would prefer, you can also customize the UID/GID at build time:
# For Debian-based image with custom UID/GID
+docker build --build-arg USER=myuser --build-arg UID=1001 --build-arg GID=1001 \
+ -t triliumnext/trilium:rootless-custom -f apps/server/Dockerfile.rootless .
+
+# For Alpine-based image with custom UID/GID
+docker build --build-arg USER=myuser --build-arg UID=1001 --build-arg GID=1001 \
+ -t triliumnext/trilium:alpine-rootless-custom -f apps/server/Dockerfile.alpine.rootless .
+
+Available build arguments:
+USER
: Username for the non-root user (default: trilium)UID
: User ID for the non-root user (default: 1000)GID
: Group ID for the non-root user (default: 1000)As Trilium can be run in Docker it also can be deployed in Kubernetes. + You can either use our Helm chart, a community Helm chart, or roll your + own Kubernetes deployment.
+The recommended way is to use a Helm chart.
+The Trilium docker container needs to be run with root privileges. The
+ node process inside the container will be started with reduced privileges
+ (uid:gid 1000:1000) after some initialization logic. Please make sure that
+ you don't use a security context (PodSecurityContext) which changes the
+ user ID. To use a different uid:gid for file storage and the application,
+ please use the USER_UID
& USER_GID
environment
+ variables.
The docker image will also fix the permissions of /home/node
so
+ you don't have to use an init container.
Official Helm chart from + TriliumNext Unofficial helm chart by ohdearaugustin: + https://github.com/ohdearaugustin/charts +
+Below is an example of how
helm repo add trilium https://triliumnext.github.io/helm-charts
+"trilium" has been added to your repositories
+After reviewing the values.yaml
from
+ the Helm chart, modifying as required and then creating your own:
helm install --create-namespace --namespace trilium trilium trilium/trilium -f values.yaml
+For more information on using Helm, please refer to the Helm documentation, + or create a Discussion in the TriliumNext GitHub Organization.
\ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Apache.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Apache.html new file mode 100644 index 0000000000..4e2bedc8aa --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Apache.html @@ -0,0 +1,79 @@ +I've assumed you have created a DNS A record for trilium.yourdomain.com
that
+ you want to use for your Trilium server.
Download docker image and create container
docker pull triliumnext/trilium:[VERSION]
+ docker create --name trilium -t -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:[VERSION]
+ Configure Apache proxy and websocket proxy
+Enable apache proxy modules
a2enmod ssl
+ a2enmod proxy
+ a2enmod proxy_http
+ a2enmod proxy_wstunnel
+ Create a new let's encrypt certificate
sudo certbot certonly -d trilium.mydomain.com
+ Choose standalone (2) and note the location of the created certificates + (typically /etc/letsencrypt/live/...)
+Create a new virtual host file for apache (you may want to use apachectl -S
to
+ determine the server root location, mine is /etc/apache2)
sudo nano /etc/apache2/sites-available/trilium.yourdomain.com.conf
+ Paste (and customize) the following text into the configuration file
+ ServerName http://trilium.yourdomain.com
+ RewriteEngine on
+ RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,QSA,R=permanent]
+
+
+ ServerName https://trilium.yourdomain.com
+ RewriteEngine On
+ RewriteCond %{HTTP:Connection} Upgrade [NC]
+ RewriteCond %{HTTP:Upgrade} websocket [NC]
+ RewriteRule /(.*) ws://localhost:8080/$1 [P,L]
+ AllowEncodedSlashes NoDecode
+ ProxyPass / http://localhost:8080/ nocanon
+ ProxyPassReverse / http://localhost:8080/
+ SSLCertificateFile /etc/letsencrypt/live/trilium.yourdomain.com/fullchain.pem
+ SSLCertificateKeyFile /etc/letsencrypt/live/trilium.yourdomain.com/privkey.pem
+ Include /etc/letsencrypt/options-ssl-apache.conf
+
+ Enable the virtual host with sudo a2ensite trilium.yourdomain.com.conf
+
Reload apache2 with sudo systemctl reload apache2
+
Create and enable a systemd service to start the docker container on boot
+Create a new empty file called /lib/systemd/system/trilium.service
with
+ the contents
[Unit]
+ Description=Trilium Server
+ Requires=docker.service
+ After=docker.service
+
+ [Service]
+ Restart=always
+ ExecStart=/usr/bin/docker start -a trilium
+ ExecStop=/usr/bin/docker stop -t 2 trilium
+
+ [Install]
+ WantedBy=local.target
+ Install, enable and start service
sudo systemctl daemon-reload
+ sudo systemctl enable trilium.service
+ sudo systemctl start trilium.service
+ Configure Nginx proxy and HTTPS. The operating system here is Ubuntu 18.04.
+Download Nginx and remove Apache2
sudo apt-get install nginx
+sudo apt-get remove apache2
+ Create configure file
cd /etc/nginx/conf.d
+vim default.conf
+ Fill the file with the context shown below, part of the setting show be + changed. Then you can enjoy your web with HTTPS forced and proxy.
# This part configures, where your Trilium server is running
+upstream trilium {
+ zone trilium 64k;
+ server 127.0.0.1:8080; # change it to a different hostname and port if non-default is used
+ keepalive 2;
+}
+
+# This part is for proxy and HTTPS configure
+server {
+ listen 443 ssl;
+ server_name trilium.example.net; #change trilium.example.net to your domain without HTTPS or HTTP.
+ ssl_certificate /etc/ssl/note/example.crt; #change /etc/ssl/note/example.crt to your path of crt file.
+ ssl_certificate_key /etc/ssl/note/example.net.key; #change /etc/ssl/note/example.net.key to your path of key file.
+ ssl_session_cache builtin:1000 shared:SSL:10m;
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
+ ssl_prefer_server_ciphers on;
+ access_log /var/log/nginx/access.log; #check the path of access.log, if it doesn't fit your file, change it
+
+ location / {
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_pass http://trilium;
+ proxy_read_timeout 90;
+ }
+}
+
+# This part is for HTTPS forced
+server {
+ listen 80;
+ server_name trilium.example.net; # change to your domain
+ return 301 https://$server_name$request_uri;
+}
+ Alternatively if you want to serve the instance under a different path + (useful e.g. if you want to serve multiple instances), update the location + block like so:
+proxy_pass
does not end on a slash as well)proxy_cookie_path
directive with the same path: this
+ allows you to stay logged in at multiple instances at the same time. location /trilium/instance-one {
+ rewrite /trilium/instance-one/(.*) /$1 break;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_pass http://trilium;
+ proxy_cookie_path / /trilium/instance-one
+ proxy_read_timeout 90;
+ }
+
+ If you are running Trilium on localhost
only or if authentication
+ is handled by another component, you can disable Trilium’s authentication
+ by adding the following to config.ini
:
[General]
+noAuthentication=true
+Disabling authentication will bypass even the Multi-Factor Authentication since + v0.94.1.
+Once logged into Trilium, the application will store this information + about the login into a cookie on the browser, but also as a session on + the server.
+If “Remember me” is checked, then the login will expire in 21 days. This
+ period can be adjusted by modifying the Session.cookieMaxAge
value
+ in config.ini
. For example, to have the session expire in one
+ day:
[Session]
+cookieMaxAge=86400
+When “Remember me” is unchecked, the behavior is different. At client/browser + level the authentication does not have any expiration date, but it will + be automatically cleared as soon as the user closes the browser. Nevertheless, + the server will also dismiss this authentication in around 24 hours from + the last interaction with the application.
+The login sessions are now stored in the same Database as the user data. In + order to view which sessions are active, open the SQL Console and run the following + query:
SELECT * FROM sessions
+Expired sessions are periodically cleaned by the server, generally an + hourly interval.
+Multi-factor authentication (MFA) is a security process that requires + users to provide two or more verification factors to gain access to a system, + application, or account. This adds an extra layer of protection beyond + just using a password.
+By requiring more than one verification method, MFA helps reduce the risk + of unauthorized access, even if someone has obtained your password. It’s + highly recommended for securing sensitive information stored in your notes.
+ +OpenID is a standardized way to let you log into websites using an account + from another service, like Google, to verify your identity.
+TOTP (Time-Based One-Time Password) is a security feature that generates + a unique, temporary code on your device, like a smartphone, which changes + every 30 seconds. You use this code, along with your password, to log into + your account, making it much harder for anyone else to access them.
+MFA can only be set up on a server instance.
+ +In order to setup OpenID, you will need to setup a authentication provider.
+ This requires a bit of extra setup. Follow these instructions to
+ setup an OpenID service through google. The Redirect URL of Trilium is https://<your-trilium-domain>/callback
.
oauthBaseUrl
, oauthClientId
and oauthClientSecret
in
+ the config.ini
file (check Configuration (config.ini or environment variables) for
+ more information).
+ TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL
, TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID
, TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET
+ TRILIUM_OAUTH_BASE_URL
, TRILIUM_OAUTH_CLIENT_ID
, TRILIUM_OAUTH_CLIENT_SECRET
+ oauthBaseUrl
should be the link of your Trilium instance server,
+ for example, https://<your-trilium-domain>
.If you don’t already have a running Authentik instance, please follow + these instructionsto set one up.
+https://<your-trilium-domain>/callback
.oauthIssuerBaseUrl
→ Use the OpenID Configuration Issuer
URL
+ from your application's overview page.oauthIssuerName
and oauthIssuerIcon
→ Set these
+ to customize the name and icon displayed on the login page. If omitted,
+ Google’s name and icon will be shown by default.Configuring TLS is essential for server installation in + Trilium. This guide details the steps to set up TLS within Trilium itself.
+For a more robust solution, consider using TLS termination with a reverse + proxy (recommended, e.g., Nginx). You can follow a guide like this for + such setups.
+You have two options for obtaining a TLS certificate:
+config.ini
Once you have your certificate, modify the config.ini
file
+ in the data directory to configure
+ Trilium to use it:
[Network]
+port=8080
+# Set to true for TLS/SSL/HTTPS (secure), false for HTTP (insecure).
+https=true
+# Path to the certificate (run "bash bin/generate-cert.sh" to generate a self-signed certificate).
+# Relevant only if https=true
+certPath=/[username]/.acme.sh/[hostname]/fullchain.cer
+keyPath=/[username]/.acme.sh/[hostname]/example.com.key
+You can also review the configuration file
+ to provide all config.ini
values as environment variables instead.
+ For example, you can configure TLS using environment variables:
export TRILIUM_NETWORK_HTTPS=true
+export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem
+export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem
+The above example shows how this is set up in an environment where the
+ certificate was generated using Let's Encrypt's ACME utility. Your paths
+ may differ. For Docker installations, ensure these paths are within a volume
+ or another directory accessible by the Docker container, such as /home/node/trilium-data/[DIR IN DATA DIRECTORY]
.
After configuring config.ini
, restart Trilium and access the
+ hostname using "https".
If you opt to use a self-signed certificate for your server instance, + note that the desktop instance will not trust it by default.
+To bypass this, disable certificate validation by setting the following + environment variable (for Linux):
export NODE_TLS_REJECT_UNAUTHORIZED=0
+trilium
+Trilium provides scripts to start in this mode, such as trilium-no-cert-check.bat
for
+ Windows.
Warning: Disabling TLS certificate validation is insecure. + Proceed only if you fully understand the implications.
\ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.html new file mode 100644 index 0000000000..cdd405eb6d --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.html @@ -0,0 +1,276 @@ +Trilium provides robust encryption capabilities through its Protected + Notes system, ensuring your sensitive information remains secure even if + your database is compromised.
+Protected notes in Trilium use AES-128-CBC encryption with + scrypt-based key derivation to protect sensitive content. The encryption + is designed to be:
+1. Generate random 16-byte IV
+2. Compute SHA-1 digest of plaintext (use first 4 bytes)
+3. Prepend digest to plaintext
+4. Encrypt (digest + plaintext) using AES-128-CBC
+5. Prepend IV to encrypted data
+6. Encode result as Base64
+1. Decode Base64 ciphertext
+2. Extract IV (first 16 bytes) and encrypted data
+3. Decrypt using AES-128-CBC with data key and IV
+4. Extract digest (first 4 bytes) and plaintext
+5. Verify integrity by comparing computed vs. stored digest
+6. Return plaintext if verification succeeds
+Access via Options → Protected Session:
+Protected Against:
+Not Protected Against:
+Causes:
+Solutions:
+Causes:
+Solutions:
+Symptoms:
+Solutions:
+If you forget your master password:
+For corrupted protected notes:
+Warning: Modifying encryption parameters requires advanced + knowledge and may break compatibility.
+For expert users, encryption parameters can be modified in the source + code:
// In my_scrypt.ts
+const scryptParams = {
+ N: 16384, // CPU/memory cost parameter
+ r: 8, // Block size parameter
+ p: 1 // Parallelization parameter
+};
+Protected notes can be accessed programmatically:
// Backend script example
+const protectedNote = api.getNote('noteId');
+if (protectedNote.isProtected) {
+ // Content will be encrypted unless in protected session
+ const content = protectedNote.getContent();
+}
+When moving to new installation:
+Remember: The security of protected notes ultimately depends on choosing + a strong master password and following security best practices for your + overall system.
\ No newline at end of file diff --git a/docs/Developer Guide/!!!meta.json b/docs/Developer Guide/!!!meta.json index aee04e341c..8cd2e71bf3 100644 --- a/docs/Developer Guide/!!!meta.json +++ b/docs/Developer Guide/!!!meta.json @@ -1,6 +1,6 @@ { "formatVersion": 2, - "appVersion": "0.97.2", + "appVersion": "0.98.0", "files": [ { "isClone": false, @@ -19,6 +19,503 @@ "attachments": [], "dirFileName": "Developer Guide", "children": [ + { + "isClone": false, + "noteId": "YNX9NN659vF2", + "notePath": [ + "jdjRLhLV3TtI", + "YNX9NN659vF2" + ], + "title": "Architecture", + "notePosition": 80, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/html", + "attributes": [], + "format": "markdown", + "attachments": [], + "dirFileName": "Architecture", + "children": [ + { + "isClone": false, + "noteId": "yfKVWBgPMQj4", + "notePath": [ + "jdjRLhLV3TtI", + "YNX9NN659vF2", + "yfKVWBgPMQj4" + ], + "title": "API-Architecture", + "notePosition": 10, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [ + { + "type": "relation", + "name": "internalLink", + "value": "1HpEf2wiYVmV", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "9GSipWxTZ81I", + "isInheritable": false, + "position": 10 + } + ], + "format": "markdown", + "dataFileName": "API-Architecture.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "9GSipWxTZ81I", + "notePath": [ + "jdjRLhLV3TtI", + "YNX9NN659vF2", + "9GSipWxTZ81I" + ], + "title": "Entity-System", + "notePosition": 20, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [ + { + "type": "relation", + "name": "internalLink", + "value": "1HpEf2wiYVmV", + "isInheritable": false, + "position": 10 + } + ], + "format": "markdown", + "dataFileName": "Entity-System.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "QR0jtNmTvYqx", + "notePath": [ + "jdjRLhLV3TtI", + "YNX9NN659vF2", + "QR0jtNmTvYqx" + ], + "title": "Monorepo-Structure", + "notePosition": 30, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [], + "format": "markdown", + "dataFileName": "Monorepo-Structure.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "KQT5Xda5Czcl", + "notePath": [ + "jdjRLhLV3TtI", + "YNX9NN659vF2", + "KQT5Xda5Czcl" + ], + "title": "README", + "notePosition": 40, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [ + { + "type": "relation", + "name": "internalLink", + "value": "1HpEf2wiYVmV", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "9GSipWxTZ81I", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "NZV7rSwYe0dV", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "yfKVWBgPMQj4", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "QR0jtNmTvYqx", + "isInheritable": false, + "position": 10 + } + ], + "format": "markdown", + "dataFileName": "README.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "1HpEf2wiYVmV", + "notePath": [ + "jdjRLhLV3TtI", + "YNX9NN659vF2", + "1HpEf2wiYVmV" + ], + "title": "Three-Layer-Cache-System", + "notePosition": 50, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [ + { + "type": "relation", + "name": "internalLink", + "value": "9GSipWxTZ81I", + "isInheritable": false, + "position": 10 + } + ], + "format": "markdown", + "dataFileName": "Three-Layer-Cache-System.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "NZV7rSwYe0dV", + "notePath": [ + "jdjRLhLV3TtI", + "YNX9NN659vF2", + "NZV7rSwYe0dV" + ], + "title": "Widget-Based-UI-Architecture", + "notePosition": 60, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [], + "format": "markdown", + "dataFileName": "Widget-Based-UI-Architecture.md", + "attachments": [] + } + ] + }, + { + "isClone": false, + "noteId": "TfoaMQkD75JI", + "notePath": [ + "jdjRLhLV3TtI", + "TfoaMQkD75JI" + ], + "title": "API Documentation", + "notePosition": 90, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/html", + "attributes": [], + "format": "markdown", + "attachments": [], + "dirFileName": "API Documentation", + "children": [ + { + "isClone": false, + "noteId": "tvrq35NqaWce", + "notePath": [ + "jdjRLhLV3TtI", + "TfoaMQkD75JI", + "tvrq35NqaWce" + ], + "title": "API Client Libraries", + "notePosition": 10, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [ + { + "type": "relation", + "name": "internalLink", + "value": "oB5PbEHAzG9t", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "eDWmn0B42ALV", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "RqCs8jiZGB1g", + "isInheritable": false, + "position": 10 + } + ], + "format": "markdown", + "dataFileName": "API Client Libraries.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "oB5PbEHAzG9t", + "notePath": [ + "jdjRLhLV3TtI", + "TfoaMQkD75JI", + "oB5PbEHAzG9t" + ], + "title": "ETAPI Complete Guide", + "notePosition": 20, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [], + "format": "markdown", + "dataFileName": "ETAPI Complete Guide.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "4sxya4t9P8AA", + "notePath": [ + "jdjRLhLV3TtI", + "TfoaMQkD75JI", + "4sxya4t9P8AA" + ], + "title": "Internal API Reference", + "notePosition": 30, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [ + { + "type": "relation", + "name": "internalLink", + "value": "oB5PbEHAzG9t", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "RqCs8jiZGB1g", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "eDWmn0B42ALV", + "isInheritable": false, + "position": 10 + } + ], + "format": "markdown", + "dataFileName": "Internal API Reference.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "RqCs8jiZGB1g", + "notePath": [ + "jdjRLhLV3TtI", + "TfoaMQkD75JI", + "RqCs8jiZGB1g" + ], + "title": "Script API Cookbook", + "notePosition": 40, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [], + "format": "markdown", + "dataFileName": "Script API Cookbook.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "eDWmn0B42ALV", + "notePath": [ + "jdjRLhLV3TtI", + "TfoaMQkD75JI", + "eDWmn0B42ALV" + ], + "title": "WebSocket API", + "notePosition": 50, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [ + { + "type": "relation", + "name": "internalLink", + "value": "4sxya4t9P8AA", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "oB5PbEHAzG9t", + "isInheritable": false, + "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "tvrq35NqaWce", + "isInheritable": false, + "position": 10 + } + ], + "format": "markdown", + "dataFileName": "WebSocket API.md", + "attachments": [] + } + ] + }, + { + "isClone": false, + "noteId": "bPMgREWrBYg4", + "notePath": [ + "jdjRLhLV3TtI", + "bPMgREWrBYg4" + ], + "title": "Plugin Development", + "notePosition": 100, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/html", + "attributes": [], + "format": "markdown", + "attachments": [], + "dirFileName": "Plugin Development", + "children": [ + { + "isClone": false, + "noteId": "bZEM5pnOzTVt", + "notePath": [ + "jdjRLhLV3TtI", + "bPMgREWrBYg4", + "bZEM5pnOzTVt" + ], + "title": "Backend Script Development", + "notePosition": 10, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [], + "format": "markdown", + "dataFileName": "Backend Script Development.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "fI5sL6zd3n9K", + "notePath": [ + "jdjRLhLV3TtI", + "bPMgREWrBYg4", + "fI5sL6zd3n9K" + ], + "title": "Custom Note Type Development", + "notePosition": 20, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [], + "format": "markdown", + "dataFileName": "Custom Note Type Development.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "gRXGO3vGuey9", + "notePath": [ + "jdjRLhLV3TtI", + "bPMgREWrBYg4", + "gRXGO3vGuey9" + ], + "title": "Custom Widget Development Guide", + "notePosition": 30, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [], + "format": "markdown", + "dataFileName": "Custom Widget Development Guid.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "3ZeRzd0e7Btc", + "notePath": [ + "jdjRLhLV3TtI", + "bPMgREWrBYg4", + "3ZeRzd0e7Btc" + ], + "title": "Frontend Script Development", + "notePosition": 40, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [], + "format": "markdown", + "dataFileName": "Frontend Script Development.md", + "attachments": [] + }, + { + "isClone": false, + "noteId": "dZnOJuAVLKxt", + "notePath": [ + "jdjRLhLV3TtI", + "bPMgREWrBYg4", + "dZnOJuAVLKxt" + ], + "title": "Theme Development Guide", + "notePosition": 50, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/markdown", + "attributes": [], + "format": "markdown", + "dataFileName": "Theme Development Guide.md", + "attachments": [] + } + ] + }, { "isClone": false, "noteId": "T2W7WCZrYZBU", @@ -27,7 +524,7 @@ "T2W7WCZrYZBU" ], "title": "Environment Setup", - "notePosition": 50, + "notePosition": 130, "prefix": null, "isExpanded": false, "type": "text", @@ -53,7 +550,7 @@ "cxfTSHIUQtt2" ], "title": "Project Structure", - "notePosition": 190, + "notePosition": 270, "prefix": null, "isExpanded": false, "type": "text", @@ -139,7 +636,7 @@ "YjerxU7Aii8X" ], "title": "Troubleshooting", - "notePosition": 200, + "notePosition": 280, "prefix": null, "isExpanded": false, "type": "text", @@ -186,7 +683,7 @@ "wbVIolLKDhe2" ], "title": "Development and architecture", - "notePosition": 220, + "notePosition": 300, "prefix": null, "isExpanded": false, "type": "text", @@ -1728,7 +2225,7 @@ "VHhyVRYK43gI" ], "title": "Building and deployment", - "notePosition": 230, + "notePosition": 310, "prefix": null, "isExpanded": false, "type": "text", @@ -1767,7 +2264,7 @@ "ibAPHul7Efvr" ], "title": "Old documentation", - "notePosition": 260, + "notePosition": 340, "prefix": null, "isExpanded": false, "type": "text", diff --git a/docs/Developer Guide/Developer Guide/API Documentation/API Client Libraries.md b/docs/Developer Guide/Developer Guide/API Documentation/API Client Libraries.md new file mode 100644 index 0000000000..b331da0991 --- /dev/null +++ b/docs/Developer Guide/Developer Guide/API Documentation/API Client Libraries.md @@ -0,0 +1,2277 @@ +# API Client Libraries +## Table of Contents + +1. [Overview](#overview) +2. [JavaScript/TypeScript Client](#javascripttypescript-client) +3. [Python Client - trilium-py](#python-client---trilium-py) +4. [Go Client](#go-client) +5. [Ruby Client](#ruby-client) +6. [PHP Client](#php-client) +7. [C# Client](#c-client) +8. [Rust Client](#rust-client) +9. [REST Client Best Practices](#rest-client-best-practices) +10. [Error Handling Patterns](#error-handling-patterns) +11. [Retry Strategies](#retry-strategies) +12. [Testing Client Libraries](#testing-client-libraries) + +## Overview + +This guide provides comprehensive examples of Trilium API client libraries in various programming languages. Each implementation follows best practices for that language while maintaining consistent functionality across all clients. + +### Common Features + +All client libraries should implement: + +* Token-based authentication +* CRUD operations for notes, attributes, branches, and attachments +* Search functionality +* Error handling with retry logic +* Connection pooling +* Request/response logging (optional) +* Rate limiting support + +## JavaScript/TypeScript Client + +### Full-Featured TypeScript Implementation + +```typescript +// trilium-client.ts + +import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; + +// Types +export interface Note { + noteId: string; + title: string; + type: string; + mime: string; + isProtected: boolean; + attributes?: Attribute[]; + parentNoteIds?: string[]; + childNoteIds?: string[]; + dateCreated: string; + dateModified: string; + utcDateCreated: string; + utcDateModified: string; +} + +export interface CreateNoteParams { + parentNoteId: string; + title: string; + type: string; + content: string; + notePosition?: number; + prefix?: string; + isExpanded?: boolean; + noteId?: string; + branchId?: string; +} + +export interface Attribute { + attributeId: string; + noteId: string; + type: 'label' | 'relation'; + name: string; + value: string; + position?: number; + isInheritable?: boolean; +} + +export interface Branch { + branchId: string; + noteId: string; + parentNoteId: string; + prefix?: string; + notePosition?: number; + isExpanded?: boolean; +} + +export interface Attachment { + attachmentId: string; + ownerId: string; + role: string; + mime: string; + title: string; + position?: number; + blobId?: string; + dateModified?: string; + utcDateModified?: string; +} + +export interface SearchParams { + search: string; + fastSearch?: boolean; + includeArchivedNotes?: boolean; + ancestorNoteId?: string; + ancestorDepth?: string; + orderBy?: string; + orderDirection?: 'asc' | 'desc'; + limit?: number; + debug?: boolean; +} + +export interface SearchResponse { + results: Note[]; + debugInfo?: any; +} + +export interface AppInfo { + appVersion: string; + dbVersion: number; + syncVersion: number; + buildDate: string; + buildRevision: string; + dataDirectory: string; + clipperProtocolVersion: string; + utcDateTime: string; +} + +export interface TriliumClientConfig { + baseUrl: string; + token: string; + timeout?: number; + retryAttempts?: number; + retryDelay?: number; + enableLogging?: boolean; +} + +// Error classes +export class TriliumError extends Error { + constructor( + message: string, + public statusCode?: number, + public code?: string, + public details?: any + ) { + super(message); + this.name = 'TriliumError'; + } +} + +export class TriliumConnectionError extends TriliumError { + constructor(message: string, details?: any) { + super(message, undefined, 'CONNECTION_ERROR', details); + this.name = 'TriliumConnectionError'; + } +} + +export class TriliumAuthError extends TriliumError { + constructor(message: string, details?: any) { + super(message, 401, 'AUTH_ERROR', details); + this.name = 'TriliumAuthError'; + } +} + +// Main client class +export class TriliumClient { + private client: AxiosInstance; + private config: RequiredHello, Trilium!
' + }); + + console.log('Created note:', note.noteId); + + // Search for notes + const searchResults = await client.searchNotes({ + search: '#todo', + limit: 10, + orderBy: 'dateModified', + orderDirection: 'desc' + }); + + console.log(`Found ${searchResults.results.length} todo notes`); + + // Add a label + await client.createAttribute({ + noteId: note.noteId, + type: 'label', + name: 'priority', + value: 'high' + }); + + } catch (error) { + if (error instanceof TriliumAuthError) { + console.error('Authentication failed:', error.message); + } else if (error instanceof TriliumConnectionError) { + console.error('Connection error:', error.message); + } else if (error instanceof TriliumError) { + console.error(`API error (${error.statusCode}):`, error.message); + } else { + console.error('Unexpected error:', error); + } + } +} +``` + +### Browser-Compatible JavaScript Client + +```javascript +// trilium-browser-client.js + +class TriliumBrowserClient { + constructor(baseUrl, token) { + this.baseUrl = baseUrl.replace(/\/$/, ''); + this.token = token; + this.headers = { + 'Authorization': token, + 'Content-Type': 'application/json' + }; + } + + async request(endpoint, options = {}) { + const url = `${this.baseUrl}${endpoint}`; + const config = { + headers: { ...this.headers, ...options.headers }, + ...options + }; + + try { + const response = await fetch(url, config); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.message || `HTTP ${response.status}: ${response.statusText}`); + } + + if (response.status === 204) { + return null; + } + + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + return response.json(); + } + + return response.text(); + } catch (error) { + console.error(`Request failed: ${endpoint}`, error); + throw error; + } + } + + // Notes + async createNote(parentNoteId, title, content, type = 'text') { + return this.request('/create-note', { + method: 'POST', + body: JSON.stringify({ + parentNoteId, + title, + type, + content + }) + }); + } + + async getNote(noteId) { + return this.request(`/notes/${noteId}`); + } + + async updateNote(noteId, updates) { + return this.request(`/notes/${noteId}`, { + method: 'PATCH', + body: JSON.stringify(updates) + }); + } + + async deleteNote(noteId) { + return this.request(`/notes/${noteId}`, { + method: 'DELETE' + }); + } + + async getNoteContent(noteId) { + return this.request(`/notes/${noteId}/content`); + } + + async updateNoteContent(noteId, content) { + return this.request(`/notes/${noteId}/content`, { + method: 'PUT', + headers: { 'Content-Type': 'text/plain' }, + body: content + }); + } + + // Search + async searchNotes(query, options = {}) { + const params = new URLSearchParams({ + search: query, + ...options + }); + + return this.request(`/notes?${params}`); + } + + // Attributes + async addLabel(noteId, name, value = '') { + return this.request('/attributes', { + method: 'POST', + body: JSON.stringify({ + noteId, + type: 'label', + name, + value + }) + }); + } + + async addRelation(noteId, name, targetNoteId) { + return this.request('/attributes', { + method: 'POST', + body: JSON.stringify({ + noteId, + type: 'relation', + name, + value: targetNoteId + }) + }); + } + + // Special notes + async getTodayNote() { + const today = new Date().toISOString().split('T')[0]; + return this.request(`/calendar/days/${today}`); + } + + async getInbox() { + const today = new Date().toISOString().split('T')[0]; + return this.request(`/inbox/${today}`); + } +} + +// Usage in browser +const trilium = new TriliumBrowserClient('http://localhost:8080/etapi', 'your-token'); + +// Create a quick note +async function createQuickNote(title, content) { + try { + const inbox = await trilium.getInbox(); + const result = await trilium.createNote(inbox.noteId, title, content); + console.log('Note created:', result.note.noteId); + return result; + } catch (error) { + console.error('Failed to create note:', error); + } +} +``` + +## Python Client - trilium-py + +### Installation + +```sh +pip install trilium-py +``` + +### Complete Python Implementation + +```python +# trilium_client.py + +import requests +from typing import Optional, Dict, List, Any, Union +from datetime import datetime, date +from dataclasses import dataclass, asdict +from enum import Enum +import time +import logging +from urllib.parse import urljoin +import json +import base64 + +# Set up logging +logger = logging.getLogger(__name__) + +# Enums +class NoteType(Enum): + TEXT = "text" + CODE = "code" + FILE = "file" + IMAGE = "image" + SEARCH = "search" + BOOK = "book" + RELATION_MAP = "relationMap" + RENDER = "render" + +class AttributeType(Enum): + LABEL = "label" + RELATION = "relation" + +# Data classes +@dataclass +class Note: + noteId: str + title: str + type: str + mime: Optional[str] = None + isProtected: bool = False + dateCreated: Optional[str] = None + dateModified: Optional[str] = None + utcDateCreated: Optional[str] = None + utcDateModified: Optional[str] = None + parentNoteIds: Optional[List[str]] = None + childNoteIds: Optional[List[str]] = None + attributes: Optional[List[Dict]] = None + +@dataclass +class CreateNoteRequest: + parentNoteId: str + title: str + type: str + content: str + notePosition: Optional[int] = None + prefix: Optional[str] = None + isExpanded: Optional[bool] = None + noteId: Optional[str] = None + branchId: Optional[str] = None + +@dataclass +class Attribute: + noteId: str + type: str + name: str + value: str = "" + position: Optional[int] = None + isInheritable: bool = False + attributeId: Optional[str] = None + +@dataclass +class Branch: + noteId: str + parentNoteId: str + prefix: Optional[str] = None + notePosition: Optional[int] = None + isExpanded: Optional[bool] = None + branchId: Optional[str] = None + +# Exceptions +class TriliumError(Exception): + """Base exception for Trilium API errors""" + def __init__(self, message: str, status_code: Optional[int] = None, details: Optional[Dict] = None): + super().__init__(message) + self.status_code = status_code + self.details = details + +class TriliumAuthError(TriliumError): + """Authentication error""" + pass + +class TriliumNotFoundError(TriliumError): + """Resource not found error""" + pass + +class TriliumConnectionError(TriliumError): + """Connection error""" + pass + +# Main client class +class TriliumClient: + """Python client for Trilium ETAPI""" + + def __init__( + self, + base_url: str, + token: str, + timeout: int = 30, + retry_attempts: int = 3, + retry_delay: float = 1.0, + verify_ssl: bool = True, + debug: bool = False + ): + self.base_url = base_url.rstrip('/') + self.token = token + self.timeout = timeout + self.retry_attempts = retry_attempts + self.retry_delay = retry_delay + self.verify_ssl = verify_ssl + self.debug = debug + + # Set up session + self.session = requests.Session() + self.session.headers.update({ + 'Authorization': token, + 'Content-Type': 'application/json' + }) + + # Configure logging + if debug: + logging.basicConfig(level=logging.DEBUG) + + def _request( + self, + method: str, + endpoint: str, + json_data: Optional[Dict] = None, + params: Optional[Dict] = None, + data: Optional[Union[str, bytes]] = None, + headers: Optional[Dict] = None, + **kwargs + ) -> Any: + """Make HTTP request with retry logic""" + url = urljoin(self.base_url, endpoint) + + # Merge headers + req_headers = self.session.headers.copy() + if headers: + req_headers.update(headers) + + # Retry logic + last_exception = None + for attempt in range(self.retry_attempts): + try: + if self.debug: + logger.debug(f"[Attempt {attempt + 1}] {method} {url}") + + response = self.session.request( + method=method, + url=url, + json=json_data, + params=params, + data=data, + headers=req_headers, + timeout=self.timeout, + verify=self.verify_ssl, + **kwargs + ) + + # Handle different status codes + if response.status_code == 401: + raise TriliumAuthError("Authentication failed", 401) + elif response.status_code == 404: + raise TriliumNotFoundError("Resource not found", 404) + elif response.status_code >= 500: + # Server error - retry + if attempt < self.retry_attempts - 1: + time.sleep(self.retry_delay * (attempt + 1)) + continue + else: + response.raise_for_status() + elif not response.ok: + error_data = {} + try: + error_data = response.json() + except: + pass + raise TriliumError( + error_data.get('message', f"HTTP {response.status_code}"), + response.status_code, + error_data + ) + + # Parse response + if response.status_code == 204: + return None + + content_type = response.headers.get('content-type', '') + if 'application/json' in content_type: + return response.json() + elif 'text' in content_type: + return response.text + else: + return response.content + + except requests.exceptions.ConnectionError as e: + last_exception = e + if attempt < self.retry_attempts - 1: + logger.warning(f"Connection error, retrying in {self.retry_delay * (attempt + 1)}s...") + time.sleep(self.retry_delay * (attempt + 1)) + else: + raise TriliumConnectionError(f"Connection failed after {self.retry_attempts} attempts") from e + except requests.exceptions.Timeout as e: + last_exception = e + if attempt < self.retry_attempts - 1: + logger.warning(f"Request timeout, retrying...") + time.sleep(self.retry_delay * (attempt + 1)) + else: + raise TriliumConnectionError("Request timeout") from e + except TriliumError: + raise + except Exception as e: + raise TriliumError(f"Unexpected error: {str(e)}") from e + + if last_exception: + raise TriliumConnectionError(f"Request failed after {self.retry_attempts} attempts") from last_exception + + # Note operations + def create_note( + self, + parent_note_id: str, + title: str, + content: str, + note_type: Union[str, NoteType] = NoteType.TEXT, + **kwargs + ) -> Dict[str, Any]: + """Create a new note""" + if isinstance(note_type, NoteType): + note_type = note_type.value + + data = { + 'parentNoteId': parent_note_id, + 'title': title, + 'type': note_type, + 'content': content, + **kwargs + } + + return self._request('POST', '/create-note', json_data=data) + + def get_note(self, note_id: str) -> Note: + """Get note by ID""" + data = self._request('GET', f'/notes/{note_id}') + return Note(**data) + + def update_note(self, note_id: str, **updates) -> Note: + """Update note properties""" + data = self._request('PATCH', f'/notes/{note_id}', json_data=updates) + return Note(**data) + + def delete_note(self, note_id: str) -> None: + """Delete a note""" + self._request('DELETE', f'/notes/{note_id}') + + def get_note_content(self, note_id: str) -> str: + """Get note content""" + return self._request('GET', f'/notes/{note_id}/content') + + def update_note_content(self, note_id: str, content: str) -> None: + """Update note content""" + self._request( + 'PUT', + f'/notes/{note_id}/content', + data=content, + headers={'Content-Type': 'text/plain'} + ) + + # Search + def search_notes( + self, + query: str, + fast_search: bool = False, + include_archived: bool = False, + ancestor_note_id: Optional[str] = None, + order_by: Optional[str] = None, + order_direction: str = 'asc', + limit: Optional[int] = None, + debug: bool = False + ) -> List[Note]: + """Search for notes""" + params = { + 'search': query, + 'fastSearch': fast_search, + 'includeArchivedNotes': include_archived + } + + if ancestor_note_id: + params['ancestorNoteId'] = ancestor_note_id + if order_by: + params['orderBy'] = order_by + params['orderDirection'] = order_direction + if limit: + params['limit'] = limit + if debug: + params['debug'] = debug + + data = self._request('GET', '/notes', params=params) + return [Note(**note) for note in data.get('results', [])] + + # Attributes + def add_label( + self, + note_id: str, + name: str, + value: str = "", + inheritable: bool = False, + position: Optional[int] = None + ) -> Attribute: + """Add a label to a note""" + data = { + 'noteId': note_id, + 'type': 'label', + 'name': name, + 'value': value, + 'isInheritable': inheritable + } + + if position is not None: + data['position'] = position + + result = self._request('POST', '/attributes', json_data=data) + return Attribute(**result) + + def add_relation( + self, + note_id: str, + name: str, + target_note_id: str, + inheritable: bool = False, + position: Optional[int] = None + ) -> Attribute: + """Add a relation to a note""" + data = { + 'noteId': note_id, + 'type': 'relation', + 'name': name, + 'value': target_note_id, + 'isInheritable': inheritable + } + + if position is not None: + data['position'] = position + + result = self._request('POST', '/attributes', json_data=data) + return Attribute(**result) + + def update_attribute(self, attribute_id: str, **updates) -> Attribute: + """Update an attribute""" + result = self._request('PATCH', f'/attributes/{attribute_id}', json_data=updates) + return Attribute(**result) + + def delete_attribute(self, attribute_id: str) -> None: + """Delete an attribute""" + self._request('DELETE', f'/attributes/{attribute_id}') + + # Branches + def clone_note( + self, + note_id: str, + parent_note_id: str, + prefix: Optional[str] = None, + note_position: Optional[int] = None + ) -> Branch: + """Clone a note to another location""" + data = { + 'noteId': note_id, + 'parentNoteId': parent_note_id + } + + if prefix: + data['prefix'] = prefix + if note_position is not None: + data['notePosition'] = note_position + + result = self._request('POST', '/branches', json_data=data) + return Branch(**result) + + def update_branch(self, branch_id: str, **updates) -> Branch: + """Update a branch""" + result = self._request('PATCH', f'/branches/{branch_id}', json_data=updates) + return Branch(**result) + + def delete_branch(self, branch_id: str) -> None: + """Delete a branch""" + self._request('DELETE', f'/branches/{branch_id}') + + # Attachments + def upload_attachment( + self, + note_id: str, + file_path: str, + title: Optional[str] = None, + mime: Optional[str] = None, + position: Optional[int] = None + ) -> Dict[str, Any]: + """Upload a file as attachment""" + import mimetypes + import os + + if title is None: + title = os.path.basename(file_path) + + if mime is None: + mime = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + with open(file_path, 'rb') as f: + content = base64.b64encode(f.read()).decode('utf-8') + + data = { + 'ownerId': note_id, + 'role': 'file', + 'mime': mime, + 'title': title, + 'content': content + } + + if position is not None: + data['position'] = position + + return self._request('POST', '/attachments', json_data=data) + + def download_attachment(self, attachment_id: str, output_path: str) -> str: + """Download an attachment""" + content = self._request('GET', f'/attachments/{attachment_id}/content') + + with open(output_path, 'wb') as f: + if isinstance(content, bytes): + f.write(content) + else: + f.write(content.encode('utf-8')) + + return output_path + + # Special notes + def get_inbox(self, target_date: Optional[Union[str, date]] = None) -> Note: + """Get inbox note for a date""" + if target_date is None: + target_date = date.today() + elif isinstance(target_date, date): + target_date = target_date.strftime('%Y-%m-%d') + + data = self._request('GET', f'/inbox/{target_date}') + return Note(**data) + + def get_day_note(self, target_date: Optional[Union[str, date]] = None) -> Note: + """Get day note for a date""" + if target_date is None: + target_date = date.today() + elif isinstance(target_date, date): + target_date = target_date.strftime('%Y-%m-%d') + + data = self._request('GET', f'/calendar/days/{target_date}') + return Note(**data) + + def get_week_note(self, target_date: Optional[Union[str, date]] = None) -> Note: + """Get week note for a date""" + if target_date is None: + target_date = date.today() + elif isinstance(target_date, date): + target_date = target_date.strftime('%Y-%m-%d') + + data = self._request('GET', f'/calendar/weeks/{target_date}') + return Note(**data) + + def get_month_note(self, month: Optional[str] = None) -> Note: + """Get month note""" + if month is None: + month = date.today().strftime('%Y-%m') + + data = self._request('GET', f'/calendar/months/{month}') + return Note(**data) + + def get_year_note(self, year: Optional[Union[str, int]] = None) -> Note: + """Get year note""" + if year is None: + year = str(date.today().year) + elif isinstance(year, int): + year = str(year) + + data = self._request('GET', f'/calendar/years/{year}') + return Note(**data) + + # Utility + def get_app_info(self) -> Dict[str, Any]: + """Get application information""" + return self._request('GET', '/app-info') + + def create_backup(self, backup_name: str) -> None: + """Create a backup""" + self._request('PUT', f'/backup/{backup_name}') + + def export_notes( + self, + note_id: str, + output_file: str, + format: str = 'html' + ) -> str: + """Export notes to ZIP file""" + content = self._request( + 'GET', + f'/notes/{note_id}/export', + params={'format': format} + ) + + with open(output_file, 'wb') as f: + f.write(content) + + return output_file + + def create_note_revision(self, note_id: str) -> None: + """Create a revision for a note""" + self._request('POST', f'/notes/{note_id}/revision') + + def refresh_note_ordering(self, parent_note_id: str) -> None: + """Refresh note ordering""" + self._request('POST', f'/refresh-note-ordering/{parent_note_id}') + +# Helper class for batch operations +class TriliumBatchClient(TriliumClient): + """Extended client with batch operations""" + + def create_notes_batch( + self, + notes: List[CreateNoteRequest], + delay: float = 0.1 + ) -> List[Dict[str, Any]]: + """Create multiple notes with delay between requests""" + results = [] + + for note_req in notes: + result = self.create_note(**asdict(note_req)) + results.append(result) + time.sleep(delay) + + return results + + def add_labels_batch( + self, + note_id: str, + labels: Dict[str, str] + ) -> List[Attribute]: + """Add multiple labels to a note""" + results = [] + + for name, value in labels.items(): + attr = self.add_label(note_id, name, value) + results.append(attr) + + return results + + def search_and_tag( + self, + search_query: str, + tag_name: str, + tag_value: str = "" + ) -> List[str]: + """Search for notes and add a tag to all results""" + notes = self.search_notes(search_query) + tagged = [] + + for note in notes: + self.add_label(note.noteId, tag_name, tag_value) + tagged.append(note.noteId) + + return tagged + +# Context manager for automatic connection handling +class TriliumContext: + """Context manager for Trilium client""" + + def __init__(self, base_url: str, token: str, **kwargs): + self.base_url = base_url + self.token = token + self.kwargs = kwargs + self.client = None + + def __enter__(self) -> TriliumClient: + self.client = TriliumClient(self.base_url, self.token, **self.kwargs) + return self.client + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.client and hasattr(self.client, 'session'): + self.client.session.close() + +# Usage examples +if __name__ == "__main__": + # Basic usage + client = TriliumClient( + base_url="http://localhost:8080/etapi", + token="your-token", + debug=True + ) + + # Create a note + result = client.create_note( + parent_note_id="root", + title="Test Note", + content="This is a test note
", + note_type=NoteType.TEXT + ) + print(f"Created note: {result['note']['noteId']}") + + # Search notes + todo_notes = client.search_notes("#todo", limit=10) + for note in todo_notes: + print(f"- {note.title}") + + # Using context manager + with TriliumContext("http://localhost:8080/etapi", "your-token") as api: + inbox = api.get_inbox() + print(f"Inbox note ID: {inbox.noteId}") + + # Batch operations + batch_client = TriliumBatchClient( + base_url="http://localhost:8080/etapi", + token="your-token" + ) + + # Tag all notes matching a search + tagged = batch_client.search_and_tag( + search_query="type:text", + tag_name="processed", + tag_value=datetime.now().isoformat() + ) + print(f"Tagged {len(tagged)} notes") +``` + +## Go Client + +```go +// trilium_client.go + +package trilium + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" +) + +// Note represents a Trilium note +type Note struct { + NoteID string `json:"noteId"` + Title string `json:"title"` + Type string `json:"type"` + Mime string `json:"mime,omitempty"` + IsProtected bool `json:"isProtected"` + DateCreated string `json:"dateCreated,omitempty"` + DateModified string `json:"dateModified,omitempty"` + UTCDateCreated string `json:"utcDateCreated,omitempty"` + UTCDateModified string `json:"utcDateModified,omitempty"` + Attributes []Attribute `json:"attributes,omitempty"` + ParentNoteIDs []string `json:"parentNoteIds,omitempty"` + ChildNoteIDs []string `json:"childNoteIds,omitempty"` +} + +// CreateNoteRequest represents a request to create a note +type CreateNoteRequest struct { + ParentNoteID string `json:"parentNoteId"` + Title string `json:"title"` + Type string `json:"type"` + Content string `json:"content"` + NotePosition int `json:"notePosition,omitempty"` + Prefix string `json:"prefix,omitempty"` + IsExpanded bool `json:"isExpanded,omitempty"` +} + +// Attribute represents a note attribute +type Attribute struct { + AttributeID string `json:"attributeId,omitempty"` + NoteID string `json:"noteId"` + Type string `json:"type"` + Name string `json:"name"` + Value string `json:"value"` + Position int `json:"position,omitempty"` + IsInheritable bool `json:"isInheritable,omitempty"` +} + +// Branch represents a note branch +type Branch struct { + BranchID string `json:"branchId,omitempty"` + NoteID string `json:"noteId"` + ParentNoteID string `json:"parentNoteId"` + Prefix string `json:"prefix,omitempty"` + NotePosition int `json:"notePosition,omitempty"` + IsExpanded bool `json:"isExpanded,omitempty"` +} + +// SearchParams represents search parameters +type SearchParams struct { + Search string `url:"search"` + FastSearch bool `url:"fastSearch,omitempty"` + IncludeArchivedNotes bool `url:"includeArchivedNotes,omitempty"` + AncestorNoteID string `url:"ancestorNoteId,omitempty"` + AncestorDepth string `url:"ancestorDepth,omitempty"` + OrderBy string `url:"orderBy,omitempty"` + OrderDirection string `url:"orderDirection,omitempty"` + Limit int `url:"limit,omitempty"` + Debug bool `url:"debug,omitempty"` +} + +// SearchResponse represents search results +type SearchResponse struct { + Results []Note `json:"results"` + DebugInfo map[string]interface{} `json:"debugInfo,omitempty"` +} + +// Client is the Trilium API client +type Client struct { + BaseURL string + Token string + HTTPClient *http.Client +} + +// NewClient creates a new Trilium client +func NewClient(baseURL, token string) *Client { + return &Client{ + BaseURL: baseURL, + Token: token, + HTTPClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// request makes an HTTP request to the API +func (c *Client) request(method, endpoint string, body interface{}) (*http.Response, error) { + url := c.BaseURL + endpoint + + var reqBody io.Reader + if body != nil { + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + reqBody = bytes.NewBuffer(jsonBody) + } + + req, err := http.NewRequest(method, url, reqBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", c.Token) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + + if resp.StatusCode >= 400 { + defer resp.Body.Close() + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(bodyBytes)) + } + + return resp, nil +} + +// CreateNote creates a new note +func (c *Client) CreateNote(req CreateNoteRequest) (*Note, *Branch, error) { + resp, err := c.request("POST", "/create-note", req) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + var result struct { + Note Note `json:"note"` + Branch Branch `json:"branch"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result.Note, &result.Branch, nil +} + +// GetNote retrieves a note by ID +func (c *Client) GetNote(noteID string) (*Note, error) { + resp, err := c.request("GET", fmt.Sprintf("/notes/%s", noteID), nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var note Note + if err := json.NewDecoder(resp.Body).Decode(¬e); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return ¬e, nil +} + +// UpdateNote updates a note +func (c *Client) UpdateNote(noteID string, updates map[string]interface{}) (*Note, error) { + resp, err := c.request("PATCH", fmt.Sprintf("/notes/%s", noteID), updates) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var note Note + if err := json.NewDecoder(resp.Body).Decode(¬e); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return ¬e, nil +} + +// DeleteNote deletes a note +func (c *Client) DeleteNote(noteID string) error { + resp, err := c.request("DELETE", fmt.Sprintf("/notes/%s", noteID), nil) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// GetNoteContent retrieves note content +func (c *Client) GetNoteContent(noteID string) (string, error) { + resp, err := c.request("GET", fmt.Sprintf("/notes/%s/content", noteID), nil) + if err != nil { + return "", err + } + defer resp.Body.Close() + + content, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + return string(content), nil +} + +// UpdateNoteContent updates note content +func (c *Client) UpdateNoteContent(noteID, content string) error { + req, err := http.NewRequest("PUT", c.BaseURL+fmt.Sprintf("/notes/%s/content", noteID), bytes.NewBufferString(content)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", c.Token) + req.Header.Set("Content-Type", "text/plain") + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + bodyBytes, _ := io.ReadAll(resp.Body) + return fmt.Errorf("API error %d: %s", resp.StatusCode, string(bodyBytes)) + } + + return nil +} + +// SearchNotes searches for notes +func (c *Client) SearchNotes(params SearchParams) (*SearchResponse, error) { + query := url.Values{} + query.Set("search", params.Search) + + if params.FastSearch { + query.Set("fastSearch", "true") + } + if params.IncludeArchivedNotes { + query.Set("includeArchivedNotes", "true") + } + if params.AncestorNoteID != "" { + query.Set("ancestorNoteId", params.AncestorNoteID) + } + if params.OrderBy != "" { + query.Set("orderBy", params.OrderBy) + } + if params.OrderDirection != "" { + query.Set("orderDirection", params.OrderDirection) + } + if params.Limit > 0 { + query.Set("limit", fmt.Sprintf("%d", params.Limit)) + } + + resp, err := c.request("GET", fmt.Sprintf("/notes?%s", query.Encode()), nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var searchResp SearchResponse + if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &searchResp, nil +} + +// AddLabel adds a label to a note +func (c *Client) AddLabel(noteID, name, value string) (*Attribute, error) { + attr := Attribute{ + NoteID: noteID, + Type: "label", + Name: name, + Value: value, + } + + resp, err := c.request("POST", "/attributes", attr) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result Attribute + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result, nil +} + +// Usage example +func Example() { + client := NewClient("http://localhost:8080/etapi", "your-token") + + // Create a note + note, branch, err := client.CreateNote(CreateNoteRequest{ + ParentNoteID: "root", + Title: "Test Note", + Type: "text", + Content: "Hello from Go!
", + }) + + if err != nil { + panic(err) + } + + fmt.Printf("Created note %s with branch %s\n", note.NoteID, branch.BranchID) + + // Search notes + results, err := client.SearchNotes(SearchParams{ + Search: "#todo", + Limit: 10, + }) + + if err != nil { + panic(err) + } + + fmt.Printf("Found %d todo notes\n", len(results.Results)) +} +``` + +## REST Client Best Practices + +### 1\. Connection Management + +```python +# Python - Connection pooling with requests +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +class RobustTriliumClient: + def __init__(self, base_url, token): + self.base_url = base_url + self.token = token + + # Configure connection pooling and retries + self.session = requests.Session() + + retry_strategy = Retry( + total=3, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE", "POST"] + ) + + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=10, + pool_maxsize=10 + ) + + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + self.session.headers.update({ + 'Authorization': token, + 'Content-Type': 'application/json' + }) +``` + +### 2\. Request Timeout Handling + +```javascript +// JavaScript - Timeout with abort controller +class TimeoutClient { + constructor(baseUrl, token, timeout = 30000) { + this.baseUrl = baseUrl; + this.token = token; + this.timeout = timeout; + } + + async request(endpoint, options = {}) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeout); + + try { + const response = await fetch(`${this.baseUrl}${endpoint}`, { + ...options, + headers: { + 'Authorization': this.token, + ...options.headers + }, + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); + } catch (error) { + if (error.name === 'AbortError') { + throw new Error(`Request timeout after ${this.timeout}ms`); + } + throw error; + } finally { + clearTimeout(timeoutId); + } + } +} +``` + +### 3\. Rate Limiting + +```python +# Python - Rate limiting with token bucket +import time +from threading import Lock + +class RateLimitedClient: + def __init__(self, base_url, token, requests_per_second=10): + self.base_url = base_url + self.token = token + self.rate_limit = requests_per_second + self.tokens = requests_per_second + self.last_update = time.time() + self.lock = Lock() + + def _wait_for_token(self): + with self.lock: + now = time.time() + elapsed = now - self.last_update + self.tokens = min( + self.rate_limit, + self.tokens + elapsed * self.rate_limit + ) + self.last_update = now + + if self.tokens < 1: + sleep_time = (1 - self.tokens) / self.rate_limit + time.sleep(sleep_time) + self.tokens = 1 + + self.tokens -= 1 + + def request(self, method, endpoint, **kwargs): + self._wait_for_token() + # Make actual request here + return self._make_request(method, endpoint, **kwargs) +``` + +### 4\. Caching + +```typescript +// TypeScript - Response caching +interface CacheEntryTest content
' + ) + + # Assertions + self.assertEqual(result['note']['noteId'], 'test123') + self.assertEqual(result['note']['title'], 'Test Note') + + # Verify request was made correctly + mock_request.assert_called_once() + call_args = mock_request.call_args + self.assertEqual(call_args[1]['method'], 'POST') + self.assertEqual(call_args[1]['url'], 'http://localhost:8080/etapi/create-note') + + @patch('requests.Session.request') + def test_search_notes(self, mock_request): + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + 'results': [ + {'noteId': 'note1', 'title': 'Note 1'}, + {'noteId': 'note2', 'title': 'Note 2'} + ] + } + mock_request.return_value = mock_response + + # Test search + results = self.client.search_notes('#todo', limit=10) + + # Assertions + self.assertEqual(len(results), 2) + self.assertEqual(results[0].noteId, 'note1') + + @patch('requests.Session.request') + def test_error_handling(self, mock_request): + # Mock error response + mock_response = Mock() + mock_response.status_code = 404 + mock_response.json.return_value = { + 'status': 404, + 'code': 'NOTE_NOT_FOUND', + 'message': 'Note not found' + } + mock_request.return_value = mock_response + + # Test error handling + with self.assertRaises(TriliumNotFoundError) as context: + self.client.get_note('invalid_id') + + self.assertEqual(context.exception.status_code, 404) + self.assertIn('Note not found', str(context.exception)) + +class TestRetryLogic(unittest.TestCase): + @patch('time.sleep') + @patch('requests.Session.request') + def test_retry_on_server_error(self, mock_request, mock_sleep): + client = TriliumClient( + base_url="http://localhost:8080/etapi", + token="test-token", + retry_attempts=3 + ) + + # Mock server error then success + mock_response_error = Mock() + mock_response_error.status_code = 500 + + mock_response_success = Mock() + mock_response_success.status_code = 200 + mock_response_success.json.return_value = {'noteId': 'test123'} + + mock_request.side_effect = [ + mock_response_error, + mock_response_error, + mock_response_success + ] + + # Should succeed after retries + result = client.get_note('test123') + self.assertEqual(result.noteId, 'test123') + + # Verify retries happened + self.assertEqual(mock_request.call_count, 3) + self.assertEqual(mock_sleep.call_count, 2) + +if __name__ == '__main__': + unittest.main() +``` + +### Integration Testing + +```javascript +// JavaScript - Integration tests with Jest +describe('TriliumClient Integration Tests', () => { + let client; + let testNoteId; + + beforeAll(() => { + client = new TriliumClient({ + baseUrl: process.env.TRILIUM_URL || 'http://localhost:8080/etapi', + token: process.env.TRILIUM_TOKEN || 'test-token' + }); + }); + + afterAll(async () => { + // Cleanup test notes + if (testNoteId) { + await client.deleteNote(testNoteId); + } + }); + + test('should create and retrieve a note', async () => { + // Create note + const createResult = await client.createNote({ + parentNoteId: 'root', + title: 'Integration Test Note', + type: 'text', + content: 'Test content
' + }); + + expect(createResult.note).toBeDefined(); + expect(createResult.note.title).toBe('Integration Test Note'); + + testNoteId = createResult.note.noteId; + + // Retrieve note + const note = await client.getNote(testNoteId); + expect(note.noteId).toBe(testNoteId); + expect(note.title).toBe('Integration Test Note'); + }); + + test('should update note content', async () => { + const newContent = 'Updated content
'; + + await client.updateNoteContent(testNoteId, newContent); + + const content = await client.getNoteContent(testNoteId); + expect(content).toBe(newContent); + }); + + test('should add and retrieve attributes', async () => { + // Add label + const attribute = await client.createAttribute({ + noteId: testNoteId, + type: 'label', + name: 'testLabel', + value: 'testValue' + }); + + expect(attribute.attributeId).toBeDefined(); + + // Retrieve note with attributes + const note = await client.getNote(testNoteId); + const label = note.attributes.find(a => a.name === 'testLabel'); + + expect(label).toBeDefined(); + expect(label.value).toBe('testValue'); + }); + + test('should search notes', async () => { + // Add searchable label + await client.createAttribute({ + noteId: testNoteId, + type: 'label', + name: 'integrationTest', + value: '' + }); + + // Search + const results = await client.searchNotes({ + search: '#integrationTest', + limit: 10 + }); + + expect(results.results).toBeDefined(); + expect(results.results.length).toBeGreaterThan(0); + + const foundNote = results.results.find(n => n.noteId === testNoteId); + expect(foundNote).toBeDefined(); + }); +}); +``` + +## Conclusion + +These client libraries provide robust, production-ready implementations for interacting with the Trilium API. Key considerations: + +1. **Choose the right language** for your use case and environment +2. **Implement proper error handling** with specific exception types +3. **Use connection pooling** for better performance +4. **Add retry logic** for resilience against transient failures +5. **Consider rate limiting** to avoid overwhelming the server +6. **Cache responses** when appropriate to reduce API calls +7. **Write comprehensive tests** for reliability +8. **Document your client** with clear examples + +For additional resources: + +* [ETAPI Complete Guide](ETAPI%20Complete%20Guide.md) +* [WebSocket API Documentation](WebSocket%20API.md) +* [Script API Cookbook](Script%20API%20Cookbook.md) \ No newline at end of file diff --git a/docs/Developer Guide/Developer Guide/API Documentation/ETAPI Complete Guide.md b/docs/Developer Guide/Developer Guide/API Documentation/ETAPI Complete Guide.md new file mode 100644 index 0000000000..8e9d694a70 --- /dev/null +++ b/docs/Developer Guide/Developer Guide/API Documentation/ETAPI Complete Guide.md @@ -0,0 +1,1798 @@ +# ETAPI Complete Guide +## ETAPI Guide + +ETAPI (External Trilium API) is the REST API for integrating external applications with Trilium Notes. This guide walks you through authentication, common operations, and best practices for building integrations. + +## Getting Started + +ETAPI provides programmatic access to your notes, attributes, and attachments through a RESTful interface. The API is designed to be predictable and easy to use, with comprehensive error messages to help you debug issues quickly. + +Base URL: `http://localhost:8080/etapi` + +## Authentication + +Before using the API, you need to authenticate your requests. There are three ways to authenticate with ETAPI. + +### Token Authentication (Recommended) + +First, generate an ETAPI token in Trilium by navigating to Options → ETAPI and clicking "Create new ETAPI token". Then include this token in your request headers: + +```sh +curl -H "Authorization: your-token" http://localhost:8080/etapi/notes/root +``` + +In Python: + +```python +import requests + +headers = {'Authorization': 'your-token'} +response = requests.get('http://localhost:8080/etapi/notes/root', headers=headers) +``` + +### Basic Authentication + +You can also use the token as a password with any username: + +```sh +curl -u "user:your-token" http://localhost:8080/etapi/notes/root +``` + +### Password Authentication + +For automated scripts, you can login with your Trilium password to receive a session token. This method is useful when you cannot store API tokens securely: + +```python +# Login and get session token +response = requests.post('http://localhost:8080/etapi/auth/login', + json={'password': 'your-password'}) +token = response.json()['authToken'] + +# Use the session token +headers = {'Authorization': token} +``` + +## API Endpoints + +### Notes + +#### Create Note + +**POST** `/etapi/create-note` + +Creating a note requires just a parent note ID and title. The content and other properties are optional: + +```json +{ + "parentNoteId": "root", + "title": "My New Note", + "type": "text", + "content": "Note content in HTML
" +} +``` + +The response includes both the created note and its branch (position in the tree): + +```python +def create_note(parent_id, title, content=""): + response = requests.post( + "http://localhost:8080/etapi/create-note", + headers={'Authorization': 'your-token'}, + json={ + "parentNoteId": parent_id, + "title": title, + "content": content + } + ) + return response.json() + +# Create a simple note +note = create_note("root", "Meeting Notes") +print(f"Created: {note['note']['noteId']}") +``` + +#### Get Note by ID + +**GET** `/etapi/notes/{noteId}` + +**cURL Example:** + +```sh +curl -X GET http://localhost:8080/etapi/notes/evnnmvHTCgIn \ + -H "Authorization: your-token" +``` + +**Response:** + +```json +{ + "noteId": "evnnmvHTCgIn", + "title": "My Note", + "type": "text", + "mime": "text/html", + "isProtected": false, + "attributes": [ + { + "attributeId": "abc123", + "noteId": "evnnmvHTCgIn", + "type": "label", + "name": "todo", + "value": "", + "position": 10, + "isInheritable": false + } + ], + "parentNoteIds": ["root"], + "childNoteIds": ["child1", "child2"], + "dateCreated": "2024-01-15 10:30:00.000+0100", + "dateModified": "2024-01-15 14:20:00.000+0100", + "utcDateCreated": "2024-01-15 09:30:00.000Z", + "utcDateModified": "2024-01-15 13:20:00.000Z" +} +``` + +#### Update Note + +**PATCH** `/etapi/notes/{noteId}` + +**Request Body:** + +```json +{ + "title": "Updated Title", + "type": "text", + "mime": "text/html" +} +``` + +**JavaScript Example:** + +```javascript +async function updateNote(noteId, updates) { + const response = await fetch(`http://localhost:8080/etapi/notes/${noteId}`, { + method: 'PATCH', + headers: { + 'Authorization': 'your-token', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(updates) + }); + + if (!response.ok) { + throw new Error(`Failed to update note: ${response.statusText}`); + } + + return response.json(); +} + +// Usage +updateNote('evnnmvHTCgIn', { title: 'New Title' }) + .then(note => console.log('Updated note:', note)) + .catch(err => console.error('Error:', err)); +``` + +#### Delete Note + +**DELETE** `/etapi/notes/{noteId}` + +```sh +curl -X DELETE http://localhost:8080/etapi/notes/evnnmvHTCgIn \ + -H "Authorization: your-token" +``` + +#### Get Note Content + +**GET** `/etapi/notes/{noteId}/content` + +Returns the content of a note. + +```python +import requests + +def get_note_content(note_id, token): + url = f"http://localhost:8080/etapi/notes/{note_id}/content" + headers = {'Authorization': token} + + response = requests.get(url, headers=headers) + return response.text # Returns HTML or text content + +content = get_note_content('evnnmvHTCgIn', 'your-token') +print(content) +``` + +#### Update Note Content + +**PUT** `/etapi/notes/{noteId}/content` + +```python +def update_note_content(note_id, content, token): + url = f"http://localhost:8080/etapi/notes/{note_id}/content" + headers = { + 'Authorization': token, + 'Content-Type': 'text/plain' + } + + response = requests.put(url, headers=headers, data=content) + return response.status_code == 204 + +# Update with HTML content +html_content = "New paragraph
" +success = update_note_content('evnnmvHTCgIn', html_content, 'your-token') +``` + +### Search + +#### Search Notes + +**GET** `/etapi/notes` + +Search notes using Trilium's powerful search syntax. The search parameter accepts keywords, labels, and complex expressions. + +```python +# Simple keyword search +results = requests.get( + "http://localhost:8080/etapi/notes", + headers={'Authorization': 'token'}, + params={'search': 'project management'} +).json() + +# Search by label +results = requests.get( + "http://localhost:8080/etapi/notes", + params={'search': '#todo'} +).json() + +# Complex search with sorting +results = requests.get( + "http://localhost:8080/etapi/notes", + params={ + 'search': 'type:text #important', + 'orderBy': 'dateModified', + 'orderDirection': 'desc', + 'limit': 10 + } +).json() +``` + +Common search patterns: + +* Keywords: `project management` +* Labels: `#todo`, `#priority=high` +* Note type: `type:text`, `type:code` +* Date ranges: `dateCreated>=2024-01-01` +* Subtree search: Use `ancestorNoteId` parameter + +### Attributes + +Attributes are key-value metadata attached to notes. There are two types: labels (name-value pairs) and relations (links to other notes). + +#### Create Attribute + +**POST** `/etapi/attributes` + +```python +# Add a simple label +requests.post( + "http://localhost:8080/etapi/attributes", + headers={'Authorization': 'token'}, + json={ + "noteId": "note123", + "type": "label", + "name": "todo" + } +) + +# Add label with value +requests.post( + "http://localhost:8080/etapi/attributes", + json={ + "noteId": "note123", + "type": "label", + "name": "priority", + "value": "high" + } +) +``` + +#### Update Attribute + +**PATCH** `/etapi/attributes/{attributeId}` + +You can only update the value and position of labels. Relations can only have their position updated. + +```python +# Update attribute value +requests.patch( + "http://localhost:8080/etapi/attributes/attr123", + json={"value": "low"} +) +``` + +#### Delete Attribute + +**DELETE** `/etapi/attributes/{attributeId}` + +### Branches + +Branches represent the position of notes in the tree structure. A note can appear in multiple locations through cloning. + +#### Clone Note to Another Location + +**POST** `/etapi/branches` + +```python +# Clone a note to a new parent +requests.post( + "http://localhost:8080/etapi/branches", + headers={'Authorization': 'token'}, + json={ + "noteId": "note123", + "parentNoteId": "newParent456" + } +) +``` + +This creates a "clone" - the same note appearing in multiple places. Changes to the note content affect all locations. + +#### Move Note Position + +**PATCH** `/etapi/branches/{branchId}` + +```python +# Change note position or prefix +requests.patch( + "http://localhost:8080/etapi/branches/branch123", + json={"notePosition": 5} +) +``` + +#### Remove Branch + +**DELETE** `/etapi/branches/{branchId}` + +Removes a note from one location. If it's the last location, the note itself is deleted. + +### Attachments + +#### Create Attachment + +**POST** `/etapi/attachments` + +```json +{ + "ownerId": "evnnmvHTCgIn", + "role": "file", + "mime": "application/pdf", + "title": "document.pdf", + "content": "base64-encoded-content", + "position": 10 +} +``` + +**Python Example with File Upload:** + +```python +import base64 + +def upload_attachment(note_id, file_path, title=None): + with open(file_path, 'rb') as f: + content = base64.b64encode(f.read()).decode('utf-8') + + import mimetypes + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + if title is None: + import os + title = os.path.basename(file_path) + + url = "http://localhost:8080/etapi/attachments" + headers = { + 'Authorization': 'your-token', + 'Content-Type': 'application/json' + } + + data = { + "ownerId": note_id, + "role": "file", + "mime": mime_type, + "title": title, + "content": content + } + + response = requests.post(url, headers=headers, json=data) + return response.json() + +# Upload a PDF +attachment = upload_attachment("evnnmvHTCgIn", "/path/to/document.pdf") +print(f"Attachment ID: {attachment['attachmentId']}") +``` + +#### Get Attachment Content + +**GET** `/etapi/attachments/{attachmentId}/content` + +```python +def download_attachment(attachment_id, output_path): + url = f"http://localhost:8080/etapi/attachments/{attachment_id}/content" + headers = {'Authorization': 'your-token'} + + response = requests.get(url, headers=headers) + + with open(output_path, 'wb') as f: + f.write(response.content) + + return output_path + +# Download attachment +download_attachment("attachId123", "/tmp/downloaded.pdf") +``` + +### Special Notes + +#### Get Inbox Note + +**GET** `/etapi/inbox/{date}` + +Gets or creates an inbox note for the specified date. + +```python +from datetime import date + +def get_inbox_note(target_date=None): + if target_date is None: + target_date = date.today() + + date_str = target_date.strftime('%Y-%m-%d') + url = f"http://localhost:8080/etapi/inbox/{date_str}" + headers = {'Authorization': 'your-token'} + + response = requests.get(url, headers=headers) + return response.json() + +# Get today's inbox +inbox = get_inbox_note() +print(f"Inbox note ID: {inbox['noteId']}") +``` + +#### Calendar Notes + +**Day Note:** + +```python +def get_day_note(date_str): + url = f"http://localhost:8080/etapi/calendar/days/{date_str}" + headers = {'Authorization': 'your-token'} + response = requests.get(url, headers=headers) + return response.json() + +day_note = get_day_note("2024-01-15") +``` + +**Week Note:** + +```python +def get_week_note(date_str): + url = f"http://localhost:8080/etapi/calendar/weeks/{date_str}" + headers = {'Authorization': 'your-token'} + response = requests.get(url, headers=headers) + return response.json() + +week_note = get_week_note("2024-01-15") +``` + +**Month Note:** + +```python +def get_month_note(month_str): + url = f"http://localhost:8080/etapi/calendar/months/{month_str}" + headers = {'Authorization': 'your-token'} + response = requests.get(url, headers=headers) + return response.json() + +month_note = get_month_note("2024-01") +``` + +**Year Note:** + +```python +def get_year_note(year): + url = f"http://localhost:8080/etapi/calendar/years/{year}" + headers = {'Authorization': 'your-token'} + response = requests.get(url, headers=headers) + return response.json() + +year_note = get_year_note("2024") +``` + +### Import/Export + +#### Export Note Subtree + +**GET** `/etapi/notes/{noteId}/export` + +Exports a note subtree as a ZIP file. + +**Query Parameters:** + +* `format`: "html" (default) or "markdown" + +```python +def export_subtree(note_id, output_file, format="html"): + url = f"http://localhost:8080/etapi/notes/{note_id}/export" + headers = {'Authorization': 'your-token'} + params = {'format': format} + + response = requests.get(url, headers=headers, params=params) + + with open(output_file, 'wb') as f: + f.write(response.content) + + return output_file + +# Export entire database +export_subtree("root", "backup.zip") + +# Export specific subtree as markdown +export_subtree("projectNoteId", "project.zip", format="markdown") +``` + +#### Import ZIP + +**POST** `/etapi/notes/{noteId}/import` + +Imports a ZIP file into a note. + +```python +def import_zip(parent_note_id, zip_file_path): + url = f"http://localhost:8080/etapi/notes/{parent_note_id}/import" + headers = {'Authorization': 'your-token'} + + with open(zip_file_path, 'rb') as f: + files = {'file': f} + response = requests.post(url, headers=headers, files=files) + + return response.json() + +# Import backup +imported = import_zip("root", "backup.zip") +print(f"Imported note ID: {imported['note']['noteId']}") +``` + +### Utility Endpoints + +#### Create Note Revision + +**POST** `/etapi/notes/{noteId}/revision` + +Forces creation of a revision for the specified note. + +```sh +curl -X POST http://localhost:8080/etapi/notes/evnnmvHTCgIn/revision \ + -H "Authorization: your-token" +``` + +#### Refresh Note Ordering + +**POST** `/etapi/refresh-note-ordering/{parentNoteId}` + +Updates note ordering in connected clients after changing positions. + +```python +def reorder_children(parent_id, note_positions): + """ + note_positions: dict of {noteId: position} + """ + headers = { + 'Authorization': 'your-token', + 'Content-Type': 'application/json' + } + + # Update each branch position + for note_id, position in note_positions.items(): + # Get the branch ID first + note = requests.get( + f"http://localhost:8080/etapi/notes/{note_id}", + headers=headers + ).json() + + for branch_id in note['parentBranchIds']: + branch = requests.get( + f"http://localhost:8080/etapi/branches/{branch_id}", + headers=headers + ).json() + + if branch['parentNoteId'] == parent_id: + # Update position + requests.patch( + f"http://localhost:8080/etapi/branches/{branch_id}", + headers=headers, + json={'notePosition': position} + ) + + # Refresh ordering + requests.post( + f"http://localhost:8080/etapi/refresh-note-ordering/{parent_id}", + headers=headers + ) + +# Reorder notes +reorder_children("parentId", { + "note1": 10, + "note2": 20, + "note3": 30 +}) +``` + +#### Get App Info + +**GET** `/etapi/app-info` + +Returns information about the Trilium instance. + +```python +def get_app_info(): + url = "http://localhost:8080/etapi/app-info" + headers = {'Authorization': 'your-token'} + response = requests.get(url, headers=headers) + return response.json() + +info = get_app_info() +print(f"Trilium version: {info['appVersion']}") +print(f"Database version: {info['dbVersion']}") +print(f"Data directory: {info['dataDirectory']}") +``` + +#### Create Backup + +**PUT** `/etapi/backup/{backupName}` + +Creates a database backup. + +```sh +curl -X PUT http://localhost:8080/etapi/backup/daily \ + -H "Authorization: your-token" +``` + +This creates a backup file named `backup-daily.db` in the data directory. + +## Common Use Cases + +### 1\. Daily Journal Entry + +```python +from datetime import date +import requests + +class TriliumJournal: + def __init__(self, base_url, token): + self.base_url = base_url + self.headers = {'Authorization': token} + + def create_journal_entry(self, content, tags=[]): + # Get today's day note + today = date.today().strftime('%Y-%m-%d') + day_note_url = f"{self.base_url}/calendar/days/{today}" + day_note = requests.get(day_note_url, headers=self.headers).json() + + # Create entry + entry_data = { + "parentNoteId": day_note['noteId'], + "title": f"Entry - {date.today().strftime('%H:%M')}", + "type": "text", + "content": content + } + + response = requests.post( + f"{self.base_url}/create-note", + headers={**self.headers, 'Content-Type': 'application/json'}, + json=entry_data + ) + + entry = response.json() + + # Add tags + for tag in tags: + self.add_tag(entry['note']['noteId'], tag) + + return entry + + def add_tag(self, note_id, tag_name): + attr_data = { + "noteId": note_id, + "type": "label", + "name": tag_name, + "value": "" + } + + requests.post( + f"{self.base_url}/attributes", + headers={**self.headers, 'Content-Type': 'application/json'}, + json=attr_data + ) + +# Usage +journal = TriliumJournal("http://localhost:8080/etapi", "your-token") +entry = journal.create_journal_entry( + "Today's meeting went well. Key decisions:
Task Management System
" + } + + response = requests.post( + f"{self.base_url}/create-note", + headers={**self.headers, 'Content-Type': 'application/json'}, + json=data + ) + + note_id = response.json()['note']['noteId'] + + # Add taskRoot label + self.add_label(note_id, "taskRoot") + return note_id + + def create_task(self, title, description, priority="medium", due_date=None): + data = { + "parentNoteId": self.task_parent_id, + "title": title, + "type": "text", + "content": f"{description}
" + } + + response = requests.post( + f"{self.base_url}/create-note", + headers={**self.headers, 'Content-Type': 'application/json'}, + json=data + ) + + task = response.json() + task_id = task['note']['noteId'] + + # Add task attributes + self.add_label(task_id, "task") + self.add_label(task_id, "todoStatus", "todo") + self.add_label(task_id, "priority", priority) + + if due_date: + self.add_label(task_id, "dueDate", due_date) + + return task + + def get_tasks(self, status=None): + if status: + search = f"#task #todoStatus={status}" + else: + search = "#task" + + params = { + 'search': search, + 'ancestorNoteId': self.task_parent_id + } + + response = requests.get( + f"{self.base_url}/notes", + headers=self.headers, + params=params + ) + + return response.json()['results'] + + def complete_task(self, task_id): + # Find the todoStatus attribute + note = requests.get( + f"{self.base_url}/notes/{task_id}", + headers=self.headers + ).json() + + for attr in note['attributes']: + if attr['name'] == 'todoStatus': + # Update status + requests.patch( + f"{self.base_url}/attributes/{attr['attributeId']}", + headers={**self.headers, 'Content-Type': 'application/json'}, + json={'value': 'done'} + ) + break + + def add_label(self, note_id, name, value=""): + data = { + "noteId": note_id, + "type": "label", + "name": name, + "value": value + } + + requests.post( + f"{self.base_url}/attributes", + headers={**self.headers, 'Content-Type': 'application/json'}, + json=data + ) + +# Usage +tasks = TriliumTaskManager("http://localhost:8080/etapi", "your-token") + +# Create tasks +task1 = tasks.create_task( + "Review API documentation", + "Check for completeness and accuracy", + priority="high", + due_date="2024-01-20" +) + +task2 = tasks.create_task( + "Update client library", + "Add new ETAPI endpoints", + priority="medium" +) + +# List pending tasks +pending = tasks.get_tasks(status="todo") +for task in pending: + print(f"- {task['title']}") + +# Complete a task +tasks.complete_task(task1['note']['noteId']) +``` + +### 3\. Knowledge Base Builder + +```python +class KnowledgeBase: + def __init__(self, base_url, token): + self.base_url = base_url + self.headers = {'Authorization': token} + + def create_article(self, category, title, content, tags=[]): + # Find or create category + category_id = self.get_or_create_category(category) + + # Create article + data = { + "parentNoteId": category_id, + "title": title, + "type": "text", + "content": content + } + + response = requests.post( + f"{self.base_url}/create-note", + headers={**self.headers, 'Content-Type': 'application/json'}, + json=data + ) + + article = response.json() + article_id = article['note']['noteId'] + + # Add tags + for tag in tags: + self.add_label(article_id, tag) + + # Add article label + self.add_label(article_id, "article") + + return article + + def get_or_create_category(self, name): + # Search for existing category + params = {'search': f'#category #categoryName={name}'} + response = requests.get( + f"{self.base_url}/notes", + headers=self.headers, + params=params + ) + + results = response.json()['results'] + if results: + return results[0]['noteId'] + + # Create new category + data = { + "parentNoteId": "root", + "title": name, + "type": "text", + "content": f"REST APIs are fundamental to modern web development...
+Content
' +}); + +// Search notes +const results = await client.searchNotes('#todo', { + orderBy: 'dateModified', + orderDirection: 'desc', + limit: 10 +}); + +// Add a label +await client.addAttribute(note.note.noteId, 'label', 'important'); +``` + +### Python Client Class + +```python +import requests +from typing import Optional, Dict, List, Any +from datetime import datetime +import json + +class TriliumETAPI: + """Python client for Trilium ETAPI""" + + def __init__(self, base_url: str, token: str): + self.base_url = base_url.rstrip('/') + self.session = requests.Session() + self.session.headers.update({ + 'Authorization': token, + 'Content-Type': 'application/json' + }) + + def _request(self, method: str, endpoint: str, **kwargs) -> Any: + """Make API request with error handling""" + url = f"{self.base_url}{endpoint}" + + try: + response = self.session.request(method, url, **kwargs) + response.raise_for_status() + + if response.status_code == 204: + return None + + return response.json() if response.content else None + + except requests.exceptions.HTTPError as e: + if response.text: + try: + error = response.json() + raise Exception(f"API Error {error.get('code')}: {error.get('message')}") + except json.JSONDecodeError: + raise Exception(f"HTTP {response.status_code}: {response.text}") + raise e + + # Note operations + def create_note( + self, + parent_note_id: str, + title: str, + content: str, + note_type: str = "text", + **kwargs + ) -> Dict: + """Create a new note""" + data = { + "parentNoteId": parent_note_id, + "title": title, + "type": note_type, + "content": content, + **kwargs + } + return self._request('POST', '/create-note', json=data) + + def get_note(self, note_id: str) -> Dict: + """Get note by ID""" + return self._request('GET', f'/notes/{note_id}') + + def update_note(self, note_id: str, updates: Dict) -> Dict: + """Update note properties""" + return self._request('PATCH', f'/notes/{note_id}', json=updates) + + def delete_note(self, note_id: str) -> None: + """Delete a note""" + self._request('DELETE', f'/notes/{note_id}') + + def get_note_content(self, note_id: str) -> str: + """Get note content""" + response = self.session.get(f"{self.base_url}/notes/{note_id}/content") + response.raise_for_status() + return response.text + + def update_note_content(self, note_id: str, content: str) -> None: + """Update note content""" + headers = {'Content-Type': 'text/plain'} + self.session.put( + f"{self.base_url}/notes/{note_id}/content", + data=content, + headers=headers + ).raise_for_status() + + # Search + def search_notes( + self, + query: str, + fast_search: bool = False, + include_archived: bool = False, + ancestor_note_id: Optional[str] = None, + order_by: Optional[str] = None, + order_direction: str = "asc", + limit: Optional[int] = None + ) -> List[Dict]: + """Search for notes""" + params = { + 'search': query, + 'fastSearch': fast_search, + 'includeArchivedNotes': include_archived + } + + if ancestor_note_id: + params['ancestorNoteId'] = ancestor_note_id + if order_by: + params['orderBy'] = order_by + params['orderDirection'] = order_direction + if limit: + params['limit'] = limit + + result = self._request('GET', '/notes', params=params) + return result.get('results', []) + + # Attributes + def add_label( + self, + note_id: str, + name: str, + value: str = "", + inheritable: bool = False + ) -> Dict: + """Add a label to a note""" + data = { + "noteId": note_id, + "type": "label", + "name": name, + "value": value, + "isInheritable": inheritable + } + return self._request('POST', '/attributes', json=data) + + def add_relation( + self, + note_id: str, + name: str, + target_note_id: str, + inheritable: bool = False + ) -> Dict: + """Add a relation to a note""" + data = { + "noteId": note_id, + "type": "relation", + "name": name, + "value": target_note_id, + "isInheritable": inheritable + } + return self._request('POST', '/attributes', json=data) + + def update_attribute(self, attribute_id: str, updates: Dict) -> Dict: + """Update an attribute""" + return self._request('PATCH', f'/attributes/{attribute_id}', json=updates) + + def delete_attribute(self, attribute_id: str) -> None: + """Delete an attribute""" + self._request('DELETE', f'/attributes/{attribute_id}') + + # Branches + def clone_note( + self, + note_id: str, + parent_note_id: str, + prefix: str = "" + ) -> Dict: + """Clone a note to another location""" + data = { + "noteId": note_id, + "parentNoteId": parent_note_id, + "prefix": prefix + } + return self._request('POST', '/branches', json=data) + + def move_note( + self, + note_id: str, + new_parent_id: str + ) -> None: + """Move a note to a new parent""" + # Get current branches + note = self.get_note(note_id) + + # Delete old branches + for branch_id in note['parentBranchIds']: + self._request('DELETE', f'/branches/{branch_id}') + + # Create new branch + self.clone_note(note_id, new_parent_id) + + # Special notes + def get_inbox(self, date: Optional[datetime] = None) -> Dict: + """Get inbox note for a date""" + if date is None: + date = datetime.now() + date_str = date.strftime('%Y-%m-%d') + return self._request('GET', f'/inbox/{date_str}') + + def get_day_note(self, date: Optional[datetime] = None) -> Dict: + """Get day note for a date""" + if date is None: + date = datetime.now() + date_str = date.strftime('%Y-%m-%d') + return self._request('GET', f'/calendar/days/{date_str}') + + # Utility + def get_app_info(self) -> Dict: + """Get application information""" + return self._request('GET', '/app-info') + + def create_backup(self, name: str) -> None: + """Create a backup""" + self._request('PUT', f'/backup/{name}') + + def export_subtree( + self, + note_id: str, + format: str = "html" + ) -> bytes: + """Export note subtree as ZIP""" + params = {'format': format} + response = self.session.get( + f"{self.base_url}/notes/{note_id}/export", + params=params + ) + response.raise_for_status() + return response.content + +# Example usage +if __name__ == "__main__": + # Initialize client + api = TriliumETAPI("http://localhost:8080/etapi", "your-token") + + # Create a note + note = api.create_note( + parent_note_id="root", + title="API Test Note", + content="Created via Python client
" + ) + print(f"Created note: {note['note']['noteId']}") + + # Add labels + api.add_label(note['note']['noteId'], "test") + api.add_label(note['note']['noteId'], "priority", "high") + + # Search + results = api.search_notes("#test", limit=10) + for result in results: + print(f"Found: {result['title']}") + + # Export backup + backup_data = api.export_subtree("root") + with open("backup.zip", "wb") as f: + f.write(backup_data) +``` + +## Rate Limiting and Best Practices + +### Rate Limiting + +ETAPI implements rate limiting for authentication endpoints: + +* **Login endpoint**: Maximum 10 requests per IP per hour +* **Other endpoints**: No specific rate limits, but excessive requests may be throttled + +### Best Practices + +#### 1\. Connection Pooling + +Reuse HTTP connections for better performance: + +```python +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry + +session = requests.Session() +retry = Retry( + total=3, + backoff_factor=0.3, + status_forcelist=[500, 502, 503, 504] +) +adapter = HTTPAdapter(max_retries=retry) +session.mount('http://', adapter) +session.mount('https://', adapter) +``` + +#### 2\. Batch Operations + +When possible, batch multiple operations: + +```python +def batch_create_notes(notes_data): + """Create multiple notes efficiently""" + created_notes = [] + + for data in notes_data: + note = api.create_note(**data) + created_notes.append(note) + + # Add small delay to avoid overwhelming server + time.sleep(0.1) + + return created_notes +``` + +#### 3\. Error Handling + +Implement robust error handling: + +```python +import time +from typing import Callable, Any + +def retry_on_error( + func: Callable, + max_retries: int = 3, + backoff_factor: float = 1.0 +) -> Any: + """Retry function with exponential backoff""" + for attempt in range(max_retries): + try: + return func() + except requests.exceptions.RequestException as e: + if attempt == max_retries - 1: + raise + + wait_time = backoff_factor * (2 ** attempt) + print(f"Request failed, retrying in {wait_time}s...") + time.sleep(wait_time) + +# Usage +note = retry_on_error( + lambda: api.create_note("root", "Title", "Content") +) +``` + +#### 4\. Caching + +Cache frequently accessed data: + +```python +from functools import lru_cache +from datetime import datetime, timedelta + +class CachedTriliumClient(TriliumETAPI): + def __init__(self, base_url: str, token: str): + super().__init__(base_url, token) + self._cache = {} + self._cache_times = {} + + def get_note_cached(self, note_id: str, max_age: int = 300): + """Get note with caching (max_age in seconds)""" + cache_key = f"note:{note_id}" + + if cache_key in self._cache: + cache_time = self._cache_times[cache_key] + if datetime.now() - cache_time < timedelta(seconds=max_age): + return self._cache[cache_key] + + note = self.get_note(note_id) + self._cache[cache_key] = note + self._cache_times[cache_key] = datetime.now() + + return note +``` + +#### 5\. Pagination for Large Results + +Handle large result sets with pagination: + +```python +def search_all_notes(api: TriliumETAPI, query: str, batch_size: int = 100): + """Search with pagination for large result sets""" + all_results = [] + offset = 0 + + while True: + results = api.search_notes( + query, + limit=batch_size, + order_by="dateCreated" + ) + + if not results: + break + + all_results.extend(results) + + if len(results) < batch_size: + break + + # Use the last note's date as reference for next batch + last_date = results[-1]['dateCreated'] + query_with_date = f"{query} dateCreated>{last_date}" + + return all_results +``` + +## Migration from Internal API + +### Key Differences + +| Aspect | Internal API | ETAPI | +| --- | --- | --- | +| **Purpose** | Trilium client communication | External integrations | +| **Authentication** | Session-based | Token-based | +| **Stability** | May change between versions | Stable interface | +| **CSRF Protection** | Required | Not required | +| **WebSocket** | Supported | Not available | +| **Documentation** | Limited | Comprehensive | + +### Migration Steps + +1. **Replace Authentication** + + ```python + # Old (Internal API) + session = requests.Session() + session.post('/api/login', data={'password': 'pass'}) + + # New (ETAPI) + headers = {'Authorization': 'etapi-token'} + ``` +2. **Update Endpoints** + + ```python + # Old + /api/notes/getNoteById/noteId + + # New + /etapi/notes/noteId + ``` +3. **Adjust Request/Response Format** + + ```python + # Old (may vary) + response = session.post('/api/notes/new', json={ + 'parentNoteId': 'root', + 'title': 'Title' + }) + + # New (standardized) + response = requests.post('/etapi/create-note', + headers=headers, + json={ + 'parentNoteId': 'root', + 'title': 'Title', + 'type': 'text', + 'content': '' + } + ) + ``` + +## Error Handling + +### Common Error Codes + +| Status | Code | Description | Resolution | +| --- | --- | --- | --- | +| 400 | BAD\_REQUEST | Invalid request format | Check request body and parameters | +| 401 | UNAUTHORIZED | Invalid or missing token | Verify authentication token | +| 404 | NOTE\_NOT\_FOUND | Note doesn't exist | Check note ID | +| 404 | BRANCH\_NOT\_FOUND | Branch doesn't exist | Verify branch ID | +| 400 | NOTE\_IS\_PROTECTED | Cannot modify protected note | Unlock protected session first | +| 429 | TOO\_MANY\_REQUESTS | Rate limit exceeded | Wait before retrying | +| 500 | INTERNAL\_ERROR | Server error | Report issue, check logs | + +### Error Response Format + +```json +{ + "status": 400, + "code": "VALIDATION_ERROR", + "message": "Note title cannot be empty" +} +``` + +### Handling Errors in Code + +```python +class ETAPIError(Exception): + def __init__(self, status, code, message): + self.status = status + self.code = code + self.message = message + super().__init__(f"{code}: {message}") + +def handle_api_response(response): + if response.status_code >= 400: + try: + error = response.json() + raise ETAPIError( + error.get('status'), + error.get('code'), + error.get('message') + ) + except json.JSONDecodeError: + raise ETAPIError( + response.status_code, + 'UNKNOWN_ERROR', + response.text + ) + + return response.json() if response.content else None + +# Usage +try: + response = requests.get( + 'http://localhost:8080/etapi/notes/invalid', + headers={'Authorization': 'token'} + ) + note = handle_api_response(response) +except ETAPIError as e: + if e.code == 'NOTE_NOT_FOUND': + print("Note doesn't exist") + else: + print(f"API Error: {e.message}") +``` + +## Performance Considerations + +### 1\. Minimize API Calls + +```python +# Bad: Multiple calls +note = api.get_note(note_id) +for child_id in note['childNoteIds']: + child = api.get_note(child_id) # N+1 problem + process(child) + +# Good: Batch processing +note = api.get_note(note_id) +children = api.search_notes( + f"note.parents.noteId={note_id}", + limit=1000 +) +for child in children: + process(child) +``` + +### 2\. Use Appropriate Search Depth + +```python +# Limit search depth for better performance +results = api.search_notes( + "keyword", + ancestor_note_id="root", + ancestor_depth="lt3" # Only search 3 levels deep +) +``` + +### 3\. Content Compression + +Enable gzip compression for large responses: + +```python +session = requests.Session() +session.headers.update({ + 'Authorization': 'token', + 'Accept-Encoding': 'gzip, deflate' +}) +``` + +### 4\. Async Operations + +Use async requests for parallel operations: + +```python +import asyncio +import aiohttp + +class AsyncTriliumClient: + def __init__(self, base_url: str, token: str): + self.base_url = base_url + self.headers = {'Authorization': token} + + async def get_note(self, session, note_id): + url = f"{self.base_url}/notes/{note_id}" + async with session.get(url, headers=self.headers) as response: + return await response.json() + + async def get_multiple_notes(self, note_ids): + async with aiohttp.ClientSession() as session: + tasks = [self.get_note(session, nid) for nid in note_ids] + return await asyncio.gather(*tasks) + +# Usage +client = AsyncTriliumClient("http://localhost:8080/etapi", "token") +notes = asyncio.run(client.get_multiple_notes(['id1', 'id2', 'id3'])) +``` + +### 5\. Database Optimization + +For bulk operations, consider: + +* Creating notes in batches +* Using transactions (via backup/restore) +* Indexing frequently searched attributes + +## Security Considerations + +### Token Management + +* Store tokens securely (environment variables, key vaults) +* Rotate tokens regularly +* Use separate tokens for different applications +* Never commit tokens to version control + +```python +import os +from dotenv import load_dotenv + +load_dotenv() + +# Load token from environment +TOKEN = os.getenv('TRILIUM_ETAPI_TOKEN') +if not TOKEN: + raise ValueError("TRILIUM_ETAPI_TOKEN not set") + +api = TriliumETAPI("http://localhost:8080/etapi", TOKEN) +``` + +### HTTPS Usage + +Always use HTTPS in production: + +```python +# Development +dev_api = TriliumETAPI("http://localhost:8080/etapi", token) + +# Production +prod_api = TriliumETAPI("https://notes.example.com/etapi", token) +``` + +### Input Validation + +Sanitize user input before sending to API: + +```python +import html +import re + +def sanitize_html(content: str) -> str: + """Basic HTML sanitization""" + # Remove script tags + content = re.sub(r'', '', content, flags=re.DOTALL) + # Remove on* attributes + content = re.sub(r'\s*on\w+\s*=\s*["\'][^"\']*["\']', '', content) + return content + +def create_safe_note(title: str, content: str): + safe_title = html.escape(title) + safe_content = sanitize_html(content) + + return api.create_note( + parent_note_id="root", + title=safe_title, + content=safe_content + ) +``` + +## Troubleshooting + +### Connection Issues + +```python +# Test connection +def test_connection(base_url, token): + try: + api = TriliumETAPI(base_url, token) + info = api.get_app_info() + print(f"Connected to Trilium {info['appVersion']}") + return True + except Exception as e: + print(f"Connection failed: {e}") + return False + +# Debug mode +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +### Common Issues and Solutions + +| Issue | Cause | Solution | +| --- | --- | --- | +| 401 Unauthorized | Invalid token | Regenerate token in Trilium Options | +| Connection refused | Server not running | Start Trilium server | +| CORS errors | Cross-origin requests | Configure CORS in Trilium settings | +| Timeout errors | Large operations | Increase timeout, use async | +| 404 Not Found | Wrong endpoint | Check ETAPI prefix in URL | +| Protected note error | Note is encrypted | Enter protected session first | + +## Additional Resources + +* [Trilium GitHub Repository](https://github.com/TriliumNext/Trilium) +* [OpenAPI Specification](#root/euAWtBArCWdw) +* [Trilium Search Documentation](https://triliumnext.github.io/Docs/Wiki/search.html) +* [Community Forum](https://github.com/TriliumNext/Trilium/discussions) \ No newline at end of file diff --git a/docs/Developer Guide/Developer Guide/API Documentation/Internal API Reference.md b/docs/Developer Guide/Developer Guide/API Documentation/Internal API Reference.md new file mode 100644 index 0000000000..a397b113d8 --- /dev/null +++ b/docs/Developer Guide/Developer Guide/API Documentation/Internal API Reference.md @@ -0,0 +1,1926 @@ +# Internal API Reference +## Table of Contents + +1. [Introduction](#introduction) +2. [Authentication and Session Management](#authentication-and-session-management) +3. [Core API Endpoints](#core-api-endpoints) +4. [WebSocket Real-time Updates](#websocket-real-time-updates) +5. [File Operations](#file-operations) +6. [Import/Export Operations](#import-export-operations) +7. [Synchronization API](#synchronization-api) +8. [When to Use Internal vs ETAPI](#when-to-use-internal-vs-etapi) +9. [Security Considerations](#security-considerations) + +## Introduction + +The Internal API is the primary interface used by the Trilium Notes client application to communicate with the server. While powerful and feature-complete, this API is primarily designed for internal use. + +### Important Notice + +**For external integrations, please use [ETAPI](ETAPI%20Complete%20Guide.md) instead.** The Internal API: + +* May change between versions without notice +* Requires session-based authentication with CSRF protection +* Is tightly coupled with the frontend application +* Has limited documentation and stability guarantees + +### Base URL + +``` +http://localhost:8080/api +``` + +### Key Characteristics + +* Session-based authentication with cookies +* CSRF token protection for state-changing operations +* WebSocket support for real-time updates +* Full feature parity with the Trilium UI +* Complex request/response formats optimized for the client + +## Authentication and Session Management + +### Password Login + +**POST** `/api/login` + +Authenticates user with password and creates a session. + +**Request:** + +```javascript +const formData = new URLSearchParams(); +formData.append('password', 'your-password'); + +const response = await fetch('http://localhost:8080/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: formData, + credentials: 'include' // Important for cookie handling +}); +``` + +**Response:** + +```json +{ + "success": true, + "message": "Login successful" +} +``` + +The server sets a session cookie (`trilium.sid`) that must be included in subsequent requests. + +### TOTP Authentication (2FA) + +If 2FA is enabled, include the TOTP token: + +```javascript +formData.append('password', 'your-password'); +formData.append('totpToken', '123456'); +``` + +### Token Authentication + +**POST** `/api/login/token` + +Generate an API token for programmatic access: + +```javascript +const response = await fetch('http://localhost:8080/api/login/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + password: 'your-password', + tokenName: 'My Integration' + }) +}); + +const { authToken } = await response.json(); +// Use this token in Authorization header for future requests +``` + +### Protected Session + +**POST** `/api/login/protected` + +Enter protected session to access encrypted notes: + +```javascript +await fetch('http://localhost:8080/api/login/protected', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + password: 'your-password' + }), + credentials: 'include' +}); +``` + +### Logout + +**POST** `/api/logout` + +```javascript +await fetch('http://localhost:8080/api/logout', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken + }, + credentials: 'include' +}); +``` + +## Core API Endpoints + +### Notes + +#### Get Note + +**GET** `/api/notes/{noteId}` + +```javascript +const response = await fetch('http://localhost:8080/api/notes/root', { + credentials: 'include' +}); + +const note = await response.json(); +``` + +**Response:** + +```json +{ + "noteId": "root", + "title": "Trilium Notes", + "type": "text", + "mime": "text/html", + "isProtected": false, + "isDeleted": false, + "dateCreated": "2024-01-01 00:00:00.000+0000", + "dateModified": "2024-01-15 10:30:00.000+0000", + "utcDateCreated": "2024-01-01 00:00:00.000Z", + "utcDateModified": "2024-01-15 10:30:00.000Z", + "parentBranches": [ + { + "branchId": "root_root", + "parentNoteId": "none", + "prefix": null, + "notePosition": 10 + } + ], + "attributes": [], + "cssClass": "", + "iconClass": "bx bx-folder" +} +``` + +#### Create Note + +**POST** `/api/notes/{parentNoteId}/children` + +```javascript +const response = await fetch('http://localhost:8080/api/notes/root/children', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + title: 'New Note', + type: 'text', + content: 'Note content
', + isProtected: false + }), + credentials: 'include' +}); + +const { note, branch } = await response.json(); +``` + +#### Update Note + +**PUT** `/api/notes/{noteId}` + +```javascript +await fetch(`http://localhost:8080/api/notes/${noteId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + title: 'Updated Title', + type: 'text', + mime: 'text/html' + }), + credentials: 'include' +}); +``` + +#### Delete Note + +**DELETE** `/api/notes/{noteId}` + +```javascript +await fetch(`http://localhost:8080/api/notes/${noteId}`, { + method: 'DELETE', + headers: { + 'X-CSRF-Token': csrfToken + }, + credentials: 'include' +}); +``` + +#### Get Note Content + +**GET** `/api/notes/{noteId}/content` + +Returns the actual content of the note: + +```javascript +const response = await fetch(`http://localhost:8080/api/notes/${noteId}/content`, { + credentials: 'include' +}); + +const content = await response.text(); +``` + +#### Save Note Content + +**PUT** `/api/notes/{noteId}/content` + +```javascript +await fetch(`http://localhost:8080/api/notes/${noteId}/content`, { + method: 'PUT', + headers: { + 'Content-Type': 'text/html', + 'X-CSRF-Token': csrfToken + }, + body: 'Updated content
', + credentials: 'include' +}); +``` + +### Tree Operations + +#### Get Branch + +**GET** `/api/branches/{branchId}` + +```javascript +const branch = await fetch(`http://localhost:8080/api/branches/${branchId}`, { + credentials: 'include' +}).then(r => r.json()); +``` + +#### Move Note + +**PUT** `/api/branches/{branchId}/move` + +```javascript +await fetch(`http://localhost:8080/api/branches/${branchId}/move`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + parentNoteId: 'newParentId', + beforeNoteId: 'siblingNoteId' // optional, for positioning + }), + credentials: 'include' +}); +``` + +#### Clone Note + +**POST** `/api/notes/{noteId}/clone` + +```javascript +const response = await fetch(`http://localhost:8080/api/notes/${noteId}/clone`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + parentNoteId: 'targetParentId', + prefix: 'Copy of ' + }), + credentials: 'include' +}); +``` + +#### Sort Child Notes + +**PUT** `/api/notes/{noteId}/sort-children` + +```javascript +await fetch(`http://localhost:8080/api/notes/${noteId}/sort-children`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + sortBy: 'title', // or 'dateCreated', 'dateModified' + reverse: false + }), + credentials: 'include' +}); +``` + +### Attributes + +#### Create Attribute + +**POST** `/api/notes/{noteId}/attributes` + +```javascript +const response = await fetch(`http://localhost:8080/api/notes/${noteId}/attributes`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + type: 'label', + name: 'todo', + value: '', + isInheritable: false + }), + credentials: 'include' +}); +``` + +#### Update Attribute + +**PUT** `/api/attributes/{attributeId}` + +```javascript +await fetch(`http://localhost:8080/api/attributes/${attributeId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + value: 'updated value' + }), + credentials: 'include' +}); +``` + +#### Delete Attribute + +**DELETE** `/api/attributes/{attributeId}` + +```javascript +await fetch(`http://localhost:8080/api/attributes/${attributeId}`, { + method: 'DELETE', + headers: { + 'X-CSRF-Token': csrfToken + }, + credentials: 'include' +}); +``` + +### Search + +#### Search Notes + +**GET** `/api/search` + +```javascript +const params = new URLSearchParams({ + query: '#todo OR #task', + fastSearch: 'false', + includeArchivedNotes: 'false', + ancestorNoteId: 'root', + orderBy: 'relevancy', + orderDirection: 'desc', + limit: '50' +}); + +const response = await fetch(`http://localhost:8080/api/search?${params}`, { + credentials: 'include' +}); + +const { results } = await response.json(); +``` + +#### Search Note Map + +**GET** `/api/search-note-map` + +Returns hierarchical structure of search results: + +```javascript +const params = new URLSearchParams({ + query: 'project', + maxDepth: '3' +}); + +const noteMap = await fetch(`http://localhost:8080/api/search-note-map?${params}`, { + credentials: 'include' +}).then(r => r.json()); +``` + +### Revisions + +#### Get Note Revisions + +**GET** `/api/notes/{noteId}/revisions` + +```javascript +const revisions = await fetch(`http://localhost:8080/api/notes/${noteId}/revisions`, { + credentials: 'include' +}).then(r => r.json()); +``` + +#### Get Revision Content + +**GET** `/api/revisions/{revisionId}/content` + +```javascript +const content = await fetch(`http://localhost:8080/api/revisions/${revisionId}/content`, { + credentials: 'include' +}).then(r => r.text()); +``` + +#### Restore Revision + +**POST** `/api/revisions/{revisionId}/restore` + +```javascript +await fetch(`http://localhost:8080/api/revisions/${revisionId}/restore`, { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken + }, + credentials: 'include' +}); +``` + +#### Delete Revision + +**DELETE** `/api/revisions/{revisionId}` + +```javascript +await fetch(`http://localhost:8080/api/revisions/${revisionId}`, { + method: 'DELETE', + headers: { + 'X-CSRF-Token': csrfToken + }, + credentials: 'include' +}); +``` + +## WebSocket Real-time Updates + +The Internal API provides WebSocket connections for real-time synchronization and updates. + +### Connection Setup + +```javascript +class TriliumWebSocket { + constructor() { + this.ws = null; + this.reconnectInterval = 5000; + this.shouldReconnect = true; + } + + connect() { + // WebSocket URL same as base URL but with ws:// protocol + const wsUrl = 'ws://localhost:8080'; + + this.ws = new WebSocket(wsUrl); + + this.ws.onopen = () => { + console.log('WebSocket connected'); + this.sendPing(); + }; + + this.ws.onmessage = (event) => { + const message = JSON.parse(event.data); + this.handleMessage(message); + }; + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error); + }; + + this.ws.onclose = () => { + console.log('WebSocket disconnected'); + if (this.shouldReconnect) { + setTimeout(() => this.connect(), this.reconnectInterval); + } + }; + } + + handleMessage(message) { + switch (message.type) { + case 'sync': + this.handleSync(message.data); + break; + case 'entity-changes': + this.handleEntityChanges(message.data); + break; + case 'refresh-tree': + this.refreshTree(); + break; + case 'create-note': + this.handleNoteCreated(message.data); + break; + case 'update-note': + this.handleNoteUpdated(message.data); + break; + case 'delete-note': + this.handleNoteDeleted(message.data); + break; + default: + console.log('Unknown message type:', message.type); + } + } + + sendPing() { + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify({ type: 'ping' })); + setTimeout(() => this.sendPing(), 30000); // Ping every 30 seconds + } + } + + send(type, data) { + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify({ type, data })); + } + } + + handleSync(data) { + // Handle synchronization data + console.log('Sync data received:', data); + } + + handleEntityChanges(changes) { + // Handle entity change notifications + changes.forEach(change => { + console.log(`Entity ${change.entityName} ${change.entityId} changed`); + }); + } + + refreshTree() { + // Refresh the note tree UI + console.log('Tree refresh requested'); + } + + handleNoteCreated(note) { + console.log('Note created:', note); + } + + handleNoteUpdated(note) { + console.log('Note updated:', note); + } + + handleNoteDeleted(noteId) { + console.log('Note deleted:', noteId); + } + + disconnect() { + this.shouldReconnect = false; + if (this.ws) { + this.ws.close(); + } + } +} + +// Usage +const ws = new TriliumWebSocket(); +ws.connect(); + +// Send custom message +ws.send('log-info', { info: 'Client started' }); + +// Clean up on page unload +window.addEventListener('beforeunload', () => { + ws.disconnect(); +}); +``` + +### Message Types + +#### Incoming Messages + +| Type | Description | Data Format | +| --- | --- | --- | +| `sync` | Synchronization data | `{ entityChanges: [], lastSyncedPush: number }` | +| `entity-changes` | Entity modifications | `[{ entityName, entityId, action }]` | +| `refresh-tree` | Tree structure changed | None | +| `create-note` | Note created | Note object | +| `update-note` | Note updated | Note object | +| `delete-note` | Note deleted | `{ noteId }` | +| `frontend-script` | Execute frontend script | `{ script, params }` | + +#### Outgoing Messages + +| Type | Description | Data Format | +| --- | --- | --- | +| `ping` | Keep connection alive | None | +| `log-error` | Log client error | `{ error, stack }` | +| `log-info` | Log client info | `{ info }` | + +### Real-time Collaboration Example + +```javascript +class CollaborativeEditor { + constructor(noteId) { + this.noteId = noteId; + this.ws = new TriliumWebSocket(); + this.content = ''; + this.lastSaved = ''; + + this.ws.handleNoteUpdated = (note) => { + if (note.noteId === this.noteId) { + this.handleRemoteUpdate(note); + } + }; + } + + async loadNote() { + const response = await fetch(`/api/notes/${this.noteId}/content`, { + credentials: 'include' + }); + this.content = await response.text(); + this.lastSaved = this.content; + } + + handleRemoteUpdate(note) { + // Check if the update is from another client + if (this.content !== this.lastSaved) { + // Show conflict resolution UI + this.showConflictDialog(note); + } else { + // Apply remote changes + this.loadNote(); + } + } + + async saveContent(content) { + this.content = content; + + await fetch(`/api/notes/${this.noteId}/content`, { + method: 'PUT', + headers: { + 'Content-Type': 'text/html', + 'X-CSRF-Token': csrfToken + }, + body: content, + credentials: 'include' + }); + + this.lastSaved = content; + } + + showConflictDialog(remoteNote) { + // Implementation of conflict resolution UI + console.log('Conflict detected with remote changes'); + } +} +``` + +## File Operations + +### Upload File + +**POST** `/api/notes/{noteId}/attachments/upload` + +```javascript +const formData = new FormData(); +formData.append('file', fileInput.files[0]); + +const response = await fetch(`/api/notes/${noteId}/attachments/upload`, { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken + }, + body: formData, + credentials: 'include' +}); + +const attachment = await response.json(); +``` + +### Download Attachment + +**GET** `/api/attachments/{attachmentId}/download` + +```javascript +const response = await fetch(`/api/attachments/${attachmentId}/download`, { + credentials: 'include' +}); + +const blob = await response.blob(); +const url = URL.createObjectURL(blob); +const a = document.createElement('a'); +a.href = url; +a.download = 'attachment.pdf'; +a.click(); +``` + +### Upload Image + +**POST** `/api/images/upload` + +```javascript +const formData = new FormData(); +formData.append('image', imageFile); +formData.append('noteId', noteId); + +const response = await fetch('/api/images/upload', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken + }, + body: formData, + credentials: 'include' +}); + +const { url, noteId: imageNoteId } = await response.json(); +``` + +## Import/Export Operations + +### Import ZIP + +**POST** `/api/import` + +```javascript +const formData = new FormData(); +formData.append('file', zipFile); +formData.append('parentNoteId', 'root'); + +const response = await fetch('/api/import', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken + }, + body: formData, + credentials: 'include' +}); + +const result = await response.json(); +``` + +### Export Subtree + +**GET** `/api/notes/{noteId}/export` + +```javascript +const params = new URLSearchParams({ + format: 'html', // or 'markdown' + exportRevisions: 'true' +}); + +const response = await fetch(`/api/notes/${noteId}/export?${params}`, { + credentials: 'include' +}); + +const blob = await response.blob(); +const url = URL.createObjectURL(blob); +const a = document.createElement('a'); +a.href = url; +a.download = 'export.zip'; +a.click(); +``` + +### Import Markdown + +**POST** `/api/import/markdown` + +```javascript +const response = await fetch('/api/import/markdown', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + parentNoteId: 'root', + content: '# Markdown Content\n\nParagraph text...', + title: 'Imported from Markdown' + }), + credentials: 'include' +}); +``` + +### Export as PDF + +**GET** `/api/notes/{noteId}/export/pdf` + +```javascript +const response = await fetch(`/api/notes/${noteId}/export/pdf`, { + credentials: 'include' +}); + +const blob = await response.blob(); +const url = URL.createObjectURL(blob); +window.open(url, '_blank'); +``` + +## Synchronization API + +### Get Sync Status + +**GET** `/api/sync/status` + +```javascript +const status = await fetch('/api/sync/status', { + credentials: 'include' +}).then(r => r.json()); + +console.log('Sync enabled:', status.syncEnabled); +console.log('Last sync:', status.lastSyncedPush); +``` + +### Force Sync + +**POST** `/api/sync/now` + +```javascript +await fetch('/api/sync/now', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken + }, + credentials: 'include' +}); +``` + +### Get Sync Log + +**GET** `/api/sync/log` + +```javascript +const log = await fetch('/api/sync/log', { + credentials: 'include' +}).then(r => r.json()); + +log.forEach(entry => { + console.log(`${entry.date}: ${entry.message}`); +}); +``` + +## Script Execution + +### Execute Script + +**POST** `/api/script/run` + +```javascript +const response = await fetch('/api/script/run', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + script: ` + const note = await api.getNote('root'); + return { title: note.title, children: note.children.length }; + `, + params: {} + }), + credentials: 'include' +}); + +const result = await response.json(); +``` + +### Execute Note Script + +**POST** `/api/notes/{noteId}/run` + +Run a script note: + +```javascript +const response = await fetch(`/api/notes/${scriptNoteId}/run`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + params: { + targetNoteId: 'someNoteId' + } + }), + credentials: 'include' +}); + +const result = await response.json(); +``` + +## Special Features + +### Calendar API + +#### Get Day Note + +**GET** `/api/calendar/days/{date}` + +```javascript +const date = '2024-01-15'; +const dayNote = await fetch(`/api/calendar/days/${date}`, { + credentials: 'include' +}).then(r => r.json()); +``` + +#### Get Week Note + +**GET** `/api/calendar/weeks/{date}` + +```javascript +const weekNote = await fetch(`/api/calendar/weeks/2024-01-15`, { + credentials: 'include' +}).then(r => r.json()); +``` + +#### Get Month Note + +**GET** `/api/calendar/months/{month}` + +```javascript +const monthNote = await fetch(`/api/calendar/months/2024-01`, { + credentials: 'include' +}).then(r => r.json()); +``` + +### Inbox Note + +**GET** `/api/inbox/{date}` + +```javascript +const inboxNote = await fetch(`/api/inbox/2024-01-15`, { + credentials: 'include' +}).then(r => r.json()); +``` + +### Note Map + +**GET** `/api/notes/{noteId}/map` + +Get visual map data for a note: + +```javascript +const mapData = await fetch(`/api/notes/${noteId}/map`, { + credentials: 'include' +}).then(r => r.json()); + +// Returns nodes and links for visualization +console.log('Nodes:', mapData.nodes); +console.log('Links:', mapData.links); +``` + +### Similar Notes + +**GET** `/api/notes/{noteId}/similar` + +Find notes similar to the given note: + +```javascript +const similarNotes = await fetch(`/api/notes/${noteId}/similar`, { + credentials: 'include' +}).then(r => r.json()); +``` + +## Options and Configuration + +### Get All Options + +**GET** `/api/options` + +```javascript +const options = await fetch('/api/options', { + credentials: 'include' +}).then(r => r.json()); +``` + +### Update Option + +**PUT** `/api/options/{optionName}` + +```javascript +await fetch(`/api/options/theme`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + value: 'dark' + }), + credentials: 'include' +}); +``` + +### Get User Preferences + +**GET** `/api/options/user` + +```javascript +const preferences = await fetch('/api/options/user', { + credentials: 'include' +}).then(r => r.json()); +``` + +## Database Operations + +### Backup Database + +**POST** `/api/database/backup` + +```javascript +const response = await fetch('/api/database/backup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + backupName: 'manual-backup' + }), + credentials: 'include' +}); + +const { backupFile } = await response.json(); +``` + +### Vacuum Database + +**POST** `/api/database/vacuum` + +```javascript +await fetch('/api/database/vacuum', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken + }, + credentials: 'include' +}); +``` + +### Get Database Info + +**GET** `/api/database/info` + +```javascript +const info = await fetch('/api/database/info', { + credentials: 'include' +}).then(r => r.json()); + +console.log('Database size:', info.size); +console.log('Note count:', info.noteCount); +console.log('Revision count:', info.revisionCount); +``` + +## When to Use Internal vs ETAPI + +### Use Internal API When: + +* Building custom Trilium clients +* Needing WebSocket real-time updates +* Requiring full feature parity with the UI +* Working within the Trilium frontend environment +* Accessing advanced features not available in ETAPI + +### Use ETAPI When: + +* Building external integrations +* Creating automation scripts +* Developing third-party applications +* Needing stable, documented API +* Working with different programming languages + +### Feature Comparison + +| Feature | Internal API | ETAPI | +| --- | --- | --- | +| **Authentication** | Session/Cookie | Token | +| **CSRF Protection** | Required | Not needed | +| **WebSocket** | Yes | No | +| **Stability** | May change | Stable | +| **Documentation** | Limited | Comprehensive | +| **Real-time updates** | Yes | No | +| **File uploads** | Complex | Simple | +| **Scripting** | Full support | Limited | +| **Synchronization** | Yes | No | + +## Security Considerations + +### CSRF Protection + +All state-changing operations require a CSRF token: + +```javascript +// Get CSRF token from meta tag or API +async function getCsrfToken() { + const response = await fetch('/api/csrf-token', { + credentials: 'include' + }); + const { token } = await response.json(); + return token; +} + +// Use in requests +const csrfToken = await getCsrfToken(); + +await fetch('/api/notes', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify(data), + credentials: 'include' +}); +``` + +### Session Management + +```javascript +class TriliumSession { + constructor() { + this.isAuthenticated = false; + this.csrfToken = null; + } + + async login(password) { + const formData = new URLSearchParams(); + formData.append('password', password); + + const response = await fetch('/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: formData, + credentials: 'include' + }); + + if (response.ok) { + this.isAuthenticated = true; + this.csrfToken = await this.getCsrfToken(); + return true; + } + + return false; + } + + async getCsrfToken() { + const response = await fetch('/api/csrf-token', { + credentials: 'include' + }); + const { token } = await response.json(); + return token; + } + + async request(url, options = {}) { + if (!this.isAuthenticated) { + throw new Error('Not authenticated'); + } + + const headers = { + ...options.headers + }; + + if (options.method && options.method !== 'GET') { + headers['X-CSRF-Token'] = this.csrfToken; + } + + return fetch(url, { + ...options, + headers, + credentials: 'include' + }); + } + + async logout() { + await this.request('/api/logout', { method: 'POST' }); + this.isAuthenticated = false; + this.csrfToken = null; + } +} + +// Usage +const session = new TriliumSession(); +await session.login('password'); + +// Make authenticated requests +const notes = await session.request('/api/notes/root').then(r => r.json()); + +// Create note with CSRF protection +await session.request('/api/notes/root/children', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'New Note', type: 'text' }) +}); + +await session.logout(); +``` + +### Protected Notes + +Handle encrypted notes properly: + +```javascript +class ProtectedNoteHandler { + constructor(session) { + this.session = session; + this.protectedSessionTimeout = null; + } + + async enterProtectedSession(password) { + const response = await this.session.request('/api/login/protected', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }) + }); + + if (response.ok) { + // Protected session expires after inactivity + this.resetProtectedSessionTimeout(); + return true; + } + + return false; + } + + resetProtectedSessionTimeout() { + if (this.protectedSessionTimeout) { + clearTimeout(this.protectedSessionTimeout); + } + + // Assume 5 minute timeout + this.protectedSessionTimeout = setTimeout(() => { + console.log('Protected session expired'); + this.onProtectedSessionExpired(); + }, 5 * 60 * 1000); + } + + async accessProtectedNote(noteId) { + try { + const note = await this.session.request(`/api/notes/${noteId}`) + .then(r => r.json()); + + if (note.isProtected) { + // Reset timeout on successful access + this.resetProtectedSessionTimeout(); + } + + return note; + } catch (error) { + if (error.message.includes('Protected session required')) { + // Prompt for password + const password = await this.promptForPassword(); + if (await this.enterProtectedSession(password)) { + return this.accessProtectedNote(noteId); + } + } + throw error; + } + } + + async promptForPassword() { + // Implementation depends on UI framework + return prompt('Enter password for protected notes:'); + } + + onProtectedSessionExpired() { + // Handle expiration (e.g., show notification, lock UI) + console.log('Please re-enter password to access protected notes'); + } +} +``` + +## Error Handling + +### Common Error Responses + +```javascript +// 401 Unauthorized +{ + "status": 401, + "message": "Authentication required" +} + +// 403 Forbidden +{ + "status": 403, + "message": "CSRF token validation failed" +} + +// 404 Not Found +{ + "status": 404, + "message": "Note 'invalidId' not found" +} + +// 400 Bad Request +{ + "status": 400, + "message": "Invalid note type: 'invalid'" +} + +// 500 Internal Server Error +{ + "status": 500, + "message": "Database error", + "stack": "..." // Only in development +} +``` + +### Error Handler Implementation + +```javascript +class APIErrorHandler { + async handleResponse(response) { + if (!response.ok) { + const error = await this.parseError(response); + + switch (response.status) { + case 401: + this.handleAuthError(error); + break; + case 403: + this.handleForbiddenError(error); + break; + case 404: + this.handleNotFoundError(error); + break; + case 400: + this.handleBadRequestError(error); + break; + case 500: + this.handleServerError(error); + break; + default: + this.handleGenericError(error); + } + + throw error; + } + + return response; + } + + async parseError(response) { + try { + const errorData = await response.json(); + return new APIError( + response.status, + errorData.message || response.statusText, + errorData + ); + } catch { + return new APIError( + response.status, + response.statusText + ); + } + } + + handleAuthError(error) { + console.error('Authentication required'); + // Redirect to login + window.location.href = '/login'; + } + + handleForbiddenError(error) { + if (error.message.includes('CSRF')) { + console.error('CSRF token invalid, refreshing...'); + // Refresh CSRF token + this.refreshCsrfToken(); + } else { + console.error('Access forbidden:', error.message); + } + } + + handleNotFoundError(error) { + console.error('Resource not found:', error.message); + } + + handleBadRequestError(error) { + console.error('Bad request:', error.message); + } + + handleServerError(error) { + console.error('Server error:', error.message); + // Show user-friendly error message + this.showErrorNotification('An error occurred. Please try again later.'); + } + + handleGenericError(error) { + console.error('API error:', error); + } + + showErrorNotification(message) { + // Implementation depends on UI framework + alert(message); + } +} + +class APIError extends Error { + constructor(status, message, data = {}) { + super(message); + this.status = status; + this.data = data; + this.name = 'APIError'; + } +} +``` + +## Performance Optimization + +### Request Batching + +```javascript +class BatchedAPIClient { + constructor() { + this.batchQueue = []; + this.batchTimeout = null; + this.batchDelay = 50; // ms + } + + async batchRequest(request) { + return new Promise((resolve, reject) => { + this.batchQueue.push({ request, resolve, reject }); + + if (!this.batchTimeout) { + this.batchTimeout = setTimeout(() => { + this.processBatch(); + }, this.batchDelay); + } + }); + } + + async processBatch() { + const batch = this.batchQueue.splice(0); + this.batchTimeout = null; + + if (batch.length === 0) return; + + try { + const response = await fetch('/api/batch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({ + requests: batch.map(b => b.request) + }), + credentials: 'include' + }); + + const results = await response.json(); + + batch.forEach((item, index) => { + if (results[index].error) { + item.reject(new Error(results[index].error)); + } else { + item.resolve(results[index].data); + } + }); + } catch (error) { + batch.forEach(item => item.reject(error)); + } + } + + async getNote(noteId) { + return this.batchRequest({ + method: 'GET', + url: `/api/notes/${noteId}` + }); + } + + async getAttribute(attributeId) { + return this.batchRequest({ + method: 'GET', + url: `/api/attributes/${attributeId}` + }); + } +} + +// Usage +const client = new BatchedAPIClient(); + +// These requests will be batched +const [note1, note2, note3] = await Promise.all([ + client.getNote('noteId1'), + client.getNote('noteId2'), + client.getNote('noteId3') +]); +``` + +### Caching Strategy + +```javascript +class CachedAPIClient { + constructor() { + this.cache = new Map(); + this.cacheExpiry = new Map(); + this.defaultTTL = 5 * 60 * 1000; // 5 minutes + } + + getCacheKey(method, url, params = {}) { + return `${method}:${url}:${JSON.stringify(params)}`; + } + + isExpired(key) { + const expiry = this.cacheExpiry.get(key); + return !expiry || Date.now() > expiry; + } + + async cachedRequest(method, url, options = {}, ttl = this.defaultTTL) { + const key = this.getCacheKey(method, url, options.params); + + if (method === 'GET' && this.cache.has(key) && !this.isExpired(key)) { + return this.cache.get(key); + } + + const response = await fetch(url, { + method, + ...options, + credentials: 'include' + }); + + const data = await response.json(); + + if (method === 'GET') { + this.cache.set(key, data); + this.cacheExpiry.set(key, Date.now() + ttl); + } + + return data; + } + + invalidate(pattern) { + for (const key of this.cache.keys()) { + if (key.includes(pattern)) { + this.cache.delete(key); + this.cacheExpiry.delete(key); + } + } + } + + async getNote(noteId) { + return this.cachedRequest('GET', `/api/notes/${noteId}`); + } + + async updateNote(noteId, data) { + const result = await fetch(`/api/notes/${noteId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify(data), + credentials: 'include' + }).then(r => r.json()); + + // Invalidate cache for this note + this.invalidate(`/api/notes/${noteId}`); + + return result; + } +} +``` + +## Advanced Examples + +### Building a Note Explorer + +```javascript +class NoteExplorer { + constructor() { + this.currentNote = null; + this.history = []; + this.historyIndex = -1; + } + + async navigateToNote(noteId) { + // Add to history + if (this.historyIndex < this.history.length - 1) { + this.history = this.history.slice(0, this.historyIndex + 1); + } + this.history.push(noteId); + this.historyIndex++; + + // Load note + this.currentNote = await this.loadNoteWithChildren(noteId); + this.render(); + } + + async loadNoteWithChildren(noteId) { + const [note, children] = await Promise.all([ + fetch(`/api/notes/${noteId}`, { credentials: 'include' }) + .then(r => r.json()), + fetch(`/api/notes/${noteId}/children`, { credentials: 'include' }) + .then(r => r.json()) + ]); + + return { ...note, children }; + } + + canGoBack() { + return this.historyIndex > 0; + } + + canGoForward() { + return this.historyIndex < this.history.length - 1; + } + + async goBack() { + if (this.canGoBack()) { + this.historyIndex--; + const noteId = this.history[this.historyIndex]; + this.currentNote = await this.loadNoteWithChildren(noteId); + this.render(); + } + } + + async goForward() { + if (this.canGoForward()) { + this.historyIndex++; + const noteId = this.history[this.historyIndex]; + this.currentNote = await this.loadNoteWithChildren(noteId); + this.render(); + } + } + + async searchInSubtree(query) { + const params = new URLSearchParams({ + query: query, + ancestorNoteId: this.currentNote.noteId, + includeArchivedNotes: 'false' + }); + + const response = await fetch(`/api/search?${params}`, { + credentials: 'include' + }); + + return response.json(); + } + + async createChildNote(title, content, type = 'text') { + const response = await fetch(`/api/notes/${this.currentNote.noteId}/children`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': await getCsrfToken() + }, + body: JSON.stringify({ title, content, type }), + credentials: 'include' + }); + + const result = await response.json(); + + // Refresh current note to show new child + this.currentNote = await this.loadNoteWithChildren(this.currentNote.noteId); + this.render(); + + return result; + } + + render() { + // Render UI - implementation depends on framework + console.log('Current note:', this.currentNote.title); + console.log('Children:', this.currentNote.children.map(c => c.title)); + } +} + +// Usage +const explorer = new NoteExplorer(); +await explorer.navigateToNote('root'); +await explorer.createChildNote('New Child', 'Content
'); +const searchResults = await explorer.searchInSubtree('keyword'); +``` + +### Building a Task Management System + +```javascript +class TaskManager { + constructor() { + this.taskRootId = null; + this.csrfToken = null; + } + + async initialize() { + this.csrfToken = await getCsrfToken(); + this.taskRootId = await this.getOrCreateTaskRoot(); + } + + async getOrCreateTaskRoot() { + // Search for existing task root + const searchParams = new URLSearchParams({ query: '#taskRoot' }); + const searchResponse = await fetch(`/api/search?${searchParams}`, { + credentials: 'include' + }); + const { results } = await searchResponse.json(); + + if (results.length > 0) { + return results[0].noteId; + } + + // Create task root + const response = await fetch('/api/notes/root/children', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': this.csrfToken + }, + body: JSON.stringify({ + title: 'Tasks', + type: 'text', + content: '${description}
` + }), + credentials: 'include' + }); + + const { note } = await response.json(); + + // Add task metadata + await Promise.all([ + this.addLabel(note.noteId, 'task'), + this.addLabel(note.noteId, 'status', 'todo'), + this.addLabel(note.noteId, 'priority', priority), + dueDate ? this.addLabel(note.noteId, 'dueDate', dueDate) : null + ].filter(Boolean)); + + return note; + } + + async addLabel(noteId, name, value = '') { + await fetch(`/api/notes/${noteId}/attributes`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': this.csrfToken + }, + body: JSON.stringify({ + type: 'label', + name, + value, + isInheritable: false + }), + credentials: 'include' + }); + } + + async getTasks(status = null, priority = null) { + let query = '#task'; + if (status) query += ` #status=${status}`; + if (priority) query += ` #priority=${priority}`; + + const params = new URLSearchParams({ + query, + ancestorNoteId: this.taskRootId, + orderBy: 'dateModified', + orderDirection: 'desc' + }); + + const response = await fetch(`/api/search?${params}`, { + credentials: 'include' + }); + + const { results } = await response.json(); + return results; + } + + async updateTaskStatus(noteId, newStatus) { + // Get task attributes + const note = await fetch(`/api/notes/${noteId}`, { + credentials: 'include' + }).then(r => r.json()); + + // Find status attribute + const statusAttr = note.attributes.find(a => a.name === 'status'); + + if (statusAttr) { + // Update existing status + await fetch(`/api/attributes/${statusAttr.attributeId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': this.csrfToken + }, + body: JSON.stringify({ value: newStatus }), + credentials: 'include' + }); + } else { + // Add status attribute + await this.addLabel(noteId, 'status', newStatus); + } + + // Add completion timestamp if marking as done + if (newStatus === 'done') { + const timestamp = new Date().toISOString(); + await this.addLabel(noteId, 'completedAt', timestamp); + } + } + + async getTaskStats() { + const [todoTasks, inProgressTasks, doneTasks] = await Promise.all([ + this.getTasks('todo'), + this.getTasks('in-progress'), + this.getTasks('done') + ]); + + return { + todo: todoTasks.length, + inProgress: inProgressTasks.length, + done: doneTasks.length, + total: todoTasks.length + inProgressTasks.length + doneTasks.length + }; + } +} + +// Usage +const taskManager = new TaskManager(); +await taskManager.initialize(); + +// Create tasks +const task1 = await taskManager.createTask( + 'Review Documentation', + 'Review and update API documentation', + 'high', + '2024-01-20' +); + +const task2 = await taskManager.createTask( + 'Fix Bug #123', + 'Investigate and fix the reported issue', + 'medium' +); + +// Get tasks +const todoTasks = await taskManager.getTasks('todo'); +console.log('Todo tasks:', todoTasks); + +// Update task status +await taskManager.updateTaskStatus(task1.noteId, 'in-progress'); + +// Get statistics +const stats = await taskManager.getTaskStats(); +console.log('Task statistics:', stats); +``` + +## Conclusion + +The Internal API provides complete access to Trilium's functionality but should be used with caution due to its complexity and potential for changes. For most external integrations, [ETAPI](ETAPI%20Complete%20Guide.md) is the recommended choice due to its stability and comprehensive documentation. + +Key takeaways: + +* Always include CSRF tokens for state-changing operations +* Handle session management carefully +* Use WebSocket for real-time updates +* Implement proper error handling +* Consider using ETAPI for external integrations +* Cache responses when appropriate for better performance + +For additional information, refer to: + +* [ETAPI Complete Guide](ETAPI%20Complete%20Guide.md) +* [Script API Cookbook](Script%20API%20Cookbook.md) +* [WebSocket API Documentation](WebSocket%20API.md) \ No newline at end of file diff --git a/docs/Developer Guide/Developer Guide/API Documentation/Script API Cookbook.md b/docs/Developer Guide/Developer Guide/API Documentation/Script API Cookbook.md new file mode 100644 index 0000000000..0338444793 --- /dev/null +++ b/docs/Developer Guide/Developer Guide/API Documentation/Script API Cookbook.md @@ -0,0 +1,1845 @@ +# Script API Cookbook +## Table of Contents + +1. [Introduction](#introduction) +2. [Backend Script Recipes](#backend-script-recipes) +3. [Frontend Script Recipes](#frontend-script-recipes) +4. [Common Patterns](#common-patterns) +5. [Note Manipulation](#note-manipulation) +6. [Attribute Operations](#attribute-operations) +7. [Search and Filtering](#search-and-filtering) +8. [Automation Examples](#automation-examples) +9. [Integration with External Services](#integration-with-external-services) +10. [Custom Widgets](#custom-widgets) +11. [Event Handling](#event-handling) +12. [Best Practices](#best-practices) + +## Introduction + +Trilium's Script API provides powerful automation capabilities through JavaScript code that runs either on the backend (Node.js) or frontend (browser). This cookbook contains practical recipes and patterns for common scripting tasks. + +### Script Types + +| Type | Environment | Access | Use Cases | +| --- | --- | --- | --- | +| **Backend Script** | Node.js | Full database, file system, network | Automation, data processing, integrations | +| **Frontend Script** | Browser | UI manipulation, user interaction | Custom widgets, UI enhancements | +| **Custom Widget** | Browser | Widget lifecycle, note context | Interactive components, visualizations | + +### Basic Script Structure + +**Backend Script:** + +```javascript +// Access to api object is automatic +const note = await api.getNoteWithLabel('todoList'); +const children = await note.getChildNotes(); + +// Return value becomes script output +return { + noteTitle: note.title, + childCount: children.length +}; +``` + +**Frontend Script:** + +```javascript +// Access to api object is automatic +api.showMessage('Script executed!'); + +// Manipulate UI +const $button = $('