Rendering & extensibility of synced notes

Updated · View the entry on sijie.xyz ↗

How StandMeet supports the presentation / extensibility layer of the notes it syncs (theorem callouts, math, diagrams, dynamic widgets) — the settled design.

Principle: content portable (write once) + presentation per host

Obsidian plugins are not runnable outside Obsidian, and even Obsidian's own Publish can't run them (it's a browser app — only core rendering + CSS survive). So StandMeet mirrors the portable markdown conventions, never imports Obsidian's plugins/CSS:

  • sync carries only portable markdown — callouts > [!theorem], KaTeX $$, wikilinks [[X]] — which degrade to blockquotes/plain text anywhere;
  • StandMeet is one renderer of that format (like Obsidian is another); presentation is defined per host (Obsidian: a CSS snippet; StandMeet: its own CSS).

Standard conventions → rendered natively

StandMeet's markdown pipeline (app/src/components/page/markdown.tsx, already KaTeX + mermaid) adds:

  1. Callouts — a remark/rehype transform: > [!theorem] Theorem 1 → <div class="callout" data-callout="theorem">, with the DOM aligned to Obsidian's so the CSS is nearly shared (the theorem box is defined once, applied on both);
  2. KaTeX (already) and wikilinks → corpus resolution (already — so a > Prereq: wikilink resolves to that concept's corpus entry).
  3. StandMeet ships its own CSS for the theorem/definition/proof boxes.

Dynamic content → a sandboxed iframe

Anything needing live JS is not a plugin import — it's a standmeet-widget fenced block whose descriptor mounts a sandboxed iframe (iframe + postMessage, the Figma / VS-Code-webview model). Thin host-defined boundary (a manifest: URL/bundle + capabilities + sizing; a postMessage schema: render-data in, resize/event/capability-request out; sandbox permissions) + plugin-defined internals; isolation-first, same principle as sandbox-js-hardening / connector-egress-guard.

Two honest tradeoffs of the iframe choice: performance — each iframe is a separate browsing context, so iframes are for rich/interactive widgets while lightweight bits (a formula) stay inline via remark/rehype; theming — iframe content doesn't inherit host CSS (the cost of isolation), so the theme passes through the message protocol / CSS variables.

Where the extra info lives (three tiers)

  • static / pre-rendered (e.g. a Dataview Publisher–baked table) → straight into the body, no extra info; prebaked HTML has its own fence, standmeet-html, sanitized then rendered (StaticHtmlBlock, d89603a65, 2026-07-06);
  • per-block dynamic widget → a fenced standmeet-widget block (descriptor inside: type/src/params/sandbox/height/seo); shipped 2026-07-06 (93c91da80, app/src/components/page/WidgetBlock.tsx). Recognition is render-side only (BLOCK_RENDERERS, markdown.tsx:58) — the importer passes the fence through as body text untouched, unlike the image-ref rewrite;
  • note-level config → frontmatter (standmeet-* keys).

What about Obsidian's plugin ecosystem? (the taxonomy)

  • authoring helpers (Templater, QuickAdd) — irrelevant: they run while writing and leave plain markdown; we just ingest it;
  • rendering plugins (KaTeX, Mermaid, TikZJax) — don't use the plugin, use the same underlying JS library directly (rendering-engines);
  • query plugins (Dataview) — do it natively and stronger: the corpus is a real Postgres DB + frontmatter + note_refs, so corpus queries beat Dataview-over-files; the first native query surface is the SDK CorpusWidget query language (query="path:math/** sort:title limit:5" — subtree · sort · cap; 113ba6a1a, 2026-09-06, sdk/packages/react/src/widgets/CorpusWidget.tsx);
  • when a plugin is genuinely required → pre-render on the Obsidian side at export: let the plugin run where it works and ingest the rendered output (Dataview → static table, Templater → expanded text), not the raw block — fits the owner-triggered export model (vault-ingestion);
  • the one exception: code execution (Jupyter-style) → route through the hardened sandbox, never a plugin import.

Two non-negotiables

  1. Widget content is user-provided → renders through the iframe sandbox + is subject to corpus ACL;
  2. SEO — iframe widgets aren't indexed (the schema's seo_indexed is now published, corpus_notes.published); SEO-critical content is server-rendered, not a widget.

Why (ties to the vault's decisions)

  • No Obsidian dependency / no lock-in — mirror conventions, don't import plugins (the CSS-over-plugins lesson);
  • content once, presentation twice — matches vault-ingestion (single vault, publish-gated) and "markdown = content, CSS = presentation";
  • graceful degradation — without the CSS a callout is still a readable blockquote.

Related notes