<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Julie Do</title>
    <description>The latest articles on DEV Community by Julie Do (@juliedechili).</description>
    <link>https://dev.to/juliedechili</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3976924%2Ffd57f873-59c2-4f99-bb97-200ef69a3b8a.jpg</url>
      <title>DEV Community: Julie Do</title>
      <link>https://dev.to/juliedechili</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/juliedechili"/>
    <language>en</language>
    <item>
      <title>What Nobody Shows You in an "AI Built My Website" Demo</title>
      <dc:creator>Julie Do</dc:creator>
      <pubDate>Fri, 24 Jul 2026 06:48:16 +0000</pubDate>
      <link>https://dev.to/juliedechili/what-nobody-shows-you-in-an-ai-built-my-website-demo-230n</link>
      <guid>https://dev.to/juliedechili/what-nobody-shows-you-in-an-ai-built-my-website-demo-230n</guid>
      <description>&lt;p&gt;It's been a rough week, not gonna lie. I spent three days debugging a Chrome extension update that Google's review team flagged for "requesting unnecessary permissions" — turns out it was a single leftover &lt;code&gt;activeTab&lt;/code&gt; scope I forgot to remove after a refactor two versions ago. Three days, one line.&lt;/p&gt;

&lt;p&gt;On top of that, a user emailed me asking why their FTP deploy "silently did nothing," and it took me an embarrassingly long time to realize their hosting provider was rate-limiting anonymous connections from datacenter IPs — not something I'd ever hit testing locally.&lt;/p&gt;

&lt;p&gt;Anyway. Not the week I planned. But it did remind me of something I've been meaning to write about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every "I built a website with AI" demo skips the same 10 seconds
&lt;/h2&gt;

&lt;p&gt;You've seen the demo a hundred times. Someone opens ChatGPT or Claude, types a prompt, a beautiful landing page appears... and then the video just &lt;em&gt;ends&lt;/em&gt;. Cut to a live URL. Cut to "look, it's deployed!"&lt;/p&gt;

&lt;p&gt;Nobody shows what happens in between.&lt;/p&gt;

&lt;p&gt;That gap gets treated like it's the hard part — like there's some serious infrastructure decision happening off-screen. Framework choice. Server config. DevOps knowledge you're supposed to already have.&lt;/p&gt;

&lt;p&gt;There isn't. That gap is maybe 80 lines of very boring code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually happening in that gap
&lt;/h2&gt;

&lt;p&gt;Strip away the marketing and "AI-generated site → live URL" is three unglamorous steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Find the HTML.&lt;/strong&gt;&lt;br&gt;
The AI's response is just text on a page. If it's ChatGPT, Claude, or Gemini, that text is sitting in a &lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;&lt;/code&gt; block in the DOM, updating as it streams in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debounceTimer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;debounceTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scanForHtmlBlocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scanForHtmlBlocks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pre code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;!DOCTYPE|&amp;lt;html/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No parsing library. You're just waiting for the DOM to stop mutating, then checking if a code block's text looks like an HTML document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Ask a hosting API to put it somewhere.&lt;/strong&gt;&lt;br&gt;
Netlify, Vercel, and GitHub Pages all expose plain HTTP APIs for exactly this. A deploy is, at its core, a POST request with your file attached and a token in the header. There's no magic — it's the same shape of request you'd write to hit any REST API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.netlify.com/api/v1/sites&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;zipBlobOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Get a URL back.&lt;/strong&gt;&lt;br&gt;
The API responds with a live URL. That's the "magic" part of the demo — it's a JSON field.&lt;/p&gt;

&lt;p&gt;That's genuinely the whole gap. Nobody shows it because there's nothing to show — it's a fetch call, not a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually gets annoying
&lt;/h2&gt;

