From 7c7797d35aa4f319fac8402a810a24f1f0cff7ba Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Thu, 25 Dec 2025 15:04:50 -0800 Subject: [PATCH 1/9] fix(share/prev_next): Prevent crashing if candide page is null When a note is not visible, attempting to export it ends up crashing the server with this error: ``` TypeError: ejs:193 191| 192| <% if (hasTree) { %> >> 193| <%- include("prev_next", { note: note, subRoot: subRoot }) %> 194| <% } %> 195| 196| ejs:1 >> 1| <% 2| // TODO: code cleanup + putting this behind a toggle/attribute 3| const previousNote = (() => { 4| // If we are at the subRoot, there is no previous Cannot read properties of undefined (reading 'hasVisibleChildren') at eval (eval at compile (/usr/src/app/main.cjs:553:203), :27:26) at eval (eval at compile (/usr/src/app/main.cjs:553:203), :34:7) at d (/usr/src/app/main.cjs:557:265) at g (/usr/src/app/main.cjs:557:251) at eval (eval at compile (/usr/src/app/main.cjs:553:203), :293:17) at d (/usr/src/app/main.cjs:557:265) at as.render (/usr/src/app/main.cjs:532:458) at Omr (/usr/src/app/main.cjs:581:109552) at Rmr (/usr/src/app/main.cjs:581:107637) at $W.prepareContent (/usr/src/app/main.cjs:653:28) { path: '' ``` fixes #8002 fixes #8162 --- packages/share-theme/src/templates/prev_next.ejs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/share-theme/src/templates/prev_next.ejs b/packages/share-theme/src/templates/prev_next.ejs index ea93cd336..ddc4919f8 100644 --- a/packages/share-theme/src/templates/prev_next.ejs +++ b/packages/share-theme/src/templates/prev_next.ejs @@ -15,13 +15,12 @@ // We are not the first child at this level so previous // should go to the end of the previous tree let candidate = children[index - 1]; - while (candidate.hasVisibleChildren()) { + while (candidate?.hasVisibleChildren()) { const children = candidate.getVisibleChildNotes(); - const lastChild = children[children.length - 1]; - candidate = lastChild; + candidate = children[children.length - 1]; } - return candidate; + return candidate ?? null; })(); const nextNote = (() => { From cb016c4307107d3e7ea57df3b8cbfce0ef457bd0 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Thu, 25 Dec 2025 16:26:58 -0800 Subject: [PATCH 2/9] Address Gemini's comment --- packages/share-theme/src/templates/prev_next.ejs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/share-theme/src/templates/prev_next.ejs b/packages/share-theme/src/templates/prev_next.ejs index ddc4919f8..38441d2c1 100644 --- a/packages/share-theme/src/templates/prev_next.ejs +++ b/packages/share-theme/src/templates/prev_next.ejs @@ -16,8 +16,13 @@ // should go to the end of the previous tree let candidate = children[index - 1]; while (candidate?.hasVisibleChildren()) { - const children = candidate.getVisibleChildNotes(); - candidate = children[children.length - 1]; + const visibleChildren = candidate.getVisibleChildNotes(); + + if (visibleChildren.length === 0) { + break; + } + + candidate = visibleChildren[visibleChildren.length - 1]; } return candidate ?? null; From 7e45aaa1da2070ce7c0899a6f5953ed88426618d Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Thu, 25 Dec 2025 21:40:42 -0800 Subject: [PATCH 3/9] for frontend js files add .js --- apps/server/src/services/export/zip/share_theme.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 1788e38b9..592eb94ec 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -115,6 +115,10 @@ export default class ShareThemeExportProvider extends ZipExportProvider { return null; } + if (type === "code" && mime === "application/javascript;env=frontend"){ + return "js"; + } + return "html"; } From 94d1181fe8611eecb17a370b99bfeebdae2d1d2c Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Thu, 25 Dec 2025 21:52:35 -0800 Subject: [PATCH 4/9] render js notes as-is --- apps/server/src/services/export/zip/share_theme.ts | 1 + apps/server/src/share/content_renderer.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 592eb94ec..44571dbd7 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -70,6 +70,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { }) : ""; content = renderNoteForExport(note, branch, basePath, noteMeta.notePath.slice(0, -1)); + // TODO: This will probably never match, but should it be exclude from running on code/jsFrontend notes? if (typeof content === "string") { content = content.replace(/href="[^"]*\.\/([a-zA-Z0-9_\/]{12})[^"]*"/g, (match, id) => { if (match.includes("/assets/")) return match; diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 70d7f2b82..897f10394 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -149,6 +149,15 @@ interface RenderArgs { } function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) { + if (renderArgs.isStatic && note.type == "code" && note.mime === "application/javascript;env=frontend") { + if (note.isProtected) { + // TODO: how to handle this case here? + throw new Error(`note ${note.noteId} is protected and cannot be exported`); + } + + return note.getContent(); + } + const { header, content, isEmpty } = getContent(note); const showLoginInShareTheme = options.getOption("showLoginInShareTheme"); const opts = { From afcd23cb99d953cda6574955b44f8f38d75848a4 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Thu, 25 Dec 2025 22:03:06 -0800 Subject: [PATCH 5/9] add a todo --- apps/server/src/services/export/zip/share_theme.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 44571dbd7..7775b52d6 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -116,6 +116,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { return null; } + // TODO: Should we allow mime to also include backend, i.e loosely check that it starts with application/javascript and ignore the rest? if (type === "code" && mime === "application/javascript;env=frontend"){ return "js"; } From 03eaebc71c61e3794f701d51acb8347edd0a39b5 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Thu, 25 Dec 2025 22:54:14 -0800 Subject: [PATCH 6/9] be loosy and honor startsWith application/javascript --- apps/server/src/services/export/zip/share_theme.ts | 3 +-- apps/server/src/share/content_renderer.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 7775b52d6..2f1784c13 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -116,8 +116,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { return null; } - // TODO: Should we allow mime to also include backend, i.e loosely check that it starts with application/javascript and ignore the rest? - if (type === "code" && mime === "application/javascript;env=frontend"){ + if (mime.startsWith("application/javascript")) { return "js"; } diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 897f10394..b217560a3 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -149,7 +149,7 @@ interface RenderArgs { } function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) { - if (renderArgs.isStatic && note.type == "code" && note.mime === "application/javascript;env=frontend") { + if (renderArgs.isStatic && note.mime.startsWith("application/javascript")) { if (note.isProtected) { // TODO: how to handle this case here? throw new Error(`note ${note.noteId} is protected and cannot be exported`); From 7e7f3ba78f91d4540c4a4ba62b8c565f4d89a391 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Thu, 25 Dec 2025 23:01:01 -0800 Subject: [PATCH 7/9] improve the protected note handling --- apps/server/src/share/content_renderer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index b217560a3..cb2a9aaa6 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -149,11 +149,11 @@ interface RenderArgs { } function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) { + // When rendering static share, non-protected JavaScript notes should be rendered as-is. if (renderArgs.isStatic && note.mime.startsWith("application/javascript")) { if (note.isProtected) { - // TODO: how to handle this case here? - throw new Error(`note ${note.noteId} is protected and cannot be exported`); - } + return `console.log("Protected note cannot be exported.");` + }; return note.getContent(); } From 37c0f7ec75ddc419c55681d9f229ac98d2c33cf4 Mon Sep 17 00:00:00 2001 From: openapphub Date: Mon, 29 Dec 2025 15:44:37 +0800 Subject: [PATCH 8/9] Fix: Change /calendar/weeks/{date} to use ISO week format (YYYY-Www) instead of date --- apps/server/etapi.openapi.yaml | 51 +++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/apps/server/etapi.openapi.yaml b/apps/server/etapi.openapi.yaml index a3990754a..f35d9ad92 100644 --- a/apps/server/etapi.openapi.yaml +++ b/apps/server/etapi.openapi.yaml @@ -341,7 +341,7 @@ paths: post: description: > Create a branch (clone a note to a different location in the tree). - In case there is a branch between parent note and child note already, + In case there is a branch between parent note and child note already, then this will update the existing branch with prefix, notePosition and isExpanded. operationId: postBranch requestBody: @@ -416,7 +416,7 @@ paths: $ref: "#/components/schemas/Error" delete: description: > - deletes a branch based on the branchId supplied. If this is the last branch of the (child) note, + deletes a branch based on the branchId supplied. If this is the last branch of the (child) note, then the note is deleted as well. operationId: deleteBranchById responses: @@ -627,8 +627,8 @@ paths: $ref: "#/components/schemas/EntityId" post: description: > - notePositions in branches are not automatically pushed to connected clients and need a specific instruction. - If you want your changes to be in effect immediately, call this service after setting branches' notePosition. + notePositions in branches are not automatically pushed to connected clients and need a specific instruction. + If you want your changes to be in effect immediately, call this service after setting branches' notePosition. Note that you need to supply "parentNoteId" of branch(es) with changed positions. operationId: postRefreshNoteOrdering responses: @@ -692,18 +692,20 @@ paths: application/json; charset=utf-8: schema: $ref: "#/components/schemas/Error" - /calendar/weeks/{date}: + /calendar/weeks/{week}: get: - description: returns a week note for a given date. Gets created if doesn't exist. - operationId: getWeekFirstDayNote + summary: Get a week note + description: Returns a week note for a given ISO week (format YYYY-Www, e.g., 2025-W01). The note is created if it doesn't exist. + operationId: getWeekNote parameters: - - name: date + - name: week in: path required: true + description: The ISO 8601 week identifier (YYYY-Www). schema: type: string - format: date - example: 2022-02-22 + pattern: "[0-9]{4}-W[0-9]{2}" + example: "2025-W01" responses: "200": description: week note @@ -859,8 +861,8 @@ components: type: http scheme: basic description: > - Basic Auth where username is arbitrary string (e.g. "trilium", not checked), - username is the ETAPI token. + Basic Auth where username is arbitrary string (e.g. "trilium", not checked), + username is the ETAPI token. To emphasize, do not use Trilium password here (won't work), only the generated ETAPI token (from Options -> ETAPI) schemas: @@ -897,13 +899,13 @@ components: notePosition: type: integer description: > - Position of the note in the parent. Normal ordering is 10, 20, 30 ... + Position of the note in the parent. Normal ordering is 10, 20, 30 ... So if you want to create a note on the first position, use e.g. 5, for second position 15, for last e.g. 1000000 prefix: type: string description: > - Prefix is branch (placement) specific title prefix for the note. - Let's say you have your note placed into two different places in the tree, + Prefix is branch (placement) specific title prefix for the note. + Let's say you have your note placed into two different places in the tree, but you want to change the title a bit in one of the placements. For this you can use prefix. isExpanded: type: boolean @@ -930,7 +932,24 @@ components: type: string type: type: string - enum: [text, code, render, file, image, search, relationMap, book, noteMap, mermaid, webView, shortcut, doc, contentWidget, launcher] + enum: + [ + text, + code, + render, + file, + image, + search, + relationMap, + book, + noteMap, + mermaid, + webView, + shortcut, + doc, + contentWidget, + launcher, + ] mime: type: string isProtected: From d96528dae4d361d82d9afcd569b1b32937638281 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 29 Dec 2025 20:38:48 +0200 Subject: [PATCH 9/9] chore(server): fix type error --- apps/server/src/share/content_renderer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 465d99196..4f9646e82 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -168,10 +168,10 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) // When rendering static share, non-protected JavaScript notes should be rendered as-is. if (renderArgs.isStatic && note.mime.startsWith("application/javascript")) { if (note.isProtected) { - return `console.log("Protected note cannot be exported.");` - }; + return `console.log("Protected note cannot be exported.");`; + } - return note.getContent(); + return note.getContent() ?? ""; } const { header, content, isEmpty } = getContent(note);