Isolation boundary
Build pipeline
Pages in this vault (file nodes)

Sandbox Tour

A native JSON Canvas page, rendered by @quartz-community/canvas-page at build time and made pannable and zoomable in the browser.

This canvas is content, not configuration: it lives at vault/experiments/sandbox-tour.canvas.

Why a canvas here

Canvas pages exercise a page type plugin rather than a transformer: the emitter produces an HTML route for a file that is not Markdown at all.

The canvas entry under layout.byPageType drops the table of contents and the right sidebar for these routes, because a canvas has no headings to list.

Shared engine

/home/loca/dev/quartz/quartz

branch v5 @ fd720a71

Builds 51 wikis. Off limits: never built, never installed into, never reconfigured from the sandbox.

Sandbox engine

sandbox/engine

pinned 5.0.0 @ 3dff48b5

Own node_modules (328 packages), own quartz.config.yaml, own generated plugin index.

Nothing crosses

No shared cache. No shared lockfile. No shared config. No shared dist. No shared origin port.

1. vault/

Markdown, .canvas and .base content — the pages you are browsing.

2. plugins/

Local sandbox-only plugins, resolved as ../plugins/<name> relative to the engine working directory.

3. build

npm run install-plugins regenerates the plugin index, then bootstrap-cli.mjs build emits HTML.

4. dist-<epoch>/

Immutable output directory. The live symlink swap is a separate, deliberate step.

Quartz Sandbox

This site is the experiment. Everything you see here is rendered by a dedicated Quartz checkout that exists for one purpose: to try out the latest Quartz plugins, layouts and rendering features on a real site, on this host, without any risk to the wikis that people actually depend on.

The documentation moved

This host used to serve the upstream Quartz documentation at the root of quartz.loca.zone. That full documentation site now lives at https://wiki.quartz.loca.zone/ — 223 pages, byte-identical route set to the old root release. The root you are reading now is the sandbox.

What is running here

PropertyValue
Enginehttps://github.com/jackyzha0/quartz.git
Branchv5 (upstream default branch)
Commit3dff48b5df6d84c9544a5ae19c8f2cbb01dc44e5
package.json version5.0.0
Node/usr/bin/node v22.23.1
npm/usr/bin/npm 10.9.8
Plugin distributionnpm packages @quartz-community/* (35 installed)
Content rootsandbox/vault/ (this vault)

The pinned commit is on top of release tag v5.0.0 (7284f12ebf6b24e69b6cc9417e71b7d26700dc17, an ancestor of the pin). Nothing here floats: the commit is fixed, npm ci installed from a committed lockfile, and the plugin index is regenerated deterministically from the sandbox config.

This is isolated from the shared engine

There is a second, completely separate Quartz on this machine at /home/loca/dev/quartz/quartz. It is the shared engine: 51 wikis are built with it. Breaking it breaks all 51. The sandbox cannot break it, and the reasons are concrete and structural rather than procedural:

Isolation boundaryShared engineThis sandbox
Checkout/home/loca/dev/quartz/quartz/home/loca/dev/quartz-sandbox/sandbox/engine
node_modulesits own install treeits own npm ci tree (328 packages)
Configurationits own quartz.config.yaml + quartz.tssandbox/engine/quartz.config.yaml (this site’s)
Build outputwiki dist trees under /home/loca/dev/wikissandbox/dist-* only
Origin portits own nginx upstreams127.0.0.1:51947, served only for this site
Local pluginsnone of the sandbox’ssandbox/plugins/*, resolved relatively

So: separate checkout, separate node_modules, separate config, separate dist, separate origin port. A plugin enabled here is not installed there. A style written here is not compiled there. A build run here writes only into sandbox/dist-*. There is no shared cache, no shared lockfile and no shared config file between the two trees.

Every experiment below is additionally labelled safe_for_shared_engine. That label answers only one question — “would this change, if someone later chose to make it, be low risk for the shared engine?” It is not permission to edit the shared engine. Nothing in this sandbox ever does.

flowchart LR
  subgraph shared["shared engine (off limits)"]
    SE["/home/loca/dev/quartz/quartz<br/>branch v5 @ fd720a71"]
    W["51 wikis<br/>/home/loca/dev/wikis"]
    SE --> W
  end
  subgraph sbx["sandbox (this site)"]
    EN["engine/<br/>pinned 5.0.0 @ 3dff48b5"]
    VA["vault/<br/>showcase content"]
    PL["plugins/<br/>local sandbox plugins"]
    DI["dist-*/"]
    VA --> EN
    PL --> EN
    EN --> DI
  end
  DI --> ORIGIN["127.0.0.1:51947"]
  ORIGIN --> NG["nginx: quartz.loca.zone"]
  W --> NG2["nginx: 51 wiki vhosts"]