&lt;p&gt;Here's the part that &lt;em&gt;isn't&lt;/em&gt; trivial, and it's not the part anyone talks about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The DOM you're watching belongs to someone else's frontend, and it changes without warning. ChatGPT, Claude, and Gemini all ship UI updates constantly, and none of them owe you a stable class name. Matching structurally (&lt;code&gt;&amp;lt;!DOCTYPE&lt;/code&gt;, &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;) instead of by CSS class is the only thing that's survived more than a few weeks for me.&lt;/li&gt;
&lt;li&gt;A single AI response can contain more than one code block — the full page, plus a CSS tweak explained separately. Grabbing "the first &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; tag" picks the wrong one more often than you'd expect.&lt;/li&gt;
&lt;li&gt;The moment you preview untrusted HTML before deploying it, you're rendering someone else's &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags. An &lt;code&gt;iframe&lt;/code&gt; with a locked-down &lt;code&gt;sandbox&lt;/code&gt; attribute is doing more real work in this whole pipeline than the deploy call itself.&lt;/li&gt;
&lt;li&gt;And the question people actually ask first isn't "which host do you support" — it's "where does my FTP password go." That one shapes the architecture more than anything else on this list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this needs a framework. It needs a &lt;code&gt;MutationObserver&lt;/code&gt;, a &lt;code&gt;fetch&lt;/code&gt; call, and paying attention to the two or three things above that actually bite you in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm telling you this
&lt;/h2&gt;

&lt;p&gt;I ended up building all of the above into a Chrome extension called &lt;a href="https://chromewebstore.google.com/detail/gihmknkabkkghpiocgnoiejagngdegea" rel="noopener noreferrer"&gt;&lt;strong&gt;HTML Deployer&lt;/strong&gt;&lt;/a&gt; — it sits on top of ChatGPT/Claude/Gemini, does the detection + preview + deploy steps above, and lets you push to Netlify, Vercel, GitHub Pages, FTP, or your own self-hosted endpoint, with a ZIP export if you'd rather do it manually.&lt;/p&gt;

&lt;p&gt;But I'd rather you walk away from this post knowing &lt;em&gt;how&lt;/em&gt; the gap works than just knowing the tool exists. If you've built something similar and hit a different wall than the ones above, I'd genuinely like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>5 Things I Learned Building a Chrome Extension That Watches ChatGPT, Claude &amp; Gemini</title>
      <dc:creator>Julie Do</dc:creator>
      <pubDate>Thu, 16 Jul 2026 09:43:05 +0000</pubDate>
      <link>https://dev.to/juliedechili/5-things-i-learned-building-a-chrome-extension-that-watches-chatgpt-claude-gemini-4a4i</link>
      <guid>https://dev.to/juliedechili/5-things-i-learned-building-a-chrome-extension-that-watches-chatgpt-claude-gemini-4a4i</guid>
      <description>&lt;p&gt;I spent the last few months building a Chrome extension that detects HTML code blocks inside ChatGPT, Claude, and Gemini and lets you deploy them straight to a live URL. The "deploy" part turned out to be the easy 20%. The hard 80% was reliably watching three completely different, constantly-changing chat UIs without breaking every other week.&lt;/p&gt;

&lt;p&gt;Here's what actually taught me something.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;MutationObserver&lt;/code&gt; is non-negotiable, but it will still lie to you
&lt;/h2&gt;

&lt;p&gt;None of these chat apps render the full response at once — they stream tokens in, which means the DOM you're watching is &lt;em&gt;incomplete&lt;/em&gt; almost every time your observer fires.&lt;/p&gt;

&lt;p&gt;My first version tried to detect a finished &lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;&lt;/code&gt; block the moment it appeared. Result: I was grabbing HTML mid-stream, cut off halfway through a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What actually worked was debouncing on DOM stability instead of DOM presence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;debounceTimer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debounceTimer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;debounceTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scanForCodeBlocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;600ms of "nothing changed" turned out to be a much more reliable signal than "the tag now exists." Not elegant, but it works across all three sites' streaming speeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Every AI chat UI restructures its DOM without telling you
&lt;/h2&gt;

&lt;p&gt;ChatGPT, Claude, and Gemini all ship frequent frontend updates, and none of them are obligated to keep a stable class name for you to hook into. I initially selected code blocks by class name (&lt;code&gt;.language-html&lt;/code&gt;, &lt;code&gt;.hljs&lt;/code&gt;, etc.) and had selectors silently break in production within two weeks of launch.&lt;/p&gt;

