Explore with AI
Open this article in an AI assistant, with a prompt that asks it for a summary and the takeaways.
SVG vs PNG: the short answer
SVG vs PNG comes down to how the image is stored. An SVG (Scalable Vector Graphics) image is a text description of shapes, such as rectangles, paths and text. The browser redraws them at whatever size the screen needs, so the image stays sharp on a phone and on a projector. A PNG is a fixed grid of pixels. Enlarged past its native size, it blurs or turns to steps.
So the content picks the format. Charts, icons, logos and diagrams begin as shapes and belong in SVG. Screenshots begin as pixels and belong in PNG. Photos belong in JPEG, which compresses them far smaller.
For reports an AI writes as HTML, the biggest difference is that inline SVG is text in the page, so the model that drew a chart can later change one number in it. A PNG chart can only be redrawn.
The decision table: inline SVG, hosted image or base64
In a single HTML file an image can arrive three ways. It can be drawn inline as SVG, referenced by URL from another host, or embedded as base64, a text encoding of the file's bytes written into the page itself.
Three rows need a sentence each.
- Agent edits later. An SVG bar's height and label are attribute values and text that an agent can change in place. A PNG holds only pixels.
- Pasted in email. Many email clients won't show inline SVG, and many block base64 images. That only matters when the HTML itself is the email body. Send a link instead and the page opens in a browser, where SVG renders normally. Heavy HTML in the email body hurts deliverability anyway.
- On HTMLvault. Publishing keeps inline SVG and image URLs, but it removes a
data:URL from<img src>. A base64 image has to go in CSS or in an SVG<image>, as shown below.
Rules for images in a single HTML file
A person or a model can check a finished page against each rule. Where HTMLvault's publishing step is the reason for a rule, the rule says so.
- Draw charts, icons, logos and diagrams as inline
<svg>, never as image files. They stay sharp at any size and stay editable as text. - Give every inline SVG
role="img"and anaria-labelledbythat points at a<title>placed as its first child. On a chart, add a<desc>that lists the values. An SVG has no alt attribute, so the title is its accessible name. - Write an SVG
<title>as plain text with no tags and no ampersands, and keep<script>and<style>outside the<svg>. Inside SVG, those elements are parsed as markup, so HTMLvault can refuse one that holds a tag, a character reference or a CDATA section. An SVG<style>of plain CSS is fine. - Use PNG for screenshots and JPEG for photos. Both began as pixels, and tracing them into SVG adds weight without adding sharpness.
- Give every
<img>alt text that says what it shows. Usealt=""only for decoration. Publishing removes empty attributes but keeps an empty alt. - Reference images by a stable https URL, or embed base64 in a CSS
url()or an SVG<image xlink:href>. Never use<img src="data:…">. HTMLvault removesdata:URLs fromsrc, so that image disappears when you publish. - Never point at a local path (
C:\Users\…,file://) or at a host that may vanish, such as an expiring signed URL or a tool's temporary preview. The reader's browser can't reach your disk, and when a borrowed host goes away, the image goes with it. - Close every element explicitly, including SVG shapes (
<rect …></rect>) and every<p>,<li>and<td>. HTMLvault refuses malformed HTML instead of repairing it.
A screenshot referenced by URL is the lightest option:
<img src="https://assets.example.com/q3/pipeline.png"
alt="CRM pipeline: 14 open deals, 3 in legal review"
width="1200" height="720" />
Here is the same screenshot embedded, for a report that must not depend on another host:
<svg viewBox="0 0 1200 720" role="img" aria-labelledby="shot-title">
<title id="shot-title">CRM pipeline: 14 open deals, 3 in legal review</title>
<image width="1200" height="720" xlink:href="data:image/png;base64,iVBORw0KGgo…"></image>
</svg>
A CSS background-image: url("data:…") also survives publishing. Give that element role="img" and an aria-label so it still has a name.
How to convert an image to base64, and when a page gets too heavy
Run the conversion with a command, not the model. A model that types out base64 spends output tokens on every character and has no way to check it got them right.
# macOS
base64 -i screenshot.png | tr -d '\n' > screenshot.b64
# Linux
base64 -w 0 screenshot.png > screenshot.b64
# Windows PowerShell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("C:\reports\screenshot.png")) | Set-Content -NoNewline screenshot.b64
Put the media type in front of the result (data:image/png;base64, or data:image/jpeg;base64,) and place it in the SVG <image> or CSS url() shown above.
Base64 writes every 3 bytes as 4 characters, so an embedded image is about a third larger than its file. Before encoding, resize the image to the size it will display at and save photos as JPEG.
One stored HTMLvault document can be at most 5 MB. Once encoded, that holds roughly 3.75 MB of image files, minus the rest of the page. As a rule of thumb, once embedded images pass a megabyte or so, host them and reference them by https URL. The page loads faster, and the AI tool doesn't have to write out megabytes of text. You can still build a large page in pieces with append mode: start a draft (create_draft_link or POST /api/v1/links/draft), send chunks of up to 200 KB with append_html, and finalize on the last call. Nothing is public and no checks run until the draft is finalized. The 5 MB ceiling still applies.
Inline SVG is text: patch a chart instead of regenerating it
An inline SVG chart keeps its numbers as attribute values and text, so when a figure changes after the report has gone out, an agent edits just those strings and leaves the rest of the page alone. A PNG chart has to be regenerated and swapped in.
On HTMLvault, a patch replaces an exact substring of the stored HTML, and the URL stays the same. You send it with patch_link over MCP, or with PATCH /api/v1/links/{slug} and a patches field. Matching is plain text, not regex. One call carries up to 200 patches and applies all of them or none. Unless a patch sets replace_all, its find string must occur exactly once in the page.
Make the chart patchable
- Give each bar, point and value label an
idso that a find string including it is unique. - Write markup close to its stored form: double-quoted attributes, explicit closing tags, lowercase HTML names and no comments, since publishing drops comments.
- Record the chart's scale in a
data-attribute so an agent can work out the new geometry from a new value. - Copy each find from the stored HTML, which
get_linkreturns. Before publishing,scan_htmlwithnormalize: "full"shows you that stored form.
Worked example: Q3 moves from $1.1M to $1.3M
This excerpt of the stored chart uses a scale of 100 units per $1M, measured up from a baseline at y=160:
<desc id="rev-desc">Q1 $0.8M, Q2 $1.0M, Q3 $1.1M</desc>
<g id="rev-bars" data-scale="100 per $1M above y=160">
<rect id="bar-q3" x="200" y="50" width="48" height="110" fill="#5A52CC"></rect>
<text id="val-q3" x="224" y="42" text-anchor="middle">$1.1M</text>
</g>
The update is one call with three patches:
find: <rect id="bar-q3" x="200" y="50" width="48" height="110" fill="#5A52CC"></rect>
replace: <rect id="bar-q3" x="200" y="30" width="48" height="130" fill="#5A52CC"></rect>
find: <text id="val-q3" x="224" y="42" text-anchor="middle">$1.1M</text>
replace: <text id="val-q3" x="224" y="22" text-anchor="middle">$1.3M</text>
find: Q3 $1.1M
replace: Q3 $1.3M
The new height is 1.3 × 100 = 130, so y is 160 − 130 = 30, and the label sits 8 units above the bar. The label's find string includes the whole element because a bare $1.1M appears twice, once in the label and once in the desc. A find that matches twice fails, and because the call is all or nothing, the page stays exactly as it was instead of ending up half-updated. The edit goes through the same secret, personal-data and URL checks as the first publish.
For whole-page replacement and the web editor, see editing published HTML without changing the URL. The MCP server guide covers connecting Claude, and the REST API accepts the same patches from a script.
Prompt you can reuse
Paste this into Claude, ChatGPT, Gemini or Cursor when you start a report. If the HTMLvault MCP server is connected, the last paragraph also covers updates.
Build this report as one self-contained HTML document with its own CSS.
Images
- Draw every chart, icon, logo and diagram as inline <svg>, never as an image file.
- Give each <svg> role="img" and aria-labelledby pointing at a <title> that is its first child. On charts, add a <desc> listing the values.
- Write each <title> as plain text: no tags, no ampersands. Keep <script> and <style> outside the <svg>.
- Screenshots are PNG and photos JPEG, referenced by the https URLs I give you, each with alt text saying what it shows.
- If I supply base64, put it in an SVG <image xlink:href> inside an <svg> with a <title>. Never put a data: URL in <img src>, never write base64 yourself, never use a local file path.
Markup
- Double-quote every attribute and close every element explicitly, including <rect></rect>, <p>, <li> and <td>. No HTML comments.
- Give every bar, value label and number-bearing cell a unique id, write each number as a literal, and record each chart's scale in a data-scale attribute.
- Keep the page under 5 MB. If images would bring it close, stop and ask me to host them.
Updates
When I send new numbers, call get_link, then patch_link with one find-and-replace per changed string. Copy each find from the stored HTML, make it unique on the page, and recompute bar geometry from data-scale.
Use it on your next report. The next time a number moves, the fix is a three-string patch to the link your reader already has, not a regenerated chart.
