Artifact architecture
Read this before writing a line of an artifact. Most of what goes wrong is decided in the first minute, by assuming an environment that does not exist.
The constraint that changes everything: no external hosts
A published artifact runs under a strict Content Security Policy that blocks requests to every external host. Not "discouraged" — blocked. That includes:
- CDN scripts (D3, Recharts, Chart.js, Plotly, Highcharts, ECharts)
- External stylesheets and web fonts (Google Fonts, any
@importof a remote URL) - Remote images
fetch/XHR/ WebSockets to any other origin
Everything must be inlined: CSS in <style>, JS in <script>, images as data: URIs, fonts as
base64 or not at all. There is no build step and no bundler — you write the page.
Three consequences that people discover the hard way:
- You will hand-build your charts. See
svg-charting, and thechartkit.jsutility that ships with it. This is less painful than it sounds — scales, ticks, and paths are a few dozen lines, and you avoid inlining 300KB of library to use 5% of it. - Mermaid is the exception. Artifacts render mermaid natively —
```mermaidfences in Markdown,<pre class="mermaid">in HTML. No library involved. Seemermaid-in-artifacts. - System font stacks only, unless you embed a font as base64 and can afford the bytes.
Write the page, not the document
The file you hand to the publisher is wrapped in <!doctype html><head>…</head><body> at publish
time. So write page content directly — no <!DOCTYPE>, <html>, <head>, or <body> tags of
your own. A minimal CSS reset is already applied.
<title>ARR Bridge</title>
<style>
:root { --bg: #fff; --ink: #14161a; /* … */ }
</style>
<main> … </main>
<script> … </script>
The <title> must be in the first 8KB of the file — that is all that gets scanned. Put it at the
very top, before a large inline <style> block pushes it out of range.
Naming
The title is a name, not a summary. It appears in the browser tab and in a gallery beside many other pages, so it needs to be picked out at a glance:
| Good | Bad | Why |
|---|---|---|
ARR Bridge |
Q3 ARR Bridge Analysis and Commentary |
The second is a description |
Runway Model |
Financial Dashboard |
Generic label, indistinguishable from ten others |
Cohort Explorer |
Cohort Explorer — Retention by Acquisition Month |
The explainer belongs in the description field |
Two to four words, distinctive to this page. Keep it stable across redeploys — a title that changes reads as a different page. Same for the favicon emoji: users find their tab by its icon, so only change it on a hard pivot in what the page is about.
The one-sentence explanation goes in the description, which becomes the gallery card's subtitle.
The data tier decision
Decide this before anything else, because it determines the whole architecture.
Tier 1 — baked snapshot (default)
Data pulled at build time and embedded in the page. Needs no runtime capability, works everywhere, is reproducible, and can be shared.
Choose it for anything reported, sent, or presented. For financial reporting it is usually correct on the merits, not just as a fallback: a figure that changes between when a deck is sent and when it is read is a defect. Stamp the as-of.
Tier 2 — live connector data
The page calls the viewer's claude.ai connectors at open time. Two constraints decide most cases:
- The source must be a connector on the organization's account. A locally-configured MCP server is not reachable from a published page.
- A page declaring connector access cannot be shared publicly — each viewer authenticates as themselves.
Choose it for an operational monitor someone watches — cash position, collections, pipeline. See
live-data-artifacts.
Tier 3 — the page is the record
Viewers write state back and everyone sees it: trackers, checklists, sign-off boards, polls. See
stateful-artifacts.
Tiers combine. A close tracker is Tier 3 for its checklist and Tier 1 for its reference figures.
Budget
The rendered page must be ≤16MB, and data: URIs count. In practice you want far less — a page
a viewer waits on is a page they stop opening.
| Element | Realistic budget |
|---|---|
| Markup + CSS + JS | 50-200KB |
| Embedded data | keep under ~500KB; pre-aggregate rather than shipping raw rows |
| Images | inline SVG where possible; a base64 PNG is rarely worth it |
| Fonts | usually skip; a system stack costs nothing |
If your data does not fit, the answer is aggregation, not compression. See artifact-data-loading.
Responsive and theme-aware, always
- Relative units, flexbox/grid,
max-width: 100%on media. - Wide content scrolls inside its own container (
overflow-x: auto) — tables, chart canvases, code. The page body must never scroll horizontally. - The viewer's theme has three states, not two: explicit dark, explicit light, and system
default. Getting this wrong is the single most common visual bug. See
artifact-theming.
Redeploy semantics
- Same file path → same URL. Edit the file, publish again, it updates in place.
- Different file path → a new artifact at a new URL. Only do that when you intend a separate page.
- Updating an artifact from an earlier session needs its URL passed explicitly; publishing without it creates a separate artifact rather than updating the existing one.
- Artifacts are private by default. Publishing does not share; the user decides that separately.
Before you build, answer these
- Which data tier, and why that one?
- What is the as-of, and where does it appear on the page?
- Does anything here need to be shared? (If yes, Tier 2 is out.)
- What is the title — a name, two to four words?
- What does the page do when data is unavailable, empty, or stale?
- Does it read in both themes and print in greyscale?
- Is every byte self-contained?
Related skills
svg-charting— building charts without a librarymermaid-in-artifacts— the one thing that renders nativelylive-data-artifacts/stateful-artifacts— tiers 2 and 3artifact-data-loading— fitting data in the budgetartifact-theming,artifact-accessibility,artifact-performance,artifact-testing