&lt;p&gt;What's held up better: matching on &lt;strong&gt;structural patterns&lt;/strong&gt; instead of class names — a &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; containing a &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt; whose text content starts with &lt;code&gt;&amp;lt;!DOCTYPE&lt;/code&gt; or &lt;code&gt;&amp;lt;html&lt;/code&gt;. It's slower to write the first time, but it doesn't care what CSS class the framework decided to use this month.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. "Detect the code" is easy. "Detect the &lt;em&gt;right&lt;/em&gt; code" is the actual problem
&lt;/h2&gt;

&lt;p&gt;A single AI response can contain multiple code blocks — a full HTML page, a CSS snippet the model explains separately, a JS fix suggested afterward. Grabbing "the first &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; tag" is wrong more often than you'd think.&lt;/p&gt;

&lt;p&gt;I ended up scoring candidate blocks instead of just taking the first match: presence of &lt;code&gt;&amp;lt;!DOCTYPE&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; scores highest, presence of &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; scores next, and blocks under ~50 characters get discarded outright (usually just an inline example, not a real page). It's not perfect, but it cut false positives dramatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Client-side preview needs a real sandbox, not just an iframe with &lt;code&gt;srcdoc&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;My first preview implementation just dumped the extracted HTML into an &lt;code&gt;iframe&lt;/code&gt; with &lt;code&gt;srcdoc&lt;/code&gt;. It worked until someone's AI-generated page included a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; that tried to reach &lt;code&gt;window.parent&lt;/code&gt; — completely benign in their case, but it's exactly the kind of thing you don't want silently allowed in an extension with content-script access to the current page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt;
  &lt;span class="na"&gt;sandbox=&lt;/span&gt;&lt;span class="s"&gt;"allow-scripts allow-same-origin"&lt;/span&gt;
  &lt;span class="na"&gt;srcdoc=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Locking &lt;code&gt;sandbox&lt;/code&gt; down explicitly, and being deliberate about &lt;em&gt;which&lt;/em&gt; permissions you grant back, is one of those five-minute fixes that matters a lot more than it looks like it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The credential question is the one users actually ask first
&lt;/h2&gt;

&lt;p&gt;I expected the first questions after launch to be about which hosts were supported. Instead, almost everyone's first question was some version of: &lt;em&gt;"where does my FTP password go?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That pushed me toward a design I'd recommend to anyone building a browser extension that touches any kind of credential:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never route credentials through your own backend if you don't have to.&lt;/li&gt;
&lt;li&gt;For FTP specifically, everything happens directly between the extension and the host — I don't sit in the middle.&lt;/li&gt;
&lt;li&gt;For self-hosted setups, I added a lightweight PHP "agent" script users drop on their own server, which accepts authenticated uploads without ever storing an FTP password inside the extension's storage at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic security engineering. But "where does my password go" is apparently the single highest-trust-impacting sentence you can answer clearly in your docs.&lt;/p&gt;




&lt;p&gt;The extension this came out of is called &lt;strong&gt;HTML Deployer&lt;/strong&gt; — it takes the HTML that ChatGPT/Claude/Gemini generate and pushes it to Netlify, GitHub Pages, FTP, or a self-hosted target, with a preview step in between. If you've hit similar DOM-stability or sandboxing problems building on top of these chat UIs, I'd genuinely like to compare notes in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Copy-Pasting AI HTML Into FTP Clients</title>
      <dc:creator>Julie Do</dc:creator>
      <pubDate>Thu, 09 Jul 2026 07:30:05 +0000</pubDate>
      <link>https://dev.to/juliedechili/stop-copy-pasting-ai-html-into-ftp-clients-26p</link>
      <guid>https://dev.to/juliedechili/stop-copy-pasting-ai-html-into-ftp-clients-26p</guid>
      <description>&lt;h2&gt;
  
  
  The workflow that was quietly wasting my time
&lt;/h2&gt;

&lt;p&gt;Here's a pattern I bet a lot of you know well:&lt;/p&gt;

&lt;p&gt;You open ChatGPT, Claude, or Gemini. You ask for a landing page, a demo, a quick prototype for a client. Thirty seconds later you've got a full HTML file — styled, responsive, ready to look at.&lt;/p&gt;

&lt;p&gt;And then... nothing. It just sits there in a chat window.&lt;/p&gt;