Each page states what it demonstrates, the exact configuration or content that produces it, what the rendered HTML proves, and its safe_for_shared_engine verdict.

ExperimentDemonstrates
Mermaid diagramsmermaid fences promoted to client-rendered diagrams
LaTeX with KaTeXinline and display math via the latex transformer
Callouts and nestingObsidian callouts, folding, arbitrary nesting
Wikilinks, transclusion, block refslink resolution, embeds, ^block-id references
Native canvas pagesJSON Canvas .canvas files as first-class pages
Native Bases pagesObsidian .base files as server-rendered database views
Graph and explorergraph tuning and file-tree explorer configuration
Search, RSS, sitemapfull-text search index plus content-index emitters
Syntax highlighting themesbuild-time Shiki themes, light and dark
Alias redirectsaliases frontmatter emitting redirect routes
Properties view and layout groupsfrontmatter property view, toolbar groups, byPageType
Unlisted and encrypted pagespages hidden from indexes, and password-encrypted pages
Custom SCSS themingsandbox-only styling through variables.scss
Local sandbox pluginsa transformer and an emitter loaded from ../plugins

Reference pages

  • Experiments index — the same list with status and grouping.
  • Sandbox configuration — the annotated config, and every difference from the upstream default.
  • Plugin roster — every plugin in the final config: kind, order, and why it is enabled or disabled.

Rules this sandbox lives by

  1. The shared engine at /home/loca/dev/quartz/quartz is never read-write, never built, never npm installed, never re-configured.
  2. /home/loca/dev/wikis is never touched by a sandbox build.
  3. Builds write only to sandbox/dist-<epoch> directories; the live symlink swap is a separate, deliberate step.
  4. Third-party analytics and comment providers stay off. See the config page for the removed analytics block.
  5. If an experiment would require a change inside the shared engine, the experiment is reported as blocked instead of performed.

Sandbox configuration

This page describes the actual final sandbox/engine/quartz.config.yaml, not a proposed configuration. The source file starts from the pinned checkout’s quartz.config.default.yaml; the changes below turn it into a deliberately self-describing showcase.

Global configuration

configuration:
  pageTitle: Quartz Sandbox
  pageTitleSuffix: ' — Quartz Sandbox'
  enableSPA: true
  enablePopovers: true
  locale: en-US
  baseUrl: quartz.loca.zone
  ignorePatterns:
    - private
    - templates
    - .obsidian

Differences from upstream default:

  • pageTitle changes from Quartz 5 to Quartz Sandbox.
  • pageTitleSuffix changes from empty to — Quartz Sandbox, making browser titles self-identifying.
  • baseUrl changes from quartz.jzhao.xyz to quartz.loca.zone so canonical URLs, OG metadata, sitemap and RSS point at this host.
  • The upstream analytics block is removed: this host sends no third-party analytics.
  • The pinned default theme remains the base palette; custom.scss adds only mode-aware, scoped sandbox accents.

Plugin source model

The pinned v5 checkout loads community plugins from npm package names:

  - source: "@quartz-community/obsidian-flavored-markdown"
    enabled: true
    order: 30

This is intentionally not the older github:quartz-community/... source syntax used by the shared engine. The two local plugins use a different, explicit boundary:

  - source: ../plugins/sandbox-reading-stats
    enabled: true
    order: 900
    options:
      wordsPerMinute: 220
      locale: en
  - source: ../plugins/sandbox-manifest
    enabled: true
    order: 900
    options:
      fileName: sandbox-manifest.json
      pretty: true

Those paths resolve relative to the engine working directory and therefore land in sandbox/plugins, never in the shared checkout. After changing this list, the generated .quartz/plugins/index.ts is regenerated with PATH=/usr/bin:/bin /usr/bin/npm run install-plugins before building.

Enabled feature families

