# Canvas to HTML
[](https://github.com/sam080626-arch/obsidian-canvas-to-html/actions/workflows/ci.yml)
[](https://github.com/sam080626-arch/obsidian-canvas-to-html/releases)
[](LICENSE)
Export an Obsidian Canvas to **one self-contained HTML file** you can email, drop
in a folder, or open on any machine. The exported file zooms and pans the way the
canvas does inside Obsidian.
No server. No build step for the reader. **No network requests at all** — every
image is inlined, every font reference is stripped, and nothing phones home when
the file is opened.
## Install
### From a release
1. Download `main.js`, `manifest.json`, and `styles.css` from the [latest release](https://github.com/sam080626-arch/obsidian-canvas-to-html/releases/latest).
2. Put all three in `/.obsidian/plugins/canvas-to-html/`.
3. Reload Obsidian, then enable **Canvas to HTML** in Settings → Community plugins.
> If your vault syncs through iCloud, copy the files in. Do not symlink them —
> iCloud does not sync symlinks reliably.
### With BRAT
Add `sam080626-arch/obsidian-canvas-to-html` as a beta plugin in
[BRAT](https://github.com/TfTHacker/obsidian42-brat).
## Usage
Open a canvas and run **Export canvas to HTML** from the command palette, or
right-click a `.canvas` file in the file explorer. The `.html` lands next to the
canvas unless you set an output folder in settings.
### In the exported file
| Input | Action |
|---|---|
| Two-finger scroll / wheel | Pan |
| Ctrl/⌘ + wheel, trackpad pinch | Zoom at the pointer |
| Drag empty space | Pan |
| `+` / `-` | Zoom in / out |
| `0`, double-click empty space | Fit to screen |
| Click an edge | Frame both of its endpoints |
Cards taller than their frame scroll internally. Corner buttons zoom, fit, and
toggle light/dark; the theme choice persists.
## What gets exported
| Canvas node | Becomes |
|---|---|
| Text card | Rendered Markdown |
| Embedded `.md` note | The note's rendered content, one level deep |
| Image | An `
` with an inlined data URI, downscaled past a size limit |
| PDF | A labelled placeholder card — the bytes are never inlined |
| Web link | A clickable link card, not a live iframe |
| Group | A tinted rect with its label, behind the cards |
| Edge | An SVG Bézier with arrowheads, in canvas coordinates |
Callouts, tables, task lists, code blocks with syntax colouring, math, and images
embedded inside notes all survive. The stylesheet mirrors Obsidian's own default
theme values — the type scale, spacing, and the canvas colour palette — so an
export reads like the canvas it came from.
## Settings
| Setting | Default |
|---|---|
| Output folder | Next to the source canvas |
| Maximum image dimension | 2000 px |
| Image re-encode quality | 0.85 |
| Size warning threshold | 25 MB |
| Default theme in the export | Follow the reader's system |
| Open the file after exporting | Off |
## Known limitations
- **Mermaid diagrams** export as a source code block until you trust the vault in
Obsidian, because Obsidian itself will not render them before that.
- **Math** renders, but MathJax's web fonts are deliberately not embedded (they
would add megabytes), so glyph metrics differ slightly from Obsidian. If a
formula fails to render at all, its TeX source is exported instead of a blank.
- **Embeds are followed one level only.** An `![[embed]]` inside an embedded note
is not expanded.
- **Community themes are not reproduced.** The export matches Obsidian's default
theme.
- Wikilinks to notes that were not exported render as dotted, unclickable text
with the target in the tooltip.
## Development
```bash
npm install
npm run dev # watch build
npm test # vitest
npm run lint # the same rules Obsidian's plugin validator applies
npm run build # lint + typecheck + production bundle
```
Two bundles are produced: the plugin (`main.js`) and the viewer runtime, which is
inlined into the plugin as a string at build time and written into every export.
### Architecture
Logic lives in pure modules; the Obsidian-coupled modules are thin and take their
dependencies through injected interfaces, so the whole pipeline is testable
without the app.
| Module | Responsibility | Obsidian API |
|---|---|---|
| `canvas-model.ts` | Parse `.canvas` JSON into a `Scene`, compute world bounds | no |
| `edges.ts` | Bézier routing and arrowhead geometry | no |
| `assets.ts` | Image sizing, encoding, data URIs, caching | no |
| `math.ts` | TeX source extraction, MathJax CSS sanitation | no |
| `clean-rendered.ts` | Strip app-only chrome, scrub links, carry math across sanitization | no |
| `serialize.ts` | Assemble the exported document | no |
| `viewer/transform.ts` | Zoom/pan/fit maths | no |
| `viewer/viewer.ts` | The runtime shipped inside each export | no |
| `resolve.ts` | Canvas nodes → renderable HTML | injected |
| `render-markdown.ts` | `MarkdownRenderer` + sanitization + math | yes |
| `main.ts` | Commands, menus, settings, orchestration | yes |
### Testing
159 tests across unit, golden-file, and jsdom suites:
```bash
npm test
```
The jsdom suites drive the exported viewer with real `wheel`, pointer, and
keyboard events and assert the resulting transform, because that layer cannot be
covered by unit tests alone. `MANUAL-VERIFICATION.md` lists what still needs a
human in real Obsidian — renderer fidelity, performance, and cross-browser checks.
Design notes and the implementation plan live in [`docs/superpowers/`](docs/superpowers/).
### Releasing
```bash
npm version patch # bumps package.json, manifest.json, versions.json together
git push --follow-tags
```
Pushing the tag runs the release workflow, which verifies the tag matches
`manifest.json`, builds, and attaches `main.js` and `manifest.json` to a GitHub
release — the layout Obsidian expects.
## License
MIT © 朱劭恩