Skip to main content

developing-share-pages

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.

Datos de origen

Repositorio
TriliumNext/Trilium
Última actividad en el origen
9 de octubre de 2026 a las 20:17
Idioma detectado de SKILL.md
inglés
Estrellas
38.278
Forks
2576

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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 apps/client/vite-plugins.mts shareTheme(): the client and standalone builds bundle the theme ``` `packages/share-theme/package.json` exports `./templates/*` and `./model/*` **from source** (core imports the model as `@triliumnext/share-theme/model/page`). The theme has no build of its own: `shareTheme()` in the client's Vite config writes it into the app's output. - `scripts.js` and its chunks go to `src/`, beside the app's chunks, so the two share what both import (Mermaid, commons, the tabs code). `/share/assets/` maps to that directory: an `express.static` route on the server, and in standalone stubs in `share/assets/` that load their namesakes in `src/`. - `scripts.css` and `tree.js` come from builds of their own, targeting `chrome96`. `scripts.css` keeps the import order of `index.ts`, which the app's chunking would change; `tree.js` is one file, since the first paint waits for it. - `src/share_theme.json` lists the files a page can load, and apart those only Mermaid loads. The static export copies them into `assets/` (`getShareThemeExportFiles()`). - A library the theme should load only on demand, and the export copy only when needed, goes in `LAZY_LIBRARIES` with its own condition in the export. ## 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 `<head>` 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` | `<html lang dir>` 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 `<script>`. - Code that must run before the first paint and needs the DOM goes in `tree.ts`'s bundle, which is render-blocking; keep it small, since every page waits for it. Everything else stays in `scripts.js`, and `~shareJs` scripts never block. - Anything needed before the first paint is driven from the root class the boot script sets (`theme-dark`/`theme-light`, `left-pane-collapsed`) — the theme switch is styled from it, not from `:checked`, so it needs no script to look right. - Look elements up by ID with `getElementById`, not `querySelector("#" + slug)` (a slug may start with a digit). - Code shared with the app comes from commons or ckeditor5 by subpath (`@triliumnext/commons/src/lib/…`, e.g. `enhanceLinkPreviews`, `getMermaidConfig`, `applyTabs`); shared content CSS comes from `packages/ckeditor5/src/theme/`. - Mermaid is a plain `import("mermaid")`, the app's own copy, so diagrams match the app; it redraws on theme change. ## Testing | Layer | How | Command | | --- | --- | --- | | page model | fake notes | `pnpm --filter @triliumnext/share-theme test` | | browser scripts | `// @vitest-environment happy-dom`; set `offsetTop` etc. by hand (no layout) | same | | inline scripts | render the `.ejs`, run in a `node:vm` context | same | | core renderer | shaca fixtures (`buildShareNote`), `getContent()` / `renderNoteContent()` | `pnpm --filter server test src/share` **and** `pnpm --filter standalone test src/share` | **The share code is held at 100% coverage** (lines, branches, functions, statements), and CI fails below it: - the share theme package, by `packages/share-theme/vitest.config.ts` (`--coverage` in the `share-theme` CI step, Codecov flag `share-theme`); - `packages/trilium-core/src/share/**` and `apps/server/src/share/**`, by the per-glob thresholds in `apps/server/vite.config.mts`; core's share code and `lightweight/share_provider.ts` again in `apps/standalone/vite.config.mts`. Each runner must reach 100% on its own; - the merged result, by the `share` project status in `codecov.yml`. Check a change with `--coverage` on the narrowest run, e.g. `npx vitest run share --coverage` in `apps/server` and `apps/standalone`; the threshold errors name the glob that falls short. Other core files print `PARSE_ERROR` noise in that run (untested files are parsed untransformed); it does not fail the run. Cover a branch with a test; remove it only when the types or the callers rule it out. Never delete a public `SNote`/`SAttribute`/`SAttachment` method for coverage: custom templates can call any of them. - The server and standalone setup files load core before a spec's `vi.mock()` runs, so a mock of a module core already imported (icon packs, search) never reaches the renderer or the handlers. Use `vi.spyOn()` on the module namespace or its default export instead, or `vi.resetModules()` with `vi.doMock()` and a dynamic import for a module tested in isolation (the platform adapters). - Core specs that render `page.ejs` with hand-built variables (the subpage-list helper in `content_renderer.spec.ts`) must be given every new variable, or EJS throws `ReferenceError`. Prefer full renders through `renderNoteContent()` for new page-level checks. - **Prove the red run.** For template or model changes, copy the file to the scratchpad, put the `HEAD` version in place (`git show HEAD:<path> > <path>`), run the spec, copy the file back. For a pure refactor, render the old and new template with the same values and compare the output. - node-html-parser elements break Vitest's pretty-printer on failure; assert `querySelector(...) === null` as a boolean, or compare mapped attributes. - Values from a `node:vm` context have another `Object` prototype: use `toEqual`, not `toStrictEqual`. - The share theme's `tsconfig.json` covers specs and browser code alike; a spec needing Node types adds `/// <reference types="node" />`. ## Running and seeing a change - In development, the client's and standalone's Vite servers answer `scripts.js` and `tree.js` from source and `scripts.css` empty (Vite injects the styles), so a script or style change needs no rebuild. To see the built output, run `pnpm client:build`; an export from a development server needs it too, since it copies the files `share_theme.json` lists. - The Node provider **caches each template after the first read**, and core changes need the server to restart: restart the dev server after changing a template or the renderer. - Standalone bundles the templates (`?raw` imports): a **new partial** must be added to `TEMPLATES` in `apps/standalone/src/lightweight/share_provider.ts`; the server reads the template folder by name and needs nothing. ## Documentation User-facing share behavior is documented in `docs/User Guide/User Guide/Advanced Usage/Sharing.md` (feature list, attribute reference, OpenGraph table) and `Sharing/Custom share template.md` (template variables). Update them in the same commit, then run the `writing-documentation` skill's `docs.mjs sync` and `check` **from the repository root** — a shell sitting inside `docs/User Guide` makes the sync fail halfway on Windows and delete hundreds of files. ## Compared with other publishers Obsidian Publish declares every site `lang="en"` (even its Chinese and Japanese help sites) and has no language setting. Quartz takes `lang` from a site locale or a note's frontmatter and formats dates with that locale. Trilium marks the page with the display language and the content with the note's content language separately, so the theme's own texts are not mislabeled.
Ver en GitHub