&lt;p&gt;To actually turn that into something you can send a client or drop into a browser tab, I was doing this, every single time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the code block out of the chat&lt;/li&gt;
&lt;li&gt;Paste it into a new file in my editor&lt;/li&gt;
&lt;li&gt;Save it as &lt;code&gt;index.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Either zip it and drag it into Netlify's dashboard, or open FileZilla, connect to FTP, and upload it to the right folder&lt;/li&gt;
&lt;li&gt;Go back, realize I forgot to check how it looks on mobile&lt;/li&gt;
&lt;li&gt;Repeat steps 1–4 after every small copy tweak&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these steps are &lt;em&gt;hard&lt;/em&gt;. That's exactly the problem — they're not hard, they're just friction, and friction is what kills momentum on small jobs. If you build landing pages, demos, or client prototypes regularly, this "last mile" between AI output and a live URL adds up to a surprising amount of dead time across a month.&lt;/p&gt;

&lt;p&gt;I tried a few workarounds first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CodePen / JSFiddle&lt;/strong&gt; — great for sharing a snippet, but not a real deployable page (no custom domain, no proper hosting, weird for client-facing work).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;netlify deploy&lt;/code&gt; via CLI&lt;/strong&gt; — fast once it's set up, but it means dropping into a terminal every time, which breaks flow when you're mid-conversation with an AI tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A local script that watches a folder and pushes to GitHub Pages&lt;/strong&gt; — worked, but was one more repo to maintain, and useless the moment I was on a different machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it solved the actual bottleneck: getting from &lt;em&gt;"the AI just wrote this HTML"&lt;/em&gt; to &lt;em&gt;"here's a live link"&lt;/em&gt; without leaving the browser tab I was already in.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built a Chrome extension for it: HTML Deployer
&lt;/h2&gt;

&lt;p&gt;The idea is simple: the extension watches the page while you're in ChatGPT, Claude, or Gemini, detects HTML code blocks as they're generated, and gives you a deploy panel right there — no copy, no paste, no context switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works, technically
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Detection&lt;/strong&gt;&lt;br&gt;
A content script scans the DOM of supported AI chat interfaces for &lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;&lt;/code&gt; blocks that look like HTML (&lt;code&gt;&amp;lt;!DOCTYPE&lt;/code&gt;, &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, or a recognizable structure). It handles multiple code blocks on the same page, so if the model gives you three revisions in a thread, you can pick the one you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Preview before you ship anything&lt;/strong&gt;&lt;br&gt;
Before any network call happens, the extension renders the extracted HTML in an isolated preview — desktop, tablet, and mobile viewports side by side. This runs entirely client-side (no server round-trip), so you catch a broken layout &lt;em&gt;before&lt;/em&gt; it's public instead of after a client opens the link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deploy targets — deliberately not locked to one host&lt;/strong&gt;&lt;br&gt;
This was the part I cared about most. A lot of "one-click deploy" tools quietly lock you into their own hosting. I didn't want that, so HTML Deployer supports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;What happens under the hood&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Netlify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Uses Netlify's deploy API with your token, gives you HTTPS + CDN instantly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same idea, Vercel's edge network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub Pages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commits the file to a repo/branch you choose — version-controlled by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FTP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Straight upload to any shared hosting you already pay for (cPanel, WHM, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-hosted (Host Agent)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A tiny PHP script you drop on your own server that accepts authenticated uploads — so you never have to store FTP credentials inside a browser extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ZIP export&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If you'd rather upload manually, or hand the file to someone else, you get a clean &lt;code&gt;.zip&lt;/code&gt; with no dependency on the extension at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last point matters for a dev audience specifically: &lt;strong&gt;your HTML is processed locally in the browser.&lt;/strong&gt; It's only sent anywhere the moment you click Deploy, and only to the destination you picked. Nothing sits on a third-party server "just in case."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Instant feedback loop&lt;/strong&gt;&lt;br&gt;
On successful deploy you get a live URL and an auto-generated QR code — handy for testing on your phone or dropping straight into a client Slack message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is actually for
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend this is groundbreaking distributed-systems engineering. It's a focused tool for a specific, repetitive gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marketers / no-code builders / solo founders&lt;/strong&gt; who use AI to generate landing pages and don't want to touch a terminal at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freelancers and independent devs&lt;/strong&gt; who ship small static sites for clients constantly and are tired of the same five manual steps for every demo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small agencies&lt;/strong&gt; running multiple microsites/campaign pages who want a lighter, less error-prone alternative to "junior opens FileZilla and hopes for the best."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your deploys already involve CI/CD pipelines, this isn't trying to replace that — it's for the &lt;em&gt;disposable&lt;/em&gt; stuff: prototypes, one-off landing pages, demos, things that don't need a full pipeline but still need to be a real, shareable URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd genuinely like feedback on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Anyone using a similar "watch the AI chat, extract the artifact" pattern for other file types (I'm curious if this generalizes beyond HTML)?&lt;/li&gt;
&lt;li&gt;If you've built self-host agents like this before, I'd love to compare notes on keeping credential handling minimal — right now the PHP agent approach avoids storing FTP creds in the extension at all, but I'm open to better patterns.&lt;/li&gt;
&lt;li&gt;Anything from the FTP/cPanel world that would trip this up on more unusual hosting setups?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extension is live on the Chrome Web Store if you want to poke at it — happy to answer anything about the implementation in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>The one-click gap nobody was solving: why I built HTML Deployer</title>
      <dc:creator>Julie Do</dc:creator>
      <pubDate>Fri, 03 Jul 2026 08:31:21 +0000</pubDate>
      <link>https://dev.to/juliedechili/the-one-click-gap-nobody-was-solving-why-i-built-html-deployer-4016</link>
      <guid>https://dev.to/juliedechili/the-one-click-gap-nobody-was-solving-why-i-built-html-deployer-4016</guid>
      <description>&lt;h2&gt;
  
  
  The one-click gap nobody was solving: why I built HTML Deployer