FamilyEnabled entriesReason
MarkdownObsidian Flavored Markdown, GitHub Flavored Markdown, Crawl Links, LaTeX, Syntax Highlighting, Description, Created/Modified DateEvery experiment page exercises at least one transform.
Native page typesCanvas Page, Bases PageReal .canvas and .base routes live in experiments/.
DiscoveryExplorer, Graph, Search, Backlinks, Breadcrumbs, Table of ContentsThe sandbox should be navigable and inspectable.
Metadata/visibilityNote Properties, Alias Redirects, Unlisted Pages, Encrypted PagesThese pages show both positive and negative discovery cases.
EmittersContent Index (sitemap + RSS), OG Image, local Sandbox ManifestMachine-readable output is part of the experiment, not an afterthought.
Navigation chromePage Title, Article Title, Content Meta, Dark Mode, Reader Mode, Footer, Favicon, Quartz Fonts, SpacerThe showcase uses a complete but restrained Quartz frame.

The exact order, options, layout entries, disabled rows and reasons are maintained in the generated roster page. The roster is the source-derived human view; the YAML file is the executable truth.

Layout differences

The toolbar group is explicit so its controls stay together:

layout:
  groups:
    toolbar:
      priority: 35
      direction: row
      gap: 0.5rem

breadcrumbs uses condition: not-index, so the home page does not display a redundant Home → Home trail. byPageType clears the right rail for canvas and bases routes, and removes reader mode from folder/tag pages where that narrow view is not useful. See the layout experiment for the rendered comparison.

Explicitly disabled

No analytics, comments, citations, hard-line-breaks, ox-hugo, roam, recent-notes, stacked-pages, tag-list, tag-page, cname, explicit-publish, quartz-themes/core or remove-draft? No: remove-draft stays enabled so draft pages cannot accidentally enter this public showcase. The full enabled/disabled truth table is on the plugin roster.

Isolation checklist

  • Checkout: sandbox/engine, pinned v5.0.0 commit.
  • Dependencies: sandbox/engine/node_modules from npm ci.
  • Content: sandbox/vault, never /home/loca/dev/wikis.
  • Output: a fresh sandbox/dist-<epoch> directory per build.
  • Origin proof: private loopback port 51947 belongs to the sandbox runtime; this showcase’s browser proof uses a separate private port in the 51960–51990 range.
  • Shared-engine baseline is checked before and after the build and remains branch v5, commit fd720a71db0ca16f885e75fe0d689a574960e3d9, with only its two pre-existing dirty paths.

If a feature needs a source change in the shared checkout, it is blocked rather than copied or monkey-patched here.

Plugin roster

This table is the human-readable projection of the final sandbox/engine/quartz.config.yaml. kind is the installed package manifest’s Quartz category; when a package declares multiple categories they are all listed. order is the configured order (a dash means the entry has no explicit order and therefore uses its manifest/default ordering). No row is aspirational: every enabled row is in the build that produced this site.

Enabled

SourceKindOrderWhy enabled
@quartz-community/created-modified-datetransformer10Stable dates in metadata and Bases formulas.
@quartz-community/note-propertiestransformer, component5Visible selected frontmatter properties.
@quartz-community/syntax-highlightingtransformer20Build-time light/dark code highlighting.
@quartz-community/obsidian-flavored-markdowntransformer30Wikilinks, callouts, mermaid, embeds, tasks and block refs.
@quartz-community/github-flavored-markdowntransformer40Tables, strikethrough and GFM task/list syntax.
@quartz-community/table-of-contentstransformer, component50Heading navigation on Markdown pages.
@quartz-community/crawl-linkstransformer60Shortest internal-link resolution and broken-link gate.
@quartz-community/descriptiontransformer70Derived descriptions for pages without one.
@quartz-community/latextransformer80KaTeX inline/display math.
@quartz-community/remove-draftfilterKeeps draft-marked content out of this public build.
@quartz-community/alias-redirectsemitter50Emits the legacy alias route used by an experiment.
@quartz-community/content-indexemitter50Emits content index, sitemap and RSS.
@quartz-community/faviconemitterSupplies the default Quartz favicon route.
@quartz-community/og-imageemitter50Generates per-page social preview images.
@quartz-community/canvas-pagepageType, component50Emits the native JSON Canvas page.
@quartz-community/content-pagepageType, componentStandard Markdown content frame.
@quartz-community/folder-pagepageType, componentGenerated folder listing pages.
@quartz-community/tag-pagepageType, componentGenerated tag listing pages.
@quartz-community/bases-pagetransformer, pageType, component50Emits the native .base database views.
@quartz-community/explorercomponent50File-tree navigation.
@quartz-community/graphcomponent50Local/global graph navigation.
@quartz-community/searchcomponent50Full-text search toolbar.
@quartz-community/backlinkscomponent50Reverse-link panel.
@quartz-community/article-titlecomponentArticle heading component.
@quartz-community/content-metacomponentDate/metadata line below titles.
@quartz-community/page-titlecomponentSite title in the left rail.
@quartz-community/darkmodecomponentLight/dark mode toggle.
@quartz-community/reader-modecomponentReader-mode toolbar toggle.
@quartz-community/breadcrumbscomponentConditional breadcrumb trail.
@quartz-community/footercomponentSandbox and wiki links in the footer.
@quartz-community/quartz-fontstransformer, emitterQuartz font resources.
@quartz-community/spacercomponent25Mobile-only left-rail spacing.
@quartz-community/unlisted-pagestransformer45Removes explicitly unlisted pages from indexes.
@quartz-community/encrypted-pagestransformer, emitter900AES-encrypts the demo body and emits its shadow index.
../plugins/sandbox-reading-statstransformer900Sandbox-only article word/minute badge.
../plugins/sandbox-manifestemitter900Sandbox-only published-content manifest.

