Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

site-deep-clone

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.com

First run installs playwright and chromium silently (~150 MB, one time). Subsequent runs start in under a second.

What gets captured

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)

Output

.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":{...}}

Install

Requires Node ≥ 18.

git clone https://github.com/<your-username>/site-deep-clone.git
cd site-deep-clone
node extract.mjs https://example.com

That's it. No npm install, no playwright install — the script does both on first run.

Usage

# 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)

Exit codes

Code Meaning
0 OK
10 playwright npm package missing
11 chromium binary missing
20 site unreachable
21 navigation timeout
30 invalid URL

Honest limits

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 (--stealth covers 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-state or --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.

Philosophy

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.

License

MIT.

About

Extract every observable layer of a live site (DOM, CSS, JS-driven animations, screenshots) — one command, no assumptions

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages