launchframe 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/clone-website/SKILL.md +473 -564
- package/.clinerules +147 -285
- package/.codex/skills/clone-website/SKILL.md +473 -564
- package/.continue/commands/clone-website.md +475 -566
- package/.continue/rules/project.md +151 -285
- package/.cursor/commands/clone-website.md +470 -561
- package/.cursor/rules/project.mdc +7 -22
- package/.gemini/commands/clone-website.toml +476 -567
- package/.github/copilot-instructions.md +147 -281
- package/.github/skills/clone-website/SKILL.md +473 -564
- package/.gitignore +49 -0
- package/.opencode/commands/clone-website.md +473 -564
- package/.windsurf/workflows/clone-website.md +470 -561
- package/AGENTS.md +65 -160
- package/README.md +162 -121
- package/bin/launchframe.mjs +343 -0
- package/docs/research/INSPECTION_GUIDE.md +80 -124
- package/package.json +97 -54
- package/src/app/globals.css +1 -93
- package/src/app/layout.tsx +16 -5
- package/src/app/page.tsx +37 -2
- package/src/lib/launchframe-config.ts +8 -0
- package/.amazonq/cli-agents/clone-website.json +0 -9
- package/.amazonq/rules/project.md +0 -281
- package/.augment/commands/clone-website.md +0 -565
- package/.claude/skills/marketing-social-proof-motion/SKILL.md +0 -47
- package/.cursor/commands/marketing-social-proof-motion.md +0 -42
- package/.nvmrc +0 -1
- package/CHANGELOG.md +0 -80
- package/START_HERE.md +0 -15
- package/docs/design-references/playwright-example.com-1440px.png +0 -0
- package/docs/design-references/playwright-example.com-390px.png +0 -0
- package/launchframe.config.json +0 -14
- package/public/images/.gitkeep +0 -0
- package/public/seo/.gitkeep +0 -0
- package/public/videos/.gitkeep +0 -0
- package/scripts/recon-playwright.mjs +0 -396
- package/src/components/marketing/scribewise-landing.tsx +0 -34
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
<!-- Canonical source: `.claude/skills/marketing-social-proof-motion/SKILL.md` — edit there first, then mirror here if needed. -->
|
|
2
|
-
|
|
3
|
-
# Marketing social proof, logos & motion
|
|
4
|
-
|
|
5
|
-
Ship sections that **feel alive**: named companies, visible marks, and **layered animation**. Generic gray blobs and static stacks fail this skill.
|
|
6
|
-
|
|
7
|
-
## Named companies & copy
|
|
8
|
-
|
|
9
|
-
- **Name real companies** where they strengthen the story: integrations (“Works with Slack, Salesforce…”), ecosystems, categories, or illustrative comparables the user allows.
|
|
10
|
-
- **Do not invent endorsements.** Prefer truthful framings — “integrations,” “compatible with,” “shared stack,” “customers like” — unless the user supplied verified quotes/customers.
|
|
11
|
-
- **Keep lists purposeful.** Match companies to `launchframe.config.json#idea` when present; avoid unrelated mega-brand name-dropping.
|
|
12
|
-
|
|
13
|
-
## Logo marks & imagery
|
|
14
|
-
|
|
15
|
-
- **Prefer real marks** when implementation-ready: monochrome SVG or PNG from vendor press/media kits, neutral compatibility strips, or OSS icon sets where license permits. Normalize to one visual system (single height, consistent padding, grayscale or duotone).
|
|
16
|
-
- **Store assets** under `public/images/logos/` or `public/images/partners/` with predictable filenames; reference **local paths** in components — no hotlinked SVGs from third-party CDNs in production UI.
|
|
17
|
-
- **If a mark is missing, blocked, or unclear:**
|
|
18
|
-
- Generate **original** imagery (image-generation tool or hand-authored SVG) — illustrated tiles, abstract glyphs, or metaphor icons tied to the **idea** — **not** counterfeit logos.
|
|
19
|
-
- Or ship **clean text lockups** (company wordmark as styled type) where safer than guessing trademark artwork.
|
|
20
|
-
|
|
21
|
-
## Motion emphasis (non-negotiable bar)
|
|
22
|
-
|
|
23
|
-
Motion sells credibility — implement several layers using **`framer-motion`** + CSS loops where appropriate; honor **`prefers-reduced-motion`**.
|
|
24
|
-
|
|
25
|
-
| Layer | Goal | Typical implementation |
|
|
26
|
-
|-------|------|-------------------------|
|
|
27
|
-
| **Entrance** | Logos/testimonials **stagger in** | Parent `variants` + `staggerChildren`; opacity + `y` or blur-in on type |
|
|
28
|
-
| **Scroll** | Proof bands **ease up / fade** on first view | `whileInView` + `viewport.once`; sensible `margin` so reveals feel early |
|
|
29
|
-
| **Ambient** | Strip feels “live” | **Infinite marquee** (slow), gradient drift, subtle opacity pulse on logos |
|
|
30
|
-
| **Hover / focus** | Cards lift; logos brighten | `whileHover` / CSS transitions on transforms + shadow |
|
|
31
|
-
| **Depth** | Premium polish | Light pointer **parallax** on hero/logo cluster (small ranges) or scroll-linked `useTransform` |
|
|
32
|
-
|
|
33
|
-
Also align tier **A–E** choreography and reduced-motion fallbacks described in **`AGENTS.md`** (Production polish → Motion choreography).
|
|
34
|
-
|
|
35
|
-
## Hand-off checklist
|
|
36
|
-
|
|
37
|
-
- [ ] Companies named in copy match user intent and truthful framing
|
|
38
|
-
- [ ] Every logo path documented; placeholders explained if generated
|
|
39
|
-
- [ ] At least **two** motion layers beyond a single fade (e.g., stagger + marquee, or scroll reveal + hover lift)
|
|
40
|
-
- [ ] Reduced-motion path removes loops and tightens entrances
|
|
41
|
-
|
|
42
|
-
When cloning an existing page, **extract** logo SVGs/rasters like any other asset; when authoring from scratch, follow this skill instead of shipping motionless filler.
|
package/.nvmrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
24
|
package/CHANGELOG.md
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [Unreleased]
|
|
9
|
-
|
|
10
|
-
### Changed
|
|
11
|
-
- Raised the project Node.js baseline to 24 across local development, CI, Docker, and contributor-facing documentation
|
|
12
|
-
|
|
13
|
-
## [0.3.1] - 2026-03-29
|
|
14
|
-
|
|
15
|
-
### Fixed
|
|
16
|
-
- `sync-agent-rules.sh` failing to resolve `@file` imports on Windows due to CRLF line endings — platform instruction files now correctly inline the Inspection Guide content
|
|
17
|
-
|
|
18
|
-
## [0.3.0] - 2026-03-29
|
|
19
|
-
|
|
20
|
-
### Added
|
|
21
|
-
- Multi-URL support for `/clone-website` — clone multiple sites in a single command with parallel processing and isolated output
|
|
22
|
-
- CI quality gates via GitHub Actions — automated lint, typecheck, and build on every push and PR
|
|
23
|
-
- `npm run typecheck` and `npm run check` scripts for local quality validation
|
|
24
|
-
- `.gitattributes` for cross-platform line ending normalization
|
|
25
|
-
- `.nvmrc` to pin Node.js 20 for contributor consistency
|
|
26
|
-
|
|
27
|
-
### Changed
|
|
28
|
-
- Streamlined PR template — removed redundant checklist items and screenshots section
|
|
29
|
-
- Improved project description and README — clearer use cases, limitations, and modern wording
|
|
30
|
-
- Refined documentation and agent rules across all platforms for clarity and consistency
|
|
31
|
-
- Fixed CRLF handling in `sync-skills.mjs` for reliable Windows operation
|
|
32
|
-
|
|
33
|
-
### Removed
|
|
34
|
-
- Outdated use case from README documentation
|
|
35
|
-
|
|
36
|
-
## [0.2.0] - 2026-03-28
|
|
37
|
-
|
|
38
|
-
### Added
|
|
39
|
-
- Multi-platform AI agent support: Claude Code, Codex CLI, OpenCode, GitHub Copilot, Cursor, Windsurf, Gemini CLI, Cline/Roo Code, Continue, Amazon Q, Augment Code, Aider
|
|
40
|
-
- Platform-specific instruction files and `/clone-website` skill for each supported agent
|
|
41
|
-
- `scripts/sync-agent-rules.sh` to regenerate platform instruction files from AGENTS.md
|
|
42
|
-
- `scripts/sync-skills.mjs` to regenerate `/clone-website` skill across all platforms
|
|
43
|
-
- GEMINI.md for Gemini CLI configuration
|
|
44
|
-
- Supported Platforms table in README
|
|
45
|
-
- "Updating for Other Platforms" documentation section in README
|
|
46
|
-
|
|
47
|
-
### Changed
|
|
48
|
-
- README now describes the project as multi-agent (Claude Code recommended, not required)
|
|
49
|
-
- AGENTS.md updated with sync script reminders
|
|
50
|
-
|
|
51
|
-
## [0.1.1] - 2026-03-28
|
|
52
|
-
|
|
53
|
-
### Added
|
|
54
|
-
- Bug report and feature request issue templates
|
|
55
|
-
- Pull request template with checklist
|
|
56
|
-
- CHANGELOG.md following Keep a Changelog format
|
|
57
|
-
- Package.json metadata (description, repository, homepage, keywords, engines)
|
|
58
|
-
|
|
59
|
-
### Fixed
|
|
60
|
-
- LICENSE copyright holder now attributed to JCodesMore
|
|
61
|
-
|
|
62
|
-
## [0.1.0] - 2026-03-28
|
|
63
|
-
|
|
64
|
-
### Added
|
|
65
|
-
- Initial template scaffold for website reverse-engineering with Claude Code
|
|
66
|
-
- `/clone-website` skill for full-site cloning pipeline
|
|
67
|
-
- `/build-from-spec` and `/customize` skills
|
|
68
|
-
- Parallel builder agents with git worktree isolation
|
|
69
|
-
- Chrome MCP integration for design token extraction
|
|
70
|
-
- Comprehensive inspection guide and project structure documentation
|
|
71
|
-
- Next.js 16 + shadcn/ui + Tailwind CSS v4 base scaffold
|
|
72
|
-
- MIT license
|
|
73
|
-
- README with badges, demo section, quick start, and star history
|
|
74
|
-
|
|
75
|
-
[Unreleased]: https://github.com/JCodesMore/ai-website-cloner-template/compare/v0.3.1...HEAD
|
|
76
|
-
[0.3.1]: https://github.com/JCodesMore/ai-website-cloner-template/compare/v0.3.0...v0.3.1
|
|
77
|
-
[0.3.0]: https://github.com/JCodesMore/ai-website-cloner-template/compare/v0.2.0...v0.3.0
|
|
78
|
-
[0.2.0]: https://github.com/JCodesMore/ai-website-cloner-template/compare/v0.1.1...v0.2.0
|
|
79
|
-
[0.1.1]: https://github.com/JCodesMore/ai-website-cloner-template/compare/v0.1.0...v0.1.1
|
|
80
|
-
[0.1.0]: https://github.com/JCodesMore/ai-website-cloner-template/releases/tag/v0.1.0
|
package/START_HERE.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Start here
|
|
2
|
-
|
|
3
|
-
Launchframe wrote files into **this folder** (your project root), including **`.cursor`** and **`.claude`**, so they work when you open this directory in your editor.
|
|
4
|
-
|
|
5
|
-
## Do this next
|
|
6
|
-
|
|
7
|
-
1. **Open this folder** in [Cursor](https://cursor.com/) (the root that contains `.cursor` — not a parent directory).
|
|
8
|
-
2. In chat, say: **Build it.**
|
|
9
|
-
|
|
10
|
-
That tells your AI to read `launchframe.config.json` and `AGENTS.md` and run the full **clone + SaaS rebrand** pipeline (same work as `/clone-website`).
|
|
11
|
-
|
|
12
|
-
## Optional
|
|
13
|
-
|
|
14
|
-
- Prefer a slash command? Use **`/clone-website`** in Cursor.
|
|
15
|
-
- Wrong URL or idea? Edit **`launchframe.config.json`**, then say **Build it** again.
|
|
Binary file
|
|
Binary file
|
package/launchframe.config.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://launchframe.dev/schema/launchframe.config.json",
|
|
3
|
-
"url": "https://vercel.com/",
|
|
4
|
-
"idea": "saas-idea",
|
|
5
|
-
"createdAt": "2026-05-15T00:27:49.833Z",
|
|
6
|
-
"launchframeVersion": "0.2.6",
|
|
7
|
-
"notes": [
|
|
8
|
-
"The /clone-website skill reads this file at the start of every run.",
|
|
9
|
-
"After scaffold: open this folder in Cursor (or your AI editor) and say **Build it** — same workflow.",
|
|
10
|
-
"`url` is the visual source-of-truth (clone its layout, spacing, tokens, motion).",
|
|
11
|
-
"`idea` is the rebranding directive applied AFTER the pixel-perfect clone.",
|
|
12
|
-
"Edit either field and re-invoke the skill to re-run."
|
|
13
|
-
]
|
|
14
|
-
}
|
package/public/images/.gitkeep
DELETED
|
File without changes
|
package/public/seo/.gitkeep
DELETED
|
File without changes
|
package/public/videos/.gitkeep
DELETED
|
File without changes
|
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Playwright recon when Browser / Chrome DevTools MCP is unavailable or broken.
|
|
4
|
-
*
|
|
5
|
-
* Reads URL from launchframe.config.json (or --url). Writes:
|
|
6
|
-
* - docs/design-references/playwright-<host>-<w>px.png (full page)
|
|
7
|
-
* - docs/research/computed-snapshot.json (styles + merged asset inventory + bot-wall hint)
|
|
8
|
-
* - docs/research/MEDIA_MANIFEST.md (table of discovered media URLs)
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* npm run recon
|
|
12
|
-
* npm run recon:headed
|
|
13
|
-
* node scripts/recon-playwright.mjs --url https://example.com --headed
|
|
14
|
-
*
|
|
15
|
-
* First time: npx playwright install chromium
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
19
|
-
import { dirname, join } from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
|
-
|
|
22
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
-
|
|
24
|
-
function parseArgs(argv) {
|
|
25
|
-
const out = { url: null, headed: false, channel: process.env.PW_CHANNEL || undefined };
|
|
26
|
-
for (let i = 0; i < argv.length; i++) {
|
|
27
|
-
const a = argv[i];
|
|
28
|
-
if (a === "--headed" || a === "-H") out.headed = true;
|
|
29
|
-
else if (a === "--url" || a === "-u") out.url = argv[++i];
|
|
30
|
-
else if (a.startsWith("--url=")) out.url = a.slice(6);
|
|
31
|
-
else if (a === "--channel" || a === "-c") out.channel = argv[++i] || undefined;
|
|
32
|
-
}
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function readLaunchframeUrl() {
|
|
37
|
-
const p = join(ROOT, "launchframe.config.json");
|
|
38
|
-
if (!existsSync(p)) return null;
|
|
39
|
-
try {
|
|
40
|
-
const j = JSON.parse(readFileSync(p, "utf8"));
|
|
41
|
-
return j.url || null;
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function safeHost(url) {
|
|
48
|
-
try {
|
|
49
|
-
return new URL(url).hostname.replace(/[^a-z0-9.-]+/gi, "_");
|
|
50
|
-
} catch {
|
|
51
|
-
return "unknown-host";
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Scroll the full document depth slowly so lazy-loaded media and below-fold DOM resolve.
|
|
57
|
-
* Repeats until scrollHeight stabilizes (handles incremental infinite layouts up to max rounds).
|
|
58
|
-
*/
|
|
59
|
-
async function scrollFullPage(page) {
|
|
60
|
-
await page.evaluate(async () => {
|
|
61
|
-
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
62
|
-
const stepSize = () => Math.max(1, Math.floor(window.innerHeight * 0.85));
|
|
63
|
-
|
|
64
|
-
let unchangedStreak = 0;
|
|
65
|
-
let prevTotal = -1;
|
|
66
|
-
|
|
67
|
-
for (let round = 0; round < 12; round++) {
|
|
68
|
-
let y = 0;
|
|
69
|
-
let limit = document.documentElement.scrollHeight;
|
|
70
|
-
|
|
71
|
-
while (y < limit) {
|
|
72
|
-
y = Math.min(y + stepSize(), limit);
|
|
73
|
-
window.scrollTo(0, y);
|
|
74
|
-
await delay(130);
|
|
75
|
-
limit = document.documentElement.scrollHeight;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
window.scrollTo(0, document.documentElement.scrollHeight);
|
|
79
|
-
await delay(420);
|
|
80
|
-
|
|
81
|
-
const h = document.documentElement.scrollHeight;
|
|
82
|
-
|
|
83
|
-
window.scrollTo(0, 0);
|
|
84
|
-
await delay(220);
|
|
85
|
-
|
|
86
|
-
if (h === prevTotal) unchangedStreak++;
|
|
87
|
-
else unchangedStreak = 0;
|
|
88
|
-
prevTotal = h;
|
|
89
|
-
|
|
90
|
-
if (unchangedStreak >= 2) break;
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function mergeAssetInventory(base, extra) {
|
|
96
|
-
if (!extra) return base;
|
|
97
|
-
const imgKey = (i) => i.src || "";
|
|
98
|
-
const seenImg = new Set(base.images.map(imgKey));
|
|
99
|
-
const images = [...base.images];
|
|
100
|
-
for (const i of extra.images) {
|
|
101
|
-
const k = imgKey(i);
|
|
102
|
-
if (k && !seenImg.has(k)) {
|
|
103
|
-
seenImg.add(k);
|
|
104
|
-
images.push(i);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const vidKey = (v) => `${v.src || ""}|${v.poster || ""}`;
|
|
109
|
-
const seenVid = new Set(base.videos.map(vidKey));
|
|
110
|
-
const videos = [...base.videos];
|
|
111
|
-
for (const v of extra.videos) {
|
|
112
|
-
const k = vidKey(v);
|
|
113
|
-
if ((v.src || v.poster) && !seenVid.has(k)) {
|
|
114
|
-
seenVid.add(k);
|
|
115
|
-
videos.push(v);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const seenBg = new Set(base.backgroundImages.map((b) => b.url));
|
|
120
|
-
const backgroundImages = [...base.backgroundImages];
|
|
121
|
-
for (const b of extra.backgroundImages) {
|
|
122
|
-
if (b.url && !seenBg.has(b.url)) {
|
|
123
|
-
seenBg.add(b.url);
|
|
124
|
-
backgroundImages.push(b);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
images,
|
|
130
|
-
videos,
|
|
131
|
-
backgroundImages,
|
|
132
|
-
svgCount: Math.max(base.svgCount ?? 0, extra.svgCount ?? 0),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function gatherPageData(page) {
|
|
137
|
-
return page.evaluate(() => {
|
|
138
|
-
const title = document.title || "";
|
|
139
|
-
const text = (document.body?.innerText || "").slice(0, 80000);
|
|
140
|
-
const likelyBotWall =
|
|
141
|
-
/just a moment/i.test(title) ||
|
|
142
|
-
/checking your browser/i.test(text) ||
|
|
143
|
-
/cf-browser-verification/i.test(document.documentElement?.innerHTML || "") ||
|
|
144
|
-
document.querySelector("#challenge-running, .cf-browser-verification, #cf-challenge-running") !== null;
|
|
145
|
-
|
|
146
|
-
const imgs = [...document.querySelectorAll("img")].map((img) => ({
|
|
147
|
-
src: img.currentSrc || img.src || "",
|
|
148
|
-
srcset: img.srcset || "",
|
|
149
|
-
alt: img.alt || "",
|
|
150
|
-
w: img.naturalWidth,
|
|
151
|
-
h: img.naturalHeight,
|
|
152
|
-
}));
|
|
153
|
-
|
|
154
|
-
const videos = [...document.querySelectorAll("video")].map((v) => ({
|
|
155
|
-
src: v.currentSrc || v.src || "",
|
|
156
|
-
poster: v.poster || "",
|
|
157
|
-
autoplay: v.autoplay,
|
|
158
|
-
loop: v.loop,
|
|
159
|
-
muted: v.muted,
|
|
160
|
-
}));
|
|
161
|
-
|
|
162
|
-
const bgUrls = [];
|
|
163
|
-
const seen = new Set();
|
|
164
|
-
for (const el of document.querySelectorAll("*")) {
|
|
165
|
-
if (bgUrls.length > 400) break;
|
|
166
|
-
const bg = getComputedStyle(el).backgroundImage;
|
|
167
|
-
if (!bg || bg === "none") continue;
|
|
168
|
-
const m = /url\(["']?([^"')]+)["']?\)/.exec(bg);
|
|
169
|
-
if (m && m[1] && !seen.has(m[1])) {
|
|
170
|
-
seen.add(m[1]);
|
|
171
|
-
bgUrls.push({ url: m[1], tag: el.tagName.toLowerCase() });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const props = [
|
|
176
|
-
"fontSize",
|
|
177
|
-
"fontWeight",
|
|
178
|
-
"fontFamily",
|
|
179
|
-
"lineHeight",
|
|
180
|
-
"letterSpacing",
|
|
181
|
-
"color",
|
|
182
|
-
"backgroundColor",
|
|
183
|
-
"padding",
|
|
184
|
-
"margin",
|
|
185
|
-
"maxWidth",
|
|
186
|
-
"display",
|
|
187
|
-
"gap",
|
|
188
|
-
"borderRadius",
|
|
189
|
-
"boxShadow",
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
function pickStyles(el) {
|
|
193
|
-
if (!el) return null;
|
|
194
|
-
const cs = getComputedStyle(el);
|
|
195
|
-
const o = {};
|
|
196
|
-
for (const p of props) o[p] = cs[p];
|
|
197
|
-
return o;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const landmarks = {
|
|
201
|
-
html: pickStyles(document.documentElement),
|
|
202
|
-
body: pickStyles(document.body),
|
|
203
|
-
h1: pickStyles(document.querySelector("h1")),
|
|
204
|
-
header: pickStyles(document.querySelector("header")),
|
|
205
|
-
main: pickStyles(document.querySelector("main")),
|
|
206
|
-
firstSection: pickStyles(document.querySelector("main section, section")),
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
title,
|
|
211
|
-
likelyBotWall,
|
|
212
|
-
textSample: text.slice(0, 24000),
|
|
213
|
-
assetInventory: {
|
|
214
|
-
images: imgs.filter((i) => i.src),
|
|
215
|
-
videos,
|
|
216
|
-
backgroundImages: bgUrls,
|
|
217
|
-
svgCount: document.querySelectorAll("svg").length,
|
|
218
|
-
},
|
|
219
|
-
computedLandmarks: landmarks,
|
|
220
|
-
};
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function writeMediaManifest(host, data, limitationsPath) {
|
|
225
|
-
const { images, videos, backgroundImages } = data.assetInventory;
|
|
226
|
-
const lines = [
|
|
227
|
-
"# MEDIA_MANIFEST (Playwright)",
|
|
228
|
-
"",
|
|
229
|
-
`Host: **${host}**`,
|
|
230
|
-
"",
|
|
231
|
-
"## Images (`<img>`)",
|
|
232
|
-
"",
|
|
233
|
-
"| src | alt | natural |",
|
|
234
|
-
"|-----|-----|---------|",
|
|
235
|
-
];
|
|
236
|
-
for (const i of images.slice(0, 200)) {
|
|
237
|
-
const s = i.src.replace(/\|/g, "\\|");
|
|
238
|
-
lines.push(`| ${s} | ${(i.alt || "").replace(/\|/g, "\\|")} | ${i.w}×${i.h} |`);
|
|
239
|
-
}
|
|
240
|
-
if (images.length > 200) lines.push(`\n_…and ${images.length - 200} more_\n`);
|
|
241
|
-
|
|
242
|
-
lines.push("", "## Video", "", "| src | poster |", "|-----|--------|");
|
|
243
|
-
for (const v of videos) {
|
|
244
|
-
lines.push(`| ${(v.src || "").replace(/\|/g, "\\|")} | ${(v.poster || "").replace(/\|/g, "\\|")} |`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
lines.push("", "## Background images (sample)", "", "| url |", "|-----|");
|
|
248
|
-
for (const b of backgroundImages.slice(0, 150)) {
|
|
249
|
-
lines.push(`| ${b.url.replace(/\|/g, "\\|")} |`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
lines.push("", "---", "", `_Generated by scripts/recon-playwright.mjs. Download with scripts/download-assets.mjs when ready._`);
|
|
253
|
-
if (limitationsPath) lines.push("", `See also: **${limitationsPath}**`);
|
|
254
|
-
|
|
255
|
-
writeFileSync(join(ROOT, "docs", "research", "MEDIA_MANIFEST.md"), lines.join("\n") + "\n", "utf8");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function writeLimitations(host, url, finalUrl, data) {
|
|
259
|
-
const p = join(ROOT, "docs", "research", "EXTRACTION_LIMITATIONS.md");
|
|
260
|
-
const lines = [
|
|
261
|
-
"# Extraction limitations",
|
|
262
|
-
"",
|
|
263
|
-
`- **Requested URL:** ${url}`,
|
|
264
|
-
`- **Final URL:** ${finalUrl}`,
|
|
265
|
-
`- **Host:** ${host}`,
|
|
266
|
-
`- **Captured at:** ${new Date().toISOString()}`,
|
|
267
|
-
`- **Tool:** Playwright (\`npm run recon\`)`,
|
|
268
|
-
"",
|
|
269
|
-
];
|
|
270
|
-
if (data.likelyBotWall) {
|
|
271
|
-
lines.push(
|
|
272
|
-
"## Bot / challenge page suspected",
|
|
273
|
-
"",
|
|
274
|
-
"Title or DOM matches common WAF/interstitial patterns. **Computed styles and screenshots may not reflect the real marketing page.** Prefer \`npm run recon:headed\` with real Chrome, or open the site manually and export assets.",
|
|
275
|
-
""
|
|
276
|
-
);
|
|
277
|
-
} else {
|
|
278
|
-
lines.push(
|
|
279
|
-
"## Notes",
|
|
280
|
-
"",
|
|
281
|
-
"Snapshot-derived data is from Playwright Chromium, not interactive MCP. Re-run after layout shifts; verify against a real browser for pixel QA.",
|
|
282
|
-
""
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
writeFileSync(p, lines.join("\n"), "utf8");
|
|
286
|
-
return "docs/research/EXTRACTION_LIMITATIONS.md";
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function main() {
|
|
290
|
-
const args = parseArgs(process.argv.slice(2));
|
|
291
|
-
const url = args.url || readLaunchframeUrl();
|
|
292
|
-
if (!url) {
|
|
293
|
-
console.error("No URL: set launchframe.config.json or pass --url https://...");
|
|
294
|
-
process.exit(1);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
let chromium;
|
|
298
|
-
try {
|
|
299
|
-
({ chromium } = await import("playwright"));
|
|
300
|
-
} catch {
|
|
301
|
-
console.error("Install Playwright: npm i -D playwright && npx playwright install chromium");
|
|
302
|
-
process.exit(1);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const host = safeHost(url);
|
|
306
|
-
mkdirSync(join(ROOT, "docs", "design-references"), { recursive: true });
|
|
307
|
-
mkdirSync(join(ROOT, "docs", "research"), { recursive: true });
|
|
308
|
-
|
|
309
|
-
const browser = await chromium.launch({
|
|
310
|
-
headless: !args.headed,
|
|
311
|
-
channel: args.channel,
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
const context = await browser.newContext({
|
|
315
|
-
userAgent:
|
|
316
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
317
|
-
locale: "en-US",
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
const page = await context.newPage();
|
|
321
|
-
|
|
322
|
-
const snapshot = {
|
|
323
|
-
fetchedAt: new Date().toISOString(),
|
|
324
|
-
requestedUrl: url,
|
|
325
|
-
finalUrl: null,
|
|
326
|
-
viewports: {},
|
|
327
|
-
title: null,
|
|
328
|
-
likelyBotWall: false,
|
|
329
|
-
assetInventory: null,
|
|
330
|
-
computedLandmarks: null,
|
|
331
|
-
textSample: null,
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
try {
|
|
335
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 });
|
|
336
|
-
await new Promise((r) => setTimeout(r, 2500));
|
|
337
|
-
snapshot.finalUrl = page.url();
|
|
338
|
-
|
|
339
|
-
let mergedInventory = null;
|
|
340
|
-
let desktopLandmarks = null;
|
|
341
|
-
|
|
342
|
-
for (const w of [1440, 390]) {
|
|
343
|
-
await page.setViewportSize({ width: w, height: w === 1440 ? 900 : 844 });
|
|
344
|
-
await scrollFullPage(page);
|
|
345
|
-
|
|
346
|
-
const shotPath = join(ROOT, "docs", "design-references", `playwright-${host}-${w}px.png`);
|
|
347
|
-
await page.screenshot({ path: shotPath, fullPage: true });
|
|
348
|
-
snapshot.viewports[w] = { screenshot: `docs/design-references/playwright-${host}-${w}px.png` };
|
|
349
|
-
|
|
350
|
-
const data = await gatherPageData(page);
|
|
351
|
-
|
|
352
|
-
if (w === 1440) {
|
|
353
|
-
snapshot.title = data.title;
|
|
354
|
-
snapshot.likelyBotWall = data.likelyBotWall;
|
|
355
|
-
mergedInventory = data.assetInventory;
|
|
356
|
-
desktopLandmarks = data.computedLandmarks;
|
|
357
|
-
snapshot.textSample = data.textSample;
|
|
358
|
-
} else {
|
|
359
|
-
mergedInventory = mergeAssetInventory(mergedInventory, data.assetInventory);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
snapshot.assetInventory = mergedInventory;
|
|
364
|
-
snapshot.computedLandmarks = desktopLandmarks;
|
|
365
|
-
} finally {
|
|
366
|
-
await browser.close();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const limRel = writeLimitations(host, url, snapshot.finalUrl, {
|
|
370
|
-
likelyBotWall: snapshot.likelyBotWall,
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
writeFileSync(
|
|
374
|
-
join(ROOT, "docs", "research", "computed-snapshot.json"),
|
|
375
|
-
JSON.stringify(snapshot, null, 2) + "\n",
|
|
376
|
-
"utf8"
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
if (snapshot.assetInventory) {
|
|
380
|
-
writeMediaManifest(host, { assetInventory: snapshot.assetInventory }, limRel);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
console.log("Playwright recon finished.");
|
|
384
|
-
console.log(` Screenshots: docs/design-references/playwright-${host}-1440px.png (+ 390px)`);
|
|
385
|
-
console.log(` Snapshot: docs/research/computed-snapshot.json`);
|
|
386
|
-
console.log(` Media list: docs/research/MEDIA_MANIFEST.md`);
|
|
387
|
-
console.log(` Limits: ${limRel}`);
|
|
388
|
-
if (snapshot.likelyBotWall) {
|
|
389
|
-
console.warn("\n⚠ Possible bot wall / challenge page — check EXTRACTION_LIMITATIONS.md and try: npm run recon:headed\n");
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
main().catch((e) => {
|
|
394
|
-
console.error(e);
|
|
395
|
-
process.exit(1);
|
|
396
|
-
});
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
|
|
3
|
-
import { Button } from "@/components/ui/button";
|
|
4
|
-
|
|
5
|
-
export function ScribewiseLanding() {
|
|
6
|
-
return (
|
|
7
|
-
<main className="relative flex flex-1 flex-col items-center justify-center gap-8 overflow-hidden px-6 py-24 text-center">
|
|
8
|
-
<div
|
|
9
|
-
aria-hidden
|
|
10
|
-
className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,color-mix(in_oklab,var(--primary)_28%,transparent),transparent)]"
|
|
11
|
-
/>
|
|
12
|
-
<p className="text-muted-foreground text-xs font-medium tracking-[0.2em] uppercase">
|
|
13
|
-
Launchframe template
|
|
14
|
-
</p>
|
|
15
|
-
<div className="max-w-2xl space-y-4">
|
|
16
|
-
<h1 className="text-balance font-semibold text-4xl tracking-tight sm:text-5xl">
|
|
17
|
-
Ready for `/clone-website`
|
|
18
|
-
</h1>
|
|
19
|
-
<p className="text-muted-foreground text-balance text-lg">
|
|
20
|
-
Add your target URL and SaaS idea via <code className="text-foreground">launchframe.config.json</code>,
|
|
21
|
-
run recon if needed, then let your agent ship the full landing.
|
|
22
|
-
</p>
|
|
23
|
-
</div>
|
|
24
|
-
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
25
|
-
<Button nativeButton={false} render={<Link href="https://github.com/evangruhlkey/launchframe" />}>
|
|
26
|
-
Launchframe docs
|
|
27
|
-
</Button>
|
|
28
|
-
<Button variant="outline" nativeButton={false} render={<Link href="/" />}>
|
|
29
|
-
Replace this page
|
|
30
|
-
</Button>
|
|
31
|
-
</div>
|
|
32
|
-
</main>
|
|
33
|
-
);
|
|
34
|
-
}
|