<?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: eklavya pandey</title>
    <description>The latest articles on DEV Community by eklavya pandey (@eklavya_pandey_163f26590b).</description>
    <link>https://dev.to/eklavya_pandey_163f26590b</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%2F4196184%2Fdbf7e119-5712-46ef-b744-c266bddfae2a.jpg</url>
      <title>DEV Community: eklavya pandey</title>
      <link>https://dev.to/eklavya_pandey_163f26590b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eklavya_pandey_163f26590b"/>
    <language>en</language>
    <item>
      <title>I built a tool that reverse-engineers any website into React in one command. Here's how the capture actually works.</title>
      <dc:creator>eklavya pandey</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:48:58 +0000</pubDate>
      <link>https://dev.to/eklavya_pandey_163f26590b/i-built-a-tool-that-reverse-engineers-any-website-into-react-in-one-command-heres-how-the-capture-35bc</link>
      <guid>https://dev.to/eklavya_pandey_163f26590b/i-built-a-tool-that-reverse-engineers-any-website-into-react-in-one-command-heres-how-the-capture-35bc</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpngl2alpfi63djpnpstl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpngl2alpfi63djpnpstl.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm a student in India and I kept doing two things by hand.&lt;/p&gt;

&lt;p&gt;One: clone a repo, open the README, find out it needs a &lt;code&gt;.env&lt;/code&gt; with eleven keys, a Postgres, and a &lt;code&gt;pnpm&lt;/code&gt; version I don't have, spend twenty minutes, give up or not.&lt;/p&gt;

&lt;p&gt;Two: see a landing page I like, open devtools, and poke around to see how they did the grid, what font that is, where the pricing data comes from.&lt;/p&gt;

&lt;p&gt;So I wrote one command that does both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx breakinggood https://github.com/someone/some-repo
npx breakinggood https://tailwindcss.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First one clones, scans the dependencies, sets up &lt;code&gt;.env&lt;/code&gt;, installs, runs, opens the browser. Second one captures the page with your own Chrome and writes a Vite + React + Tailwind project that matches it, with a mock API server that replays the real API responses. It's MIT, on npm, repo is &lt;a href="https://github.com/eklavya2201/breakinggood" rel="noopener noreferrer"&gt;eklavya2201/breakinggood&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is about the second half, the website capture, because that's where all the interesting decisions were. The repo half is at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just screenshot-to-code
&lt;/h2&gt;

&lt;p&gt;There are a lot of "paste a screenshot, get React" tools. I tried a few before writing this and the output always looked &lt;em&gt;roughly&lt;/em&gt; right and was wrong in every detail. A picture loses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the actual font (the model guesses Inter, every time)&lt;/li&gt;
&lt;li&gt;exact colours (it eyeballs &lt;code&gt;#1a1a1a&lt;/code&gt; as &lt;code&gt;#000&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;spacing and radii&lt;/li&gt;
&lt;li&gt;all the text below the fold&lt;/li&gt;
&lt;li&gt;and, the one that annoyed me most, the data. The pricing cards are hard-coded. The testimonials are lorem ipsum with a hat on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A browser has all of that. So the capture doesn't start from a picture. It starts from the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The capture, step by step
&lt;/h2&gt;

&lt;p&gt;Everything lives in one file, &lt;code&gt;src/site/capture.ts&lt;/code&gt;, about 330 lines of Playwright.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Use the Chrome that's already installed.&lt;/strong&gt; &lt;code&gt;playwright-core&lt;/code&gt; can drive an installed Chrome or Edge through the &lt;code&gt;channel&lt;/code&gt; option. So the first launch attempt is &lt;code&gt;chromium.launch({ channel: "chrome" })&lt;/code&gt;, then &lt;code&gt;msedge&lt;/code&gt;, then a Playwright Chromium if one happens to exist. Nobody downloads 150 MB of browser for a CLI they're trying once. There's a &lt;code&gt;BREAKINGGOOD_BROWSER&lt;/code&gt; env var if you want to point it at a specific binary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Record the network before navigating.&lt;/strong&gt; A &lt;code&gt;page.on("response")&lt;/code&gt; listener runs from the start. It keeps only &lt;code&gt;xhr&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt; resource types, and for JSON responses under status 400 it reads the body and keeps the first 6000 characters. Everything else (images, scripts, CSS) is ignored. Then it deduplicates by method + URL-without-query and caps at 40 requests. This is the list that becomes the mock server later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Load, settle, scroll.&lt;/strong&gt; &lt;code&gt;goto&lt;/code&gt; with &lt;code&gt;domcontentloaded&lt;/code&gt;, then wait for &lt;code&gt;networkidle&lt;/code&gt; with a 15 s cap (it's best-effort, SPAs never go idle). Then it clicks through a short list of cookie-banner button labels ("Accept all", "I agree", "Got it"...) because otherwise half of every screenshot is a GDPR modal. Then it scrolls the page in 700 px steps down to 12000 px so lazy-loaded sections actually render, and scrolls back to the top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Read the computed styles, not the CSS.&lt;/strong&gt; This is the part I'd argue is the whole trick. One &lt;code&gt;page.evaluate&lt;/code&gt; walks the first 4000 elements in the body and calls &lt;code&gt;getComputedStyle&lt;/code&gt; on each. For every leaf element with text it counts the font family, text colour, and font size. For every element with a non-transparent background it adds the background colour &lt;em&gt;weighted by the element's area&lt;/em&gt;. Border radii get counted too. Then it takes the top N of each map.&lt;/p&gt;

