2026-09-23·by Sijie Wang#standmeet#architecture#decision

rendering-and-extensibility

Rendering & extensibility of synced notes

Parent: obsidian-sync-mechanism

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 requiredpre-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.
about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →