--- name: developing-share-pages description: Use when working on Trilium's share functionality — shared pages under `/share/`, the static HTML (share-theme) export, the share theme package (`packages/share-theme`: EJS templates, page model, browser scripts and CSS), core's share renderer (`packages/trilium-core/src/share/content_renderer.ts`, `handlers.ts`, shaca), the per-platform share providers, `~shareTemplate` custom templates, `~shareHtml` snippets, or any `#share*` label. Covers where each piece runs (server, desktop, standalone worker, visitor's browser), the render pipeline from note to page, the page model and its template variables, what custom templates are promised, CSS ordering, how to test each layer (both core runners, happy-dom, node:vm), and the traps already hit on this code. --- # Developing shared pages A shared note is rendered **on the backend** into a complete HTML page by core, using the share theme's EJS templates, and then enhanced **in the visitor's browser** by the share theme's script bundle. The same renderer produces the static HTML export, with every page written to a ZIP. ## Where the code lives ``` packages/trilium-core/src/share/ backend, runs in server, desktop and the standalone worker route_paths.ts SHARE_ROUTE_PATHS (import-free, so a platform can register routes cheaply) handlers.ts transport-neutral handlers: credentials, protected notes, raw, images, search content_renderer.ts note → content HTML (getContent), page render (renderNoteContent, renderNoteForExport), preparePageContent, highlighting shaca/ share cache: SNote/SBranch/SAttribute/SAttachment over a read-only SQL view share_provider.ts ShareProvider interface: sql, readTemplate, isScriptingEnabled, isReady apps/server/src/share/ Express adapter (routes.ts) + Node provider (share_provider.ts) apps/standalone/src/lightweight/ browser provider (share_provider.ts: templates bundled ?raw) and route adapter (browser_routes.ts) packages/trilium-core/src/services/export/zip/share_theme.ts static export (renderNoteForExport) packages/share-theme/ src/model/page.ts the page model: pure functions from notes to template values src/templates/ page.ejs + partials (boot_script, tree_item, toc_item, prev_next, 404) src/index.ts browser entry; imports CSS in cascade order, calls each setup*() src/page/ page chrome: one script next to its CSS (layout, header, navigation, search, toc, theme_switch, footer) src/content/ content enhancements (math, mermaid) and content CSS scripts/build.ts esbuild → dist/scripts.js + dist/scripts.css, and dist/tree.js on its own (no code splitting, so it loads as one file) (run by tsx) ``` `packages/share-theme/package.json` exports `./templates/*` and `./model/*` **from source** (core imports the model as `@triliumnext/share-theme/model/page`); everything else resolves to `dist/`. ## The render pipeline 1. **Route** — `handlers.ts` resolves the note through shaca, checks `shareCredentials`, rejects protected notes, and calls `renderNoteContent(note, canAccessEmbed)`. A page route first awaits `ensureShareHighlighting()` (the renderer is synchronous; language registration is not). 2. **Content** — `getContent(note, options)` renders by note type into `{ header, content, isEmpty }`. Text goes through `renderText()` (node-html-parser): include-note embeds via the commons resolver (`resolveContentEmbed`), link previews via commons markup, reference links, inline links via `getShareLink()`, syntax highlighting bounded by `shouldSyntaxHighlight()`. 3. **Template values** — `renderNoteContentInternal()` builds the variables: the legacy ones (`note`, `content`, `subRoot`, `cssToLoad`, `jsToLoad`, `t`, `utils`, `ancestors`, …) plus the page model's (`head`, `snippets`, `logo`, `prevNext`, `navigation`, `childLinks`, `language`, `lastUpdated`, `contentClasses`). 4. **Template** — a `~shareTemplate` (only when backend scripting is enabled) gets those values with the content as it is. The default `page.ejs` additionally gets the output of `preparePageContent()`: content with heading anchors and image `alt`/`loading`, `headings` and `toc`. 5. **Browser** — `tree.js`, the first entry of `jsToLoad` and the only `blocking="render"` one, restores the tree's expansion, scroll position and clicked clone before the first paint. `scripts.js` then wires the expand buttons, search, ToC scroll tracking, theme switch, footer date, math, Mermaid, link previews and tabs. `boot_script.ejs` runs inline in `
` before the first paint (theme class, collapsed panes, `window.glob`). The static export calls `renderNoteForExport()` with `isStatic: true`, which also expands embeds at every depth and drops the login link and the last-updated date. ## The page model (`packages/share-theme/src/model/page.ts`) Everything a template would otherwise compute lives here, as pure functions over `ShareNote`, the structural subset of a note that both `SNote` and `BNote` satisfy (`pnpm typecheck` verifies this; extend the interface rather than importing core types). Its spec runs on plain fake notes (`fakeNote()` / `addChild()` in `page.spec.ts`) — no shaca, no database. | Function | Feeds | | --- | --- | | `getPageHead` | title, description, no-index, OpenGraph (relative image completed with `#shareOpenGraphURL`), `metaTags` | | `getHtmlSnippets` | `~shareHtml` per `#shareHtmlLocation` | | `getSiteLogo` | logo link and size (`#shareLogoWidth/Height` are proportions; drawn 32 px wide) | | `getShareLink` | **the only** link-target rule: first non-blank of `#shareExternalLink`, `#shareExternal`, else `./shareId` — used by the tree, subpages, index and inline links | | `getNavigationTree`, `getSiteAncestorIds` | the tree; expansion follows the first parent **inside the site** | | `getPrevNextLinks` | tree-order previous/next, inside the site; hidden notes get none | | `getTableOfContents` | nests `PageHeading`s from core's `preparePageContent()` | | `getChildLinks`, `getContentClasses` | subpage list, `#content` classes | | `getPageLanguages`, `getLastUpdated` | `` from the display language, `#content lang dir` from the content language when it differs, the date via `Intl` | Rules: - **Templates only print.** A condition belongs in a template only if it tests a value the model already produced (`navigation.length`, `childLinks.length`). Anything that walks notes, reads labels or transforms HTML goes into the model (or into core when it changes `content`). - **Content transforms belong to core**, in the parse `renderText()`/`preparePageContent()` already does — never a regex over HTML in a template. - A clone's "parent" is always the **first parent inside the site** (`getSitePosition()`), never `getParentNotes()[0]`. ## What custom templates are promised `~shareTemplate` notes are copies of an earlier `page.ejs`, documented on the *Custom share template* page of the User Guide. So: - **Never remove or rename a template variable**, even an unused one (`header` is always `""`). Add new ones and list them in that page's table. - Custom templates receive `content` **without** the anchors and image attributes `preparePageContent()` adds: copies of the old template add their own, and doubling them shows twice. `headings`/`toc` are passed to the default template only. - Partials of a custom template resolve from its child notes, not from the theme's templates. ## Browser side - **Every module imports its own CSS**; `index.ts` imports in cascade order (a module's CSS lands where it is first imported). Moving an import moves CSS; compare the bundle's declarations when reordering. - **No inline scripts besides `boot_script.ejs`.** Top-level `const`/`let` in a classic inline script is a global binding shared with `~shareHtml` snippets — keep everything inside the IIFE. `boot_script.spec.ts` enforces both and that `page.ejs` has no `