Communitygithub.com

braxtonROSE4/clone-any-website

Reverse-engineer and pixel-perfect clone any live website, from a single landing page to an entire site. Captures DOM, CSS design tokens, assets, fonts, and compiled JS config data, reverse-engineers scroll animations and stateful interactions, rebuilds the site, and proves fidelity with a scored benchmark.

What is clone-any-website?

This skill enables an AI agent to clone any live website with pixel-perfect accuracy, capturing not just the visual appearance but also the underlying DOM structure, CSS design tokens, fonts, assets, and compiled JavaScript configuration data. It goes beyond simple screenshot-based cloning by programmatically extracting ground truth data from the live page, then reverse-engineering scroll animations and stateful interactions to rebuild the site either as a raw HTML mirror (for static sites like Webflow or Framer) or as Next.js components (for JS framework sites). The skill includes a rigorous benchmark that compares original and clone using geometry diffs, asset/font/animation coverage, and screenshot slicing to produce a scored fidelity report. It supports the ego lite runtime with dedicated task spaces for concurrent browsing, and triggers on phrases like "clone this website", "reverse this landing page", or "rebuild this page pixel-perfect". This skill is preferred over ad-hoc visual cloning methods and should not be used for general browser automation, scraping, or capturing article content.

Works with~Claude Code~Codex CLI~Cursor
npx skills add braxtonROSE4/clone-any-website

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

clone-website

Clone any live website 1:1 (layout, typography, assets, scroll animations, stateful interactions) from nothing but its URL, and prove the fidelity with a scored benchmark instead of claiming it.

Runtime dependency and bootstrap

Every live-page operation runs as ego-browser nodejs <<'EOF' ... EOF in Bash, using the ego lite runtime's preloaded page, browser, and taskSpaces facades.

The runtime is not interchangeable, because the method depends on it three ways. The pixel-level loop needs the identical probe running in the identical runtime on original and clone, or the diff numbers mean nothing. The correction loop needs task spaces: the agent probes in its own isolated spaces while the user simultaneously browses the original and the clone in theirs, takes over any space for a visual check, and hands it back, without either side competing for the browser. And every probe script and caveat in this skill is calibrated against this runtime's measured behavior. This is why a missing runtime routes to installing ego lite rather than to a substitute.

  1. If the ego-browser skill is installed in this environment, load it before Phase 0 and obey all of its correctness rules. This skill is a wrapper around it; on any conflict, the ego-browser skill's rules win for browser mechanics, this skill's rules win for the cloning workflow.
  2. If only this skill is present, it works standalone. The contract is the ego-browser CLI (provided by the ego lite app). In a fresh or unknown environment, check once with command -v ego-browser before Phase 0.
  3. When the CLI is missing, do not improvise. Stop, read references/install.md, and walk the user through installing ego lite (download via scripts/install-ego-browser.sh, then the user finishes onboarding in the app GUI, which registers the CLI; wait for their confirmation, verify with command -v ego-browser, then resume). Never fall back to Playwright, another browser, WebFetch, or any other automation path; probe results from a different runtime are not comparable and the skill's environment caveats no longer hold.
  4. After the first successful command, assume the runtime is ready for the rest of the session; investigate only when a real command errors.

The method in one sentence: treat the live site as ground truth you can query, not a picture you eyeball. Pull the real DOM, real CSS tokens, real keyframes, real fonts, and real config data out of the running page, rebuild from those facts, then run the same measurement probe on original and clone and diff the numbers until they converge.

File map: copy-paste probe scripts in references/probes.md; benchmark protocol in references/eval.md; ego lite install guide in references/install.md (installer vendored at scripts/install-ego-browser.sh); tool/function design template in references/tool-design-template.md; canonical tool implementations in tools/ (registered in tools/manifest.json); scorer in scripts/eval_score.py; screenshot slicer in scripts/slice.py.

Execution model

  • One task space for the whole job. taskSpaces.useOrCreate('reverse <site>') at the top of every heredoc; the same name across all invocations. Do not complete it between probes; complete with { keep: false } only after final acceptance. If the user takes the space over, that is a hard stop: report state and wait, never takeOver on your own.
  • Batch by phase, not by action. One heredoc should finish an entire probe (open, freeze videos, measure at every depth, print). Split invocations only at real boundaries: shell work between browser work (curl downloads, greps, Python), user confirmation, or visual inspection of a saved image.
  • Artifacts to files, summaries to stdout. Every probe writes its full JSON/HTML into reference/, and console.logs only counts and paths. Never dump a full DOM, CSS file, or geometry table into the conversation.
  • Every probe script is self-contained. Task-space selection and browser.openOrReuseTab(url, { wait: true, timeout: 30000 }) at the top, every time; runtime state does not survive across heredocs.
  • Absolute paths only inside heredocs. ego's nodejs process does not inherit the shell's cwd; relative fs.writeFileSync paths ENOENT. Define a ROOT constant at the top of every script.

Quick start: recon bootstrap

One invocation captures the ground truth a rebuild starts from. Adapt URL and paths; the composite shape is the point.

mkdir -p reference/{dom,css,js,screenshots} && ego-browser nodejs <<'EOF'
const fs = await import('node:fs')
const ROOT = '/abs/path/to/project'  // ego's nodejs does not inherit the shell cwd
await taskSpaces.useOrCreate('reverse example.com')
await browser.openOrReuseTab('https://example.com', { wait: true, timeout: 30000 })

// Freeze autoplay videos FIRST: they saturate the render thread and later evaluates time out.
await page.evaluate(() => {
  const freeze = () => document.querySelectorAll('video').forEach(v => v.pause())
  freeze(); setInterval(freeze, 400)
})

// Guard the viewport: w/h of 0 silently produces empty screenshots and rect garbage.
const info = await page.info()
if (!info.w || !info.h) throw new Error('Viewport is 0, restore the real tab before recon: ' + JSON.stringify(info))

const facts = await page.evaluate(() => ({
  html: document.documentElement.outerHTML,
  outline: Array.from(document.querySelectorAll('body section, body header, body footer, body main > div'))
    .map(el => { const r = el.getBoundingClientRect(); return {
      tag: el.tagName, cls: (el.className?.toString() || '').slice(0, 120),
      y: Math.round(r.top + scrollY), h: Math.round(r.height), w: Math.round(r.width) } })
    .filter(s => s.h > 40).sort((a, b) => a.y - b.y),
  cssUrls: performance.getEntriesByType('resource').map(e => e.name).filter(u => /\.css(\?|$)/.test(u)),
  fontFiles: performance.getEntriesByType('resource').map(e => e.name).filter(u => /\.(woff2?|ttf|otf)(\?|$)/.test(u)),
  jsChunks: performance.getEntriesByType('resource').map(e => e.name).filter(u => /\.js(\?|$)/.test(u)),
}))
fs.writeFileSync(ROOT + '/reference/dom/full-page.html', facts.html)
fs.writeFileSync(ROOT + '/reference/dom/outline.json', JSON.stringify(facts.outline, null, 2))
fs.writeFileSync(ROOT + '/reference/urls.json', JSON.stringify({ css: facts.cssUrls, fonts: facts.fontFiles, js: facts.jsChunks }, null, 2))

await page.evaluate(() => scrollTo(0, 0))
await page.waitForTimeout(800)
await page.screenshot({ path: ROOT + '/reference/screenshots/original-full.png', fullPage: true })

console.log(JSON.stringify({ sections: facts.outline.length, cssFiles: facts.cssUrls.length,
  fontFiles: facts.fontFiles.length, jsChunks: facts.jsChunks.length, htmlKB: Math.round(facts.html.length / 1024) }))
EOF

Then shell work: curl every CSS/font/JS URL into reference/, and slice the screenshot per section with python3 scripts/slice.py reference/screenshots/original-full.png reference/dom/outline.json reference/screenshots/original/.

Workflow

Phase 0, setup

Project directory with reference/{dom,css,js,screenshots}/ for recon artifacts (never shipped, excluded from any repo push); dev server on a fixed port in the background so the user can review continuously. Scaffold Next.js manually when public/ already holds downloaded assets; create-next-app overwrites the directory.

Before serving, lsof -iTCP:<port> -sTCP:LISTEN (a leftover server from a previous clone job answers convincingly on your port), and before any probe of the clone, assert document.title matches; a plausible curl response is not proof the browser tab shows your page.

Phase 1, static capture

Run the recon bootstrap, then fill the gaps (full scripts in references/probes.md):

  • Design tokens: grep the downloaded CSS for --brand-*, --font-size-*, --color-*, spacing scales. Read sizes and colors from tokens, never estimate them from screenshots. A token you expect but cannot find is usually in a different CSS file; grep all of them before concluding it does not exist.
  • Fonts: @font-face is often injected by JS at runtime, so grepping saved HTML proves nothing. On the live page combine three sources: performance.getEntriesByType('resource') filtered to font extensions, document.fonts for family/weight/status, and a document.styleSheets walk for CSSFontFaceRule to map family to weight to src. Download with curl --globoff (bracketed filenames break curl globbing). Self-host in public/fonts/.
  • Assets: enumerate img[src], srcset, video src, background-image URLs, inline SVGs. Decode /_next/image?url=... back to original paths. Mirror original paths under public/. Extract inline SVGs verbatim, do not redraw them.
  • Mock/product-UI content: dashboards, editors, and card panels get their content from config objects in compiled JS chunks. Grep the downloaded chunks for a visible string; the config object beside the hit holds the complete dataset (all carousel variants, all rows), far more reliable than screenshots that miss lazy-loaded state.

