How we extracted standalone HTML files from 7 / 13 AI prototyping tools
byKadhir Mani
(12.3 minutes)
<section data-section-id='a8edf699-5e9f-48e9-a173-a42e03e36b30'><collapsible-card data-icon="file-text" data-title="TL;DR results"><p><span style="white-space: pre-wrap;">After testing thirteen prototyping platforms, here's the full picture we saw + a visualization on file size (y-axis in kb):</span></p><p><bar-chart data-height="450"><bar-line value="3.2" color="#22C55E">ChatGPT</bar-line><bar-line value="205" color="#22C55E">Claude</bar-line><bar-line value="270" color="#22C55E">Gemini Canvas</bar-line><bar-line value="288" color="#22C55E">Bolt</bar-line><bar-line value="293" color="#22C55E">Lovable</bar-line><bar-line value="497" color="#22C55E">Replit</bar-line><bar-line value="1334" color="#F59E0B">Figma Make</bar-line><bar-line value="74.3" color="#EF4444">V0</bar-line><bar-line value="487" color="#EF4444">Hostinger</bar-line><bar-line value="960" color="#EF4444">Anything</bar-line><bar-line value="3500" color="#EF4444">Canva Code</bar-line><bar-line value="0" color="#EF4444">UX Pilot</bar-line><bar-line value="0" color="#EF4444">Base44</bar-line></bar-chart></p><p><b><strong style="white-space: pre-wrap;">Green</strong></b><span style="white-space: pre-wrap;"> = self-contained HTML achieved, </span><b><strong style="white-space: pre-wrap;">Amber</strong></b><span style="white-space: pre-wrap;"> = requires HTTP server, </span><b><strong style="white-space: pre-wrap;">Red</strong></b><span style="white-space: pre-wrap;"> = not achievable</span></p></collapsible-card><p><br></p></section>
<section data-section-id='12e4a8f5-9680-4d8f-99a3-78b95d5d8fd4'><h2 id='12e4a8f5-9680-4d8f-99a3-78b95d5d8fd4'>Background</h2><p><span style="white-space: pre-wrap;">Prototypes are one of our favorite ways to work. They're surprisingly fun to build, and they're far more information-dense than a doc or a whiteboard session. A new way of communicating, honestly.</span></p><p><br></p><p><span style="white-space: pre-wrap;">And right now, there are so many tools to build them with: Claude, ChatGPT, Lovable, Bolt, V0, Replit, and a dozen others can all spin up a working UI in seconds.</span></p><p><br></p><p><span style="white-space: pre-wrap;">We've tried many of them, but we kept running into the same issues:</span></p><ul><li value="1"><span style="white-space: pre-wrap;">Every tool locks your prototype inside its own environment</span></li><li value="2"><span style="white-space: pre-wrap;">Sharing means sending a link that sometimes requires an account, or a live URL that may disappear</span></li><li value="3"><span style="white-space: pre-wrap;">Version control is a pain, especially when we're trying to get feedback</span></li></ul><p><br></p><p><span style="white-space: pre-wrap;">For example, I once wanted to try a few different button-layout configurations, light reorganizing, nothing fancy. Should I clone the prototype so I can make multiple versions side by side? How do I send them to the team for feedback? Does each one need its own URL?</span></p><p><br></p><p><span style="white-space: pre-wrap;">I ended up using one prototype, taking screenshots, and pasting them into Slack. This setup sucks and defeats the point of doing this in the first place.</span><br><br><span style="white-space: pre-wrap;">Honestly, we wanted something simpler: </span><b><strong style="white-space: pre-wrap;">a self-contained HTML file</strong></b><span style="white-space: pre-wrap;">.</span><b><strong style="white-space: pre-wrap;"> </strong></b><span style="white-space: pre-wrap;">No dependencies, no login, no cloud. Something we could save and open offline in any browser and expect to work forever. That's a lot easier to work with, very portable.</span></p><p><br></p><p><span style="white-space: pre-wrap;">Ideally, we can create a prototype in any tool, point our platform at it, and get a saved snapshot. That way, we get the nice UX of these prototyping tools while still getting an easy-to-share, easy-to-manage artifact.</span></p><p><br></p><p><span style="white-space: pre-wrap;">Long story short, we got it to work! Mostly, but holy moly, it was a real pain to get here.</span></p><p><br></p><p><span style="white-space: pre-wrap;">The spread alone tells the story: ChatGPT handed us a clean, tiny self-contained 3.2 KB file out of the box, while Canva generated a bloated 3.5 MB bundle that still couldn't run offline. And that's just the file size...we learned a lot more about how each platform works along the way.</span></p><p><br></p><p><span style="white-space: pre-wrap;">This post walks through what we had to do and what we learned about each platform. As a fun bonus, you'll get to see how various prototyping applications interpreted the same request π.</span></p><p><br></p></section>
<section data-section-id='1ffdd389-4121-4b75-b3ef-7e1e108cbd2c'><h2 id='1ffdd389-4121-4b75-b3ef-7e1e108cbd2c'>Setup</h2><p><span style="white-space: pre-wrap;">We used a single, consistent prompt across all tools:</span></p><p><callout icon="rocket" color="#8B5CF6">"Let's build a fun little todo application. I want it to be galaxy themed, easy to understand."</callout></p><p><b><strong style="white-space: pre-wrap;">Why this prompt?</strong></b></p><ul><li value="1"><b><strong style="white-space: pre-wrap;">TODO app</strong></b><ul><li value="1"><span style="white-space: pre-wrap;">Universal reference point, lots of examples out there</span></li></ul></li><li value="2"><b><strong style="white-space: pre-wrap;">Fun + Galaxy themed</strong></b><ul><li value="1"><span style="white-space: pre-wrap;">Room for interpretation, visual flair, creative rendering</span></li></ul></li><li value="3"><b><strong style="white-space: pre-wrap;">Easy to understand</strong></b><ul><li value="1"><span style="white-space: pre-wrap;">Should keep the complexity low, making self-containment easier</span></li></ul></li></ul><p><br></p><p><b><strong style="white-space: pre-wrap;">What we're evaluating in the output:</strong></b></p><ul><li value="1"><span style="white-space: pre-wrap;">Can the result be extracted as a single, self-contained </span><code spellcheck="false" style="white-space: pre-wrap;"><span>.html</span></code><span style="white-space: pre-wrap;"> file that opens in any browser with no network required?</span></li><li value="2"><span style="white-space: pre-wrap;">How big is our resolved HTML file?</span></li></ul><p><br></p><p><i><em style="white-space: pre-wrap;">Note: we'll refrain from outright ranking the platforms against each other since they all have their own strengths and weaknesses.</em></i></p><p><br></p></section>
<section data-section-id='1042e9c1-beac-43f0-b75b-e34990c1b583'><h2 id='1042e9c1-beac-43f0-b75b-e34990c1b583'>Claude</h2><p><span style="white-space: pre-wrap;">Right off the bat, we ended up with two weird problems:</span></p><ul><li value="1"><span style="white-space: pre-wrap;">Sometimes Claude gave us a TSX / JSX file, other times it gave us an HTML file</span><ul><li value="1"><span style="white-space: pre-wrap;">We never figured out a reliable way to understand when Claude would generate one over the other </span></li></ul></li><li value="2"><span style="white-space: pre-wrap;">The HTML files did not render identically to the Claude UI</span></li></ul><p><br></p><p><span style="white-space: pre-wrap;">We liked that it gave us a nice, simple file, but the file wasn't self-contained. So we had to do two things:</span></p><ul><li value="1"><span style="white-space: pre-wrap;">TSX / JSX, throw that into an isolated bundler on our server, which compiled it into an HTML file</span><ul><li value="1"><span style="white-space: pre-wrap;">Took a bit of iterating, but we ended up using esbuild with some base configurations for this</span></li></ul></li><li value="2"><span style="white-space: pre-wrap;">The HTML was weird, turns out it needs a bunch of CSS variables to render correctly</span><ul><li value="1"><span style="white-space: pre-wrap;">Which fortunately, Claude was kind enough to generate that file for us - attaching it here in case it helps anyone else</span></li><li value="2"><span style="white-space: pre-wrap;">This goes into the final HTML file in a style tag, as required</span></li></ul></li></ul><p><br></p><p><file-upload data-src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=55e3f8b6-c2c0-42ed-a07a-973db04b301b" data-file-name="claude-widget-host.css" data-description="" data-file-type="text/css"></file-upload></p><p><span style="white-space: pre-wrap;">And with those two problems solved, presto!</span></p><h3><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=79fcac6e-fedf-4f5f-979c-a8fc32d468ef" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe><span style="white-space: pre-wrap;">HTML file achieved β
οΈ, size = 205 kb</span></h3><p><br></p></section>
<section data-section-id='e6a2bf3c-9540-4ef0-a3f8-f37dd9e364fe'><h2 id='e6a2bf3c-9540-4ef0-a3f8-f37dd9e364fe'>ChatGPT</h2><p><span style="white-space: pre-wrap;">Super straightforward, downloaded as an HTML file, and came out fully resolved out of the box. No changes required. No weird bundling, actually self-encapsulated βοΈ:</span></p><h3><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=1fe6eb99-456f-45e6-ad02-52fa2c07c91b" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe><span style="white-space: pre-wrap;">HTML file achieved β
οΈ, size = 3.2 kb</span></h3><p><span style="white-space: pre-wrap;">So tiny! So exciting!</span></p><p><br></p></section>
<section data-section-id='95c53627-5614-437d-8051-67007e1d083e'><h2 id='95c53627-5614-437d-8051-67007e1d083e'>Gemini canvas</h2><p><span style="white-space: pre-wrap;">Gave us a TSX file, which okay no problem, we did this for Claude already, should work out of the box.</span></p><p><br></p><p><span style="white-space: pre-wrap;">Except in one of our runs, it also included the lucide icon library, and it required tailwind to render correctly. Which we'll admit made the output TSX file super token efficient, but did complicate our build system here.</span></p><p><br></p><p><span style="white-space: pre-wrap;">No problem, we can add this to our bundling step. In the end, we pull in a set of allowlisted dependencies (like lucide, date-fns, lodash-es, etc) and give esbuild access to those node_modules at compile time. Note, we only allow a small set of trusted packages, so random, uncontrolled imports canβt slip in. Can't be too careful these days with the sheer number of vulnerabilities being pushed.</span></p><p><br></p><p><span style="white-space: pre-wrap;">Next, the tailwind classes are scanned out of the source and compiled into CSS using @tailwindcss/oxide and @tailwindcss/node. Then everything is inlined into one final HTML file for us.</span></p><p><br></p><p><span style="white-space: pre-wrap;">And terrific, now we can support Gemini canvas:</span></p><h3><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=d83dec1f-06d9-4ac5-a71d-2dc40a1df78f" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe><span style="white-space: pre-wrap;">HTML file achieved β
οΈ, size = 270 kb</span></h3><p><span style="white-space: pre-wrap;">Honestly, have to say integrating lucide and tailwind is pretty cool. It allows for a higher level of output token efficiency. In other words, for each output token, by leveraging these external libraries, we can get "more frontend" out of it.</span></p><p><br></p></section>
<section data-section-id='386a50aa-3c00-427b-85bc-3cfea33f2273'><h2 id='386a50aa-3c00-427b-85bc-3cfea33f2273'>Lovable</h2><p><span style="white-space: pre-wrap;">Next, we get into the gnarlier platforms. The model providers are great because they give us a single file to begin with; they're not in the business of hosting stuff. But Lovable (and others) are in the business of hosting things, so they give us a link instead.</span></p><p><br></p><p><span style="white-space: pre-wrap;">So the question is, how can we convert a hosted link into a single file for our purposes?</span></p><p><br></p><p><span style="white-space: pre-wrap;">At a high level, we act like a browser and:</span></p><ol><li value="1"><span style="white-space: pre-wrap;">Fetch the HTML</span></li><li value="2"><span style="white-space: pre-wrap;">Download the CSS and JS that are requested from the same server</span></li><li value="3"><span style="white-space: pre-wrap;">Bundle it all together into a single HTML file</span></li></ol><p><br></p><p><span style="white-space: pre-wrap;">It roughly worked, but in our particular application here at Lovable, it looks like it also used an image that it was failing to fetch. So we grabbed that as well, converted it into a base64, and threw that into the final HTML.</span></p><p><br></p><p><span style="white-space: pre-wrap;">Perfect. We get our resolved file:</span></p><h3><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=befaf16e-1c4d-4f01-ab9f-85af2894fa83" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe><span style="white-space: pre-wrap;">HTML file achieved β
οΈ, size = 293 kb</span></h3><p><span style="white-space: pre-wrap;">We also noticed the prototype was trying to access local storage and session storage for metric tracking or something. Also happened on some of the other platforms. We're still not totally sure what it's doing, but it meant our iframes here had to accommodate accordingly.</span></p><p><br></p></section>
<section data-section-id='7eb3f482-4b52-4cf3-928e-557d47f1ad1e'><h2 id='7eb3f482-4b52-4cf3-928e-557d47f1ad1e'>Replit</h2><p><span style="white-space: pre-wrap;">Worked similar to Lovable - looks like it's some sort of a Vite-based SPA. Easy to grab and compile everything, very predictable structure:</span></p><h3><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=2d4f3738-fbd7-476f-b0c2-dbe9f4aa672d" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe><span style="white-space: pre-wrap;">HTML file achieved β
οΈ, size = 497 kb</span></h3><p><span style="white-space: pre-wrap;">Though this file was a tad larger than I was expecting. Inspecting it, it has </span><a href="https://bundlephobia.com/package/@radix-ui/themes@3.3.0" rel="noreferrer"><span style="white-space: pre-wrap;">Radix</span></a><span style="white-space: pre-wrap;">, </span><a href="https://" rel="noreferrer"><span style="white-space: pre-wrap;">TanStack query</span></a><span style="white-space: pre-wrap;">, </span><a href="https://bundlephobia.com/package/lucide@1.17.0" rel="noreferrer"><span style="white-space: pre-wrap;">lucide</span></a><span style="white-space: pre-wrap;"> icons embedded into it. Must be some of the defaults Replit uses to create, would explain the larger size compared to Lovable.</span></p><p><br></p><p><span style="white-space: pre-wrap;">Similar to Gemini canvas, the output tokens from the model will have higher efficiency through this method, though the resulting bundle size and complexity take a hit. Probably worth it? Also you need less tokens which we like, less room for the models to mess it up.</span></p><p><br></p></section>
<section data-section-id='321bf6af-4cb0-42a9-9d3e-602ea5cb9026'><h2 id='321bf6af-4cb0-42a9-9d3e-602ea5cb9026'>V0</h2><p><span style="white-space: pre-wrap;">Vercel has the benefit of maintaining NextJS and Turbo. We expected them to lean into their technologies when assembling prototypes. And sure enough, even for our simple TODO application, it relies on an API layer running on a NextJS server.</span></p><p><br></p><p><span style="white-space: pre-wrap;">We needed to rewrite the </span><code spellcheck="false" style="white-space: pre-wrap;"><span>_next</span></code><span style="white-space: pre-wrap;"> chunk fetches back to the original application server so the lazy-loaded rendering worked. In other words, the final HTML file still references Vercel's CDN at runtime, so even the frontend isn't a self-contained file, unfortunately.</span></p><p><br></p><p><span style="white-space: pre-wrap;">You'll notice the state management doesn't work:</span></p><h3><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=4932c7fb-03a1-4eda-9206-acf3ed675194" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe><span style="white-space: pre-wrap;">HTML file not achievable βοΈ, size = 74.3kb</span></h3><p><br></p></section>
<section data-section-id='60df3a41-6a9e-4768-8281-04012452135f'><h2 id='60df3a41-6a9e-4768-8281-04012452135f'>Figma make</h2><p><span style="white-space: pre-wrap;">This one was weird, it did not work out of the box with the setup we had so far. Looks like Figma make pages aren't self-contained SPAs, they're HTML shells around their SitesRuntime. If you inspect the raw HTML from the public Figma make:</span></p><pre spellcheck="false" data-language="html" data-highlight-language="html"><span style="white-space: pre-wrap;"><</span><span style="white-space: pre-wrap;">script</span><span style="white-space: pre-wrap;"> </span><span style="white-space: pre-wrap;">type</span><span style="white-space: pre-wrap;">=</span><span style="white-space: pre-wrap;">"</span><span style="white-space: pre-wrap;">module</span><span style="white-space: pre-wrap;">"</span><span style="white-space: pre-wrap;">></span><br><span style="white-space: pre-wrap;"> import {SitesRuntime} from '/_runtimes/sites-runtime.XXXX.js';</span><br><span style="white-space: pre-wrap;"> const sitesRuntime = new SitesRuntime({</span><br><span style="white-space: pre-wrap;"> container: document.getElementById('container'),</span><br><span style="white-space: pre-wrap;"> env: 'published',</span><br><span style="white-space: pre-wrap;"> bundleId: 'XXX',</span><br><span style="white-space: pre-wrap;"> </span><br><span style="white-space: pre-wrap;"> loadComponentsOverNetwork: true,</span><br><span style="white-space: pre-wrap;"> assetsVersion: 'v11',</span><br><span style="white-space: pre-wrap;"> fontsVersion: 'v1',</span><br><span style="white-space: pre-wrap;"> videosVersion: 'v1',</span><br><span style="white-space: pre-wrap;"> codeComponentsVersion: 'v2',</span><br><span style="white-space: pre-wrap;"> withBaseStyles: false,</span><br><span style="white-space: pre-wrap;"> reportingDomain: "https://www.figma.com",</span><br><span style="white-space: pre-wrap;"> bundleCreationDate: '2026-06-09 22:48:02 UTC',</span><br><span style="white-space: pre-wrap;"> isFigmake: true,</span><br><span style="white-space: pre-wrap;"> enableMetaTags: true,</span><br><span style="white-space: pre-wrap;"> renderOptions: {</span><br><span style="white-space: pre-wrap;"> metronomeEnabled: false,</span><br><span style="white-space: pre-wrap;"> transform3dEnabled: false,</span><br><span style="white-space: pre-wrap;"> },</span><br><span style="white-space: pre-wrap;"> });</span><br><span style="white-space: pre-wrap;"> </span><span style="white-space: pre-wrap;"></</span><span style="white-space: pre-wrap;">script</span><span style="white-space: pre-wrap;">></span></pre><p><br></p><p><span style="white-space: pre-wrap;">We ended up faking the Figma runtime to get it to resolve the HTML. We managed to bundle that together, but ran into a few headaches. Namely it's loading components, API calls, and JSON files through a dynamic </span><code spellcheck="false" style="white-space: pre-wrap;"><span>import( )</span></code><span style="white-space: pre-wrap;"> . We needed to go download those files to disk temporarily, and register them in a bootstrap script in the HTML's header.</span></p><p><br></p><p><span style="white-space: pre-wrap;">If you look inside the resulting HTML file, you'll see a </span><code spellcheck="false" style="white-space: pre-wrap;"><span>registerModuleImportMap</span></code><span style="white-space: pre-wrap;"> that does the registration for the import calls. In effect, we're faking the Figma runtime to get it everything it needs, but it's not perfect unfortunately.</span></p><p><br></p><p><span style="white-space: pre-wrap;">There are two constraints the file has to satisfy to run correctly:</span></p><ol><li value="1"><b><strong style="white-space: pre-wrap;">It needs an HTTP origin.</strong></b><span style="white-space: pre-wrap;"> The import map uses absolute paths (e.g. </span><code spellcheck="false" style="white-space: pre-wrap;"><span>/</span></code><span style="white-space: pre-wrap;">), so it must be served over HTTP/HTTPS β not opened as a local </span><code spellcheck="false" style="white-space: pre-wrap;"><span>file://</span></code><span style="white-space: pre-wrap;"> URL. Without an origin, </span><code spellcheck="false" style="white-space: pre-wrap;"><span>replaceState</span></code><span style="white-space: pre-wrap;"> can't fake the root route and routing breaks.</span></li><li value="2"><b><strong style="white-space: pre-wrap;">The bootstrap must complete before any imports resolve.</strong></b><span style="white-space: pre-wrap;"> Our fetch-patching code runs as part of the bootstrap script. If the browser starts resolving module imports before that script finishes, the patches aren't in place yet and network calls go uninterrupted.</span></li></ol><p><br></p><p><span style="white-space: pre-wrap;">Together, these mean you can't open the file locally, but serving it through an HTTP server (like our platform) satisfies both constraints and works great:</span></p><p><br></p><p><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=04bf2e7b-a2b3-4747-b393-f2e371878d64" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe><span style="white-space: pre-wrap;">For our purposes here, we'll consider that mostly a win, unfortunate that it doesn't work offline:</span></p><h3><span style="white-space: pre-wrap;">HTML file achieved β
οΈ*, size = 1,334 kb</span></h3><p><span style="white-space: pre-wrap;">*assuming you can host the file on an HTTP server</span></p><p><br></p></section>
<section data-section-id='3d867cd7-c722-4afe-a1c6-339dbdbdb135'><h2 id='3d867cd7-c722-4afe-a1c6-339dbdbdb135'>Bolt</h2><p><span style="white-space: pre-wrap;">Easy, same Vite-based SPA setup as Lovable and Replit. Worked out of the box with the existing pipeline:</span></p><h3><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=4c2a0ced-2c46-436c-8296-e191cc2704d0" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe><span style="white-space: pre-wrap;">HTML file achieved β
οΈ*, size = 288 kb</span></h3><p><span style="white-space: pre-wrap;">Inspecting the HTML file, Bolt is calling out to supabase with some default credentials baked in client side. We originally inspected it because we noticed it would take an unusually long time for the initial load. That was a tad surprising, guess it's using that to store our TODOs? Won't work offline, but at least self encapsulated.</span></p><p><br></p><p><b><strong style="white-space: pre-wrap;">*assuming you have network access</strong></b></p><p><br></p><p><span style="white-space: pre-wrap;">We were originally sketched out about client side credentials, but looks like that's </span><a href="https://www.reddit.com/r/Supabase/comments/rwdrlo/how_is_it_secure_to_use_supabase_on_the_client/" rel="noreferrer"><span style="white-space: pre-wrap;">intentional</span></a><span style="white-space: pre-wrap;">.</span></p><p><br></p></section>
<section data-section-id='dcfff19b-b4ab-4ca7-a963-cd1c75e216a5'><h2 id='dcfff19b-b4ab-4ca7-a963-cd1c75e216a5'>Canva code</h2><p><span style="white-space: pre-wrap;">This one was a real pain. Canva hosted apps have three layers as far as we can tell:</span></p><ul><li value="1"><span style="white-space: pre-wrap;">A shell page that loads some massive webpack bundles</span><ul><li value="1"><span style="white-space: pre-wrap;">which also loads a </span><code spellcheck="false" style="white-space: pre-wrap;"><span>window.bootstrap</span></code><span style="white-space: pre-wrap;"> JSON blob that describes the design</span></li></ul></li><li value="2"><span style="white-space: pre-wrap;">A codelet iframe where our actual application lives</span></li><li value="3"><span style="white-space: pre-wrap;">Canva chrome footer, reporting errors, heartbeat polling, and metrics</span></li></ul><p><br></p><p><span style="white-space: pre-wrap;">Long story short, we ran into </span><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS" rel="noreferrer"><span style="white-space: pre-wrap;">CORS</span></a><span style="white-space: pre-wrap;"> and </span><a href="https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity" rel="noreferrer"><span style="white-space: pre-wrap;">SRI</span></a><span style="white-space: pre-wrap;"> issues that forced us to inline the shell server-side, string-patch the footer fetches, globally rewrite the </span><code spellcheck="false" style="white-space: pre-wrap;"><span>_assets/</span></code><span style="white-space: pre-wrap;"> paths its trying to load back to original Canva code app, and patch dynamic script/link/iframe creation so lazy chunks also go back to the Canva app - similar to V0.</span></p><p><br></p><p><span style="white-space: pre-wrap;">Definitely not a self-contained file:</span></p><collapsible-card data-icon="file-text" data-title="Open to see" data-default-collapsed="true"><h3><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=b805a4ec-4eef-4e81-876a-dcc9be87e5a7" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe></h3></collapsible-card><p><i><em style="white-space: pre-wrap;">Note: closed by default to prevent autoscroll</em></i></p><p><br></p><h3><span style="white-space: pre-wrap;">HTML file not achievable βοΈ, size = 3.5 mb</span></h3><p><span style="white-space: pre-wrap;">MB!! So big my god. And requires more dynamically loaded code to run π±. Maybe we'll do a follow up post going through all the things we learned about how Canva code works one day.</span></p><p><br></p></section>
<section data-section-id='00adca0c-de09-4431-882a-cd65cc64b64f'><h2 id='00adca0c-de09-4431-882a-cd65cc64b64f'>Anything.com</h2><p><span style="white-space: pre-wrap;">Looks like this one leverages a lot of Next.js, so there's an API route here (on the backend server) that we can't do anything about. But on top of that, there were many CDN chunks. We ended up rewriting the page-origin assets back to the original created.app and used an embedded Bootstrap script, so at runtime the /_next/ asset also loads from the original CDN.</span></p><p><br></p><p><span style="white-space: pre-wrap;">Means we can't get a self encapsulated HTML file, we still call back to anything.com's servers:</span></p><h3><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=39573d25-b9b0-45cc-a352-bd4a473c9a39" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe><span style="white-space: pre-wrap;">HTML file not achievable βοΈ, size = 960 kb</span></h3><p><br></p></section>
<section data-section-id='b132005b-8486-45c2-b756-7467cce03709'><h2 id='b132005b-8486-45c2-b756-7467cce03709'>Base44</h2><p><span style="white-space: pre-wrap;">Immediately ran into an issue where Base44 wanted us to login before it would let us do anything. We can't load this thing in the server context, can't even see if we can make it self-encapsulated</span></p><p><br></p><p><span style="white-space: pre-wrap;">Here's the prototype it generated:</span></p><collapsible-card data-icon="file-text" data-title="Open to see" data-default-collapsed="true"><p><iframe src="https://orbit-task-labs.base44.app" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe></p></collapsible-card><p><i><em style="white-space: pre-wrap;">Note: closed by default to prevent autoscroll</em></i></p><p><br></p><h3><span style="white-space: pre-wrap;">HTML file not achievable βοΈ</span></h3><p><br></p></section>
<section data-section-id='25d295b7-61bd-4cc3-a126-039ee9a72f5c'><h2 id='25d295b7-61bd-4cc3-a126-039ee9a72f5c'>Hostinger horizons</h2><p><span style="white-space: pre-wrap;">This one ran into the same issue as V0, there are some backend APIs it requires for even our simple TODO application. So it's not possible to fully self encapsulate, but we wanted the frontend piece at least.</span></p><p><br></p><p><span style="white-space: pre-wrap;">As we kept digging further, looks like hostinger uses the domain suffix for: horizons apps, parked domains, their website builder application, and legacy PHP uploads. Meaning even figuring out if this is a page we can bundle was a bit of a hassle, along with which links to load into our final file.</span></p><p><br></p><p><span style="white-space: pre-wrap;">But once we figured that out (we only let Vite </span><code spellcheck="false" style="white-space: pre-wrap;"><span>/assets</span></code><span style="white-space: pre-wrap;"> through), then the standard Vite-based SPA setup did the trick for the frontend only (you'll notice the state management doesn't work):</span></p><h3><iframe src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=1bc9fee9-1f96-4374-a963-c682f58d2c02" style="width: 100%;" height="400" frameborder="0" allowfullscreen="true"></iframe><span style="white-space: pre-wrap;">HTML file not achievable βοΈ, size = 487 kb</span></h3><p><br></p></section>
<section data-section-id='9a2f7332-a6e9-415d-929c-69d5f393f2c3'><h2 id='9a2f7332-a6e9-415d-929c-69d5f393f2c3'>Take-aways</h2><p><span style="white-space: pre-wrap;">After testing thirteen prototyping platforms, here's the full picture we saw + a visualization on file size (y-axis in kb):</span></p><p><bar-chart data-height="450"><bar-line value="3.2" color="#22C55E">ChatGPT</bar-line><bar-line value="205" color="#22C55E">Claude</bar-line><bar-line value="270" color="#22C55E">Gemini Canvas</bar-line><bar-line value="288" color="#22C55E">Bolt</bar-line><bar-line value="293" color="#22C55E">Lovable</bar-line><bar-line value="497" color="#22C55E">Replit</bar-line><bar-line value="1334" color="#F59E0B">Figma Make</bar-line><bar-line value="74.3" color="#EF4444">V0</bar-line><bar-line value="487" color="#EF4444">Hostinger</bar-line><bar-line value="960" color="#EF4444">Anything</bar-line><bar-line value="3500" color="#EF4444">Canva Code</bar-line><bar-line value="0" color="#EF4444">UX Pilot</bar-line><bar-line value="0" color="#EF4444">Base44</bar-line></bar-chart></p><p><b><strong style="white-space: pre-wrap;">Green</strong></b><span style="white-space: pre-wrap;"> = self-contained HTML achieved, </span><b><strong style="white-space: pre-wrap;">Amber</strong></b><span style="white-space: pre-wrap;"> = requires HTTP server, </span><b><strong style="white-space: pre-wrap;">Red</strong></b><span style="white-space: pre-wrap;"> = not achievable</span></p><p><br></p><p><span style="white-space: pre-wrap;">Model providers are the most portable.</span><b><strong style="white-space: pre-wrap;"> </strong></b><span style="white-space: pre-wrap;">Claude, ChatGPT, and Gemini hand you a file from the start. Though the dedicated prototyping platforms are nicer to build in. We can still bundle many of them down to a single file though it takes meaningfully more work to do so, assuming your prototype isn't super complex.</span></p><p><br></p><p><span style="white-space: pre-wrap;">A few patterns worth calling out:</span></p><ul><li value="1"><b><strong style="white-space: pre-wrap;">Token efficiency vs. portability tradeoff</strong></b><span style="white-space: pre-wrap;"> β Tailwind is a concise way for models to style apps, but it pulls in a CSS compiler at build time and complicates self-containment. Heavy default dependencies (React, Radix, lucide, TanStack Query, etc) compound the bundle size quickly. Though leveraging all of the above can cause the model output token efficiency to be high, less room for it to mess up.</span></li><li value="2"><b><strong style="white-space: pre-wrap;">Two dominant architectures</strong></b><span style="white-space: pre-wrap;"> β Frontend-focused platforms (Lovable, Bolt, Replit, Hostinger) almost universally reach for a </span><b><strong style="white-space: pre-wrap;">Vite SPA</strong></b><span style="white-space: pre-wrap;">. Fullstack platforms (V0, Anything) use </span><b><strong style="white-space: pre-wrap;">Next.js</strong></b><span style="white-space: pre-wrap;">, which makes a truly offline bundle essentially impossible β even the "frontend" ends up coupled to a server through lazy loading chunks, etc.</span></li><li value="3"><b><strong style="white-space: pre-wrap;">Login walls are a hard blocker</strong></b><span style="white-space: pre-wrap;"> β Base44 and UX Pilot couldn't be evaluated in a server context at all. Public-accessible output is table stakes for self-encapsulation at scale.</span></li></ul><p><br></p><p><feedback data-feedbacknodeid="d79168f0-313c-424b-af63-2683b51b3394" data-title="Any prototyping platforms we've missed?" data-datatype="TEXT" data-metadata="{"type":"TEXT"}" data-description="We'll try them in the next post!"></feedback></p><p><br></p><p><span style="white-space: pre-wrap;">Side note, we've made this functionality free on the platform. Just start a blank doc, add a prototype node, and select "snapshot" in the upload dialog. You can download the HTML after its done processing. Still more edge cases we're actively patching. Shoot us a note at support@productnow.ai if a prototype is giving you trouble!</span></p><p><br></p><p><img src="https://api.productnow-prod.com/storage/getStorageObject?storageObjectId=9f1d0807-c970-4800-b26e-df2a29d6d969" alt="9f1d0807-c970-4800-b26e-df2a29d6d969" data-alignment="center" data-card="true" width="378" height="530"></p></section>