&lt;p&gt;Weighting backgrounds by area matters. Without it, the most common background is whatever colour the 300 tiny icon wrappers have. With it, the page background and the hero colour come out first. Text colour is weighted by count instead, because body text is a lot of small elements and that's correct.&lt;/p&gt;

&lt;p&gt;The result is a small token block that goes into the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fonts: Inter | JetBrains Mono
text colours: rgb(15, 23, 42) | rgb(100, 116, 139) | ...
backgrounds (by area): rgb(255, 255, 255) | rgb(248, 250, 252) | ...
border radii: 8px | 12px | 9999px
content max-width: 1280px
dark mode: false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dark mode is decided from the luminance of the page background, which is the body background, else the html background, else the largest painted background. Transparent counts as white.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A simplified DOM.&lt;/strong&gt; The full HTML of a modern page is hundreds of KB of class soup. The capture walks the body recursively and keeps only the tag name, a handful of attributes (&lt;code&gt;href&lt;/code&gt;, &lt;code&gt;src&lt;/code&gt;, &lt;code&gt;alt&lt;/code&gt;, &lt;code&gt;placeholder&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;aria-label&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;), and the text. Scripts, styles, SVG internals, iframes go. Anything &lt;code&gt;display: none&lt;/code&gt; goes. Classes go, all of them, because Tailwind classes from the original are noise once you have the computed tokens. Text nodes are capped at 300 characters, depth at 14, and the whole thing at 45 KB.&lt;/p&gt;

&lt;p&gt;The model gets this plus a semantic outline (every &lt;code&gt;header&lt;/code&gt;, &lt;code&gt;nav&lt;/code&gt;, &lt;code&gt;section&lt;/code&gt;, &lt;code&gt;h1&lt;/code&gt;-&lt;code&gt;h3&lt;/code&gt; with its first 80 characters), the first 80 links, and the first 40 real images with their natural sizes. Images use the original public URLs, so the rebuild shows the real pictures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Tiled screenshots.&lt;/strong&gt; Full-page screenshots of long pages are 8000+ px tall and the vision APIs reject or downscale them. So the page is sliced from the top into JPEG tiles, 1800 px tall at 1440 wide, max four tiles. Then the viewport switches to 390 x 844 and takes three more tiles for mobile. JPEG at quality 82 keeps each tile under a few hundred KB. The mobile tiles are what tell the model how the grid collapses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Fingerprints.&lt;/strong&gt; Cheap checks: &lt;code&gt;__NEXT_DATA__&lt;/code&gt;, &lt;code&gt;#__nuxt&lt;/code&gt;, &lt;code&gt;__remixContext&lt;/code&gt;, &lt;code&gt;data-wf-site&lt;/code&gt;, &lt;code&gt;wp-content&lt;/code&gt;, a &lt;code&gt;generator&lt;/code&gt; meta tag. They go in the prompt as "likely original stack" and don't do much, but they're nice to see in the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt
&lt;/h2&gt;

&lt;p&gt;The system prompt is about 30 lines. It asks for a fixed set of files (package.json, vite config, &lt;code&gt;src/App.jsx&lt;/code&gt;, one component per major section, &lt;code&gt;mock/db.json&lt;/code&gt;, &lt;code&gt;mock/server.js&lt;/code&gt;), Tailwind v4 with an &lt;code&gt;@theme&lt;/code&gt; block built from the captured palette and fonts, real text from the DOM, original image URLs, and a dependency-free Node HTTP server on port 8787 that serves every captured API path from &lt;code&gt;db.json&lt;/code&gt;. The Vite config proxies &lt;code&gt;/api&lt;/code&gt; to it. The React code has to fetch from &lt;code&gt;/api/...&lt;/code&gt; wherever the original site fetched data, so the clone actually makes network calls instead of hard-coding the pricing table.&lt;/p&gt;