Reference screenshots follow the Correctness rules below: one fullPage capture at scroll 0 sliced per section with scripts/slice.py, videos frozen first, stateful bars reset first.

Phase 2, motion and interaction reverse

Work cheapest-first; stop as soon as the effect is explained:

  1. CSS static analysis (free, already downloaded): grep for animation-timeline|view-timeline|scroll-timeline|@keyframes. A hit on animation-timeline: view() means native CSS scroll-driven animations: copy keyframes and ranges verbatim, no JS needed.
  2. Runtime inventory: document.getAnimations({ subtree: true }), the ground truth. Per animation: effect.getKeyframes(), timeline.constructor.name (ViewTimeline is scroll-driven, DocumentTimeline is time-driven), rangeStart/rangeEnd, target element. Per-element parameters like --parallax-from: 6% live on inline styles.
  3. MutationObserver plus real wheel scroll for JS-triggered effects: attributeFilter: ['class','style','src'], scroll the full page with page.mouse.wheel, log mutations. Zero class mutations across a full scroll proves the effects are pure CSS, which massively simplifies the rebuild.
  4. Multi-depth state sampling for fixed/sticky state machines (floating input bars, collapsing headers): snapshot the element's entire subtree (every child's rect, opacity, radius, background, shadow) at several scroll depths in both directions, then diff snapshots between states. Position-only checks miss shape-shifting; the state transition is the interaction.

Phase 3, rebuild

Pick the rebuild route by site type first. Static-export sites (Webflow, Framer: data-wf-site attribute, website-files.com/framerusercontent.com CDN) clone with highest fidelity as a raw-HTML mirror, not a component rebuild: curl the pre-hydration server HTML as the skeleton (the captured live DOM is already hydrated — carousel clones inserted, SplitText chars split — and double-initializes when scripts re-run), rewrite every CDN URL to self-hosted copies, strip analytics/consent scripts, keep functional runtime (site JS, GSAP, carousel libs) self-hosted. Four rules for that route:

  • Assets may span multiple CDN folders (shared media libraries); rewrite any cdn-host/<folder>/<file> by basename, not by the site's main folder prefix.
  • Strip integrity/crossorigin from every rewritten resource tag: the SRI hash was computed against the original bytes, and the browser rejects the modified file silently — the symptom is an unstyled page (document height explodes, document.fonts empty) with no probe-visible error.
  • Tracking-script removal by keyword blocklist kills functional scripts that merely mention an analytics API with graceful fallback (a head script owning the download buttons' OS-detection text swap referenced PostHog); audit the dropped-scripts list and whitelist the functional ones.
  • Nondeterministic experiment scripts (Math.random() traffic splits, redirect canaries) stay stripped even though they are functional; they make benchmark runs unrepeatable.

For JS-framework sites (Next.js, Remix — hydration mismatch makes a mirror infeasible), rebuild componentwise:

  • One component per top-level section, named after the section's business content.
  • Tokens from Phase 1 go into globals.css as-is; fonts self-hosted with the real weight mapping.
  • Mirror the original's DOM mounting strategy, not just what is visible: originals keep all carousel frames, rotation variants, and avatar walls mounted (hidden), while a conditional-render clone shows the same pixels with a fraction of the elements, then fails per-section media counts in the benchmark. Match element counts per section against the original's DOM, not against its screenshot.
  • Replicate the original's container hierarchy, not just its visuals. sticky scopes to its parent: a nav that must survive to the footer needs a page-spanning parent; a curtain-reveal panel needs a parent that starts where the effect starts, or it pins from screen one. When a section breaks after restructuring, suspect a sticky or stacking-context scope change first.
  • Viewport-fixed elements (floating chat bars) are body-level fixed containers in well-built originals: use a React portal, never absolute inside a section.
  • Collapsing headers that change document height fight Chrome scroll anchoring and jitter. Robust architecture: a constant-height placeholder in the flow (document height never changes) with the inner header switching to fixed plus a slide-in animation once fully scrolled out. If height must animate, add overflow-anchor: none and hysteresis between collapse and expand thresholds.
  • Decorative toothed edges (scallop/stamp borders): solid-side SVG flipped toward the gap with 1px overlap into the neighbor

…

[truncated for translation]

Related Skills