Disabled

SourceKindOrderWhy disabled
@quartz-community/citationstransformer85No citation data in this showcase.
@quartz-community/hard-line-breakstransformer90Not part of the showcase’s Markdown contract.
@quartz-community/ox-hugotransformer91No ox-hugo source files.
@quartz-community/roamtransformer92No Roam-specific syntax.
@quartz-themes/coretransformerKeep sandbox SCSS and pinned palette explicit.
@quartz-community/explicit-publishfilterThis vault uses public-by-default content plus explicit unlisted/encrypted demos.
@quartz-community/commentscomponentNo third-party comment service or credentials.
@quartz-community/tag-listcomponentTag pages and frontmatter already demonstrate tags without extra chrome.
@quartz-community/recent-notescomponentThe Bases recent-notes view is the deliberate discovery example.
@quartz-community/stacked-pagescomponentNot needed; avoids competing page stacking with SPA navigation.
@quartz-community/cnameemitterThis host is not GitHub Pages; nginx owns the hostname.

Options that matter

  • crawl-links uses markdownLinkResolution: shortest, disableBrokenWikilinks: true and externalLinkIcon: true; the built proof found no unresolved internal anchors.
  • content-index uses enableSiteMap: true, enableRSS: true, rssLimit: 20 and rssFullHtml: false.
  • og-image uses the light-mode 1200×630 default dimensions and includes the root page.
  • encrypted-pages uses passwordField: password, 600,000 PBKDF2 iterations and static/encryptedContentIndex.json.
  • note-properties includes only description, tags and aliases.
  • graph and explorer options are shown verbatim on their experiment page.
  • The two relative local entries above are the exact block sent by SandboxPlugin and merged verbatim; their effects are proved on the local plugin page.

Safe boundary

Enabling a plugin in this table never changes /home/loca/dev/quartz/quartz. The package source tree, generated plugin index, config, node_modules, vault and dist all belong to the sandbox checkout. See the isolation statement and the annotated config.

Native Bases pages

What this demonstrates

@quartz-community/bases-page renders Obsidian Bases.base files — as database-like views over the vault’s own notes. It is the most ambitious plugin in this sandbox: it ships a small expression language (lexer, Pratt parser, bytecode compiler and stack interpreter) so that .base formulas evaluate at build time.

The base in this vault queries this very site: every Markdown page, grouped by area, with computed columns.

Open it: Sandbox notes base

Configuration used

  - source: "@quartz-community/bases-page"
    enabled: true
    options:
      defaultViewType: table
      linkResolution: shortest
    order: 50

linkResolution must match crawl-linksmarkdownLinkResolution (both shortest here), otherwise links rendered inside view cells resolve differently from links in prose.

Layout override for this page type:

layout:
  byPageType:
    bases:
      exclude:
        - table-of-contents
        - reader-mode

A base view has no prose headings, and reader mode’s narrow column fights a wide table.

Content used

filters:
  and:
    - file.ext == "md"