&lt;p&gt;Two lines in that prompt took the most iterations:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Stay compact: aim for under 1500 lines in total. Put repeated content (nav items, feature cards, logos, testimonials, footer columns) in plain arrays and render with .map().&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without that, tailwindcss.com produced a 3000-line &lt;code&gt;App.jsx&lt;/code&gt; and the model ran out of output tokens halfway through the footer. With it, it's around 900 lines across eight files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File format.&lt;/strong&gt; The model writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== FILE: src/App.jsx ===
...
=== END FILE ===
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not JSON. JSON means every file is one giant escaped string, and a single unescaped quote in a 1000-line response kills the whole thing. The delimiter format is parsed with one regex, tolerates a stray code fence inside a block, and refuses any path with &lt;code&gt;..&lt;/code&gt;. If &lt;code&gt;package.json&lt;/code&gt; or &lt;code&gt;src/App.jsx&lt;/code&gt; is missing from the output, it fails loudly instead of running half a project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Providers.&lt;/strong&gt; The capture is the same regardless of model. There's a thin provider layer for Anthropic, OpenAI, Groq, OpenRouter, Gemini and Ollama, picked from whichever API key you have in your environment. Screenshots only go to models that take images. Text-only models still get the DOM, the tokens and the API bodies, and the output is noticeably worse but usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8000 px screenshots.&lt;/strong&gt; First version sent one full-page PNG. The API silently downscaled it to a blur. Tiles fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model running out of output.&lt;/strong&gt; See the "stay compact" line above. &lt;code&gt;maxTokens&lt;/code&gt; is 128k but the fix was asking for less, not allowing more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workspace packages flagged as foreign.&lt;/strong&gt; On the repo side, the scanner looked at &lt;code&gt;resolved&lt;/code&gt; in the lockfile and flagged &lt;code&gt;packages/foo&lt;/code&gt; workspace links as non-registry packages. Now it only flags entries whose &lt;code&gt;resolved&lt;/code&gt; has a URL scheme that isn't the npm or yarn registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie banners in every screenshot.&lt;/strong&gt; The label list above. It's not clever and it works on most sites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hero is a video or a canvas.&lt;/strong&gt; Nothing to do. The screenshot shows a frame, the DOM shows a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;, the model puts a placeholder. Fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The repo side, briefly
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npx breakinggood &amp;lt;github url&amp;gt;&lt;/code&gt; shallow-clones, detects the stack (Node package manager from lockfiles, Python, Go, Rust, Docker Compose), and before installing anything runs a scan in &lt;code&gt;src/repo/scan.ts&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lockfile packages with &lt;code&gt;hasInstallScript: true&lt;/code&gt; that aren't on a list of about 70 well-known native packages (esbuild, sharp, prisma, playwright...)&lt;/li&gt;
&lt;li&gt;packages whose &lt;code&gt;resolved&lt;/code&gt; URL is outside the public registry&lt;/li&gt;
&lt;li&gt;direct dependencies published less than 90 days ago (one registry fetch per dep, 6 s total budget, skipped silently if offline)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;preinstall&lt;/code&gt; / &lt;code&gt;postinstall&lt;/code&gt; / &lt;code&gt;prepare&lt;/code&gt; hooks that match &lt;code&gt;curl ... | sh&lt;/code&gt;, &lt;code&gt;powershell -enc&lt;/code&gt;, &lt;code&gt;base64 -d&lt;/code&gt;, &lt;code&gt;eval(atob(&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setup.py&lt;/code&gt; / &lt;code&gt;pyproject.toml&lt;/code&gt; calling &lt;code&gt;os.system&lt;/code&gt;, &lt;code&gt;subprocess&lt;/code&gt;, &lt;code&gt;urllib&lt;/code&gt;, &lt;code&gt;exec&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;source files with 200+ character base64 blobs or long &lt;code&gt;\x&lt;/code&gt; escape runs&lt;/li&gt;
&lt;li&gt;a committed real &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;High findings ask before continuing. It is not a sandbox and I say so in the README. It catches the obvious stuff, which is the stuff that actually shows up in the npm incidents I've read about.&lt;/p&gt;

&lt;p&gt;Then it copies &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;, fills anything named like a secret (SECRET, JWT, SESSION, SALT, and so on) with a random value so the app at least boots, installs, runs the dev script, watches its output for the localhost URL or port it prints, and opens the browser there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pages behind a login, Cloudflare challenges, and bot walls don't capture. There's an &lt;a href="https://github.com/eklavya2201/breakinggood/issues/6" rel="noopener noreferrer"&gt;open issue&lt;/a&gt; to at least detect the challenge page and say so instead of rebuilding it.&lt;/li&gt;
&lt;li&gt;Heavy canvas or WebGL pages come out as a static frame.&lt;/li&gt;
&lt;li&gt;It rebuilds structure and style of public pages. The content and the brand belong to whoever owns the site. There's a &lt;code&gt;--sanitize&lt;/code&gt; flag that swaps the brand name and logo for neutral placeholders, and every clone writes a provenance file saying where it came from.&lt;/li&gt;
&lt;li&gt;The quality ceiling is the model. Same capture, different model, visibly different output.&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx breakinggood https://tailwindcss.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README has untouched before/after screenshots of Hacker News and tailwindcss.com. If you run it on something and it does badly, open an issue with the URL, that's the most useful thing right now. There are a few &lt;code&gt;good first issue&lt;/code&gt;s too: Svelte output, a &lt;code&gt;--framework next&lt;/code&gt; option, Deno and Bun detection.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/eklavya2201/breakinggood" rel="noopener noreferrer"&gt;https://github.com/eklavya2201/breakinggood&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
