# Share a jupyter nbconvert HTML Export — A Link, Not an Attachment

Canonical: https://commareports.com/share-nbconvert-html
Published: 2026-09-01

> nbconvert --to html gives you one self-contained file. Publish it to Comma with one curl for a stable URL, anchored comments on individual cells, and a revision every time you re-run the notebook.

# Share an nbconvert HTML export

`jupyter nbconvert --to html` is the most reliable thing in the notebook
ecosystem. It takes an `.ipynb` and gives you one file that renders in any
browser, with the plots where you left them.

Then the file sits in your working directory, and the actual question —
how does someone else see this — starts over from scratch. Email it and
it's an attachment nobody can comment on. Push it to a gist and the
scripts don't run. Commit it and the diff is a wall of base64.

## Publish it

```bash
jupyter nbconvert --to html --embed-images analysis.ipynb

curl -X POST https://commareports.com/api/v1/reports \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html analysis.html \
        '{title: "Churn analysis — cohort 2026-Q2", html: $html}')"
```

Or drag `analysis.html` into [the app](https://commareports.com/).

`--embed-images` is the flag that matters. Without it, matplotlib output
is written to a sibling `analysis_files/` directory and the published
page has broken image slots — the classic
[broken-CSS-and-images failure](/html-report-broken-css) of a
self-contained-looking export that isn't.

Add `--no-input` when the reader is a stakeholder rather than a
colleague. The distinction is real: the same notebook is a method
document for one audience and a set of conclusions for the other.

## What the URL changes

- **Comments land on the cell.** "Is this filtering out the trial
  accounts?" pinned to the cell it's about, not paraphrased in Slack
  three days later. See [commenting on HTML](/comment-on-html).
- **Re-runs are revisions.** PATCH the same id when the data refreshes.
  Same URL, new numbers, old numbers still there to compare.
- **Interactive outputs survive.** Plotly, Bokeh, Altair and ipywidgets
  static output all render — scripts run inside a sandboxed iframe
  (`allow-scripts`, no `allow-same-origin`).
- **Private by default.** A notebook usually holds real customer numbers.
  Access is per report — team, email-domain gate, named reviewers, or a
  [password-protected link](/password-protect-html-report). See the
  [sharing model](/docs/sharing).

## On a schedule

The notebook that answers a recurring question should not need you to run
it. A [routine](/docs/routines) executes it on a cron and PATCHes the same
report, so the weekly number is current at a URL people already have —
see [scheduled HTML reports](/features/routines/scheduled-html-reports).

## Limits

- **HTML body: 5 MB.** `--embed-images` inlines every figure as base64,
  so a notebook with fifty plots gets large fast. Downsample the figures,
  or drop `--embed-images` and upload the `_files/` directory alongside as
  assets.
- **Scripts run, sandboxed**: `allow-scripts`, no `allow-same-origin`.
- **60 requests/minute per token.**

## Try it

Comma is free — unlimited reports, unlimited commenters, unlimited
revision history.

**[Create your first report →](https://commareports.com/)**

### Related

- [Share a Jupyter notebook](/share-jupyter-notebook-html) — the whole workflow
- [Papermill runs](/share-papermill-report) — parameterized, on a schedule
- [Jupyter integration](/with/jupyter) · [Publish from CI](/docs/ci)