formulas:
  area: |
    if(file.inFolder("experiments"), "experiment",
    if(file.inFolder("config"), "configuration",
    if(file.inFolder("plugins"), "plugins", "entry")))
  updated: file.mtime.relative()
  tag_count: file.tags.length()
properties:
  title:
    displayName: Page
  formula.area:
    displayName: Area
views:
  - type: table
    name: Every sandbox page
    groupBy:
      property: formula.area
      direction: ASC
    sort:
      - property: formula.area
        direction: ASC
  - type: table
    name: Experiments only
    filters:
      and:
        - file.inFolder("experiments")
  - type: list
    name: Recently updated
    limit: 10
  - type: cards
    name: Cards
    limit: 24

Four views over one dataset: a grouped table, a filtered table, a capped list and a cards view. The reader switches between them with tabs; all four are materialised in the HTML.

What the rendered output proves

The base route emits a rendered table DOM — header cells carrying the displayNames from properties, one row per matching note, and computed cells for formula.area, formula.updated and formula.tag_count — plus a view switcher with the four view names. Because the views are server-side rendered at build time, the rows are in the static HTML: no client-side query runs.

It also proves the filter and the visibility interaction: experiments/unlisted-demo.md is a Markdown file in experiments/, so file.ext == "md" matches it, yet it is absent from every view because bases-page honours the unlisted convention and skips unlisted pages in both the entry loop and the internal lookup used by .asFile().

safe_for_shared_engine

false — not because it is fragile, but because it is the largest new surface here: a whole expression engine plus four view renderers evaluated for every matching note, and its views are baked at build time (they never re-hydrate client-side, unlike graph, explorer and search). Adopting it across 51 wikis would change build cost and introduce a second link resolution setting that must be kept in sync with crawl-links. Sandbox-only until someone measures it there.

Mermaid diagrams

What this demonstrates

@quartz-community/obsidian-flavored-markdown recognises a fenced code block whose language is mermaid and rewrites it at build time. The emitted HTML is not an image: it is a <code class="mermaid"> element carrying the diagram source in a data-clipboard attribute, wrapped with an expand button and a fullscreen #mermaid-container dialog. The mermaid library then draws the SVG in the browser.

That split matters. The build is fast and deterministic (no headless rendering during build), and the diagram source stays in the page — copyable, diffable, and accessible to text search — instead of being flattened into a binary.

Configuration used

  - source: "@quartz-community/obsidian-flavored-markdown"
    enabled: true
    options:
      enableInHtmlEmbed: false
      enableCheckbox: true
      mermaid: true
      callouts: true
      parseTags: true
      parseBlockReferences: true
    order: 30

mermaid: true is the plugin default; it is written out explicitly here so this page states its own contract rather than relying on an upstream default that could change.

Content used

```mermaid
flowchart TD
  A["quartz.config.yaml"] --> B["install-plugins"]
  B --> C["plugin index (.quartz/plugins/index.ts)"]
  C --> D["bootstrap-cli.mjs build"]
  D --> E["dist-<epoch>/"]
```

Rendered result

flowchart TD
  A["quartz.config.yaml"] --> B["install-plugins"]
  B --> C["plugin index (.quartz/plugins/index.ts)"]
  C --> D["bootstrap-cli.mjs build"]
  D --> E["dist-&lt;epoch&gt;/"]

A second diagram, to show that more than one per page works and that a different mermaid grammar (sequence) is passed through untouched:

sequenceDiagram
  participant O as Operator
  participant B as build
  participant D as dist
  O->>B: install-plugins
  B->>B: regenerate plugin index
  O->>B: bootstrap-cli build
  B->>D: emit HTML routes
  D-->>O: serve on 127.0.0.1

What the rendered output proves

The emitted HTML for this page contains class="mermaid" on a <code> element, a data-clipboard attribute holding the exact diagram source, an aria-label="Expand mermaid diagram" button and a #mermaid-container dialog — all present in the static HTML with no JavaScript executed.

Drawing the SVG is a client-side step performed by the mermaid library, so a browser with no route to mermaid’s module CDN will show the diagram source and the container, but no <svg>. The build-time contract (class="mermaid" + data-clipboard + expand control) is what this sandbox asserts, because that is the part the engine is responsible for.

safe_for_shared_engine

true — it is a per-page content feature of a transformer the shared engine already enables by default, and pages without mermaid fences emit byte-identical HTML.

LaTeX with KaTeX

What this demonstrates

@quartz-community/latex with renderEngine: katex converts $...$ and $$...$$ into KaTeX’s HTML + MathML output during the build. The browser receives finished markup and a stylesheet; it does not download or run a math library, and it does not reflow the page after load.

Configuration used

  - source: "@quartz-community/latex"
    enabled: true
    options:
      renderEngine: katex
    order: 80

Content used

Inline: the sandbox serves $n = 1$ site while the shared engine serves $n = 51$.
 
$$
T_\text{total} = \sum_{i=1}^{n} \left( t_\text{parse}^{(i)} + t_\text{render}^{(i)} \right)
$$
 
$$
\begin{aligned}
  \text{isolation} &= \text{checkout} \times \text{node\_modules} \times \text{config} \\
                   &\quad \times \text{dist} \times \text{port}
\end{aligned}
$$

Rendered result

Inline: the sandbox serves site while the shared engine serves .

A displayed sum, because build cost scales with page count and not with plugin count:

An aligned environment, to show that KaTeX’s multi-line environments survive the transform:

A fraction and a matrix, exercising nested groups:

What the rendered output proves

The emitted HTML contains class="katex" spans, a <math MathML subtree (KaTeX emits both an accessible MathML copy and visually styled HTML), and katex-display wrappers for the $$ blocks. Because all of that is in the static HTML, the math is present with JavaScript disabled — the assertion for this page is the presence of katex and katex-display in the DOM dump, not a screenshot.

safe_for_shared_engine

true — pages with no $ math are unaffected, and the only global cost is the KaTeX stylesheet, which the engine already ships behind this plugin.

Callouts and nesting

What this demonstrates

@quartz-community/obsidian-flavored-markdown turns > [!type] blockquotes into <blockquote class="callout" data-callout="type"> with a generated title row, an icon and — for the +/- variants — a collapsible body. Nesting works because the transform runs on the blockquote tree, so a callout inside a callout is just a nested blockquote.

Callouts carry real information architecture value: the type is in the DOM as data-callout, so a reader’s stylesheet, a print sheet or a scraper can distinguish a warning from an aside without parsing prose.

Configuration used

  - source: "@quartz-community/obsidian-flavored-markdown"
    enabled: true
    options:
      enableInHtmlEmbed: false
      enableCheckbox: true
      mermaid: true
      callouts: true
      parseTags: true
      parseBlockReferences: true
    order: 30

Content used

> [!note] A note with a custom title
> 
> Body text.
 
> [!warning]- Collapsed by default
> 
> Hidden until the reader expands it.
 
> [!tip] Outer
> 
> > [!example] Nested one level
> > > [!quote] Nested two levels

Rendered result

A note with a custom title

The title after the type is arbitrary text. The type still lands in the DOM as data-callout="note", so styling and scraping stay type-driven.

Explicitly expanded

A trailing + marks the callout collapsible but open on load.

Nesting, three levels deep, each level a distinct type:

Outer tip

The outer body renders normally.

Nested example

A callout inside a callout.

Nested quote

And one inside that. Each level gets its own data-callout value, so the nesting is structural rather than cosmetic.

The remaining built-in types, to prove the icon and colour map is complete rather than a subset:

Abstract

Summary-style callout.

Question

Interrogative callout.

Success

Positive-outcome callout.

Failure

Negative-outcome callout.

Danger

High-severity callout.

Bug

Defect callout.

Info

Neutral informational callout.

A callout containing other block content — list, checkboxes, code and math — to show the body is a full markdown context, not a text span:

Mixed content inside a callout

  • a list item
  • a checked task (enableCheckbox: true)
  • an unchecked task
PATH=/usr/bin:/bin /usr/bin/npm run install-plugins

Inline math still works here: .

What the rendered output proves

The emitted HTML contains multiple data-callout="..." attributes with distinct values (note, warning, tip, example, quote, abstract, question, success, failure, danger, bug, info), class="callout" blockquotes, callout-title rows, and is-collapsed on the - variant. The nested case shows a data-callout blockquote inside another data-callout blockquote in the DOM tree.

safe_for_shared_engine

true — callouts are opt-in per paragraph, already the transformer’s default, and add no global assets beyond the stylesheet the plugin always ships.

isolated byno shared statebuilds viaemitsproduces