&lt;/h2&gt;

&lt;p&gt;A few months ago I watched a marketer friend do something that made me wince.&lt;/p&gt;

&lt;p&gt;She'd spent twenty minutes in ChatGPT crafting a landing page for a product launch. The copy was sharp, the layout was clean, the CTA button even had a nice little hover animation. She was proud of it — and she should have been.&lt;/p&gt;

&lt;p&gt;Then she asked me: "Okay, how do I actually put this online?"&lt;/p&gt;

&lt;p&gt;I told her to copy the code, create a repo, connect it to Netlify, wait for the build... and I watched her face fall somewhere around the word "repo." She closed the tab. That landing page never went live. Not because the idea was bad, not because the code was bad — because there was a wall between "I have HTML" and "I have a URL," and nobody had bothered to tear it down.&lt;/p&gt;

&lt;p&gt;That's the moment this project actually started, even though I didn't write a single line of code that day.&lt;/p&gt;

&lt;h3&gt;
  
  
  The wall everyone pretends isn't there
&lt;/h3&gt;

&lt;p&gt;Once I started paying attention, I saw the same wall everywhere. Not just with marketers — with freelancers sending demo links to clients, with solo founders trying to spin up a bio page between meetings, with small agencies juggling a dozen campaign microsites at once.&lt;/p&gt;

&lt;p&gt;They could all &lt;em&gt;generate&lt;/em&gt; a page in seconds. ChatGPT, Claude, Gemini — doesn't matter which. The AI part had gotten absurdly good. But turning that generated block of HTML into something with an actual link you could send someone? That still meant terminal commands, Git, hosting dashboards, FTP clients, or — worst of all — pinging a developer for something that should take thirty seconds.&lt;/p&gt;

&lt;p&gt;The tools to &lt;em&gt;create&lt;/em&gt; had leapt a decade ahead. The tools to &lt;em&gt;ship&lt;/em&gt; were stuck in 2012. I couldn't stop thinking about how absurd that gap was.&lt;/p&gt;

&lt;h3&gt;
  
  
  First attempt: I tried to build a platform
&lt;/h3&gt;

&lt;p&gt;My first instinct, embarrassingly, was to build Yet Another Hosting Platform. Upload your HTML, we host it, here's your link. I got a rough version working in a weekend and immediately hated it.&lt;/p&gt;

&lt;p&gt;Here's the problem: the moment you build your own hosting platform, you're asking people to trust you with their site's uptime, forever. You're also locking them in — if they ever want to move to their own domain, their own Netlify account, their own infrastructure, tough luck. I'd just be recreating the exact kind of walled garden that made the original workflow so painful in the first place, just with a shinier wrapper.&lt;/p&gt;

