Three paths to turn a multi-asset HTML page into a single self-contained file: a build-tool step, an LLM prompt, or a manual pass with DevTools. When each one fits.
You have an HTML file that pulls in five external things: a CSS framework via CDN, a chart library via CDN, a font from Google, two images from an asset host, and a JSON file for the data. It works on your machine. The first time someone opens it on a flight, four of the five fail to load.
The fix is to bundle everything into the HTML itself. The page becomes one bigger file that opens correctly with no network connection. The path to get there has three flavors depending on what tools you already have in front of you.
For HTML files that are part of a recurring workflow — a report you regenerate weekly, a dashboard that updates with new data, a deliverable you ship to clients in a known pattern — a build step that inlines on every regeneration is the cleanest setup.
The shape that works across most stacks:
html-bundler-webpack-plugin handles HTML as an entry point and inlines CSS, JS, and images per a config rule.vite-plugin-singlefile rolls the whole output into a single HTML on vite build.--bundle flag plus a small post-process that inlines the CSS output into the HTML..html file with relative-path imports.Cost: an hour or two of initial setup. Win: every regeneration produces a portable file with zero extra steps. If the file ships more than three or four times, the setup pays for itself.
For one-off conversion — you have an HTML file and you need a portable version once, not a recurring build — pasting the file into Claude / ChatGPT / Cursor and asking for inlining is faster than configuring a build tool.
The prompt that works:
Rewrite this HTML as a single self-contained file. Inline all CSS in
<style>blocks (download external stylesheets and embed). Inline all JavaScript in<script>blocks (download CDN scripts and embed). Replace Google Font links with a system font stack. Base64-encode any small images and embed inline; for large images, ask whether to keep them external. Replace any fetch calls with the hardcoded data if I provide it; if I don't, flag them.Output: one HTML file, no external HTTP requests.
The model usually handles this in one pass. For files with heavy framework dependencies (a Tailwind CDN, a full React build), the output gets verbose (~200-500KB) but the file is portable.
This is the same shape as the cross-tool portable-HTML prompts used at generation time — the conversion case is just the retroactive version. For Claude artifacts specifically, the export checklist covers the five categories that quietly stay external.
For HTML that's already mostly inline and just has a few external references left, manual is sometimes the fastest path.
The walkthrough:
font, css, js, img. Each row is a thing that needs to either be inlined or deliberately kept external.<link rel="stylesheet" href="..."> or <script src="..."> tag with the inline equivalent (<style> or <script>).@font-face rule with the woff2 base64-encoded, or switch to a system font stack (the simpler choice for most cases).data:image/png;base64,.... Large images, decide whether they're worth embedding — sometimes the right answer is to keep them external and accept the dependency, or to swap them for inline SVG.For files with five or fewer external references, this takes 10-15 minutes. For files with twenty references, switch to Path 1 or Path 2.
The SingleFile browser extension (Chrome / Firefox) automates a chunk of this — it captures whatever the browser currently sees as one HTML file with assets inlined. Useful for pages that render correctly in the browser but came from a build system you don't control.
Whichever path you take, the same five things tend to stay external if you don't name them explicitly:
| Category | Inline how |
|---|---|
| CSS files | <style> block with the file contents |
| JavaScript files | <script> block with the file contents |
| Fonts | @font-face + base64 woff2 OR system font stack |
| Images | data:image/...;base64,... for small; inline SVG for icons |
| Fetch / API calls | Hardcoded JSON literal at the top of the script |
The fifth one — fetch calls — is the one that breaks portability hardest because it fails at runtime, not load time. The HTML loads fine; the page sits empty waiting for data that never arrives. For a snapshot deliverable, hardcoding the data at export time is the right shape.
Once the file is inlined, drop it at a private link in under 60 seconds — useful for confirming the file actually opens cleanly on a different network than the one you built it on. Free, no card, 7-day self-destruct.
Three checks, in order of how often each catches a real issue:
https://. Open in an editor, search the text for https://. Every hit is a reference that may need inlining (or be acceptable to leave external). Reasonable exceptions: schema.org URLs in JSON-LD, deliberate links the user is meant to click, references in comments.fetch( and XMLHttpRequest. Any runtime data load needs to either be inlined as a hardcoded constant or be deliberate.A file that passes all three opens identically on every device that has a browser.
A few cases where the external dependency is the right shape:
The split is roughly: inline anything the page can't function without; keep external anything that's optional, shared, or genuinely large.
Inlining isn't a build system. For a real web project with multiple pages, shared components, ongoing development, and team workflows — the modular structure (separate CSS, JS, asset files; build step; deploy pipeline) is correct and pays off.
The inline pass is for the moment of delivery, when the file leaves the project repo and becomes something one person opens on their machine. For the delivery channel decision afterward — private link, public host, attachment — the file shape matters less than where it lands. The self-contained file is what makes any of those channels work cleanly.
One caution worth pairing with this: inlining moves weight into the file rather than removing it. Inline a CDN framework you barely use and the page is self-contained and several megabytes. Trimming the page before you inline it is the pass that keeps the result light enough to open instantly on cellular.
The attachment arrived, nothing was blocked, and your recipient sees markup instead of the page. That's three different email failures people keep merging into one. Here's which one you hit, and why the fix isn't a different attachment.
Upload an .html file to Drive, open it, and you get a wall of markup instead of the page. Nothing is broken — Drive is doing exactly what a filing cabinet does. Here's the mechanism, the workarounds people try, and what actually renders it.
Email providers cap attachments around 20-25MB. The fix when the file is bigger isn't to compress harder — it's to send the URL instead.
miinideck turns a single HTML file into an unguessable link with optional password and expiry. Default-private, never indexed.