# Share an Interactive HTML Report — Charts That Still Work at the Link

Canonical: https://commareports.com/interactive-html-reports
Published: 2026-08-21

> Most ways to share an HTML report kill its JavaScript: email strips it, Confluence disables the macro, Jenkins' CSP blocks it, GitHub renders markdown. Comma stores report HTML verbatim and runs it in a sandboxed iframe — Chart.js, Plotly and D3 dashboards keep working behind a private link.

# Share an interactive HTML report

You built a dashboard. It has a chart that filters when you click the
legend, a table that sorts, three tabs. It works perfectly in
`file:///Users/you/report.html`.

Then you try to give it to someone.

- **Email** strips the scripts, or the client refuses the attachment
  outright for having them.
- **Slack** shows a filename. The recipient downloads it, their browser
  warns them, they open it, and now there are two copies drifting apart.
- **CI artifacts** hand back a zip behind a login, on a retention clock.
- **Confluence** disabled the HTML macro years ago — most Cloud
  instances can't render one at all without an extra app.
- **GitHub** renders markdown in a job summary and `.ipynb` as static
  output; a job summary strips scripts and iframes outright.
- **Jenkins** serves HTML Publisher output under a
  `Content-Security-Policy` that blocks inline script and style, which is
  why your report shows up as unstyled markup.
- **GitHub Pages** works — and publishes it to the entire internet,
  unless you're on Enterprise Cloud.

So people screenshot the chart into Slack and the interactivity, which
was the whole point, never leaves the laptop it was built on.

## What Comma does instead

Comma stores report HTML **verbatim** — scripts, inline handlers,
embedded data and all — and renders it inside an iframe with
`sandbox="allow-scripts"` and **no** `allow-same-origin`.

That combination is the whole design. The frame gets an _opaque origin_:
your report's JavaScript executes normally, but it cannot read the app's
DOM, cookies, `localStorage`, or session — there is no same-origin
relationship to exploit. The sandbox is the security boundary, so the
HTML doesn't have to be scrubbed to be safe to host. An earlier version
of the API did strip `<script>` on ingest; it broke interactive reports
without adding anything the sandbox wasn't already covering, so it's
gone.

Practically: a Chart.js dashboard is a Chart.js dashboard at the link.

```bash
curl -fsS -X POST https://commareports.com/api/v1/reports \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html dashboard.html \
        '{title: "Q3 pipeline dashboard", html: $html}')"
```

The response carries the report id and its share URL. Or skip the curl
and drag the file into the app — same result.

## What works, and the one thing that doesn't

**Works:**

- Inline `<script>`, inline event handlers, ES modules in the document.
- Chart.js, Plotly, D3, ECharts, Vega-Lite — inlined or loaded from a
  CDN. Subresources fetched inside the sandboxed frame aren't governed by
  the app's CSP.
- Tabs, accordions, sortable tables, client-side filters, `<details>`,
  CSS animation, responsive media queries.
- Reports exported by tools that produce one self-contained file:
  [Lighthouse](/share-lighthouse-report), [pytest-html
  `--self-contained-html`](/share-pytest-report), [ydata-profiling](/share-eda-report),
  [Quarto with `embed-resources: true`](/with/quarto),
  [nbconvert](/with/jupyter), `dbt docs generate --static`.

**Doesn't work:** a report that fetches a sibling file at view time.

An opaque origin has no base URL, so `fetch("data/results.json")` has
nothing to resolve against. [Allure](/share-allure-report) is the classic
case — its UI is a single-page app that loads its results over XHR, which
is also why it's blank when you open it over `file://`. Two fixes:

1. **Inline the data.** Most generators have a self-contained mode. It's
   one flag and it makes the artifact durable everywhere, not just here.
2. **Upload the siblings as assets.** Drop the whole directory (or a zip
   of it) in, and relative `src`, `href`, `srcset` and `url()` references
   are rewritten to the uploaded copies — enough for `<script src>`,
   stylesheets, fonts and images, though not for a runtime `fetch`.

Size: the HTML body is capped at **5 MB**; assets are 25 MB per file,
250 MB and 500 files per report.

## The part that isn't just hosting

A static host gives you a URL. What it can't give you is the sentence
someone was about to type in Slack.

Comma renders the report inside that sandbox with an [anchored comment
layer](/comment-on-html) on top. A reader highlights the row, the
paragraph, or the chart caption and pins a thread to it — "this cohort
is double-counted after the July migration" lands _on_ the number it
describes. Threads survive republishing: `PATCH` the same report id with
a regenerated file and the URL stays, a revision is appended, and open
threads carry across so a reader can see what changed and what was
already argued about.

And it's private by default. A dashboard built from real data belongs
behind an access level — private, invite-only, any signed-in user — not
on a public static host that happens to be convenient. See the [sharing
model](/docs/sharing).

## For agents and pipelines

The same endpoint is what an agent or a CI job uses. A Claude Code or
Cursor agent attached through [Comma's MCP server](/mcp) calls
`create_report` with the HTML it just generated and returns the link;
[publishing from CI](/docs/ci) is one `curl` in the job. Interactive or
static, it's the same write path and the same [scoped
token](/docs/api-tokens).

## Try it

Comma is free — unlimited reports, unlimited commenters, unlimited
revision history. Take the dashboard you were about to screenshot and
publish the file instead.

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

### Related

- [How to share an HTML report](/share-html-report) — the six properties a shared report needs
- [Comment on an HTML report](/comment-on-html) — how anchored threads work
- [Publish from CI](/docs/ci) — the one-curl pipeline pattern
- [Share a Jupyter notebook](/with/jupyter) — nbconvert, Plotly, and widget state