&lt;p&gt;I scrapped it. Worse than scrapping it — I sat on the idea for a couple of weeks not sure it was worth solving at all, since every direction I tried seemed to reintroduce the same friction I was trying to remove.&lt;/p&gt;

&lt;h3&gt;
  
  
  The unlock: don't move the destination, move the moment
&lt;/h3&gt;

&lt;p&gt;The breakthrough wasn't technical, it was almost embarrassingly simple: the problem was never &lt;em&gt;where&lt;/em&gt; people were hosting. Netlify is fine. GitHub Pages is fine. Plenty of people already have FTP access to hosting they're already paying for. The problem was the distance between "AI just finished generating this code" and "I am now doing something about it."&lt;/p&gt;

&lt;p&gt;So instead of building a destination, I built a bridge that lives exactly where the pain happens — inside the AI chat itself.&lt;/p&gt;

&lt;p&gt;That's the core idea behind HTML Deployer: a Chrome extension that watches ChatGPT, Claude, and Gemini conversations in real time, detects when an HTML code block shows up, and puts a Deploy button right next to it. No copy-paste. No new tab. No context switch. You're still looking at the conversation where the idea was born when you click the button that makes it real.&lt;/p&gt;

&lt;p&gt;And on the other side of that button, you get to choose: Netlify, Vercel, GitHub Pages, your own FTP hosting, a self-hosted agent on your own server, or just a clean ZIP if you want to do it manually. No lock-in. If you already have hosting, use it. If you don't, get some in one click. The extension's job ends at "your HTML is now live" — it doesn't try to own what happens after.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the detection layer was the hard part
&lt;/h3&gt;

&lt;p&gt;The UI is the easy 20%. The hard 80% was making detection actually reliable across three different chat interfaces that weren't designed to be scraped, that change their DOM structure without warning, and that sometimes render multiple code blocks in a single response.&lt;/p&gt;

&lt;p&gt;A few things I had to solve along the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distinguishing an actual deployable HTML document from a stray &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; snippet someone was showing for explanation, not deployment.&lt;/li&gt;
&lt;li&gt;Handling streaming responses — code blocks that are still being typed out character by character shouldn't trigger a half-broken deploy button.&lt;/li&gt;
&lt;li&gt;Making the preview render instantly, across desktop, tablet, and mobile viewports, without spinning up a server, so people catch layout problems before the page is public instead of after.&lt;/li&gt;
&lt;li&gt;Being paranoid about credentials. FTP passwords and API tokens are exactly the kind of thing that should never touch a server I control. Everything happens client-side; nothing is stored on our infrastructure until the moment you hit Deploy, and even then it goes straight to your chosen destination.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point mattered more than any feature. Every person in this space — freelancer, agency, solo builder — has the same quiet fear sitting under the excitement: &lt;em&gt;what if this breaks something for a client, or leaks a credential I'm responsible for?&lt;/em&gt; If the tool didn't earn trust on that front first, none of the convenience would matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually changed once it worked
&lt;/h3&gt;

&lt;p&gt;The first time I generated a page in Claude, clicked Deploy, and got back a live HTTPS URL with a QR code before I'd even finished my coffee, it felt almost anticlimactic — which is exactly how a good tool should feel. Not magical. Just... no longer an obstacle.&lt;/p&gt;

&lt;p&gt;That agency owner scenario I mentioned earlier — the one juggling a dozen campaign microsites — turned a 30-minute manual FTP routine into something closer to a minute, without changing the hosting they already trusted.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual lesson
&lt;/h3&gt;

&lt;p&gt;If I had to compress this into one sentence for anyone building dev tools: the biggest wins aren't always in making something new possible — sometimes they're in deleting the five boring steps between "I already did the hard, creative part" and "this exists in the world now." AI didn't need another platform. It needed the wall between generation and publishing to just... not be there.&lt;/p&gt;

&lt;p&gt;I still think about my friend closing that tab. That landing page never seeing daylight is a small thing, but it's the small thing multiplied across everyone who's ever built something good in an AI chat and then quietly given up at the last step. That's the gap HTML Deployer is trying to close — one Deploy button at a time.&lt;/p&gt;

