A Playwright Pipeline That Reverse-Engineers and Rebuilds Any Website with Parallel AI Agents
1. Foreword
Hello everyone, I'm alien. Today we'll talk about a feature that can clone beautiful websites with one click — the usage and implementation principles of ai-website-cloner-template.
What can you gain from this document?
- A new approach for AI to automatically reverse-engineer web pages;
- An introduction to using Playwright;
- An introduction to ai-website-cloner-template;
The usage of ai-website-cloner-template is very simple:
Step 1: Download the template from GitHub to your local machine:
git clone https://github.com/JCodesMore/ai-website-cloner-template.git
Step 2: Run Claude Code and execute /clone-website (this is essentially a skill at the template project level)
Without further ado, let's use the Juejin website as an example (https://juejin.cn) and first look at the achieved effect:
PC-side effect comparison (left: generated / right: original):
Mobile-side effect comparison (left: generated / right: original):
Multi-device compatibility scenario:
Overall evaluation of the generated code:
- The overall UI restoration is decent, though not as perfect as expected; the restoration degree can reach over 70% based on personal testing;
- For some details, the mobile adaptation is done quite well;
- The quality and maintainability of the generated code are acceptable (this is essentially influenced significantly by the model itself);
Next, let's look at how ai-website-cloner-template handles this internally.
2. Core Implementation
2.1 Core Principles
The principle of ai-website-cloner-template can be divided into four parts:
Phase 1: Reconnaissance
After getting the target URL, the first thing is to figure out "what it looks like and how it moves."
Screenshots: Use Playwright to launch a real browser and capture full-page and above-the-fold screenshots at three standard viewports (1440×900, 768×1024, 390×844). Why three viewports? Because responsive breakpoints are not guessed; they need to be verified.
Capture design tokens: Run JS in the browser, traverse all DOM nodes on the page, call getComputedStyle() to aggregate and deduplicate the font, font size, color, background, border-radius, and spacing of each node. This is not copying CSS source code but reading the final computed values—it can penetrate @apply, CSS variables, and media queries.
Interaction scanning: Static screenshots can only see "what it looks like now," but tab switching changes content, scrolling might change header styles, and hovering might change card colors. So it also simulates scrolling to different positions, clicking tabs, and mouse hovering, capturing each interaction state.
Output: A bunch of screenshots + a bunch of JSON (tokens.json, assets.json, topology.json, etc.), all under docs/research/ and docs/design-references/.
Let's take a look:
Phase 2: Foundation
After reconnaissance, start laying the groundwork. This step must be completed first before all subsequent components can begin work.
Design tokens implementation: Map the colors and font sizes captured in Phase 1 to Tailwind v4's @theme block and :root CSS variables. This way, when writing components later, classes like bg-primary and text-muted-foreground are directly usable without checking color values each time.
Font strategy: Decide how to load fonts based on the target site's font family. If the target site uses Google Fonts, directly use next/font/google; if it uses pure system fonts like Juejin (PingFang SC, Microsoft YaHei), fall back to the system font stack, saving external network requests.
Icon deduplication: The target site page might have thirty or forty SVG nodes, but many are duplicates. After deduplication with a script, only a dozen or so unique ones remain; organize them into React components and put them in icons.tsx.
Asset download: Batch download all images, logos, and favicons from the page using a script into public/images/ and public/seo/, organized by purpose. For placeholder images that cannot be downloaded, use hash-based gradient color blocks based on IDs (each ID has a unique color, visually reasonable).
Output: globals.css configured, icons.tsx configured, public/images/ configured.
Phase 3: Spec & Build
This is the most critical stage of the pipeline and the part that best embodies the "spec-driven" philosophy.
Write spec files: Instead of writing code for each section first, write a markdown-format specification document (*.spec.md). This document is the construction contract for the builder agent, clearly stating:
- What the target component looks like (screenshot path)
- Pixel-level styles (font size 17px, padding 12px 20px 0px, color #252933, all real values from getComputedStyle, not "approximate")
- Real content data (titles, authors, thumbnails, all real text captured from the target DOM, not Lorem Ipsum)
- Interaction model (what happens on hover, click, whether there is scroll-driven behavior)
Complexity budget: A single spec cannot exceed 150 lines. Juejin's homepage has 12 sections, so it's split into 12 specs, each assigned to a builder agent. The finer the split, the more focused the single agent's task, and the higher the quality.
Worktree concurrency: Each builder agent works on an independent git worktree branch without conflicts. After completion, the orchestrator merges them one by one into main, and immediately runs npm run check (lint + typecheck + build) after each merge. This is the "Build Must Always Compile" principle—the main branch always remains in a buildable state.
Output: One React component file per section + its corresponding spec file.
Phase 4: Assembly & QA
All components are written; now they need to be assembled into a complete page and verified to see if it really resembles the target site.
Assembly: page.tsx combines all components according to z-index and breakpoints. For example, the header is sticky top (z-index: 250), and the mobile floating download button needs position: fixed (z-index: 100), not blocking the header but covering the footer.
Visual QA: Use a script to simultaneously capture screenshots of the "target site" and "our version" at three viewports, generating target-.png and clone-.png for side-by-side comparison. Priority:
- Size differences (most easily exposed—width, height, padding, gap)
- Color differences (most sensitive to the naked eye—background, text, borders)
- Layout differences (flex direction, grid column count, whether sticky fails)
In the implementation process, the core logic is to truly obtain the page state inside the browser, and Playwright plays a core role.
2.2 Introduction to Playwright
First, let's look at what Playwright is?
Playwright is an open-source library by Microsoft for end-to-end testing and browser automation. It allows you to control real browsers (Chrome, Firefox, Safari) with code, automatically completing operations like clicking, filling, taking screenshots, and reading page content.
For example, if we want to launch a browser in a Node environment, we can directly call the corresponding API:
// 1. Launch browser
const { chromium } = require('playwright');
const browser = await chromium.launch({ headless: true });
You can also create an independent incognito window:
// 2. Create a new context (independent incognito window)
const context = await browser.newContext();
Operating the browser essentially allows us to leverage this capability to complete automated testing verification like e2e:
// Click
await page.click('button#submit');
// Navigate
await page.goto('https://example.com');
You can also take screenshots to capture the effect:
await page.screenshot({ path: 'screenshot.png' });
Interested students can research it; here is a summary of some API capabilities:
3. Specific Implementation Ideas
After introducing the core principles, let's pick some key scenarios and look at the core implementation:
Multi-viewport screenshots:
const viewports = [
{ name: "desktop-1440", width: 1440, height: 900 },
{ name: "tablet-768", width: 768, height: 1024 },
{ name: "mobile-390", width: 390, height: 844 },
];
for (const vp of viewports) {
const ctx = await browser.newContext({
viewport: { width: vp.width, height: vp.height },
locale: "zh-CN",
userAgent: "Mozilla/5.0 ... Chrome/131.0.0.0 ...",
});
const page = await ctx.newPage();
await page.goto(url, { waitUntil: "networkidle", timeout: 60000 });
await page.waitForTimeout(2500); // wait for hydration
await page.screenshot({ path: `${outDir}/${vp.name}-full.png`, fullPage: true });
await ctx.close();
}
Capture one fullPage screenshot across three standard viewports. Note that after waitUntil: "networkidle", you need to wait an additional 2.5 seconds — Juejin uses a lot of lazy-loaded images and IntersectionObserver; networkidle does not mean "completely stable."
Style restoration: extracting global style tokens:
page.evaluate() runs a piece of JS, traversing all DOM nodes and calling getComputedStyle():
const tokens = await page.evaluate(() => {
const all = document.querySelectorAll("*");
const fonts = new Set(), sizes = new Set(), colors = new Set();
for (const el of all) {
const s = getComputedStyle(el);
fonts.add(s.fontFamily);
sizes.add(s.fontSize);
if (s.color !== "rgba(0, 0, 0, 0)") colors.add(s.color);
}
return { fonts: [...fonts], sizes: [...sizes].sort(), colors: [...colors] };
});
After capturing Juejin's real tokens, they look like this:
Primary Blue: rgb(30, 128, 255) → #1e80ff
Page Background: rgb(242, 243, 245) → #f2f3f5
Card Background: rgb(255, 255, 255) → #ffffff
Body Text: rgb(37, 41, 51) → #252933
Secondary Text: rgb(81, 87, 103) → #515767
Tertiary Text: rgb(134, 144, 156) → #86909c
Border: rgb(228, 230, 235) → #e4e6eb
Danger Red: rgb(246, 66, 66) → #f64242
Font Family: PingFang SC, Microsoft YaHei, Helvetica Neue
Mandatory interaction scanning:
This step is the essence of the entire skill. Static screenshots can only see "what it looks like now," but the site is "alive": tab switching changes content, scrolling changes header styles, hovering changes card background colors.
Take Juejin as an example for a complete sweep:
// Scroll sweep: capture header styles at different scrollY
for (const y of [0, 300, 600, 900, 1200, 1500, 1800, 2094]) {
await page.evaluate((sy) => window.scrollTo({ top: sy, behavior: "instant" }), y);
await page.waitForTimeout(400);
const data = await page.evaluate(() => {
const header = document.querySelector("header");
return {
scrollY: window.scrollY,
headerBg: header ? getComputedStyle(header).backgroundColor : null,
headerShadow: header ? getComputedStyle(header).boxShadow : null,
};
});
scrollSnapshots.push(data);
}
// Click sweep: click the "Latest" tab to see if content changes
const tabClick = await page.evaluate(() => {
const target = [...document.querySelectorAll("a, button, div, span")].find(
(el) => el.textContent?.trim() === "最新" && el.children.length === 0
);
if (!target) return { ok: false };
target.click();
return { ok: true };
});
await page.waitForTimeout(1500);
const latestContent = await page.evaluate(() => {
return document.querySelectorAll(".entry").length; // Real data: 1 card
});
Actual testing found that after switching Juejin's "Latest" tab, the number of articles dropped from 16 to 1, indicating this is not a pure UI switch but a real re-fetch of data. When cloning, useState is used to switch between two static mocks, but production deployment requires backend API cooperation.
Page topology mapping:
List all sections and annotate interaction models:
1. Header — Static (no scroll change)
2. LeftSidebar — Click (navigation)
3. ArticleList — Click (tab switch)
4. ArticleCard — Hover (subtle bg)
5. CtaBanner — Static
6. RightRail — Static (refresh button mock)
7. Footer — Static
8. MobileAppPill — Static (position: fixed)
The output of this step is PAGE_TOPOLOGY.md, serving as an index for subsequent spec files.
The above is the Phase 1 part. The subsequent two parts systematically process styles, assets, and the specification specs for generating components, which will not be introduced in detail here. Let's look at how visual diffing is done in Phase 4.
The QA script iterates through three viewports to capture target and clone screenshots respectively:
for (const vp of viewports) {
const ourCtx = await browser.newContext({ viewport: vp });
const ourPage = await ourCtx.newPage();
await ourPage.goto("http://localhost:3010/", { waitUntil: "networkidle" });
await ourPage.screenshot({ path: `clone-${vp.name}-full.png`, fullPage: true });
const targetCtx = await browser.newContext({ viewport: vp });
const targetPage = await targetCtx.newPage();
await targetPage.goto("https://juejin.cn/", { waitUntil: "networkidle" });
await targetPage.screenshot({ path: `target-${vp.name}-full.png`, fullPage: true });
}
Priority for finding differences in comparison:
- Size differences: width, height, padding, gap (most easily exposed)
- Color differences: background, text, borders (most sensitive to the naked eye)
- Layout differences: flex direction, grid column count, sticky failure
In the Juejin practical test, mobile immediately exposed 3 differences (tag wrap, author truncate, button-in-anchor), while desktop/tablet passed in one go.
4. Summary
The real value of ai-website-cloner-template is not "being able to clone websites"—that is the job of DevTools itself. It reconstructs a seemingly simple "screenshot + copy code" into a reusable, verifiable, concurrent engineering pipeline: first Reconnaissance to capture computed values, then Foundation to build the skeleton, then Spec & Build's spec-driven multi-agent parallel generation, and finally Assembly & QA for multi-viewport visual regression. The reversal of the order to write spec first, then write code is the watershed from "AI toy" to "AI engineering."
I suggest everyone clone a copy locally, pick an admired site to run through it, and personally experience the real feeling of "spec-driven + multi-agent parallelism." Those who act first see the future first. Thanks for reading, see you in the next article. Friends who like it can like + bookmark to support updates on "whimsical ideas" in AI scenarios.
Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.
If it's drawn with canvas, it basically won't achieve the effect at all.
Let the OP test it.