One command. Captures everything a browser sees. No assumptions.
You opened the inspector, took a screenshot, started writing the clone — and three hours later it still looks wrong. The font weight is off by 100. The hover state is missing. The hero animation only fires on a Framer Motion transform that getAnimations() can't see. The fonts are CSS-in-JS and don't appear in any .css file. The grid breaks at 768px because you guessed the breakpoint.
Cloning a live site by hand means you guess. Every value not measured is a bug waiting.
What you need is the same thing a browser has: every computed style on every element, every @keyframes from every source (including the <style> tags React injected at runtime), every transform that Framer Motion writes via requestAnimationFrame, every hover state that only appears when the mouse moves, every section after lazy reveal triggers, every breakpoint actually rendering. Across the whole site, including the routes the entry page links to.
That's what this does. One command. Single line of JSON output you can parse. No flags to learn — every observable layer is captured by default.
node extract.mjs https://example.comFirst run installs playwright and chromium silently (~150 MB, one time). Subsequent runs start in under a second.
| Layer | How |
|---|---|
| Pre-JS HTML | fetch with browser User-Agent |
| Post-JS DOM | Playwright documentElement.outerHTML |
| Every CSS / JS / font / image | Network log + download |
Inline <style> tags + document.adoptedStyleSheets |
Covers CSS-in-JS (Framer, styled-components, emotion) |
@keyframes / @media / @font-face / @supports |
Same-origin via CSSOM, cross-origin via regex parse |
| Computed styles for every element, grouped by class signature | One representative per className → covers everything without exploding the JSON |
| Live animations across 5 scroll positions | getAnimations() at 0/25/50/75/100% |
| JS-driven animations (Framer Motion / GSAP / anime.js) | MutationObserver on style/class/transform during scroll trip |
| Hover/focus computed-style diffs | page.hover() + before/after diff on every interactive element |
| Per-section screenshots | <section> iteration + scrollIntoView + dwell (handles lazy reveal) |
| Multi-breakpoint screenshots | 390 / 768 / 1024 / 1440, top + mid + bottom + fullPage |
| Theme variants | Auto-toggle [data-theme] if present |
| Internal route crawl | <a href> discovery + /sitemap.xml |
| Framework detection | Framer / Next / Webflow / Wix / WordPress / Shopify / Astro / Gatsby / SvelteKit / React / Vue, with implications |
| Stealth bypass | playwright-extra + stealth plugin (default ON for Cloudflare-protected sites) |
.ref/<hostname>/
raw.html
data/
summary.md ← human-readable digest, read this first
framework.json ← what stack + implications
dom-snapshot.json ← cssVars, headlines, regions, groupedComputed, ...
at-rules-from-files.json ← @keyframes from every CSS source
mutations.json ← JS-driven animation targets
hover-diffs.json ← per-element hover state changes
sections.json ← section discovery metadata
scroll-states.json ← animations seen at each scroll depth
network.json ← full request log
inline-styles/ ← every <style> tag + adoptedStyleSheets
routes/ ← per-route DOM snapshots
css/ js/ fonts/ images/ ← downloaded assets
screenshots/
bp-{w}-{top,mid,bottom,full}.png
section-NN-{label}.png
theme-{light,dark}.png
The last line of stdout is a single parseable JSON envelope:
▸ RESULT: {"version":"2.3.0","outDir":"...","framework":{...},"counts":{...},"paths":{...}}
Requires Node ≥ 18.
git clone https://github.com/<your-username>/site-deep-clone.git
cd site-deep-clone
node extract.mjs https://example.comThat's it. No npm install, no playwright install — the script does both on first run.
# Default — captures everything
node extract.mjs <url> [output-dir]Optional flags:
| Flag | Purpose |
|---|---|
--no-hover |
skip hover-state capture |
--no-crawl |
only the entry page, no auto-discovery of routes |
--no-stealth |
disable bot-detection bypass |
--no-mutations |
skip MutationObserver (faster on static sites) |
--no-sections |
skip per-section screenshots |
--routes=/a,/b,/c |
extra paths beyond auto-crawl |
--storage-state=path |
Playwright storage state file (cookies + localStorage) |
--basic-auth=user:pass |
HTTP basic auth |
--wait=ms |
extra wait after networkidle (default 2000) |
--breakpoints=W,W,... |
viewport widths (default 390,768,1024,1440) |
--interactions |
clicks safe interactive elements (opt-in: can navigate / break state) |
| Code | Meaning |
|---|---|
0 |
OK |
10 |
playwright npm package missing |
11 |
chromium binary missing |
20 |
site unreachable |
21 |
navigation timeout |
30 |
invalid URL |
What this captures: anything observable to an anonymous browser session. That covers the vast majority of marketing sites, portfolios, SPAs, dashboards, no-code sites.
What it does not capture:
- Click-triggered modals (use
--interactions, accept the risk). - Cloudflare Turnstile / hCaptcha challenges (
--stealthcovers about 70% of bot detection; advanced challenges need a CAPTCHA solver service, not in scope). - WebGL shader runtime state (pixels go in screenshots; logic is in the downloaded JS files).
- WebSocket message streams.
- Authenticated content without
--storage-stateor--basic-auth. :active(mouse-down) pseudo-state.
For a true byte-for-byte replay you want a HAR recording, not this. This is for cloning the design and behavior — the parts a person can see.
Every value in the output came from observing a real browser. There are no heuristics that say "this looks like X so it's probably Y." If something is in the snapshot, the browser saw it that way. If it isn't, the script didn't observe it — and the summary will tell you so.
This matters because clones drift. A guessed letter-spacing: -0.025em looks fine until you compare to the real -2px. A guessed transition: opacity 0.9s looks fine until you notice the original is 1.2s cubic-bezier(0.25, 0.1, 0.25, 1). Drift compounds. By the fifth section it's a different site.
This script is also designed to be invoked by an autonomous agent, not just a human. The framework detection runs first and prints implications so the agent knows what to expect. The exit codes are stable. The final RESULT line is parseable JSON. The data/summary.md orders findings by what a human (or agent) needs to read first.
MIT.