&lt;p&gt;If you're curious, it's a free Chrome extension and works with ChatGPT, Claude, and Gemini. &lt;/p&gt;

&lt;p&gt;If you want to try it: &lt;a href="https://chromewebstore.google.com/detail/gihmknkabkkghpiocgnoiejagngdegea" rel="noopener noreferrer"&gt;HTML Deployer on the Chrome Web Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Would genuinely love to hear how other builders here are handling the "AI output → live thing" problem in their own stacks — I doubt HTML is the only place this wall exists.&lt;/p&gt;

&lt;h1&gt;
  
  
  webdev #ai #chrome #productivity
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>html</category>
      <category>devops</category>
    </item>
    <item>
      <title>I built a Chrome extension to deploy AI-generated HTML in one click</title>
      <dc:creator>Julie Do</dc:creator>
      <pubDate>Tue, 16 Jun 2026 08:02:09 +0000</pubDate>
      <link>https://dev.to/juliedechili/i-built-a-chrome-extension-to-deploy-ai-generated-html-in-one-click-3l1a</link>
      <guid>https://dev.to/juliedechili/i-built-a-chrome-extension-to-deploy-ai-generated-html-in-one-click-3l1a</guid>
      <description>&lt;h2&gt;
  
  
  The problem I kept running into
&lt;/h2&gt;

&lt;p&gt;Every time I used Claude or ChatGPT to build a quick landing page or a&lt;br&gt;
microsite prototype, I hit the same wall at the end.&lt;/p&gt;

&lt;p&gt;The HTML looked great. But getting it &lt;em&gt;live&lt;/em&gt; meant:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the code out of the chat window&lt;/li&gt;
&lt;li&gt;Create a new project on Netlify (or push to a GitHub repo)&lt;/li&gt;
&lt;li&gt;Wait for the deploy pipeline&lt;/li&gt;
&lt;li&gt;Share the link&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a developer, that's maybe 5 minutes of boring overhead. But for a marketer or no-code builder? It might as well be a locked door.&lt;/p&gt;

&lt;p&gt;And even for me — when I'm doing it 10+ times a week across small&lt;br&gt;
client projects — those 5-minute tasks add up fast.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;HTML Deployer&lt;/strong&gt; is a Chrome extension that sits inside your AI chat session (ChatGPT, Claude, Gemini) and adds a &lt;strong&gt;Deploy button directly next to every HTML code block&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Click it → preview the page on desktop, tablet and mobile → choose&lt;br&gt;
where to publish → done.&lt;/p&gt;

&lt;p&gt;No copy-paste. No tab switching. No terminal.&lt;/p&gt;


&lt;h2&gt;
  
  
  The four deploy targets
&lt;/h2&gt;

&lt;p&gt;I wanted to avoid locking users into a single platform. So the&lt;br&gt;
extension currently supports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Netlify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free CDN, HTTPS, custom domain — easiest for most people&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast preview URLs, zero config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub Pages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Version-controlled static hosting, free forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FTP / cPanel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your own shared hosting — most agencies already have this&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-hosted agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A tiny PHP file on your server, no FTP credentials stored in the extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ZIP export&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Download and upload manually — maximum flexibility&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjdhclc5eg2m6mdor5au9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjdhclc5eg2m6mdor5au9.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
The self-hosted option was a deliberate design choice. A lot of&lt;br&gt;
freelancers and small agencies have existing hosting accounts they're paying for. There was no reason to force them onto Netlify just because it's the path of least resistance.&lt;/p&gt;


&lt;h2&gt;
  
  
  How it actually works (technical overview)
&lt;/h2&gt;

&lt;p&gt;When the extension loads on a supported AI page, it uses a&lt;br&gt;
&lt;code&gt;MutationObserver&lt;/code&gt; to watch for new &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt; blocks being rendered in the DOM. When it detects a block that looks like an HTML document (it checks for &lt;code&gt;&amp;lt;!DOCTYPE&lt;/code&gt;, &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, or &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tags), it injects asmall Deploy button into the parent element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified detection logic&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pre code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isHTMLDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hdInjected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;injectDeployButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hdInjected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the preview, the extension renders the HTML in a sandboxed &lt;code&gt;iframe&lt;/code&gt;inside a side panel. It uses CSS transforms to simulate three viewport sizes without making any network requests — everything stays local.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foba3mrlv2qffxkcx4j1w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foba3mrlv2qffxkcx4j1w.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
For Netlify and GitHub deploys, the extension uses their respective&lt;br&gt;
REST APIs. Credentials are stored in Chrome's &lt;code&gt;storage.local&lt;/code&gt; (not&lt;br&gt;
synced across devices, not sent to our servers).&lt;/p&gt;

