mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-30 01:36:24 +01:00 
			
		
		
		
	Compare commits
	
		
			40 Commits
		
	
	
		
			feat/rice-
			...
			feat/clean
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 64f191b83b | ||
|  | 979fbe2e76 | ||
|  | ea5564c6e6 | ||
|  | 6ab8750726 | ||
|  | 923eabd750 | ||
|  | c52d6a6384 | ||
|  | 519b9648c6 | ||
|  | b25fd6ca3a | ||
|  | 2bd8c215ff | ||
|  | 34e7901de9 | ||
|  | 30a191cedf | ||
|  | 128d8907c3 | ||
|  | 3b1d7d045e | ||
|  | 0ae9a29e0d | ||
|  | f4b5ed73ad | ||
|  | 43166dbeb5 | ||
|  | 00b5aef890 | ||
|  | 1b7266f083 | ||
|  | d1d4b47111 | ||
|  | c90364bd76 | ||
|  | 6dc687ef43 | ||
|  | 0e31aab1ab | ||
|  | b0030f89b7 | ||
|  | 9f0a0238cc | ||
|  | dabdfaddec | ||
|  | a8901e6dc8 | ||
|  | ab901a5d32 | ||
|  | 56fc2d9b30 | ||
|  | df45fa2e1e | ||
|  | 9a11fc13d7 | ||
|  | d72a0d3c69 | ||
|  | 0be508ed70 | ||
|  | 37f3a9b19d | ||
|  | 81d2fbc057 | ||
|  | 1aecf66cbe | ||
|  | fbe7d64e00 | ||
|  | c9d151289c | ||
|  | bba2f6db64 | ||
|  | 6725f81a00 | ||
|  | 8487c8cd03 | 
| @@ -1743,9 +1743,17 @@ | |||||||
|     "show_login_link": "在共享主题中显示登录链接", |     "show_login_link": "在共享主题中显示登录链接", | ||||||
|     "show_login_link_description": "在共享页面底部添加登录链接", |     "show_login_link_description": "在共享页面底部添加登录链接", | ||||||
|     "check_share_root": "检查共享根状态", |     "check_share_root": "检查共享根状态", | ||||||
|  |     "check_share_root_error": "检查共享根状态时发生意外错误,请检查日志以获取更多信息。", | ||||||
|  |     "share_note_title": "'{{noteTitle}}'", | ||||||
|     "share_root_found": "共享根笔记 '{{noteTitle}}' 已准备好", |     "share_root_found": "共享根笔记 '{{noteTitle}}' 已准备好", | ||||||
|     "share_root_not_found": "未找到带有 #shareRoot 标签的笔记", |     "share_root_not_found": "未找到带有 #shareRoot 标签的笔记", | ||||||
|     "share_root_not_shared": "笔记 '{{noteTitle}}' 具有 #shareRoot 标签,但未共享" |     "share_root_not_shared": "笔记 '{{noteTitle}}' 具有 #shareRoot 标签,但未共享", | ||||||
|  |     "share_root_multiple_found": "找到多个具有 #shareRoot 标签的共享笔记:{{- foundNoteTitles}}。将使用笔记 {{- activeNoteTitle}} 作为共享根笔记。", | ||||||
|  |     "share_path": "共享路径", | ||||||
|  |     "share_path_description": "共享笔记的 URL 前缀(例如 '/share' --> '/share/noteId' 或 '/custom-path' --> '/custom-path/noteId')。支持多级嵌套(例如 '/custom-path/sub-path' --> '/custom-path/sub-path/noteId')。刷新页面以应用更改。", | ||||||
|  |     "share_path_placeholder": "/share 或 /custom-path", | ||||||
|  |     "share_subtree": "共享子树", | ||||||
|  |     "share_subtree_description": "共享整个子树,而不是仅共享笔记" | ||||||
|   }, |   }, | ||||||
|   "time_selector": { |   "time_selector": { | ||||||
|     "invalid_input": "输入的时间值不是有效数字。", |     "invalid_input": "输入的时间值不是有效数字。", | ||||||
|   | |||||||
| @@ -1907,9 +1907,17 @@ | |||||||
|     "show_login_link": "Show Login link in Share theme", |     "show_login_link": "Show Login link in Share theme", | ||||||
|     "show_login_link_description": "Add a login link to the Share page footer", |     "show_login_link_description": "Add a login link to the Share page footer", | ||||||
|     "check_share_root": "Check Share Root Status", |     "check_share_root": "Check Share Root Status", | ||||||
|  |     "check_share_root_error": "An unexpected error happened while checking the Share Root Status, please check the logs for more information.", | ||||||
|  |     "share_note_title": "'{{noteTitle}}'", | ||||||
|     "share_root_found": "Share root note '{{noteTitle}}' is ready", |     "share_root_found": "Share root note '{{noteTitle}}' is ready", | ||||||
|     "share_root_not_found": "No note with #shareRoot label found", |     "share_root_not_found": "No note with #shareRoot label found", | ||||||
|     "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not shared" |     "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared", | ||||||
|  |     "share_root_multiple_found": "Found multiple shared notes with a #shareRoot label: {{- foundNoteTitles}}. The note {{- activeNoteTitle}} will be used as shared root note.", | ||||||
|  |     "share_path": "Share path", | ||||||
|  |     "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' or '/custom-path' --> '/custom-path/noteId'). Multiple levels of nesting are supported (e.g. '/custom-path/sub-path' --> '/custom-path/sub-path/noteId'). Refresh the page to apply the changes.", | ||||||
|  |     "share_path_placeholder": "/share or /custom-path", | ||||||
|  |     "share_subtree": "Share subtree", | ||||||
|  |     "share_subtree_description": "Share the entire subtree, not just the note" | ||||||
|   }, |   }, | ||||||
|   "time_selector": { |   "time_selector": { | ||||||
|     "invalid_input": "The entered time value is not a valid number.", |     "invalid_input": "The entered time value is not a valid number.", | ||||||
|   | |||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | // Ensure sharePath always starts with a single slash and does not end with (one or multiple) trailing slashes | ||||||
|  | export function normalizeSharePathInput(sharePathInput: string) { | ||||||
|  |     const REGEXP_STARTING_SLASH = /^\/+/g; | ||||||
|  |     const REGEXP_TRAILING_SLASH = /\b\/+$/g; | ||||||
|  |  | ||||||
|  |     const normalizedSharePath = (!sharePathInput.startsWith("/") | ||||||
|  |         ? `/${sharePathInput}` | ||||||
|  |         : sharePathInput) | ||||||
|  |         .replaceAll(REGEXP_TRAILING_SLASH, "") | ||||||
|  |         .replaceAll(REGEXP_STARTING_SLASH, "/"); | ||||||
|  |  | ||||||
|  |     return normalizedSharePath; | ||||||
|  | } | ||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | import { describe, it, expect } from "vitest"; | ||||||
|  | import { normalizeSharePathInput } from "./share_path_utils.js"; | ||||||
|  |  | ||||||
|  | type TestCase<T extends (...args: any) => any> = [ | ||||||
|  |     desc: string, | ||||||
|  |     fnParams: Parameters<T>, | ||||||
|  |     expected: ReturnType<T> | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | describe("ShareSettingsOptions", () => { | ||||||
|  |  | ||||||
|  |     describe("#normalizeSharePathInput", () => { | ||||||
|  |  | ||||||
|  |         const testCases: TestCase<typeof normalizeSharePathInput>[] = [ | ||||||
|  |             [ | ||||||
|  |                 "should handle multiple trailing '/' and remove them completely", | ||||||
|  |                 ["/trailingtest////"], | ||||||
|  |                 "/trailingtest" | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |                 "should handle multiple starting '/' and replace them by a single '/'", | ||||||
|  |                 ["////startingtest"], | ||||||
|  |                 "/startingtest" | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |                 "should handle multiple starting & trailing '/' and replace them by a single '/'", | ||||||
|  |                 ["////startingAndTrailingTest///"], | ||||||
|  |                 "/startingAndTrailingTest" | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |                 "should not remove any '/' other than at the end or start of the input", | ||||||
|  |                 ["/test/with/subpath"], | ||||||
|  |                 "/test/with/subpath" | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |                 "should prepend the string with a '/' if it does not start with one", | ||||||
|  |                 ["testpath"], | ||||||
|  |                 "/testpath" | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |                 "should not change anything, if the string is a single '/'", | ||||||
|  |                 ["/"], | ||||||
|  |                 "/" | ||||||
|  |             ], | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         testCases.forEach((testCase) => { | ||||||
|  |             const [desc, fnParams, expected] = testCase; | ||||||
|  |             it(desc, () => { | ||||||
|  |                 const actual = normalizeSharePathInput(...fnParams); | ||||||
|  |                 expect(actual).toStrictEqual(expected); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  | }) | ||||||
							
								
								
									
										11
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -6,8 +6,7 @@ class="image"> | |||||||
|   <img style="aspect-ratio:1144/660;" src="Sharing_image.png" width="1144" |   <img style="aspect-ratio:1144/660;" src="Sharing_image.png" width="1144" | ||||||
|   height="660"> |   height="660"> | ||||||
|   </figure> |   </figure> | ||||||
|    |   <h2>Features, interaction and limitations</h2> | ||||||
| <h2>Features, interaction and limitations</h2> |  | ||||||
|   <ul> |   <ul> | ||||||
|     <li>Searching by note title.</li> |     <li>Searching by note title.</li> | ||||||
|     <li>Automatic dark/light mode based on the user's browser settings.</li> |     <li>Automatic dark/light mode based on the user's browser settings.</li> | ||||||
| @@ -189,11 +188,9 @@ class="image"> | |||||||
|         <img src="Sharing_share-single-note.png" alt="Share Note"> |         <img src="Sharing_share-single-note.png" alt="Share Note"> | ||||||
|       </p> |       </p> | ||||||
|     </li> |     </li> | ||||||
|     <li> |     <li><strong>Access the Shared Note</strong>: The link provided will open the | ||||||
|       <p><strong>Access the Shared Note</strong>: The link provided will open the |       note in your browser. If your server is not configured with a public IP, | ||||||
|         note in your browser. If your server is not configured with a public IP, |       the URL will refer to <code>localhost (127.0.0.1)</code>.</li> | ||||||
|         the URL will refer to <code>localhost (127.0.0.1)</code>.</p> |  | ||||||
|     </li> |  | ||||||
|   </ol> |   </ol> | ||||||
|   <h2>Sharing a note subtree</h2> |   <h2>Sharing a note subtree</h2> | ||||||
|   <p>When you share a note, you actually share the entire subtree of notes |   <p>When you share a note, you actually share the entire subtree of notes | ||||||
|   | |||||||
| @@ -97,6 +97,8 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([ | |||||||
|     "allowedHtmlTags", |     "allowedHtmlTags", | ||||||
|     "redirectBareDomain", |     "redirectBareDomain", | ||||||
|     "showLoginInShareTheme", |     "showLoginInShareTheme", | ||||||
|  |     "shareSubtree", | ||||||
|  |     "sharePath", | ||||||
|     "splitEditorOrientation", |     "splitEditorOrientation", | ||||||
|     "seenCallToActions", |     "seenCallToActions", | ||||||
|  |  | ||||||
|   | |||||||
| @@ -80,6 +80,7 @@ const GET = "get", | |||||||
|     DEL = "delete"; |     DEL = "delete"; | ||||||
|  |  | ||||||
| function register(app: express.Application) { | function register(app: express.Application) { | ||||||
|  |  | ||||||
|     route(GET, "/", [auth.checkAuth, csrfMiddleware], indexRoute.index); |     route(GET, "/", [auth.checkAuth, csrfMiddleware], indexRoute.index); | ||||||
|     route(GET, "/login", [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage); |     route(GET, "/login", [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage); | ||||||
|     route(GET, "/set-password", [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPasswordPage); |     route(GET, "/set-password", [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPasswordPage); | ||||||
|   | |||||||
| @@ -37,9 +37,26 @@ function checkAuth(req: Request, res: Response, next: NextFunction) { | |||||||
|             // Check if any note has the #shareRoot label |             // Check if any note has the #shareRoot label | ||||||
|             const shareRootNotes = attributes.getNotesWithLabel("shareRoot"); |             const shareRootNotes = attributes.getNotesWithLabel("shareRoot"); | ||||||
|             if (shareRootNotes.length === 0) { |             if (shareRootNotes.length === 0) { | ||||||
|  |                 // should this be a translation string? | ||||||
|                 res.status(404).json({ message: "Share root not found. Please set up a note with #shareRoot label first." }); |                 res.status(404).json({ message: "Share root not found. Please set up a note with #shareRoot label first." }); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             // Get the configured share path | ||||||
|  |             const sharePath = options.getOption("sharePath") || '/share'; | ||||||
|  |  | ||||||
|  |             // Check if we're already at the share path to prevent redirect loops | ||||||
|  |             if (req.path === sharePath || req.path.startsWith(`${sharePath}/`)) { | ||||||
|  |                 log.info(`checkAuth: Already at share path, skipping redirect. Path: ${req.path}, SharePath: ${sharePath}`); | ||||||
|  |                 next(); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Redirect to the share path | ||||||
|  |             log.info(`checkAuth: Redirecting to share path. From: ${req.path}, To: ${sharePath}`); | ||||||
|  |             res.redirect(`${sharePath}/`); | ||||||
|  |         } else { | ||||||
|  |             res.redirect("login"); | ||||||
|         } |         } | ||||||
|         res.redirect(hasRedirectBareDomain ? "share" : "login"); |         res.redirect(hasRedirectBareDomain ? "share" : "login"); | ||||||
|     } else if (currentTotpStatus !== lastAuthState.totpEnabled || currentSsoStatus !== lastAuthState.ssoEnabled) { |     } else if (currentTotpStatus !== lastAuthState.totpEnabled || currentSsoStatus !== lastAuthState.ssoEnabled) { | ||||||
| @@ -81,15 +98,6 @@ function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| function checkApiAuth(req: Request, res: Response, next: NextFunction) { |  | ||||||
|     if (!req.session.loggedIn && !noAuthentication) { |  | ||||||
|         console.warn(`Missing session with ID '${req.sessionID}'.`); |  | ||||||
|         reject(req, res, "Logged in session not found"); |  | ||||||
|     } else { |  | ||||||
|         next(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function checkAppInitialized(req: Request, res: Response, next: NextFunction) { | function checkAppInitialized(req: Request, res: Response, next: NextFunction) { | ||||||
|     if (!sqlInit.isDbInitialized()) { |     if (!sqlInit.isDbInitialized()) { | ||||||
|         res.redirect("setup"); |         res.redirect("setup"); | ||||||
|   | |||||||
| @@ -196,8 +196,10 @@ const defaultOptions: DefaultOption[] = [ | |||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     // Share settings |     // Share settings | ||||||
|  |     { name: "sharePath", value: "/share", isSynced: true }, | ||||||
|     { name: "redirectBareDomain", value: "false", isSynced: true }, |     { name: "redirectBareDomain", value: "false", isSynced: true }, | ||||||
|     { name: "showLoginInShareTheme", value: "false", isSynced: true }, |     { name: "showLoginInShareTheme", value: "false", isSynced: true }, | ||||||
|  |     { name: "shareSubtree", value: "false", isSynced: true }, | ||||||
|  |  | ||||||
|     // AI Options |     // AI Options | ||||||
|     { name: "aiEnabled", value: "false", isSynced: true }, |     { name: "aiEnabled", value: "false", isSynced: true }, | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ export function getContent(note: SNote) { | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     if (note.type === "text") { |     if (note.type === "text") { | ||||||
|         renderText(result, note); |         renderText(result, note, relativePath); | ||||||
|     } else if (note.type === "code") { |     } else if (note.type === "code") { | ||||||
|         renderCode(result); |         renderCode(result); | ||||||
|     } else if (note.type === "mermaid") { |     } else if (note.type === "mermaid") { | ||||||
| @@ -106,10 +106,10 @@ function renderText(result: Result, note: SNote) { | |||||||
|  |  | ||||||
|         if (result.content.includes(`<span class="math-tex">`)) { |         if (result.content.includes(`<span class="math-tex">`)) { | ||||||
|             result.header += ` |             result.header += ` | ||||||
| <script src="../${assetPath}/node_modules/katex/dist/katex.min.js"></script> | <script src="${relativePath}${assetPath}/node_modules/katex/dist/katex.min.js"></script> | ||||||
| <link rel="stylesheet" href="../${assetPath}/node_modules/katex/dist/katex.min.css"> | <link rel="stylesheet" href="${relativePath}${assetPath}/node_modules/katex/dist/katex.min.css"> | ||||||
| <script src="../${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script> | <script src="${relativePath}${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script> | ||||||
| <script src="../${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script> | <script src="${relativePath}${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script> | ||||||
| <script> | <script> | ||||||
| document.addEventListener("DOMContentLoaded", function() { | document.addEventListener("DOMContentLoaded", function() { | ||||||
|     renderMathInElement(document.getElementById('content')); |     renderMathInElement(document.getElementById('content')); | ||||||
|   | |||||||
| @@ -44,3 +44,39 @@ describe("Share API test", () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | describe("Share Routes - Asset Path Calculation", () => { | ||||||
|  |     it("should calculate correct relative path depth for different share paths", () => { | ||||||
|  |         // Helper function to simulate the path depth calculation | ||||||
|  |         const calculateRelativePath = (sharePath: string) => { | ||||||
|  |             const pathDepth = sharePath.split('/').filter(segment => segment.length > 0).length; | ||||||
|  |             return '../'.repeat(pathDepth); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // Test single level path | ||||||
|  |         expect(calculateRelativePath("/share")).toBe("../"); | ||||||
|  |  | ||||||
|  |         // Test double level path | ||||||
|  |         expect(calculateRelativePath("/sharePath/test")).toBe("../../"); | ||||||
|  |  | ||||||
|  |         // Test triple level path | ||||||
|  |         expect(calculateRelativePath("/my/custom/share")).toBe("../../../"); | ||||||
|  |  | ||||||
|  |         // Test root path | ||||||
|  |         expect(calculateRelativePath("/")).toBe(""); | ||||||
|  |  | ||||||
|  |         // Test path with trailing slash | ||||||
|  |         expect(calculateRelativePath("/share/")).toBe("../"); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it("should handle normalized share paths correctly", () => { | ||||||
|  |         const calculateRelativePath = (sharePath: string) => { | ||||||
|  |             const pathDepth = sharePath.split('/').filter(segment => segment.length > 0).length; | ||||||
|  |             return '../'.repeat(pathDepth); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // Test the examples from the original TODO comment | ||||||
|  |         expect(calculateRelativePath("/sharePath")).toBe("../"); | ||||||
|  |         expect(calculateRelativePath("/sharePath/test")).toBe("../../"); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import safeCompare from "safe-compare"; | import safeCompare from "safe-compare"; | ||||||
|  |  | ||||||
| import type { Request, Response, Router } from "express"; | import type { Request, Response, Router, NextFunction } from "express"; | ||||||
|  |  | ||||||
| import shaca from "./shaca/shaca.js"; | import shaca from "./shaca/shaca.js"; | ||||||
| import shacaLoader from "./shaca/shaca_loader.js"; | import shacaLoader from "./shaca/shaca_loader.js"; | ||||||
| @@ -139,17 +139,21 @@ function renderImageAttachment(image: SNote, res: Response, attachmentName: stri | |||||||
| } | } | ||||||
|  |  | ||||||
| function register(router: Router) { | function register(router: Router) { | ||||||
|     function renderNote(note: SNote, req: Request, res: Response) { |         function renderNote(note: SNote, req: Request, res: Response) { | ||||||
|  |         // Calculate the correct relative path depth based on the current request path | ||||||
|  |         // We need to go up one level for each path segment in the request URL | ||||||
|  |         const pathSegments = req.path.split('/').filter(segment => segment.length > 0); | ||||||
|  |         const relativePath = '../'.repeat(pathSegments.length); | ||||||
|  |  | ||||||
|         if (!note) { |         if (!note) { | ||||||
|             console.log("Unable to find note ", note); |             console.log("Unable to find note ", note); | ||||||
|             res.status(404); |             res.status(404); | ||||||
|             renderDefault(res, "404"); |             renderDefault(res, "404", { relativePath, t }); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!checkNoteAccess(note.noteId, req, res)) { |         if (!checkNoteAccess(note.noteId, req, res)) { | ||||||
|             requestCredentials(res); |             requestCredentials(res); | ||||||
|  |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -161,18 +165,20 @@ function register(router: Router) { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const { header, content, isEmpty } = contentRenderer.getContent(note); |         const { header, content, isEmpty } = contentRenderer.getContent(note, relativePath); | ||||||
|         const subRoot = getSharedSubTreeRoot(note); |         const subRoot = getSharedSubTreeRoot(note); | ||||||
|         const showLoginInShareTheme = options.getOption("showLoginInShareTheme"); |         const showLoginInShareTheme = options.getOption("showLoginInShareTheme"); | ||||||
|  |  | ||||||
|         const opts = { |         const opts = { | ||||||
|             note, |             note, | ||||||
|             header, |             header, | ||||||
|             content, |             content, | ||||||
|             isEmpty, |             isEmpty, | ||||||
|             subRoot, |             subRoot, | ||||||
|             assetPath: isDev ? assetPath : `../${assetPath}`, |             assetPath: isDev ? assetPath : `${relativePath}${assetPath}`, | ||||||
|             assetUrlFragment, |             assetUrlFragment, | ||||||
|             appPath: isDev ? appPath : `../${appPath}`, |             appPath: isDev ? appPath : `${relativePath}${appPath}`, | ||||||
|  |             relativePath, | ||||||
|             showLoginInShareTheme, |             showLoginInShareTheme, | ||||||
|             t, |             t, | ||||||
|             isDev |             isDev | ||||||
| @@ -219,184 +225,165 @@ function register(router: Router) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     router.get("/share/", (req, res) => { |     // Dynamic dispatch middleware | ||||||
|         if (req.path.substr(-1) !== "/") { |     router.use((req: Request, res: Response, next: NextFunction) => { | ||||||
|             res.redirect("../share/"); |         const sharePath = options.getOptionOrNull("sharePath") || "/share"; | ||||||
|             return; |         // Only handle requests starting with sharePath | ||||||
|  |         if (req.path === sharePath || req.path.startsWith(sharePath + "/")) { | ||||||
|  |             // Remove sharePath prefix to get the remaining path | ||||||
|  |             const subPath = req.path.slice(sharePath.length); | ||||||
|  |             // Handle root path | ||||||
|  |             if (subPath === "" || subPath === "/") { | ||||||
|  |                 shacaLoader.ensureLoad(); | ||||||
|  |                 if (!shaca.shareRootNote) { | ||||||
|  |                     res.status(404).json({ message: "Share root not found" }); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 renderNote(shaca.shareRootNote, req, res); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Handle /:shareId | ||||||
|  |             const shareIdMatch = subPath.match(/^\/([^/]+)$/); | ||||||
|  |             if (shareIdMatch) { | ||||||
|  |                 shacaLoader.ensureLoad(); | ||||||
|  |                 const shareId = shareIdMatch[1]; | ||||||
|  |                 const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; | ||||||
|  |                 renderNote(note, req, res); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Handle /api/notes/:noteId | ||||||
|  |             const apiNoteMatch = subPath.match(/^\/api\/notes\/([^/]+)$/); | ||||||
|  |             if (apiNoteMatch) { | ||||||
|  |                 shacaLoader.ensureLoad(); | ||||||
|  |                 const noteId = apiNoteMatch[1]; | ||||||
|  |                 let note: SNote | boolean; | ||||||
|  |                 if (!(note = checkNoteAccess(noteId, req, res))) return; | ||||||
|  |                 addNoIndexHeader(note, res); | ||||||
|  |                 res.json(note.getPojo()); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Handle /api/notes/:noteId/download | ||||||
|  |             const apiNoteDownloadMatch = subPath.match(/^\/api\/notes\/([^/]+)\/download$/); | ||||||
|  |             if (apiNoteDownloadMatch) { | ||||||
|  |                 shacaLoader.ensureLoad(); | ||||||
|  |                 const noteId = apiNoteDownloadMatch[1]; | ||||||
|  |                 let note: SNote | boolean; | ||||||
|  |                 if (!(note = checkNoteAccess(noteId, req, res))) return; | ||||||
|  |                 addNoIndexHeader(note, res); | ||||||
|  |                 const filename = utils.formatDownloadTitle(note.title, note.type, note.mime); | ||||||
|  |                 res.setHeader("Content-Disposition", utils.getContentDisposition(filename)); | ||||||
|  |                 res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); | ||||||
|  |                 res.setHeader("Content-Type", note.mime); | ||||||
|  |                 res.send(note.getContent()); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Handle /api/images/:noteId/:filename | ||||||
|  |             const apiImageMatch = subPath.match(/^\/api\/images\/([^/]+)\/([^/]+)$/); | ||||||
|  |             if (apiImageMatch) { | ||||||
|  |                 shacaLoader.ensureLoad(); | ||||||
|  |                 const noteId = apiImageMatch[1]; | ||||||
|  |                 let image: SNote | boolean; | ||||||
|  |                 if (!(image = checkNoteAccess(noteId, req, res))) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if (image.type === "image") { | ||||||
|  |                     // normal image | ||||||
|  |                     res.set("Content-Type", image.mime); | ||||||
|  |                     addNoIndexHeader(image, res); | ||||||
|  |                     res.send(image.getContent()); | ||||||
|  |                 } else if (image.type === "canvas") { | ||||||
|  |                     renderImageAttachment(image, res, "canvas-export.svg"); | ||||||
|  |                 } else if (image.type === "mermaid") { | ||||||
|  |                     renderImageAttachment(image, res, "mermaid-export.svg"); | ||||||
|  |                 } else if (image.type === "mindMap") { | ||||||
|  |                     renderImageAttachment(image, res, "mindmap-export.svg"); | ||||||
|  |                 } else { | ||||||
|  |                     res.status(400).json({ message: "Requested note is not a shareable image" }); | ||||||
|  |                 } | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Handle /api/attachments/:attachmentId/image/:filename | ||||||
|  |             const apiAttachmentImageMatch = subPath.match(/^\/api\/attachments\/([^/]+)\/image\/([^/]+)$/); | ||||||
|  |             if (apiAttachmentImageMatch) { | ||||||
|  |                 shacaLoader.ensureLoad(); | ||||||
|  |                 const attachmentId = apiAttachmentImageMatch[1]; | ||||||
|  |                 let attachment: SAttachment | boolean; | ||||||
|  |                 if (!(attachment = checkAttachmentAccess(attachmentId, req, res))) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if (attachment.role === "image") { | ||||||
|  |                     res.set("Content-Type", attachment.mime); | ||||||
|  |                     addNoIndexHeader(attachment.note, res); | ||||||
|  |                     res.send(attachment.getContent()); | ||||||
|  |                 } else { | ||||||
|  |                     res.status(400).json({ message: "Requested attachment is not a shareable image" }); | ||||||
|  |                 } | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Handle /api/attachments/:attachmentId/download | ||||||
|  |             const apiAttachmentDownloadMatch = subPath.match(/^\/api\/attachments\/([^/]+)\/download$/); | ||||||
|  |             if (apiAttachmentDownloadMatch) { | ||||||
|  |                 shacaLoader.ensureLoad(); | ||||||
|  |                 const attachmentId = apiAttachmentDownloadMatch[1]; | ||||||
|  |                 let attachment: SAttachment | boolean; | ||||||
|  |                 if (!(attachment = checkAttachmentAccess(attachmentId, req, res))) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 addNoIndexHeader(attachment.note, res); | ||||||
|  |                 const filename = utils.formatDownloadTitle(attachment.title, null, attachment.mime); | ||||||
|  |                 res.setHeader("Content-Disposition", utils.getContentDisposition(filename)); | ||||||
|  |                 res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); | ||||||
|  |                 res.setHeader("Content-Type", attachment.mime); | ||||||
|  |                 res.send(attachment.getContent()); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Handle /api/notes/:noteId/view | ||||||
|  |             const apiNoteViewMatch = subPath.match(/^\/api\/notes\/([^/]+)\/view$/); | ||||||
|  |             if (apiNoteViewMatch) { | ||||||
|  |                 shacaLoader.ensureLoad(); | ||||||
|  |                 const noteId = apiNoteViewMatch[1]; | ||||||
|  |                 let note: SNote | boolean; | ||||||
|  |                 if (!(note = checkNoteAccess(noteId, req, res))) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 addNoIndexHeader(note, res); | ||||||
|  |                 res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); | ||||||
|  |                 res.setHeader("Content-Type", note.mime); | ||||||
|  |                 res.send(note.getContent()); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Handle /api/notes 搜索 | ||||||
|  |             const apiNotesSearchMatch = subPath.match(/^\/api\/notes$/); | ||||||
|  |             if (apiNotesSearchMatch) { | ||||||
|  |                 shacaLoader.ensureLoad(); | ||||||
|  |                 const ancestorNoteId = req.query.ancestorNoteId ?? "_share"; | ||||||
|  |                 if (typeof ancestorNoteId !== "string") { | ||||||
|  |                     res.status(400).json({ message: "'ancestorNoteId' parameter is mandatory." }); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 // This will automatically return if no ancestorNoteId is provided and there is no shareIndex | ||||||
|  |                 if (!checkNoteAccess(ancestorNoteId, req, res)) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 const { search } = req.query; | ||||||
|  |                 if (typeof search !== "string" || !search?.trim()) { | ||||||
|  |                     res.status(400).json({ message: "'search' parameter is mandatory." }); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 const searchContext = new SearchContext({ ancestorNoteId: ancestorNoteId }); | ||||||
|  |                 const searchResults = searchService.findResultsWithQuery(search, searchContext); | ||||||
|  |                 const filteredResults = searchResults.map((sr) => { | ||||||
|  |                     const fullNote = shaca.notes[sr.noteId]; | ||||||
|  |                     const startIndex = sr.notePathArray.indexOf(ancestorNoteId); | ||||||
|  |                     const localPathArray = sr.notePathArray.slice(startIndex + 1).filter((id) => shaca.notes[id]); | ||||||
|  |                     const pathTitle = localPathArray.map((id) => shaca.notes[id].title).join(" / "); | ||||||
|  |                     return { id: fullNote.shareId, title: fullNote.title, score: sr.score, path: pathTitle }; | ||||||
|  |                 }); | ||||||
|  |                 res.json({ results: filteredResults }); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |         next(); | ||||||
|         shacaLoader.ensureLoad(); |  | ||||||
|  |  | ||||||
|         if (!shaca.shareRootNote) { |  | ||||||
|             res.status(404).json({ message: "Share root note not found" }); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         renderNote(shaca.shareRootNote, req, res); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     router.get("/share/:shareId", (req, res) => { |  | ||||||
|         shacaLoader.ensureLoad(); |  | ||||||
|  |  | ||||||
|         const { shareId } = req.params; |  | ||||||
|  |  | ||||||
|         const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; |  | ||||||
|  |  | ||||||
|         renderNote(note, req, res); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     router.get("/share/api/notes/:noteId", (req, res) => { |  | ||||||
|         shacaLoader.ensureLoad(); |  | ||||||
|         let note: SNote | boolean; |  | ||||||
|  |  | ||||||
|         if (!(note = checkNoteAccess(req.params.noteId, req, res))) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         addNoIndexHeader(note, res); |  | ||||||
|  |  | ||||||
|         res.json(note.getPojo()); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     router.get("/share/api/notes/:noteId/download", (req, res) => { |  | ||||||
|         shacaLoader.ensureLoad(); |  | ||||||
|  |  | ||||||
|         let note: SNote | boolean; |  | ||||||
|  |  | ||||||
|         if (!(note = checkNoteAccess(req.params.noteId, req, res))) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         addNoIndexHeader(note, res); |  | ||||||
|  |  | ||||||
|         const filename = utils.formatDownloadTitle(note.title, note.type, note.mime); |  | ||||||
|  |  | ||||||
|         res.setHeader("Content-Disposition", utils.getContentDisposition(filename)); |  | ||||||
|  |  | ||||||
|         res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); |  | ||||||
|         res.setHeader("Content-Type", note.mime); |  | ||||||
|  |  | ||||||
|         res.send(note.getContent()); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename |  | ||||||
|     router.get("/share/api/images/:noteId/:filename", (req, res) => { |  | ||||||
|         shacaLoader.ensureLoad(); |  | ||||||
|  |  | ||||||
|         let image: SNote | boolean; |  | ||||||
|  |  | ||||||
|         if (!(image = checkNoteAccess(req.params.noteId, req, res))) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (image.type === "image") { |  | ||||||
|             // normal image |  | ||||||
|             res.set("Content-Type", image.mime); |  | ||||||
|             addNoIndexHeader(image, res); |  | ||||||
|             res.send(image.getContent()); |  | ||||||
|         } else if (image.type === "canvas") { |  | ||||||
|             renderImageAttachment(image, res, "canvas-export.svg"); |  | ||||||
|         } else if (image.type === "mermaid") { |  | ||||||
|             renderImageAttachment(image, res, "mermaid-export.svg"); |  | ||||||
|         } else if (image.type === "mindMap") { |  | ||||||
|             renderImageAttachment(image, res, "mindmap-export.svg"); |  | ||||||
|         } else { |  | ||||||
|             res.status(400).json({ message: "Requested note is not a shareable image" }); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename |  | ||||||
|     router.get("/share/api/attachments/:attachmentId/image/:filename", (req, res) => { |  | ||||||
|         shacaLoader.ensureLoad(); |  | ||||||
|  |  | ||||||
|         let attachment: SAttachment | boolean; |  | ||||||
|  |  | ||||||
|         if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (attachment.role === "image") { |  | ||||||
|             res.set("Content-Type", attachment.mime); |  | ||||||
|             addNoIndexHeader(attachment.note, res); |  | ||||||
|             res.send(attachment.getContent()); |  | ||||||
|         } else { |  | ||||||
|             res.status(400).json({ message: "Requested attachment is not a shareable image" }); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     router.get("/share/api/attachments/:attachmentId/download", (req, res) => { |  | ||||||
|         shacaLoader.ensureLoad(); |  | ||||||
|  |  | ||||||
|         let attachment: SAttachment | boolean; |  | ||||||
|  |  | ||||||
|         if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         addNoIndexHeader(attachment.note, res); |  | ||||||
|  |  | ||||||
|         const filename = utils.formatDownloadTitle(attachment.title, null, attachment.mime); |  | ||||||
|  |  | ||||||
|         res.setHeader("Content-Disposition", utils.getContentDisposition(filename)); |  | ||||||
|  |  | ||||||
|         res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); |  | ||||||
|         res.setHeader("Content-Type", attachment.mime); |  | ||||||
|  |  | ||||||
|         res.send(attachment.getContent()); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // used for PDF viewing |  | ||||||
|     router.get("/share/api/notes/:noteId/view", (req, res) => { |  | ||||||
|         shacaLoader.ensureLoad(); |  | ||||||
|  |  | ||||||
|         let note: SNote | boolean; |  | ||||||
|  |  | ||||||
|         if (!(note = checkNoteAccess(req.params.noteId, req, res))) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         addNoIndexHeader(note, res); |  | ||||||
|  |  | ||||||
|         res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); |  | ||||||
|         res.setHeader("Content-Type", note.mime); |  | ||||||
|  |  | ||||||
|         res.send(note.getContent()); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // Used for searching, require noteId so we know the subTreeRoot |  | ||||||
|     router.get("/share/api/notes", (req, res) => { |  | ||||||
|         shacaLoader.ensureLoad(); |  | ||||||
|  |  | ||||||
|         const ancestorNoteId = req.query.ancestorNoteId ?? "_share"; |  | ||||||
|  |  | ||||||
|         if (typeof ancestorNoteId !== "string") { |  | ||||||
|             res.status(400).json({ message: "'ancestorNoteId' parameter is mandatory." }); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // This will automatically return if no ancestorNoteId is provided and there is no shareIndex |  | ||||||
|         if (!checkNoteAccess(ancestorNoteId, req, res)) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const { search } = req.query; |  | ||||||
|  |  | ||||||
|         if (typeof search !== "string" || !search?.trim()) { |  | ||||||
|             res.status(400).json({ message: "'search' parameter is mandatory." }); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const searchContext = new SearchContext({ ancestorNoteId: ancestorNoteId }); |  | ||||||
|         const searchResults = searchService.findResultsWithQuery(search, searchContext); |  | ||||||
|         const filteredResults = searchResults.map((sr) => { |  | ||||||
|             const fullNote = shaca.notes[sr.noteId]; |  | ||||||
|             const startIndex = sr.notePathArray.indexOf(ancestorNoteId); |  | ||||||
|             const localPathArray = sr.notePathArray.slice(startIndex + 1).filter((id) => shaca.notes[id]); |  | ||||||
|             const pathTitle = localPathArray.map((id) => shaca.notes[id].title).join(" / "); |  | ||||||
|             return { id: fullNote.shareId, title: fullNote.title, score: sr.score, path: pathTitle }; |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         res.json({ results: filteredResults }); |  | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -123,6 +123,16 @@ When accessing a share, the sub-notes will be displayed in a tree on the left. B | |||||||
|  |  | ||||||
| To do so, create a shared text note and apply the `shareIndex` label. When viewed, the list of shared roots will be displayed at the bottom of the note. | To do so, create a shared text note and apply the `shareIndex` label. When viewed, the list of shared roots will be displayed at the bottom of the note. | ||||||
|  |  | ||||||
|  | ### Redirect Bare Domain to Share Page | ||||||
|  |  | ||||||
|  | This option can be enabled under `Option → Other → Share Settings`. When activated, anonymous users accessing the bare domain will be redirected to the Share page, preventing them from seeing the login option and thereby improving security.   | ||||||
|  | To ensure accessibility for legitimate users, you can also enable a login link on the Share page, allowing yourself to access the login screen if you're redirected there. | ||||||
|  |  | ||||||
|  | ### Setting a Custom Share Path | ||||||
|  |  | ||||||
|  | This option can be enabled under `Option → Other → Share Settings`. It allows you to customize the share URL prefix before the `noteId`. Nested paths are supported.   | ||||||
|  | If you're using a proxy service, make sure to update its configuration accordingly to reflect the new path structure. | ||||||
|  |  | ||||||
| ## Attribute reference | ## Attribute reference | ||||||
|  |  | ||||||
| <table><thead><tr><th>Attribute</th><th>Description</th></tr></thead><tbody><tr><td><code>shareHiddenFromTree</code></td><td>this note is hidden from left navigation tree, but still accessible with its URL</td></tr><tr><td><code>shareExternalLink</code></td><td>note will act as a link to an external website in the share tree</td></tr><tr><td><code>shareAlias</code></td><td>define an alias using which the note will be available under <code>https://your_trilium_host/share/[your_alias]</code></td></tr><tr><td><code>shareOmitDefaultCss</code></td><td>default share page CSS will be omitted. Use when you make extensive styling changes.</td></tr><tr><td><code>shareRoot</code></td><td>marks note which is served on /share root.</td></tr><tr><td><code>shareDescription</code></td><td>define text to be added to the HTML meta tag for description</td></tr><tr><td><code>shareRaw</code></td><td>Note will be served in its raw format, without HTML wrapper. See also <a class="reference-link" href="Sharing/Serving%20directly%20the%20content%20o.md">Serving directly the content of a note</a> for an alternative method without setting an attribute.</td></tr><tr><td><code>shareDisallowRobotIndexing</code></td><td><p>Indicates to web crawlers that the page should not be indexed of this note by:</p><ul><li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li><li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li></ul></td></tr><tr><td><code>shareCredentials</code></td><td>require credentials to access this shared note. Value is expected to be in format <code>username:password</code>. Don't forget to make this inheritable to apply to child-notes/images.</td></tr><tr><td><code>shareIndex</code></td><td>Note with this label will list all roots of shared notes.</td></tr><tr><td><code>shareHtmlLocation</code></td><td>defines where custom HTML injected via <code>~shareHtml</code> relation should be placed. Applied to the HTML snippet note itself. Format: <code>location:position</code> where location is <code>head</code>, <code>body</code>, or <code>content</code> and position is <code>start</code> or <code>end</code>. Defaults to <code>content:end</code>.</td></tr></tbody></table> | <table><thead><tr><th>Attribute</th><th>Description</th></tr></thead><tbody><tr><td><code>shareHiddenFromTree</code></td><td>this note is hidden from left navigation tree, but still accessible with its URL</td></tr><tr><td><code>shareExternalLink</code></td><td>note will act as a link to an external website in the share tree</td></tr><tr><td><code>shareAlias</code></td><td>define an alias using which the note will be available under <code>https://your_trilium_host/share/[your_alias]</code></td></tr><tr><td><code>shareOmitDefaultCss</code></td><td>default share page CSS will be omitted. Use when you make extensive styling changes.</td></tr><tr><td><code>shareRoot</code></td><td>marks note which is served on /share root.</td></tr><tr><td><code>shareDescription</code></td><td>define text to be added to the HTML meta tag for description</td></tr><tr><td><code>shareRaw</code></td><td>Note will be served in its raw format, without HTML wrapper. See also <a class="reference-link" href="Sharing/Serving%20directly%20the%20content%20o.md">Serving directly the content of a note</a> for an alternative method without setting an attribute.</td></tr><tr><td><code>shareDisallowRobotIndexing</code></td><td><p>Indicates to web crawlers that the page should not be indexed of this note by:</p><ul><li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li><li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li></ul></td></tr><tr><td><code>shareCredentials</code></td><td>require credentials to access this shared note. Value is expected to be in format <code>username:password</code>. Don't forget to make this inheritable to apply to child-notes/images.</td></tr><tr><td><code>shareIndex</code></td><td>Note with this label will list all roots of shared notes.</td></tr><tr><td><code>shareHtmlLocation</code></td><td>defines where custom HTML injected via <code>~shareHtml</code> relation should be placed. Applied to the HTML snippet note itself. Format: <code>location:position</code> where location is <code>head</code>, <code>body</code>, or <code>content</code> and position is <code>start</code> or <code>end</code>. Defaults to <code>content:end</code>.</td></tr></tbody></table> | ||||||
|   | |||||||
| @@ -135,6 +135,8 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi | |||||||
|     // Share settings |     // Share settings | ||||||
|     redirectBareDomain: boolean; |     redirectBareDomain: boolean; | ||||||
|     showLoginInShareTheme: boolean; |     showLoginInShareTheme: boolean; | ||||||
|  |     shareSubtree: boolean; | ||||||
|  |     sharePath: string; | ||||||
|  |  | ||||||
|     // AI/LLM integration options |     // AI/LLM integration options | ||||||
|     aiEnabled: boolean; |     aiEnabled: boolean; | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| <html lang="en"> | <html lang="en"> | ||||||
| <head> | <head> | ||||||
|     <meta charset="utf-8"> |     <meta charset="utf-8"> | ||||||
|     <link rel="shortcut icon" href="../favicon.ico"> |     <link rel="shortcut icon" href="<%= relativePath %>favicon.ico"> | ||||||
|     <title><%= t("share_404.title") %></title> |     <title><%= t("share_404.title") %></title> | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
|   | |||||||
| @@ -81,7 +81,7 @@ | |||||||
| </head> | </head> | ||||||
| <% | <% | ||||||
| const customLogoId = subRoot.note.getRelation("shareLogo")?.value; | const customLogoId = subRoot.note.getRelation("shareLogo")?.value; | ||||||
| const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `../${assetUrlFragment}/images/icon-color.svg`; | const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `${relativePath}${assetUrlFragment}/images/icon-color.svg`; | ||||||
| const logoWidth = subRoot.note.getLabelValue("shareLogoWidth") ?? 53; | const logoWidth = subRoot.note.getLabelValue("shareLogoWidth") ?? 53; | ||||||
| const logoHeight = subRoot.note.getLabelValue("shareLogoHeight") ?? 40; | const logoHeight = subRoot.note.getLabelValue("shareLogoHeight") ?? 40; | ||||||
| const mobileLogoHeight = logoHeight && logoWidth ? 32 / (logoWidth / logoHeight) : ""; | const mobileLogoHeight = logoHeight && logoWidth ? 32 / (logoWidth / logoHeight) : ""; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user