&lt;p&gt;For FTP deploys, the extension talks to a small backend proxy (because browsers can't open raw TCP connections). The FTP credentials are encrypted in transit and not logged or stored on our side.&lt;/p&gt;


&lt;h2&gt;
  
  
  The self-host option explained
&lt;/h2&gt;

&lt;p&gt;Some users — particularly agencies with client hosting accounts — don't want to enter FTP credentials into a browser extension at all. That's a completely reasonable security concern.&lt;/p&gt;

&lt;p&gt;The self-hosted mode works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You download a single &lt;code&gt;agent.php&lt;/code&gt; file (~30 lines)&lt;/li&gt;
&lt;li&gt;You upload it to your own server&lt;/li&gt;
&lt;li&gt;You point the extension at your server URL + a secret token you set&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The extension sends the HTML payload to your own server, which writes the file to disk. Your FTP credentials never leave your server. The extension only knows the public URL of the agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// Simplified agent.php&lt;/span&gt;
&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-secret-token-here'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REQUEST_METHOD'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'token'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filename'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'index.html'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'html'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_HOST'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not fancy, but effective.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the workflow actually looks like
&lt;/h2&gt;

&lt;p&gt;Here's a typical session for a freelancer building a landing page for a client:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Claude&lt;/li&gt;
&lt;li&gt;Ask it to build a landing page with a specific offer and CTA&lt;/li&gt;
&lt;li&gt;HTML Deployer auto-detects the code block&lt;/li&gt;
&lt;li&gt;Click the Deploy button in the sidebar&lt;/li&gt;
&lt;li&gt;Preview looks good on mobile — approve&lt;/li&gt;
&lt;li&gt;Select Netlify, click Deploy&lt;/li&gt;
&lt;li&gt;Get a live URL + QR code in about 8 seconds&lt;/li&gt;
&lt;li&gt;Send the URL to the client for feedback&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole thing from "AI generates the code" to "client has a link" is under 2 minutes. Previously, that same flow was 10–15 minutes of&lt;br&gt;
switching between tabs, copying code, creating Netlify projects, etc.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned building this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MutationObserver is your friend — but also your enemy.&lt;/strong&gt;&lt;br&gt;
AI chat interfaces re-render constantly. ChatGPT in particular&lt;br&gt;
re-renders code blocks multiple times as streaming completes. I had to add debouncing and a &lt;code&gt;dataset&lt;/code&gt; flag to avoid injecting the button multiple times into the same block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser storage is trickier than it looks.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;chrome.storage.local&lt;/code&gt; has a 5MB limit per item. For an extension that stores deploy history, that fills up faster than you'd expect if someone is deploying large HTML files with embedded base64 images. I had to add a cleanup routine that prunes old history entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Users are more security-conscious than I expected.&lt;/strong&gt;&lt;br&gt;
The most common first question was: &lt;em&gt;"Does this store my FTP&lt;br&gt;
credentials?"&lt;/em&gt; — not &lt;em&gt;"How does the preview work?"&lt;/em&gt;. That pushed me to build the self-host option earlier than planned, and to make the&lt;br&gt;
security model explicit in the onboarding flow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;HTML Deployer is free to install on Chrome Web Store:&lt;br&gt;
→ &lt;a href="https://chromewebstore.google.com/detail/gihmknkabkkghpiocgnoiejagngdegea" rel="noopener noreferrer"&gt;Install HTML Deployer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have a use case that doesn't fit the current deploy targets, or you find a bug, feel free to drop a comment below or reach out.&lt;/p&gt;

&lt;p&gt;Would genuinely love to hear how other builders are handling the&lt;br&gt;
"AI output → live page" gap in their workflows.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>html</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
