framer-export 4.4.3 → 5.0.0-beta.1

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/dist/cli/index.js CHANGED
@@ -15,15 +15,15 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "framer-export",
18
- version: "4.4.3",
19
- description: "Export any Framer, Webflow, or Wix site into a fully working local mirror. Downloads all assets, strips badges, rewrites URLs, and pretty-prints JS.",
18
+ version: "5.0.0-beta.1",
19
+ description: "Export 25+ website platforms (Framer, Webflow, Wix, Shopify, Notion, WordPress, Squarespace, Ghost, and more) into a fully working local mirror. Downloads all assets, strips badges, rewrites URLs, and pretty-prints JS.",
20
20
  type: "module",
21
21
  main: "dist/cli/index.js",
22
22
  types: "dist/cli/index.d.ts",
23
23
  bin: {
24
- "framer-export": "./bin/framer-export.js",
25
- fexport: "./bin/fexport.js",
26
- framerexport: "./bin/framerexport.js"
24
+ "framer-export": "bin/framer-export.js",
25
+ fexport: "bin/fexport.js",
26
+ framerexport: "bin/framerexport.js"
27
27
  },
28
28
  files: [
29
29
  "dist/",
@@ -44,6 +44,14 @@ var init_package = __esm({
44
44
  "framer",
45
45
  "webflow",
46
46
  "wix",
47
+ "shopify",
48
+ "notion",
49
+ "wordpress",
50
+ "squarespace",
51
+ "ghost",
52
+ "carrd",
53
+ "bubble",
54
+ "gumroad",
47
55
  "exporter",
48
56
  "export",
49
57
  "scraper",
@@ -164,7 +172,7 @@ function showBanner() {
164
172
  `
165
173
  ${ui.primary.bold("f-export")} ${ui.muted(`v${package_default.version}`)} ${chip("beta ui")}`
166
174
  );
167
- console.log(` ${ui.text.bold("Framer Export")} ${ui.muted("for Framer, Webflow, and Wix")}
175
+ console.log(` ${ui.text.bold("Framer Export")} ${ui.muted("25+ platforms \xB7 local mirror")}
168
176
  `);
169
177
  return;
170
178
  }
@@ -177,7 +185,7 @@ function showBanner() {
177
185
  ` ${ui.muted(`v${package_default.version}`)} ${ui.text.bold("Framer Export")} ${chip("fexport")} ${ui.muted("local mirror exporter")}`
178
186
  );
179
187
  console.log(
180
- ` ${ui.muted("Framer")} ${ui.border("/")} ${ui.muted("Webflow")} ${ui.border("/")} ${ui.muted("Wix")} ${ui.border("\xB7")} ${ui.primary("clean assets")} ${ui.border("\xB7")} ${ui.secondary("local serve")}
188
+ ` ${ui.muted("Framer")} ${ui.border("/")} ${ui.muted("Webflow")} ${ui.border("/")} ${ui.muted("Wix")} ${ui.border("/")} ${ui.muted("+22 more")} ${ui.border("\xB7")} ${ui.primary("clean assets")} ${ui.border("\xB7")} ${ui.secondary("local serve")}
181
189
  `
182
190
  );
183
191
  }
@@ -204,6 +212,1756 @@ var init_banner = __esm({
204
212
  }
205
213
  });
206
214
 
215
+ // src/platforms/framer.ts
216
+ var FONT_EXTS, framer;
217
+ var init_framer = __esm({
218
+ "src/platforms/framer.ts"() {
219
+ "use strict";
220
+ FONT_EXTS = [".woff2", ".woff", ".ttf", ".otf"];
221
+ framer = {
222
+ name: "framer",
223
+ displayName: "Framer",
224
+ category: "builder",
225
+ priority: 95,
226
+ detectByUrl(url) {
227
+ return /\.framer\.(app|website)|framercanvas\.com/.test(url);
228
+ },
229
+ detectByHtml(html) {
230
+ return html.includes('id="main"') && (html.includes("framerstatic.com") || html.includes("framerusercontent.com"));
231
+ },
232
+ stripDomains: ["events.framer.com", "api.framer.com", "collect.frameranalytics.com"],
233
+ stripSelectors: [
234
+ 'script[src*="events.framer.com"]',
235
+ "#__framer-badge-container",
236
+ "#__framer-badge",
237
+ 'link[href*="canvas-sandbox"]',
238
+ 'script[src*="framer.com/bootstrap"]'
239
+ ],
240
+ stripPatterns: [
241
+ /<div id="__framer-badge-container"[^>]*><\/div>/g,
242
+ /<script>try\{if\(localStorage\.get\("__framer_force_showing_editorbar_since"\)\)[^<]*<\/script>/g
243
+ ],
244
+ hydrationTimeout: 1e4,
245
+ needsHydrationCheck: true,
246
+ mapAssetDir(host, pathname, ext) {
247
+ if (host.includes("framerusercontent.com")) {
248
+ if (pathname.startsWith("/images/")) return "assets/images";
249
+ if (pathname.startsWith("/assets/")) {
250
+ return FONT_EXTS.includes(ext) ? "assets/fonts" : "assets/misc";
251
+ }
252
+ if (pathname.startsWith("/sites/")) {
253
+ if (ext === ".mjs" || ext === ".js") return "scripts/vendor";
254
+ if (ext === ".json") return "data";
255
+ if (ext === ".css") return "styles";
256
+ if (ext === ".framercms") return "data";
257
+ return "assets/misc";
258
+ }
259
+ if (pathname.startsWith("/modules/")) {
260
+ return ext === ".framercms" ? "data" : "scripts/modules";
261
+ }
262
+ return "assets/misc";
263
+ }
264
+ if (host.includes("app.framerstatic.com")) {
265
+ if (ext === ".css") return "styles";
266
+ if (ext === ".mjs" || ext === ".js") return "scripts/vendor";
267
+ if (ext === ".woff2" || ext === ".woff") return "assets/fonts";
268
+ if (ext === ".png" || ext === ".svg") return "assets/images";
269
+ return "assets/misc";
270
+ }
271
+ if (host.includes("framercanvas.com") || host.includes("framer.com")) {
272
+ if (ext === ".mjs" || ext === ".js") return "scripts/vendor";
273
+ if (ext === ".css") return "styles";
274
+ return "assets/misc";
275
+ }
276
+ return null;
277
+ }
278
+ };
279
+ }
280
+ });
281
+
282
+ // src/platforms/webflow.ts
283
+ var IMG_EXTS, FONT_EXTS2, VIDEO_EXTS, webflow;
284
+ var init_webflow = __esm({
285
+ "src/platforms/webflow.ts"() {
286
+ "use strict";
287
+ IMG_EXTS = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif"];
288
+ FONT_EXTS2 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
289
+ VIDEO_EXTS = [".mp4", ".webm", ".ogg"];
290
+ webflow = {
291
+ name: "webflow",
292
+ displayName: "Webflow",
293
+ category: "builder",
294
+ priority: 90,
295
+ detectByUrl(url) {
296
+ return /\.webflow\.(io|com)/.test(url);
297
+ },
298
+ detectByHtml(html) {
299
+ return html.includes("data-wf-site") || html.includes("w-webflow-badge") || html.includes("Webflow") || html.includes("webflow.js");
300
+ },
301
+ stripDomains: ["js-agent.newrelic.com", "cdn.heapanalytics.com", "tr.snapchat.com"],
302
+ stripSelectors: [".w-webflow-badge", 'link[href*="editorbar"]'],
303
+ stripPatterns: [
304
+ /<a[^>]*class="[^"]*w-webflow-badge[^"]*"[^>]*>[\s\S]*?<\/a>/g,
305
+ /<a[^>]*href="[^"]*webflow\.com\?utm_campaign=brandjs[^"]*"[^>]*>[\s\S]*?<\/a>/g,
306
+ /<style>[^<]*\.w-webflow-badge[^<]*<\/style>/g,
307
+ /Powered by <a[^>]*href="[^"]*webflow\.com"[^>]*>[^<]*<\/a>/g,
308
+ /<!-- This site was created in Webflow\.[^>]*-->/g,
309
+ /<html([^>]*) data-wf-domain="[^"]*"/g,
310
+ /<html([^>]*) data-wf-page="[^"]*"/g,
311
+ /<html([^>]*) data-wf-site="[^"]*"/g,
312
+ /<html([^>]*) data-wf-status="[^"]*"/g,
313
+ /<meta[^>]*content="Webflow"[^>]*>/g
314
+ ],
315
+ hydrationTimeout: 2e3,
316
+ needsHydrationCheck: false,
317
+ mapAssetDir(host, pathname, ext) {
318
+ if (host.includes("website-files.com") || host.includes("webflow.com") || host.includes("uploads-ssl.webflow.com")) {
319
+ if (VIDEO_EXTS.includes(ext)) return "assets/videos";
320
+ if (pathname.includes("/images/") || IMG_EXTS.includes(ext)) return "assets/images";
321
+ if (pathname.includes("/css/") || ext === ".css") return "styles";
322
+ if (pathname.includes("/js/") || ext === ".js") return "scripts/vendor";
323
+ if (pathname.includes("/gsap/") || pathname.includes("/plugins/")) return "scripts/vendor";
324
+ if (FONT_EXTS2.includes(ext)) return "assets/fonts";
325
+ return "assets/misc";
326
+ }
327
+ if (host.includes("d3e54v103j8qbb.cloudfront.net")) {
328
+ if (ext === ".js") return "scripts/vendor";
329
+ if (ext === ".css") return "styles";
330
+ if (IMG_EXTS.includes(ext)) return "assets/images";
331
+ return "assets/misc";
332
+ }
333
+ return null;
334
+ }
335
+ };
336
+ }
337
+ });
338
+
339
+ // src/platforms/wix.ts
340
+ var IMG_EXTS2, FONT_EXTS3, VIDEO_EXTS2, wix;
341
+ var init_wix = __esm({
342
+ "src/platforms/wix.ts"() {
343
+ "use strict";
344
+ IMG_EXTS2 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
345
+ FONT_EXTS3 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
346
+ VIDEO_EXTS2 = [".mp4", ".webm", ".ogg"];
347
+ wix = {
348
+ name: "wix",
349
+ displayName: "Wix",
350
+ category: "builder",
351
+ priority: 88,
352
+ detectByUrl(url) {
353
+ return /\.wixsite\.com|\.wix\.com/.test(url);
354
+ },
355
+ detectByHtml(html) {
356
+ return html.includes("wix-viewer-model") || html.includes('id="WIX_ADS"') || html.includes("X-Wix-") || html.includes("Wix.com") || html.includes("wixcode-sdk") || html.includes("data-wix") || html.includes("wix-code") || html.includes("WixCode") || /\bwix\.com\b/.test(html) || /meta\s+name=["']generator["']\s+content=["'][^"']*Wix[^"']*["']/i.test(html) || /_wix_/i.test(html) || /wix-ecom/i.test(html);
357
+ },
358
+ stripDomains: [
359
+ "frog.wix.com",
360
+ "bi.wixapi.com",
361
+ "fed.wixcodescheduler.com",
362
+ "editor.wix.com",
363
+ "panorama.wixapps.net"
364
+ ],
365
+ stripSelectors: ["#WIX_ADS", ".wix-ads", "#wix-badge", "#SCROLL_TO_TOP"],
366
+ stripPatterns: [
367
+ /<div[^>]*class="[^"]*wix-ads[^"]*"[^>]*>[\s\S]*?<\/div>/g
368
+ ],
369
+ hydrationTimeout: 3e3,
370
+ needsHydrationCheck: false,
371
+ mapAssetDir(host, pathname, ext) {
372
+ if (host === "video.wixstatic.com") {
373
+ return "assets/videos";
374
+ }
375
+ if (host.includes("wixstatic.com")) {
376
+ if (VIDEO_EXTS2.includes(ext)) return "assets/videos";
377
+ if (pathname.includes("/images/") || IMG_EXTS2.includes(ext)) return "assets/images";
378
+ if (ext === ".css") return "styles";
379
+ if (ext === ".js") return "scripts/vendor";
380
+ if (FONT_EXTS3.includes(ext)) return "assets/fonts";
381
+ return "assets/misc";
382
+ }
383
+ if (host.includes("parastorage.com")) {
384
+ if (ext === ".js") return "scripts/vendor";
385
+ if (ext === ".css") return "styles";
386
+ if (FONT_EXTS3.includes(ext)) return "assets/fonts";
387
+ if (IMG_EXTS2.includes(ext)) return "assets/images";
388
+ return "assets/misc";
389
+ }
390
+ return null;
391
+ }
392
+ };
393
+ }
394
+ });
395
+
396
+ // src/platforms/builder/bubble.ts
397
+ var IMG_EXTS3, FONT_EXTS4, VIDEO_EXTS3, bubble;
398
+ var init_bubble = __esm({
399
+ "src/platforms/builder/bubble.ts"() {
400
+ "use strict";
401
+ IMG_EXTS3 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
402
+ FONT_EXTS4 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
403
+ VIDEO_EXTS3 = [".mp4", ".webm", ".ogg"];
404
+ bubble = {
405
+ name: "bubble",
406
+ displayName: "Bubble",
407
+ category: "builder",
408
+ priority: 85,
409
+ detectByUrl(url) {
410
+ return /\.bubbleapps\.io/i.test(url);
411
+ },
412
+ detectByHtml(html) {
413
+ return html.includes("window.appquery = make_proxy(") || html.includes("window._bubble_page_load_data") || html.includes("window.bubble_session_uid") || html.includes("/package/pre_run_jquery_js/");
414
+ },
415
+ // Telemetry only (Plausible-based, on standard-plan apps). NEVER the asset CDN.
416
+ stripDomains: ["pluginpul.se"],
417
+ // Post-process removal of any telemetry <script> that survived into the DOM.
418
+ stripSelectors: ['script[src*="pluginpul.se"]'],
419
+ stripPatterns: [
420
+ // "Made with Bubble" badge — injected at runtime by run.js and gated by
421
+ // window._p.no_branding, so it never appears in the raw SSR HTML (only in
422
+ // the captured rendered DOM). Best-effort strip anchored on its bubble.io
423
+ // target: matches an <a> pointing at the bubble.io ROOT (optionally with a
424
+ // tracking query), which is tight enough to spare normal deep-links to
425
+ // bubble.io content pages.
426
+ /<a\b[^>]*href="https?:\/\/(?:www\.)?bubble\.io\/?(?:\?[^"]*)?"[^>]*>[\s\S]*?<\/a>/gi
427
+ ],
428
+ needsHydrationCheck: true,
429
+ hydrationTimeout: 8e3,
430
+ hydrationSelector: "#main-page",
431
+ captureRenderedDom: true,
432
+ mapAssetDir(host, pathname, ext) {
433
+ if (host.includes("meta-fonts.cdn.bubble.io")) {
434
+ if (FONT_EXTS4.includes(ext)) return "assets/fonts";
435
+ if (ext === ".css") return "styles";
436
+ if (IMG_EXTS3.includes(ext)) return "assets/images";
437
+ return "assets/misc";
438
+ }
439
+ if (host.includes("cdn.bubble.io")) {
440
+ if (VIDEO_EXTS3.includes(ext)) return "assets/videos";
441
+ if (pathname.includes("/images/") || IMG_EXTS3.includes(ext)) return "assets/images";
442
+ if (ext === ".css") return "styles";
443
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
444
+ if (FONT_EXTS4.includes(ext)) return "assets/fonts";
445
+ if (ext === ".json") return "data";
446
+ return "assets/misc";
447
+ }
448
+ if (host.includes("d3dqmih97rcqmh.cloudfront.net") || host.includes("d1muf25xaso8hp.cloudfront.net")) {
449
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
450
+ if (ext === ".css") return "styles";
451
+ if (FONT_EXTS4.includes(ext)) return "assets/fonts";
452
+ if (IMG_EXTS3.includes(ext)) return "assets/images";
453
+ if (ext === ".json") return "data";
454
+ return "assets/misc";
455
+ }
456
+ return null;
457
+ }
458
+ };
459
+ }
460
+ });
461
+
462
+ // src/platforms/builder/carrd.ts
463
+ var IMG_EXTS4, FONT_EXTS5, VIDEO_EXTS4, carrd;
464
+ var init_carrd = __esm({
465
+ "src/platforms/builder/carrd.ts"() {
466
+ "use strict";
467
+ IMG_EXTS4 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
468
+ FONT_EXTS5 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
469
+ VIDEO_EXTS4 = [".mp4", ".webm", ".ogg"];
470
+ carrd = {
471
+ name: "carrd",
472
+ displayName: "Carrd",
473
+ category: "builder",
474
+ priority: 85,
475
+ detectByUrl(url) {
476
+ return /\.carrd\.co/i.test(url);
477
+ },
478
+ detectByHtml(html) {
479
+ return (
480
+ // Hallmark inline runtime IIFE, present on every Carrd site (new and old).
481
+ html.includes("$inner = $('.inner')") || // Free-plan "( Made with Carrd )" badge anchor.
482
+ html.includes('href="https://carrd.co"') || // Carrd's component-class scheme (…-component). Webflow uses w-* prefixes and
483
+ // Wix uses random ids, so this family does not collide with the frozen trio.
484
+ /class="[^"]*\b(?:text|image|container|buttons|icons|divider|links)-component\b/.test(html) || // Current-gen structural nesting: .site-wrapper > .site-main (both required, so
485
+ // neither generic-sounding class triggers detection on its own).
486
+ html.includes('class="site-wrapper"') && html.includes('class="site-main"')
487
+ );
488
+ },
489
+ // No third-party analytics/telemetry is injected by default (profile
490
+ // analyticsScripts is empty), and fonts.googleapis.com is a needed asset host,
491
+ // so nothing is blocked during capture.
492
+ stripDomains: [],
493
+ // The badge id sits directly on the <p> (there is no wrapper div and no
494
+ // class="credits"); removing #credits drops the whole "Made with Carrd" line.
495
+ stripSelectors: ["#credits"],
496
+ stripPatterns: [
497
+ // Free-plan badge paragraph: <p id="credits"><a href="https://carrd.co">…</a></p>
498
+ /<p[^>]*id="credits"[^>]*>[\s\S]*?<\/p>/gi,
499
+ // Belt-and-suspenders: the branded badge anchor itself (anchored on the exact
500
+ // "( Made with Carrd )" text so a genuine user link to carrd.co is never removed).
501
+ /<a[^>]*href="https:\/\/carrd\.co"[^>]*>\s*\(\s*Made with Carrd\s*\)\s*<\/a>/gi
502
+ ],
503
+ // ssr-static: the raw SSR HTML is complete, so no hydration wait is required
504
+ // (profile needsHydrationCheck=false, hydrationTimeoutMs=0). antiBot is Cloudflare
505
+ // but the pages fetched cleanly in research; per the authoring guide we keep a
506
+ // small capture buffer instead of the literal 0 ms.
507
+ hydrationTimeout: 2e3,
508
+ needsHydrationCheck: false,
509
+ mapAssetDir(host, pathname, ext) {
510
+ if (host.includes("fonts.googleapis.com")) {
511
+ return "styles";
512
+ }
513
+ if (host.includes("carrd.co")) {
514
+ if (VIDEO_EXTS4.includes(ext)) return "assets/videos";
515
+ if (pathname.includes("/assets/images/") || IMG_EXTS4.includes(ext)) return "assets/images";
516
+ if (pathname.includes("/assets/css/") || ext === ".css") return "styles";
517
+ if (pathname.includes("/assets/js/") || ext === ".js" || ext === ".mjs") return "scripts/vendor";
518
+ if (FONT_EXTS5.includes(ext)) return "assets/fonts";
519
+ return "assets/misc";
520
+ }
521
+ return null;
522
+ }
523
+ };
524
+ }
525
+ });
526
+
527
+ // src/platforms/builder/duda.ts
528
+ var IMG_EXTS5, FONT_EXTS6, VIDEO_EXTS5, duda;
529
+ var init_duda = __esm({
530
+ "src/platforms/builder/duda.ts"() {
531
+ "use strict";
532
+ IMG_EXTS5 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
533
+ FONT_EXTS6 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
534
+ VIDEO_EXTS5 = [".mp4", ".webm", ".ogg"];
535
+ duda = {
536
+ name: "duda",
537
+ displayName: "Duda",
538
+ category: "builder",
539
+ priority: 82,
540
+ detectByUrl(url) {
541
+ return /\.(?:multiscreensite|dudaone)\.com/i.test(url);
542
+ },
543
+ detectByHtml(html) {
544
+ return html.includes("atob('RFVEQU9ORQ==')") || // window.Parameters SiteType → 'DUDAONE'
545
+ html.includes('id="dmRoot"') || html.includes("DM_DIRECT") || // window.Parameters productId
546
+ html.includes("d_track_campaign");
547
+ },
548
+ // No external telemetry/analytics hosts on Duda (tracking is inline, first-party).
549
+ stripDomains: [],
550
+ // Inline first-party tracking <script id="..."> elements (removed by id).
551
+ stripSelectors: ["#d_track_campaign", "#d_track_personalization", "#d_track_sp"],
552
+ stripPatterns: [
553
+ // "Made with Duda" badge → https://www.duda.co/inspiration
554
+ /<a[^>]*href="[^"]*duda\.co\/inspiration[^"]*"[^>]*>[\s\S]*?<\/a>/g
555
+ ],
556
+ hydrationTimeout: 0,
557
+ needsHydrationCheck: false,
558
+ mapAssetDir(host, pathname, ext) {
559
+ const isCdnWebsite = host.endsWith(".cdn-website.com");
560
+ const isMsCdn = host === "ms-cdn.multiscreensite.com" || host === "irp-cdn.multiscreensite.com";
561
+ if (!isCdnWebsite && !isMsCdn) return null;
562
+ if (VIDEO_EXTS5.includes(ext)) return "assets/videos";
563
+ if (IMG_EXTS5.includes(ext) || pathname.includes("/images/")) return "assets/images";
564
+ if (ext === ".css") return "styles";
565
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
566
+ if (FONT_EXTS6.includes(ext)) return "assets/fonts";
567
+ const isResizeProxy = host.startsWith("irp") || host.startsWith("lirp");
568
+ return isResizeProxy ? "assets/images" : "assets/misc";
569
+ }
570
+ };
571
+ }
572
+ });
573
+
574
+ // src/platforms/builder/squarespace.ts
575
+ var IMG_EXTS6, FONT_EXTS7, VIDEO_EXTS6, STATIC_EDGE_RE, squarespace;
576
+ var init_squarespace = __esm({
577
+ "src/platforms/builder/squarespace.ts"() {
578
+ "use strict";
579
+ IMG_EXTS6 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
580
+ FONT_EXTS7 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
581
+ VIDEO_EXTS6 = [".mp4", ".webm", ".ogg"];
582
+ STATIC_EDGE_RE = /^static\d+\.squarespace\.com$/i;
583
+ squarespace = {
584
+ name: "squarespace",
585
+ displayName: "Squarespace",
586
+ category: "builder",
587
+ priority: 82,
588
+ detectByUrl(url) {
589
+ return /\.squarespace\.com/i.test(url);
590
+ },
591
+ detectByHtml(html) {
592
+ return html.includes("<!-- This is Squarespace. -->") || html.includes("Static.SQUARESPACE_CONTEXT") || html.includes("<!-- End of Squarespace Headers -->") || html.includes("images.squarespace-cdn.com");
593
+ },
594
+ // First-party analytics only; no default third-party telemetry host to block.
595
+ stripDomains: [],
596
+ // No editor chrome / badge in public SSR HTML on paid or template sites.
597
+ stripSelectors: [],
598
+ stripPatterns: [],
599
+ hydrationTimeout: 3e3,
600
+ needsHydrationCheck: false,
601
+ mapAssetDir(host, pathname, ext) {
602
+ if (host.includes("images.squarespace-cdn.com")) {
603
+ if (VIDEO_EXTS6.includes(ext)) return "assets/videos";
604
+ if (ext === ".css") return "styles";
605
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
606
+ if (FONT_EXTS7.includes(ext)) return "assets/fonts";
607
+ if (ext === ".json") return "data";
608
+ return "assets/images";
609
+ }
610
+ if (host.includes("file.squarespace-cdn.com")) {
611
+ if (VIDEO_EXTS6.includes(ext)) return "assets/videos";
612
+ if (IMG_EXTS6.includes(ext)) return "assets/images";
613
+ if (ext === ".css") return "styles";
614
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
615
+ if (FONT_EXTS7.includes(ext)) return "assets/fonts";
616
+ if (ext === ".json") return "data";
617
+ return "assets/misc";
618
+ }
619
+ if (host.includes("assets.squarespace.com") || STATIC_EDGE_RE.test(host)) {
620
+ if (VIDEO_EXTS6.includes(ext)) return "assets/videos";
621
+ if (ext === ".css") return "styles";
622
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
623
+ if (FONT_EXTS7.includes(ext)) return "assets/fonts";
624
+ if (ext === ".json") return "data";
625
+ if (IMG_EXTS6.includes(ext) || pathname.includes("/images/")) return "assets/images";
626
+ return "assets/misc";
627
+ }
628
+ if (host.includes("definitions.sqspcdn.com")) {
629
+ if (ext === ".json") return "data";
630
+ if (ext === ".css") return "styles";
631
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
632
+ return "assets/misc";
633
+ }
634
+ return null;
635
+ }
636
+ };
637
+ }
638
+ });
639
+
640
+ // src/platforms/builder/strikingly.ts
641
+ var IMG_EXTS7, FONT_EXTS8, VIDEO_EXTS7, strikingly;
642
+ var init_strikingly = __esm({
643
+ "src/platforms/builder/strikingly.ts"() {
644
+ "use strict";
645
+ IMG_EXTS7 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
646
+ FONT_EXTS8 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
647
+ VIDEO_EXTS7 = [".mp4", ".webm", ".ogg"];
648
+ strikingly = {
649
+ name: "strikingly",
650
+ displayName: "Strikingly",
651
+ category: "builder",
652
+ priority: 85,
653
+ detectByUrl(url) {
654
+ return /\.mystrikingly\.com/i.test(url);
655
+ },
656
+ detectByHtml(html) {
657
+ return html.includes("strikingly-host-suffix") || html.includes('data-hypernova-key="SiteBootstrapper"') || html.includes('id="s-page-client-container"') || html.includes("strikinglycdn.com");
658
+ },
659
+ // Telemetry / error-monitoring only — NEVER the strikinglycdn asset CDN.
660
+ stripDomains: [
661
+ "google-analytics.com",
662
+ "www.google-analytics.com",
663
+ "notify.bugsnag.com",
664
+ "sessions.bugsnag.com"
665
+ ],
666
+ // The "Powered by Strikingly" footer section carries the unique class
667
+ // `.show-strikingly-logo` (inside #s-footer-section-container).
668
+ stripSelectors: [".show-strikingly-logo"],
669
+ stripPatterns: [
670
+ // Hydration-rendered badge anchor → https://www.strikingly.com/?ref=logo…&utm_campaign=footer_pbs
671
+ /<a[^>]*href="[^"]*strikingly\.com\/\?ref=logo[^"]*"[^>]*>[\s\S]*?<\/a>/gi
672
+ ],
673
+ // renderStrategy: react-hydration → use the rendered DOM as the HTML source.
674
+ hydrationTimeout: 3e3,
675
+ needsHydrationCheck: false,
676
+ hydrationSelector: "#s-page-client-container",
677
+ captureRenderedDom: true,
678
+ mapAssetDir(host, pathname, ext) {
679
+ if (host.includes("strikinglycdn.com")) {
680
+ if (VIDEO_EXTS7.includes(ext)) return "assets/videos";
681
+ if (host.includes("static-fonts-css")) {
682
+ if (FONT_EXTS8.includes(ext)) return "assets/fonts";
683
+ if (ext === ".css") return "styles";
684
+ }
685
+ if (host.includes("static-assets")) {
686
+ if (ext === ".css") return "styles";
687
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
688
+ if (FONT_EXTS8.includes(ext)) return "assets/fonts";
689
+ if (IMG_EXTS7.includes(ext) || pathname.includes("/images/")) return "assets/images";
690
+ return "assets/misc";
691
+ }
692
+ if (IMG_EXTS7.includes(ext) || pathname.includes("/images/")) return "assets/images";
693
+ if (FONT_EXTS8.includes(ext)) return "assets/fonts";
694
+ if (ext === ".css") return "styles";
695
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
696
+ return "assets/misc";
697
+ }
698
+ if (host.includes("cdnjs.cloudflare.com")) {
699
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
700
+ if (ext === ".css") return "styles";
701
+ if (FONT_EXTS8.includes(ext)) return "assets/fonts";
702
+ if (IMG_EXTS7.includes(ext)) return "assets/images";
703
+ return "assets/misc";
704
+ }
705
+ return null;
706
+ }
707
+ };
708
+ }
709
+ });
710
+
711
+ // src/platforms/builder/tilda.ts
712
+ var IMG_EXTS8, FONT_EXTS9, VIDEO_EXTS8, tilda;
713
+ var init_tilda = __esm({
714
+ "src/platforms/builder/tilda.ts"() {
715
+ "use strict";
716
+ IMG_EXTS8 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
717
+ FONT_EXTS9 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
718
+ VIDEO_EXTS8 = [".mp4", ".webm", ".ogg"];
719
+ tilda = {
720
+ name: "tilda",
721
+ displayName: "Tilda",
722
+ category: "builder",
723
+ priority: 80,
724
+ detectByUrl(url) {
725
+ return /\.tilda\.ws/i.test(url);
726
+ },
727
+ detectByHtml(html) {
728
+ return html.includes("data-tilda-project-id") || html.includes("data-tilda-page-id") || html.includes("tildacdn") || html.includes('id="allrecords"');
729
+ },
730
+ // Third-party telemetry only. NEVER the tildacdn asset hosts (they carry the
731
+ // page's real images/CSS/JS); the Tilda stat script is stripped by selector.
732
+ stripDomains: ["www.google-analytics.com", "google-analytics.com"],
733
+ stripSelectors: ["#tildacopy", ".t-tildalabel", 'script[src*="tilda-stat"]'],
734
+ stripPatterns: [
735
+ // "Made on Tilda" badge link (free-plan only). No nested <a>, so the
736
+ // non-greedy </a> match is safe even though the anchor wraps inner <div>s.
737
+ /<a[^>]*href="[^"]*tilda\.cc[^"]*"[^>]*>[\s\S]*?<\/a>/g
738
+ ],
739
+ hydrationTimeout: 0,
740
+ needsHydrationCheck: false,
741
+ mapAssetDir(host, pathname, ext) {
742
+ if (host.includes("tildacdn")) {
743
+ if (host.startsWith("thb.")) return "assets/images";
744
+ if (VIDEO_EXTS8.includes(ext)) return "assets/videos";
745
+ if (ext === ".css" || pathname.includes("/css/")) return "styles";
746
+ if (ext === ".js" || ext === ".mjs" || pathname.includes("/js/")) return "scripts/vendor";
747
+ if (FONT_EXTS9.includes(ext) || pathname.includes("/fonts/")) return "assets/fonts";
748
+ if (ext === ".json") return "data";
749
+ if (IMG_EXTS8.includes(ext) || pathname.includes("/img/") || pathname.includes("/images/") || pathname.includes("/tild")) {
750
+ return "assets/images";
751
+ }
752
+ return "assets/misc";
753
+ }
754
+ return null;
755
+ }
756
+ };
757
+ }
758
+ });
759
+
760
+ // src/platforms/builder/weebly.ts
761
+ var IMG_EXTS9, FONT_EXTS10, VIDEO_EXTS9, weebly;
762
+ var init_weebly = __esm({
763
+ "src/platforms/builder/weebly.ts"() {
764
+ "use strict";
765
+ IMG_EXTS9 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
766
+ FONT_EXTS10 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
767
+ VIDEO_EXTS9 = [".mp4", ".webm", ".ogg"];
768
+ weebly = {
769
+ name: "weebly",
770
+ displayName: "Weebly",
771
+ category: "builder",
772
+ priority: 82,
773
+ detectByUrl(url) {
774
+ return /\.weebly\.com/i.test(url);
775
+ },
776
+ detectByHtml(html) {
777
+ return html.includes("Weebly.footer.setupContainer") || html.includes("weebly-footer-signup-container-v3") || html.includes("wsite-content") || html.includes("window._W");
778
+ },
779
+ // Third-party telemetry only. NEVER list editmysite.com here — that host serves
780
+ // the platform CSS/JS/images (cdn1/cdn2/cdn11) and blocking it deletes assets.
781
+ // The Weebly _W.Analytics trackers (main.js, stl.js) also live on that CDN, so
782
+ // they are intentionally not blocked (blocking the host would kill assets too).
783
+ stripDomains: ["google-analytics.com"],
784
+ // Footer "Made with Weebly" signup badge: unique container id (empty in SSR
785
+ // HTML, populated client-side) plus the script that injects it.
786
+ stripSelectors: ["#weebly-footer-signup-container-v3", 'script[src*="footerSignup.js"]'],
787
+ // Best-effort removal of the rendered badge anchor should it appear inline.
788
+ stripPatterns: [/<a[^>]*href="[^"]*weebly\.com\/signup[^"]*"[^>]*>[\s\S]*?<\/a>/g],
789
+ hydrationTimeout: 0,
790
+ needsHydrationCheck: false,
791
+ mapAssetDir(host, pathname, ext) {
792
+ if (host.includes("editmysite.com")) {
793
+ if (VIDEO_EXTS9.includes(ext)) return "assets/videos";
794
+ if (pathname.includes("/images/") || IMG_EXTS9.includes(ext)) return "assets/images";
795
+ if (pathname.includes("/css/") || ext === ".css") return "styles";
796
+ if (pathname.includes("/js/") || ext === ".js" || ext === ".mjs") return "scripts/vendor";
797
+ if (FONT_EXTS10.includes(ext)) return "assets/fonts";
798
+ return "assets/misc";
799
+ }
800
+ return null;
801
+ }
802
+ };
803
+ }
804
+ });
805
+
806
+ // src/platforms/landing/clickfunnels.ts
807
+ var IMG_EXTS10, FONT_EXTS11, VIDEO_EXTS10, clickfunnels;
808
+ var init_clickfunnels = __esm({
809
+ "src/platforms/landing/clickfunnels.ts"() {
810
+ "use strict";
811
+ IMG_EXTS10 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
812
+ FONT_EXTS11 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
813
+ VIDEO_EXTS10 = [".mp4", ".webm", ".ogg"];
814
+ clickfunnels = {
815
+ name: "clickfunnels",
816
+ displayName: 'ClickFunnels (2.0 / "CF2")',
817
+ category: "landing",
818
+ priority: 82,
819
+ detectByUrl(url) {
820
+ return /\.myclickfunnels\.com/i.test(url);
821
+ },
822
+ detectByHtml(html) {
823
+ return html.includes("cf-head-scripts") || html.includes("window.cfRootDomain") || html.includes("cf-lander-serialized-custom-fonts") || html.includes("data-page-element");
824
+ },
825
+ // Telemetry / analytics hosts blocked during capture. NOT the asset CDNs
826
+ // (statics.myclickfunnels.com / images.clickfunnels.com) and NOT the functional
827
+ // third-party libs (cdnjs / code.jquery.com / fontawesome) or Turnstile.
828
+ stripDomains: ["bam.nr-data.net", "js-agent.newrelic.com", "events.myclickfunnels.com"],
829
+ // No CF-specific badge/editor container has a static id/class in the delivered
830
+ // HTML, so there is nothing selector-strippable here.
831
+ stripSelectors: [],
832
+ // BADGE: The "Powered by ClickFunnels" badge is default-on per docs but was
833
+ // toggled OFF/HIDDEN on both verified samples (0 occurrences in the SSR HTML),
834
+ // and this handler does not capture the rendered DOM. No reliable static markup
835
+ // exists to anchor a regex on, so no badge stripPattern is emitted (a loose match
836
+ // on clickfunnels.com would risk removing legitimate links). If a future sample
837
+ // exposes the badge anchor, add a tight /<a[^>]*href="[^"]*clickfunnels\.com[^"]*"...>/ here.
838
+ stripPatterns: [],
839
+ // ssr-static: SSR body is complete, no hydration wait needed.
840
+ hydrationTimeout: 0,
841
+ needsHydrationCheck: false,
842
+ mapAssetDir(host, pathname, ext) {
843
+ if (host.includes("statics.myclickfunnels.com")) {
844
+ if (VIDEO_EXTS10.includes(ext)) return "assets/videos";
845
+ if (pathname.includes("/image/") || pathname.includes("/images/") || IMG_EXTS10.includes(ext))
846
+ return "assets/images";
847
+ if (ext === ".css") return "styles";
848
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
849
+ if (FONT_EXTS11.includes(ext)) return "assets/fonts";
850
+ return "assets/misc";
851
+ }
852
+ if (host.includes("images.clickfunnels.com")) {
853
+ if (VIDEO_EXTS10.includes(ext)) return "assets/videos";
854
+ if (FONT_EXTS11.includes(ext)) return "assets/fonts";
855
+ return "assets/images";
856
+ }
857
+ return null;
858
+ }
859
+ };
860
+ }
861
+ });
862
+
863
+ // src/platforms/landing/elementor.ts
864
+ var IMG_EXTS11, FONT_EXTS12, VIDEO_EXTS11, elementor;
865
+ var init_elementor = __esm({
866
+ "src/platforms/landing/elementor.ts"() {
867
+ "use strict";
868
+ IMG_EXTS11 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
869
+ FONT_EXTS12 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
870
+ VIDEO_EXTS11 = [".mp4", ".webm", ".ogg"];
871
+ elementor = {
872
+ name: "elementor",
873
+ displayName: "Elementor",
874
+ category: "landing",
875
+ priority: 42,
876
+ // Only Elementor Cloud/Hosting uses a platform subdomain; self-hosted sites live
877
+ // on the owner's own domain and are caught by detectByHtml / detectGenerator.
878
+ detectByUrl(url) {
879
+ return /\.elementor\.cloud/i.test(url);
880
+ },
881
+ // OR of four independent, Elementor-specific markers (none match Framer/Webflow/Wix,
882
+ // and #2/#3 survive optimization plugins that strip the generator meta):
883
+ // - Elementor plugin asset directory
884
+ // - Elementor "kit" body class
885
+ // - root wrapper data-attribute
886
+ // - frontend bootstrap JS global
887
+ detectByHtml(html) {
888
+ return html.includes("/wp-content/plugins/elementor/") || html.includes("elementor-kit-") || html.includes("data-elementor-type=") || html.includes("elementorFrontendConfig");
889
+ },
890
+ // Matched against the <meta name="generator"> content only, e.g.
891
+ // "Elementor 3.31.3; features: ...". Anchored so it fires solely on Elementor's own
892
+ // generator string, never on the "WordPress 6.7.5" / "Site Kit by Google" siblings.
893
+ detectGenerator: /^Elementor\s+\d+\.\d+/i,
894
+ // No default third-party telemetry: googletagmanager / cdn.usefathom.com hosts seen
895
+ // on sample sites are site-owner additions, not an Elementor platform default.
896
+ stripDomains: [],
897
+ // No front-end editor chrome or badge element to remove.
898
+ stripSelectors: [],
899
+ // Verified: no "Made with Elementor" badge in the front-end DOM → nothing to strip.
900
+ stripPatterns: [],
901
+ hydrationTimeout: 0,
902
+ needsHydrationCheck: false,
903
+ mapAssetDir(host, pathname, ext) {
904
+ if (!pathname.includes("/wp-content/") && !pathname.includes("/wp-includes/")) {
905
+ return null;
906
+ }
907
+ if (VIDEO_EXTS11.includes(ext)) return "assets/videos";
908
+ if (ext === ".css") return "styles";
909
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
910
+ if (FONT_EXTS12.includes(ext)) return "assets/fonts";
911
+ if (IMG_EXTS11.includes(ext)) return "assets/images";
912
+ if (ext === ".json") return "data";
913
+ return "assets/misc";
914
+ }
915
+ };
916
+ }
917
+ });
918
+
919
+ // src/platforms/landing/instapage.ts
920
+ var IMG_EXTS12, FONT_EXTS13, VIDEO_EXTS12, instapage;
921
+ var init_instapage = __esm({
922
+ "src/platforms/landing/instapage.ts"() {
923
+ "use strict";
924
+ IMG_EXTS12 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
925
+ FONT_EXTS13 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
926
+ VIDEO_EXTS12 = [".mp4", ".webm", ".ogg"];
927
+ instapage = {
928
+ name: "instapage",
929
+ displayName: "Instapage",
930
+ category: "landing",
931
+ priority: 82,
932
+ detectByUrl(url) {
933
+ return /\.(?:pagedemo|pageserve)\.co/i.test(url);
934
+ },
935
+ detectByHtml(html) {
936
+ return html.includes("instapageSp") || html.includes("_instapageSnowplow") || html.includes("anthill.instapage.com") || html.includes("pageserver.page2");
937
+ },
938
+ // Telemetry / analytics hosts only — blocked during capture. The fastcdn.co
939
+ // asset CDN is deliberately NOT listed here (those assets are mirrored).
940
+ stripDomains: [
941
+ "anthill.instapage.com",
942
+ "cdn.instapagemetrics.com",
943
+ "heatmap-events-collector.instapage.com",
944
+ "heatmap.services"
945
+ ],
946
+ stripSelectors: [
947
+ 'script[src*="instapagemetrics.com"]',
948
+ 'script[src*="anthill.instapage.com"]',
949
+ 'script[src*="heatmap.services"]'
950
+ ],
951
+ stripPatterns: [
952
+ // Hidden local-storage helper iframe (runtime-injected on live pages; stripped
953
+ // here best-effort in case it appears in the captured markup).
954
+ /<iframe[^>]*d3mwhxgzltpnyp\.cloudfront\.net[^>]*>[\s\S]*?<\/iframe>/g
955
+ ],
956
+ hydrationTimeout: 0,
957
+ needsHydrationCheck: false,
958
+ mapAssetDir(host, pathname, ext) {
959
+ if (host.includes("fastcdn.co")) {
960
+ if (VIDEO_EXTS12.includes(ext)) return "assets/videos";
961
+ if (IMG_EXTS12.includes(ext) || pathname.includes("/u/")) return "assets/images";
962
+ if (ext === ".css") return "styles";
963
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
964
+ if (FONT_EXTS13.includes(ext)) return "assets/fonts";
965
+ if (ext === ".json") return "data";
966
+ return "assets/misc";
967
+ }
968
+ if (host.includes("d3mwhxgzltpnyp.cloudfront.net")) {
969
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
970
+ if (ext === ".css") return "styles";
971
+ if (IMG_EXTS12.includes(ext)) return "assets/images";
972
+ if (FONT_EXTS13.includes(ext)) return "assets/fonts";
973
+ return "assets/misc";
974
+ }
975
+ return null;
976
+ }
977
+ };
978
+ }
979
+ });
980
+
981
+ // src/platforms/landing/systemeio.ts
982
+ var IMG_EXTS13, FONT_EXTS14, VIDEO_EXTS13, systemeio;
983
+ var init_systemeio = __esm({
984
+ "src/platforms/landing/systemeio.ts"() {
985
+ "use strict";
986
+ IMG_EXTS13 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
987
+ FONT_EXTS14 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
988
+ VIDEO_EXTS13 = [".mp4", ".webm", ".ogg"];
989
+ systemeio = {
990
+ name: "systemeio",
991
+ displayName: "Systeme.io",
992
+ category: "landing",
993
+ priority: 80,
994
+ detectByUrl(url) {
995
+ return /\.systeme\.io/i.test(url);
996
+ },
997
+ detectByHtml(html) {
998
+ return html.includes("<!-- Created with https://systeme.io -->") || html.includes("d3fit27i5nzkqh.cloudfront.net") || html.includes("systeme.io/?sa=") || html.includes("d3syewzhvzylbl.cloudfront.net");
999
+ },
1000
+ // First-party click/funnel telemetry — blocked during capture, never downloaded.
1001
+ stripDomains: ["log.systeme.io"],
1002
+ stripSelectors: ['script[src*="log.systeme.io"]'],
1003
+ stripPatterns: [
1004
+ // Footer credit/affiliate badge: <a href="https://systeme.io/?sa=<code>" ...>SITE CREDIT</a>.
1005
+ // Match on the `systeme.io/?sa=` href affiliate param (visible label is owner-editable).
1006
+ /<a[^>]*href="[^"]*systeme\.io\/\?sa=[^"]*"[^>]*>[\s\S]*?<\/a>/g,
1007
+ // Branding comment injected right after <!DOCTYPE html>.
1008
+ /<!--\s*Created with https:\/\/systeme\.io\s*-->/g
1009
+ ],
1010
+ hydrationTimeout: 0,
1011
+ needsHydrationCheck: false,
1012
+ mapAssetDir(host, pathname, ext) {
1013
+ if (host.includes("d3fit27i5nzkqh.cloudfront.net")) {
1014
+ if (ext === ".css") return "styles";
1015
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1016
+ if (FONT_EXTS14.includes(ext)) return "assets/fonts";
1017
+ if (VIDEO_EXTS13.includes(ext)) return "assets/videos";
1018
+ if (IMG_EXTS13.includes(ext)) return "assets/images";
1019
+ if (ext === ".json") return "data";
1020
+ return "assets/misc";
1021
+ }
1022
+ if (host.includes("d3syewzhvzylbl.cloudfront.net")) {
1023
+ if (FONT_EXTS14.includes(ext) || pathname.includes("/fonts/")) return "assets/fonts";
1024
+ if (ext === ".css") return "styles";
1025
+ if (IMG_EXTS13.includes(ext)) return "assets/images";
1026
+ return "assets/misc";
1027
+ }
1028
+ if (host.includes("d1yei2z3i6k35z.cloudfront.net")) {
1029
+ if (VIDEO_EXTS13.includes(ext)) return "assets/videos";
1030
+ if (IMG_EXTS13.includes(ext) || pathname.includes("/images/")) return "assets/images";
1031
+ if (FONT_EXTS14.includes(ext)) return "assets/fonts";
1032
+ if (ext === ".css") return "styles";
1033
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1034
+ return "assets/misc";
1035
+ }
1036
+ if (host.includes("d2543nuuc0wvdg.cloudfront.net")) {
1037
+ if (VIDEO_EXTS13.includes(ext)) return "assets/videos";
1038
+ if (IMG_EXTS13.includes(ext)) return "assets/images";
1039
+ if (FONT_EXTS14.includes(ext)) return "assets/fonts";
1040
+ if (ext === ".css") return "styles";
1041
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1042
+ return "assets/misc";
1043
+ }
1044
+ return null;
1045
+ }
1046
+ };
1047
+ }
1048
+ });
1049
+
1050
+ // src/platforms/landing/unbounce.ts
1051
+ var IMG_EXTS14, FONT_EXTS15, VIDEO_EXTS14, unbounce;
1052
+ var init_unbounce = __esm({
1053
+ "src/platforms/landing/unbounce.ts"() {
1054
+ "use strict";
1055
+ IMG_EXTS14 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1056
+ FONT_EXTS15 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1057
+ VIDEO_EXTS14 = [".mp4", ".webm", ".ogg"];
1058
+ unbounce = {
1059
+ name: "unbounce",
1060
+ displayName: "Unbounce",
1061
+ category: "landing",
1062
+ priority: 82,
1063
+ detectByUrl(url) {
1064
+ return /\.(?:unbouncepages|ubpages)\.com/i.test(url);
1065
+ },
1066
+ detectByHtml(html) {
1067
+ return html.includes("lp-pom-root") || html.includes("window.ub.page") || html.includes("builder-assets.unbounce.com") || html.includes("ubembed.com");
1068
+ },
1069
+ // No first-party telemetry/analytics hosts to block (profile analyticsScripts: []).
1070
+ // The asset CDNs (builder-assets / cloudfront / ubembed) must NOT be listed here.
1071
+ stripDomains: [],
1072
+ // The badge selector (#lp-pom-root a[href*="unbounce.com"]) is a descendant
1073
+ // combinator, unsupported by stripSelectors — handled via stripPatterns below.
1074
+ stripSelectors: [],
1075
+ stripPatterns: [
1076
+ // Best-effort removal of the "Made with Unbounce" branded footer link.
1077
+ // Anchored on the exact brand host so it never matches *.unbouncepages.com /
1078
+ // *.ubpages.com hosting links. Markup is approximate (badge markup UNVERIFIED).
1079
+ /<a[^>]*href="https?:\/\/(?:www\.)?unbounce\.com[^"]*"[^>]*>[\s\S]*?<\/a>/gi
1080
+ ],
1081
+ hydrationTimeout: 0,
1082
+ needsHydrationCheck: false,
1083
+ mapAssetDir(host, pathname, ext) {
1084
+ if (host.includes("builder-assets.unbounce.com") || host.includes("assets.unbounce.com")) {
1085
+ if (VIDEO_EXTS14.includes(ext)) return "assets/videos";
1086
+ if (IMG_EXTS14.includes(ext) || pathname.includes("/lp-image")) return "assets/images";
1087
+ if (ext === ".css") return "styles";
1088
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1089
+ if (FONT_EXTS15.includes(ext)) return "assets/fonts";
1090
+ if (ext === ".json") return "data";
1091
+ return "assets/misc";
1092
+ }
1093
+ if (host.includes("ubembed.com")) {
1094
+ if (ext === ".css") return "styles";
1095
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1096
+ if (VIDEO_EXTS14.includes(ext)) return "assets/videos";
1097
+ if (IMG_EXTS14.includes(ext)) return "assets/images";
1098
+ if (FONT_EXTS15.includes(ext)) return "assets/fonts";
1099
+ return "assets/misc";
1100
+ }
1101
+ if (host.includes("cloudfront.net")) {
1102
+ if (VIDEO_EXTS14.includes(ext)) return "assets/videos";
1103
+ if (IMG_EXTS14.includes(ext) || pathname.includes("/images/")) return "assets/images";
1104
+ if (ext === ".css") return "styles";
1105
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1106
+ if (FONT_EXTS15.includes(ext)) return "assets/fonts";
1107
+ return "assets/misc";
1108
+ }
1109
+ return null;
1110
+ }
1111
+ };
1112
+ }
1113
+ });
1114
+
1115
+ // src/platforms/cms/ghost.ts
1116
+ var IMG_EXTS15, FONT_EXTS16, VIDEO_EXTS15, ghost;
1117
+ var init_ghost = __esm({
1118
+ "src/platforms/cms/ghost.ts"() {
1119
+ "use strict";
1120
+ IMG_EXTS15 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1121
+ FONT_EXTS16 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1122
+ VIDEO_EXTS15 = [".mp4", ".webm", ".ogg"];
1123
+ ghost = {
1124
+ name: "ghost",
1125
+ displayName: "Ghost",
1126
+ category: "cms",
1127
+ priority: 78,
1128
+ detectByUrl(url) {
1129
+ return /\.ghost\.io/i.test(url);
1130
+ },
1131
+ detectByHtml(html) {
1132
+ return html.includes("/assets/built/screen.css") || html.includes("/public/cards.min.css") || html.includes("cdn.jsdelivr.net/ghost/") || html.includes("gh-powered-by");
1133
+ },
1134
+ // Matched against <meta name="generator"> content only. Version drifts per site
1135
+ // (observed 6.51 and 6.39), so match the pattern rather than a literal version.
1136
+ detectGenerator: /Ghost [0-9.]+/i,
1137
+ // First-party ghost-stats is the only analytics; it POSTs to the site's own
1138
+ // origin, so there is no third-party telemetry host to block during capture.
1139
+ stripDomains: [],
1140
+ // The "Powered by Ghost" badge's unique class sits on the WRAPPER <div>, whose
1141
+ // only child is the <a>. The post-processor's naive `.class` stripper is
1142
+ // non-greedy to the first close tag, so `.gh-powered-by` would delete only up to
1143
+ // the inner </a> and orphan a dangling </div>. Since selectors run before
1144
+ // patterns, that orphan cannot be cleaned up afterwards. The badge is therefore
1145
+ // removed cleanly (both tags) by the wrapper-div stripPattern below instead.
1146
+ stripSelectors: [],
1147
+ stripPatterns: [
1148
+ // <div class="gh-powered-by"><a href="https://ghost.org/" ...>Powered by Ghost</a></div>
1149
+ /<div[^>]*class="[^"]*gh-powered-by[^"]*"[^>]*>[\s\S]*?<\/div>/g,
1150
+ // Bare <a href="…ghost.org…">Powered by Ghost</a> fallback if the wrapper drops.
1151
+ /<a[^>]*href="[^"]*ghost\.org[^"]*"[^>]*>[\s\S]*?<\/a>/g,
1152
+ // Identifying <meta name="generator" content="Ghost X.Y">.
1153
+ /<meta[^>]*name="generator"[^>]*content="Ghost[^"]*"[^>]*>/gi
1154
+ ],
1155
+ hydrationTimeout: 0,
1156
+ needsHydrationCheck: false,
1157
+ scrollStrategy: "paginated",
1158
+ mapAssetDir(host, pathname, ext) {
1159
+ if (host.includes("storage.ghost.io")) {
1160
+ if (VIDEO_EXTS15.includes(ext)) return "assets/videos";
1161
+ if (pathname.includes("/images/") || IMG_EXTS15.includes(ext)) return "assets/images";
1162
+ if (FONT_EXTS16.includes(ext)) return "assets/fonts";
1163
+ if (ext === ".css") return "styles";
1164
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1165
+ return "assets/misc";
1166
+ }
1167
+ if (host.includes("cdn.jsdelivr.net")) {
1168
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1169
+ if (ext === ".css") return "styles";
1170
+ if (FONT_EXTS16.includes(ext)) return "assets/fonts";
1171
+ if (IMG_EXTS15.includes(ext)) return "assets/images";
1172
+ return "assets/misc";
1173
+ }
1174
+ return null;
1175
+ }
1176
+ };
1177
+ }
1178
+ });
1179
+
1180
+ // src/platforms/cms/notion.ts
1181
+ var IMG_EXTS16, FONT_EXTS17, VIDEO_EXTS16, notion;
1182
+ var init_notion = __esm({
1183
+ "src/platforms/cms/notion.ts"() {
1184
+ "use strict";
1185
+ IMG_EXTS16 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1186
+ FONT_EXTS17 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1187
+ VIDEO_EXTS16 = [".mp4", ".webm", ".ogg"];
1188
+ notion = {
1189
+ name: "notion",
1190
+ displayName: "Notion (Notion Sites)",
1191
+ category: "cms",
1192
+ priority: 85,
1193
+ detectByUrl(url) {
1194
+ return /\.notion\.site/i.test(url);
1195
+ },
1196
+ detectByHtml(html) {
1197
+ return html.includes('data-notion-html="web"') || html.includes('id="notion-app"') || html.includes("__notion_html_async") || html.includes("window.__notion_boot_data");
1198
+ },
1199
+ // Notion's own Splunk telemetry collector — blocked during capture. NEVER add
1200
+ // the asset hosts (www.notion.so / *.amazonaws.com) here; they serve real files.
1201
+ stripDomains: ["http-inputs-notion.splunkcloud.com"],
1202
+ // Pre-hydration loading chrome. captureRenderedDom normally replaces these once
1203
+ // #notion-app hydrates; stripping them is a defensive no-content cleanup in case
1204
+ // hydration is partial.
1205
+ stripSelectors: ["#initial-loading-spinner", "#skeleton", "#skeleton-sidebar"],
1206
+ stripPatterns: [
1207
+ // "Built with Notion" watermark is React-injected post-hydration (ABSENT from
1208
+ // raw HTML — selector/href unverified). Best-effort removal from the captured
1209
+ // rendered DOM: an <a> pointing at notion.so/notion.com that also carries the
1210
+ // badge's signature text. Both conditions keep this from matching real content.
1211
+ /<a[^>]*href="[^"]*notion\.(so|com)[^"]*"[^>]*>[\s\S]*?Built with Notion[\s\S]*?<\/a>/gi
1212
+ ],
1213
+ hydrationTimeout: 8e3,
1214
+ needsHydrationCheck: true,
1215
+ hydrationSelector: "#notion-app",
1216
+ captureRenderedDom: true,
1217
+ mapAssetDir(host, pathname, ext) {
1218
+ if (host.includes("img.notionusercontent.com") || host.includes("app.notion.com")) {
1219
+ if (VIDEO_EXTS16.includes(ext)) return "assets/videos";
1220
+ if (FONT_EXTS17.includes(ext)) return "assets/fonts";
1221
+ if (IMG_EXTS16.includes(ext) || pathname.includes("/image")) return "assets/images";
1222
+ return "assets/images";
1223
+ }
1224
+ if (host.includes("prod-files-secure.s3.us-west-2.amazonaws.com") || host === "s3.us-west-2.amazonaws.com") {
1225
+ if (VIDEO_EXTS16.includes(ext)) return "assets/videos";
1226
+ if (IMG_EXTS16.includes(ext)) return "assets/images";
1227
+ if (FONT_EXTS17.includes(ext)) return "assets/fonts";
1228
+ if (ext === ".css") return "styles";
1229
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1230
+ return "assets/misc";
1231
+ }
1232
+ if (host.includes("www.notion.so") || host.includes("notion.so")) {
1233
+ if (ext === ".css") return "styles";
1234
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1235
+ if (FONT_EXTS17.includes(ext)) return "assets/fonts";
1236
+ if (IMG_EXTS16.includes(ext)) return "assets/images";
1237
+ if (ext === ".json") return "data";
1238
+ return "assets/misc";
1239
+ }
1240
+ return null;
1241
+ }
1242
+ };
1243
+ }
1244
+ });
1245
+
1246
+ // src/platforms/cms/wordpress.ts
1247
+ var IMG_EXTS17, FONT_EXTS18, VIDEO_EXTS17, wordpress;
1248
+ var init_wordpress = __esm({
1249
+ "src/platforms/cms/wordpress.ts"() {
1250
+ "use strict";
1251
+ IMG_EXTS17 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1252
+ FONT_EXTS18 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1253
+ VIDEO_EXTS17 = [".mp4", ".webm", ".ogg"];
1254
+ wordpress = {
1255
+ name: "wordpress",
1256
+ displayName: "WordPress",
1257
+ category: "cms",
1258
+ priority: 40,
1259
+ detectByUrl(url) {
1260
+ return /\.wordpress\.com|\/wp-(content|includes|json)\//i.test(url);
1261
+ },
1262
+ detectByHtml(html) {
1263
+ return html.includes("/wp-includes/") || html.includes("/wp-content/") || html.includes("api.w.org") || html.includes("_wpemojiSettings");
1264
+ },
1265
+ // Matched against the <meta name="generator"> content only. Covers both
1266
+ // "WordPress <version>" (self-hosted) and exact "WordPress.com" (hosted).
1267
+ detectGenerator: /WordPress/i,
1268
+ // Jetpack / WP.com Stats trackers only — never the asset CDN.
1269
+ stripDomains: ["stats.wp.com", "pixel.wp.com"],
1270
+ // Remove the leftover tracker <script> tags after their requests are blocked.
1271
+ stripSelectors: ['script[src*="stats.wp.com"]', 'script[src*="pixel.wp.com"]'],
1272
+ // No badge to strip; drop the generator meta as an attribution cleanup
1273
+ // (shape mirrors webflow.ts). Matches "WordPress 6.9.4" and "WordPress.com".
1274
+ stripPatterns: [
1275
+ /<meta[^>]*name=["']generator["'][^>]*content=["'][^"']*WordPress[^"']*["'][^>]*>/gi
1276
+ ],
1277
+ hydrationTimeout: 0,
1278
+ needsHydrationCheck: false,
1279
+ mapAssetDir(host, pathname, ext) {
1280
+ if (host.includes("i0.wp.com") || host.includes("i1.wp.com") || host.includes("i2.wp.com")) {
1281
+ return "assets/images";
1282
+ }
1283
+ if (host.includes("gravatar.com")) {
1284
+ return "assets/images";
1285
+ }
1286
+ if (host.includes("fonts.wp.com")) {
1287
+ if (ext === ".css") return "styles";
1288
+ if (IMG_EXTS17.includes(ext)) return "assets/images";
1289
+ return "assets/fonts";
1290
+ }
1291
+ if (host.includes("s0.wp.com") || host.includes("s1.wp.com") || host.includes("s2.wp.com") || host.includes("c0.wp.com") || host.includes("s.w.org")) {
1292
+ if (VIDEO_EXTS17.includes(ext)) return "assets/videos";
1293
+ if (IMG_EXTS17.includes(ext) || pathname.includes("/images/")) return "assets/images";
1294
+ if (ext === ".css") return "styles";
1295
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1296
+ if (FONT_EXTS18.includes(ext)) return "assets/fonts";
1297
+ return "assets/misc";
1298
+ }
1299
+ return null;
1300
+ }
1301
+ };
1302
+ }
1303
+ });
1304
+
1305
+ // src/platforms/course/kajabi.ts
1306
+ var IMG_EXTS18, FONT_EXTS19, VIDEO_EXTS18, kajabi;
1307
+ var init_kajabi = __esm({
1308
+ "src/platforms/course/kajabi.ts"() {
1309
+ "use strict";
1310
+ IMG_EXTS18 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1311
+ FONT_EXTS19 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1312
+ VIDEO_EXTS18 = [".mp4", ".webm", ".ogg"];
1313
+ kajabi = {
1314
+ name: "kajabi",
1315
+ displayName: "Kajabi",
1316
+ category: "course",
1317
+ priority: 85,
1318
+ detectByUrl(url) {
1319
+ return /\.mykajabi\.com/i.test(url);
1320
+ },
1321
+ // Four Kajabi-only, tier-independent tells. Badge markers are deliberately NOT
1322
+ // used here (the "Powered by Kajabi" badge is absent on paid plans). None of
1323
+ // these strings appear on Framer / Webflow / Wix output.
1324
+ detectByHtml(html) {
1325
+ return html.includes("var Kajabi = Kajabi") || // inline bootstrap: `var Kajabi = Kajabi || {};`
1326
+ html.includes("Kajabi.currentSiteUser") || // inline theme bootstrap global
1327
+ html.includes("kjb-settings-id=") || // Kajabi theme-editor data attribute
1328
+ html.includes("kajabi-cdn.com");
1329
+ },
1330
+ // Telemetry / analytics hosts only — NEVER the kajabi-cdn.com asset hosts.
1331
+ stripDomains: ["rs-dp.kajabi.com", "cdn.rudderlabs.com"],
1332
+ // The "Powered by Kajabi" badge wrapper carries a unique class — remove it via
1333
+ // selector (first-match). stripPatterns below covers repeats / rendered markup.
1334
+ stripSelectors: [".footer__powered-by"],
1335
+ stripPatterns: [
1336
+ // Whole badge wrapper: <div class="footer__powered-by"> … </div>
1337
+ // (real markup has indentation/newlines between the div and the anchor).
1338
+ /<div[^>]*class="[^"]*footer__powered-by[^"]*"[^>]*>[\s\S]*?<\/div>/g,
1339
+ // Belt-and-suspenders: the branded referral anchor itself.
1340
+ /<a[^>]*href="[^"]*app\.kajabi\.com\/r\/[^"]*powered_by=true[^"]*"[^>]*>[\s\S]*?<\/a>/g
1341
+ ],
1342
+ // ssr-static: the raw SSR HTML is complete, so no hydration wait is needed
1343
+ // (profile needsHydrationCheck=false, hydrationTimeoutMs=0). antiBot is
1344
+ // Cloudflare but the pages fetched cleanly in research; per the authoring
1345
+ // guide we keep a small capture buffer instead of the literal 0 ms.
1346
+ hydrationTimeout: 2e3,
1347
+ needsHydrationCheck: false,
1348
+ // Both cdnDomains live under *.kajabi-cdn.com
1349
+ // - kajabi-storefronts-production.kajabi-cdn.com
1350
+ // - kajabi-app-assets.kajabi-cdn.com
1351
+ // so a single host match covers every declared CDN host; classify by ext/path.
1352
+ mapAssetDir(host, pathname, ext) {
1353
+ if (host.includes("kajabi-cdn.com")) {
1354
+ if (VIDEO_EXTS18.includes(ext)) return "assets/videos";
1355
+ if (pathname.includes("/images/") || IMG_EXTS18.includes(ext)) return "assets/images";
1356
+ if (ext === ".css") return "styles";
1357
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1358
+ if (FONT_EXTS19.includes(ext)) return "assets/fonts";
1359
+ if (ext === ".json") return "data";
1360
+ return "assets/misc";
1361
+ }
1362
+ return null;
1363
+ }
1364
+ };
1365
+ }
1366
+ });
1367
+
1368
+ // src/platforms/course/podia.ts
1369
+ var IMG_EXTS19, FONT_EXTS20, VIDEO_EXTS19, podia;
1370
+ var init_podia = __esm({
1371
+ "src/platforms/course/podia.ts"() {
1372
+ "use strict";
1373
+ IMG_EXTS19 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1374
+ FONT_EXTS20 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1375
+ VIDEO_EXTS19 = [".mp4", ".webm", ".ogg"];
1376
+ podia = {
1377
+ name: "podia",
1378
+ displayName: "Podia",
1379
+ category: "course",
1380
+ priority: 80,
1381
+ detectByUrl(url) {
1382
+ return /\.podia\.com/i.test(url);
1383
+ },
1384
+ detectByHtml(html) {
1385
+ return (
1386
+ // Constant Turnstile site-key present on every Podia storefront (incl. custom domains).
1387
+ html.includes("0x4AAAAAAAJ5kwYW6AH1ybLx") || // Shared framework asset host (storefront-<hash>.css/.js, user-site-<hash>.js).
1388
+ html.includes("cdn.podia.com") || // React section-adapter islands unique to Podia's creator UI.
1389
+ html.includes('data-react-component="creator_ui/section_adapters/') || // Stimulus controller wiring the editable page sections.
1390
+ html.includes('data-controller="editor--page-section"')
1391
+ );
1392
+ },
1393
+ // Telemetry only — NEVER the asset CDN. Podia's default analytics is Google Tag Manager.
1394
+ stripDomains: ["www.googletagmanager.com"],
1395
+ stripSelectors: ['script[src*="googletagmanager.com"]'],
1396
+ stripPatterns: [
1397
+ // "Powered by Podia" footer badge — React-rendered anchor in the captured DOM.
1398
+ // Its href is https://www.podia.com?utm_medium=poweredby&utm_source=footer
1399
+ // (the "&" may serialize as "&amp;"). Anchor on the stable brand host + the
1400
+ // badge-unique utm_medium=poweredby token; tolerate single/double quotes and any
1401
+ // chars between host and token (e.g. a "/" before the query string).
1402
+ /<a[^>]*href=['"][^'"]*podia\.com[^'"]*utm_medium=poweredby[^'"]*['"][^>]*>[\s\S]*?<\/a>/gi
1403
+ ],
1404
+ hydrationTimeout: 4e3,
1405
+ needsHydrationCheck: true,
1406
+ // Content lives in these React islands rather than a #main root.
1407
+ hydrationSelector: ".react-page-section",
1408
+ captureRenderedDom: true,
1409
+ mapAssetDir(host, pathname, ext) {
1410
+ if (host.includes("cdn.podia.com")) {
1411
+ if (VIDEO_EXTS19.includes(ext)) return "assets/videos";
1412
+ if (ext === ".css") return "styles";
1413
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1414
+ if (FONT_EXTS20.includes(ext)) return "assets/fonts";
1415
+ if (IMG_EXTS19.includes(ext) || pathname.includes("/images/")) return "assets/images";
1416
+ return "assets/misc";
1417
+ }
1418
+ if (host.includes(".podia.com") && pathname.includes("/content-assets/")) {
1419
+ if (VIDEO_EXTS19.includes(ext)) return "assets/videos";
1420
+ if (IMG_EXTS19.includes(ext)) return "assets/images";
1421
+ if (FONT_EXTS20.includes(ext)) return "assets/fonts";
1422
+ return "assets/misc";
1423
+ }
1424
+ return null;
1425
+ }
1426
+ };
1427
+ }
1428
+ });
1429
+
1430
+ // src/platforms/course/teachable.ts
1431
+ var IMG_EXTS20, FONT_EXTS21, VIDEO_EXTS20, teachable;
1432
+ var init_teachable = __esm({
1433
+ "src/platforms/course/teachable.ts"() {
1434
+ "use strict";
1435
+ IMG_EXTS20 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1436
+ FONT_EXTS21 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1437
+ VIDEO_EXTS20 = [".mp4", ".webm", ".ogg"];
1438
+ teachable = {
1439
+ name: "teachable",
1440
+ displayName: "Teachable",
1441
+ category: "course",
1442
+ priority: 82,
1443
+ detectByUrl(url) {
1444
+ return /\.teachable\.com/i.test(url);
1445
+ },
1446
+ detectByHtml(html) {
1447
+ return html.includes("teachablecdn.com") || html.includes("eventable.internal.teachable.com") || html.includes("Teach Online with") || html.includes("src=school_footer");
1448
+ },
1449
+ stripDomains: [
1450
+ "cdn.heapanalytics.com",
1451
+ "bam.nr-data.net",
1452
+ "www.recaptcha.net",
1453
+ "fast.wistia.com"
1454
+ ],
1455
+ stripSelectors: [
1456
+ // Analytics <script src> tags left in the HTML (hosts also blocked at capture
1457
+ // via stripDomains). The powered-by badge is handled by stripPatterns instead
1458
+ // — a `.class` selector can't match its single-quoted, multi-child anchor.
1459
+ 'script[src*="heapanalytics.com"]',
1460
+ 'script[src*="nr-data.net"]'
1461
+ ],
1462
+ stripPatterns: [
1463
+ // Footer "Teach Online with" badge — anchored on its unique powered-by class.
1464
+ /<a[^>]*class=['"][^'"]*powered-by[^'"]*['"][^>]*>[\s\S]*?<\/a>/gi,
1465
+ // Fallback: anchored on the teachable.com ?src=school_footer badge href.
1466
+ /<a[^>]*href=['"][^'"]*teachable\.com[^'"]*src=school_footer[^'"]*['"][^>]*>[\s\S]*?<\/a>/gi
1467
+ ],
1468
+ hydrationTimeout: 0,
1469
+ needsHydrationCheck: false,
1470
+ mapAssetDir(host, pathname, ext) {
1471
+ if (host.includes("teachablecdn.com")) {
1472
+ if (VIDEO_EXTS20.includes(ext)) return "assets/videos";
1473
+ if (IMG_EXTS20.includes(ext)) return "assets/images";
1474
+ if (FONT_EXTS21.includes(ext)) return "assets/fonts";
1475
+ if (ext === ".css") return "styles";
1476
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1477
+ if (ext === ".json") return "data";
1478
+ if (pathname.includes("/images/")) return "assets/images";
1479
+ if (pathname.includes("/packs/")) return "scripts/vendor";
1480
+ return "assets/misc";
1481
+ }
1482
+ return null;
1483
+ }
1484
+ };
1485
+ }
1486
+ });
1487
+
1488
+ // src/platforms/course/thinkific.ts
1489
+ var IMG_EXTS21, FONT_EXTS22, VIDEO_EXTS21, thinkific;
1490
+ var init_thinkific = __esm({
1491
+ "src/platforms/course/thinkific.ts"() {
1492
+ "use strict";
1493
+ IMG_EXTS21 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1494
+ FONT_EXTS22 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1495
+ VIDEO_EXTS21 = [".mp4", ".webm", ".ogg"];
1496
+ thinkific = {
1497
+ name: "thinkific",
1498
+ displayName: "Thinkific",
1499
+ category: "course",
1500
+ priority: 82,
1501
+ detectByUrl(url) {
1502
+ return /\.thinkific\.com/i.test(url);
1503
+ },
1504
+ // Four Thinkific-only tells that hold on both subdomain and custom-domain
1505
+ // sites. The "Powered by Thinkific" badge is deliberately NOT used here (it is
1506
+ // removable on paid plans). None of these strings appear on Framer / Webflow /
1507
+ // Wix output.
1508
+ detectByHtml(html) {
1509
+ return html.includes("window.Thinkific") || // inline bootstrap: `window.Thinkific = window.Thinkific || {}`
1510
+ html.includes("assets.thinkific.com") || // theme CSS/JS bundle host
1511
+ html.includes("cdn.thinkific.com") || // toga-css icon-font host
1512
+ html.includes("thnc.current_user-initialized");
1513
+ },
1514
+ // Telemetry / third-party embed hosts only (profile.analyticsScripts) — NEVER
1515
+ // the *.thinkific.com asset CDN. These are matched against the request HOSTNAME
1516
+ // only (capture.ts: `host.includes(d)`), so each entry MUST be a bare hostname —
1517
+ // a path such as "www.google.com/recaptcha" can never match and would be dead.
1518
+ // `fast.wistia.net` = Wistia video player; `www.google.com` = sign-in reCAPTCHA
1519
+ // (api.js served from www.google.com/recaptcha; the whole host is safe to block
1520
+ // on a static mirror — no legit Thinkific asset is served from it).
1521
+ stripDomains: ["fast.wistia.net", "www.google.com"],
1522
+ // The badge is removed via stripPatterns (below), NOT a `.class` stripSelector.
1523
+ // The class-based stripper (output.ts) is a non-greedy `…>[\s\S]*?<\/[^>]*>` that
1524
+ // stops at the FIRST closing tag, so on the nested badge markup
1525
+ // (<div class="footer__white-label"><a><svg><title>…</title>…</svg></a></div>) it
1526
+ // would cut at </title>, leaving orphan </svg></a></div> AND deleting the opening
1527
+ // <div>/<a> tags that the stripPatterns regexes anchor on (stripSelectors run
1528
+ // before stripPatterns). So no stripSelectors are used here.
1529
+ stripSelectors: [],
1530
+ stripPatterns: [
1531
+ // Whole badge wrapper: <div class="footer__white-label"> … </div>
1532
+ // (inner anchor + inline SVG titled "Teach online with Thinkific").
1533
+ /<div[^>]*class="[^"]*footer__white-label[^"]*"[^>]*>[\s\S]*?<\/div>/g,
1534
+ // Belt-and-suspenders: the branded powered-by referral anchor itself.
1535
+ /<a[^>]*href="[^"]*thinkific\.com[^"]*utm_medium=powered-by[^"]*"[^>]*>[\s\S]*?<\/a>/g
1536
+ ],
1537
+ // ssr-static: the raw SSR HTML is complete, so no hydration wait is needed
1538
+ // (profile needsHydrationCheck=false, hydrationTimeoutMs=0). antiBot is
1539
+ // Cloudflare (rocket-loader observed) but pages fetched cleanly in research;
1540
+ // per the authoring guide we keep a small capture buffer instead of literal 0.
1541
+ hydrationTimeout: 2e3,
1542
+ needsHydrationCheck: false,
1543
+ // cdnDomains: assets.thinkific.com, cdn.thinkific.com, cdn-themes.thinkific.com,
1544
+ // import.cdn.thinkific.com, files.cdn.thinkific.com (all *.thinkific.com), plus
1545
+ // the bare s3.amazonaws.com upload host. Theme bundles/fonts live on the
1546
+ // thinkific.com hosts; uploaded media on import.cdn / files.cdn (front S3).
1547
+ mapAssetDir(host, pathname, ext) {
1548
+ if (host.includes("thinkific.com")) {
1549
+ if (VIDEO_EXTS21.includes(ext)) return "assets/videos";
1550
+ if (pathname.includes("/images/") || IMG_EXTS21.includes(ext)) return "assets/images";
1551
+ if (ext === ".css") return "styles";
1552
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1553
+ if (FONT_EXTS22.includes(ext)) return "assets/fonts";
1554
+ if (ext === ".json") return "data";
1555
+ return "assets/misc";
1556
+ }
1557
+ if (host.includes("s3.amazonaws.com")) {
1558
+ if (VIDEO_EXTS21.includes(ext)) return "assets/videos";
1559
+ if (IMG_EXTS21.includes(ext)) return "assets/images";
1560
+ if (FONT_EXTS22.includes(ext)) return "assets/fonts";
1561
+ if (ext === ".css") return "styles";
1562
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1563
+ return "assets/misc";
1564
+ }
1565
+ return null;
1566
+ }
1567
+ };
1568
+ }
1569
+ });
1570
+
1571
+ // src/platforms/ecommerce/gumroad.ts
1572
+ var IMG_EXTS22, FONT_EXTS23, VIDEO_EXTS22, gumroad;
1573
+ var init_gumroad = __esm({
1574
+ "src/platforms/ecommerce/gumroad.ts"() {
1575
+ "use strict";
1576
+ IMG_EXTS22 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1577
+ FONT_EXTS23 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1578
+ VIDEO_EXTS22 = [".mp4", ".webm", ".ogg"];
1579
+ gumroad = {
1580
+ name: "gumroad",
1581
+ displayName: "Gumroad",
1582
+ category: "ecommerce",
1583
+ priority: 80,
1584
+ detectByUrl(url) {
1585
+ return /\.gumroad\.com|\bgum\.co\b/i.test(url);
1586
+ },
1587
+ detectByHtml(html) {
1588
+ return html.includes("cable.gumroad.com") || html.includes("assets.gumroad.com") || html.includes("ns/fb/gumroad#") || html.includes("gumroad:product");
1589
+ },
1590
+ // Telemetry / analytics hosts only — never the asset CDN.
1591
+ // gumroad-analytics.com is Gumroad's first-party analytics proxy
1592
+ // (domain_settings.third_party_analytics_domain); GA / FB Pixel / TikTok Pixel
1593
+ // are injected client-side only when the creator enables them.
1594
+ stripDomains: [
1595
+ "gumroad-analytics.com",
1596
+ "www.googletagmanager.com",
1597
+ "connect.facebook.net",
1598
+ "analytics.tiktok.com"
1599
+ ],
1600
+ // Defensively drop any surviving analytics <script> tags from the rendered DOM
1601
+ // (stripDomains already blocks them at the network layer during capture).
1602
+ stripSelectors: [
1603
+ 'script[src*="gumroad-analytics.com"]',
1604
+ 'script[src*="googletagmanager.com"]',
1605
+ 'script[src*="connect.facebook.net"]',
1606
+ 'script[src*="analytics.tiktok.com"]'
1607
+ ],
1608
+ // No server-rendered badge to remove (badgePresent = false, confirmed).
1609
+ stripPatterns: [],
1610
+ hydrationTimeout: 8e3,
1611
+ needsHydrationCheck: true,
1612
+ hydrationSelector: "#app",
1613
+ captureRenderedDom: true,
1614
+ mapAssetDir(host, pathname, ext) {
1615
+ if (host.includes("assets.gumroad.com") || host.includes("public-files.gumroad.com") || host.includes("static-2.gumroad.com")) {
1616
+ if (VIDEO_EXTS22.includes(ext)) return "assets/videos";
1617
+ if (pathname.includes("/fonts/") || FONT_EXTS23.includes(ext)) return "assets/fonts";
1618
+ if (pathname.includes("/stylesheets/") || ext === ".css") return "styles";
1619
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1620
+ if (pathname.includes("/images/") || IMG_EXTS22.includes(ext)) return "assets/images";
1621
+ if (ext === ".json") return "data";
1622
+ return "assets/misc";
1623
+ }
1624
+ return null;
1625
+ }
1626
+ };
1627
+ }
1628
+ });
1629
+
1630
+ // src/platforms/ecommerce/shopify.ts
1631
+ var IMG_EXTS23, FONT_EXTS24, VIDEO_EXTS23, shopify;
1632
+ var init_shopify = __esm({
1633
+ "src/platforms/ecommerce/shopify.ts"() {
1634
+ "use strict";
1635
+ IMG_EXTS23 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1636
+ FONT_EXTS24 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1637
+ VIDEO_EXTS23 = [".mp4", ".webm", ".ogg"];
1638
+ shopify = {
1639
+ name: "shopify",
1640
+ displayName: "Shopify",
1641
+ category: "ecommerce",
1642
+ priority: 80,
1643
+ detectByUrl(url) {
1644
+ return /\.myshopify\.com/i.test(url);
1645
+ },
1646
+ detectByHtml(html) {
1647
+ return html.includes("cdn.shopify.com") || html.includes("/cdn/shop/") || html.includes("Shopify.shop") || html.includes("ShopifyAnalytics");
1648
+ },
1649
+ // Telemetry only — never the asset CDN.
1650
+ stripDomains: ["monorail-edge.shopifysvc.com"],
1651
+ // The "Powered by Shopify" badge is a NESTED <span class="shopify-powered-by">
1652
+ // <a …>…</a></span>. The post-processor's `.class` strip only deletes up to the
1653
+ // FIRST closing tag (the inner </a>), which would leave an orphan </span>; worse,
1654
+ // stripSelectors run BEFORE stripPatterns, so it would consume the opening <span>
1655
+ // and stop the clean span regex below from matching. So the badge is removed
1656
+ // purely via stripPatterns — no `.class` selector.
1657
+ stripSelectors: [],
1658
+ stripPatterns: [
1659
+ // Whole badge container: <span class="shopify-powered-by" data-shopify="powered-by"><a href="…shopify.com?utm_campaign=poweredby…">Powered by Shopify</a></span>
1660
+ /<span[^>]*class="[^"]*shopify-powered-by[^"]*"[^>]*>[\s\S]*?<\/span>/gi,
1661
+ // Fallback: the brand anchor itself (href carries ?utm_campaign=poweredby), in case the <span> wrapper is absent or customised.
1662
+ /<a[^>]*href="[^"]*shopify\.com\?utm_campaign=poweredby[^"]*"[^>]*>[\s\S]*?<\/a>/gi
1663
+ ],
1664
+ hydrationTimeout: 3500,
1665
+ needsHydrationCheck: true,
1666
+ mapAssetDir(host, pathname, ext) {
1667
+ const isShopifyAssetPath = pathname.includes("/cdn/shop/");
1668
+ if (host.includes("cdn.shopify.com") || isShopifyAssetPath) {
1669
+ if (VIDEO_EXTS23.includes(ext)) return "assets/videos";
1670
+ if (IMG_EXTS23.includes(ext)) return "assets/images";
1671
+ if (ext === ".css") return "styles";
1672
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1673
+ if (FONT_EXTS24.includes(ext)) return "assets/fonts";
1674
+ if (ext === ".json") return "data";
1675
+ return "assets/misc";
1676
+ }
1677
+ if (host.includes("fonts.shopifycdn.com")) {
1678
+ if (FONT_EXTS24.includes(ext)) return "assets/fonts";
1679
+ return "assets/misc";
1680
+ }
1681
+ if (host.includes("extensions.shopifycdn.com")) {
1682
+ if (VIDEO_EXTS23.includes(ext)) return "assets/videos";
1683
+ if (IMG_EXTS23.includes(ext)) return "assets/images";
1684
+ if (ext === ".css") return "styles";
1685
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1686
+ if (FONT_EXTS24.includes(ext)) return "assets/fonts";
1687
+ return "assets/misc";
1688
+ }
1689
+ return null;
1690
+ }
1691
+ };
1692
+ }
1693
+ });
1694
+
1695
+ // src/platforms/ai/gamma.ts
1696
+ var IMG_EXTS24, FONT_EXTS25, VIDEO_EXTS24, gamma;
1697
+ var init_gamma = __esm({
1698
+ "src/platforms/ai/gamma.ts"() {
1699
+ "use strict";
1700
+ IMG_EXTS24 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1701
+ FONT_EXTS25 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1702
+ VIDEO_EXTS24 = [".mp4", ".webm", ".ogg"];
1703
+ gamma = {
1704
+ name: "gamma",
1705
+ displayName: "Gamma",
1706
+ category: "ai",
1707
+ priority: 85,
1708
+ detectByUrl(url) {
1709
+ return /\.gamma\.site/i.test(url);
1710
+ },
1711
+ detectByHtml(html) {
1712
+ return html.includes("assets.gammahosted.com") || html.includes("imgproxy.gamma.app") || html.includes("cdn.gamma.app") || html.includes("gamma-moveable-wrapper");
1713
+ },
1714
+ // No telemetry/analytics hosts observed in the profile.
1715
+ stripDomains: [],
1716
+ // No site badge and no editor chrome to strip on published Gamma sites.
1717
+ stripSelectors: [],
1718
+ stripPatterns: [],
1719
+ hydrationTimeout: 8e3,
1720
+ needsHydrationCheck: true,
1721
+ hydrationSelector: "#__next",
1722
+ scrollStrategy: "standard",
1723
+ mapAssetDir(host, pathname, ext) {
1724
+ if (host.includes("assets.gammahosted.com")) {
1725
+ if (ext === ".css") return "styles";
1726
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1727
+ if (ext === ".json") return "data";
1728
+ if (FONT_EXTS25.includes(ext)) return "assets/fonts";
1729
+ if (VIDEO_EXTS24.includes(ext)) return "assets/videos";
1730
+ if (IMG_EXTS24.includes(ext)) return "assets/images";
1731
+ return "assets/misc";
1732
+ }
1733
+ if (host.includes("imgproxy.gamma.app")) {
1734
+ return "assets/images";
1735
+ }
1736
+ if (host.includes("assets.api.gamma.app")) {
1737
+ return "assets/images";
1738
+ }
1739
+ if (host.includes("cdn.gamma.app")) {
1740
+ if (VIDEO_EXTS24.includes(ext)) return "assets/videos";
1741
+ if (FONT_EXTS25.includes(ext)) return "assets/fonts";
1742
+ if (ext === ".css") return "styles";
1743
+ if (ext === ".js" || ext === ".mjs") return "scripts/vendor";
1744
+ if (IMG_EXTS24.includes(ext)) return "assets/images";
1745
+ return "assets/images";
1746
+ }
1747
+ return null;
1748
+ }
1749
+ };
1750
+ }
1751
+ });
1752
+
1753
+ // src/platforms/registry.ts
1754
+ function sortedByPriority() {
1755
+ return [...PLATFORM_REGISTRY].sort((a, b) => b.priority - a.priority);
1756
+ }
1757
+ function platformsByCategory() {
1758
+ const out = {};
1759
+ for (const cat of CATEGORY_ORDER) out[cat] = [];
1760
+ for (const handler of PLATFORM_REGISTRY) {
1761
+ if (!out[handler.category]) out[handler.category] = [];
1762
+ out[handler.category].push(handler);
1763
+ }
1764
+ for (const cat of CATEGORY_ORDER) {
1765
+ out[cat].sort((a, b) => b.priority - a.priority);
1766
+ }
1767
+ return out;
1768
+ }
1769
+ var PLATFORM_REGISTRY, CATEGORY_LABELS, CATEGORY_ORDER;
1770
+ var init_registry = __esm({
1771
+ "src/platforms/registry.ts"() {
1772
+ "use strict";
1773
+ init_framer();
1774
+ init_webflow();
1775
+ init_wix();
1776
+ init_bubble();
1777
+ init_carrd();
1778
+ init_duda();
1779
+ init_squarespace();
1780
+ init_strikingly();
1781
+ init_tilda();
1782
+ init_weebly();
1783
+ init_clickfunnels();
1784
+ init_elementor();
1785
+ init_instapage();
1786
+ init_systemeio();
1787
+ init_unbounce();
1788
+ init_ghost();
1789
+ init_notion();
1790
+ init_wordpress();
1791
+ init_kajabi();
1792
+ init_podia();
1793
+ init_teachable();
1794
+ init_thinkific();
1795
+ init_gumroad();
1796
+ init_shopify();
1797
+ init_gamma();
1798
+ PLATFORM_REGISTRY = [
1799
+ // frozen trio first (highest priority, unchanged behaviour)
1800
+ framer,
1801
+ webflow,
1802
+ wix,
1803
+ // builders
1804
+ bubble,
1805
+ carrd,
1806
+ duda,
1807
+ squarespace,
1808
+ strikingly,
1809
+ tilda,
1810
+ weebly,
1811
+ // landing
1812
+ clickfunnels,
1813
+ elementor,
1814
+ instapage,
1815
+ systemeio,
1816
+ unbounce,
1817
+ // cms
1818
+ ghost,
1819
+ notion,
1820
+ wordpress,
1821
+ // course
1822
+ kajabi,
1823
+ podia,
1824
+ teachable,
1825
+ thinkific,
1826
+ // ecommerce
1827
+ gumroad,
1828
+ shopify,
1829
+ // ai
1830
+ gamma
1831
+ ];
1832
+ CATEGORY_LABELS = {
1833
+ builder: "Website Builders",
1834
+ landing: "Landing Pages & Funnels",
1835
+ cms: "CMS & Blogs",
1836
+ course: "Courses & Membership",
1837
+ ecommerce: "E-commerce",
1838
+ ai: "AI & Presentations"
1839
+ };
1840
+ CATEGORY_ORDER = [
1841
+ "builder",
1842
+ "landing",
1843
+ "cms",
1844
+ "course",
1845
+ "ecommerce",
1846
+ "ai"
1847
+ ];
1848
+ }
1849
+ });
1850
+
1851
+ // src/platforms/detect.ts
1852
+ function readGenerator(html) {
1853
+ const m = html.match(/<meta[^>]+name=["']generator["'][^>]*content=["']([^"']*)["']/i) || html.match(/<meta[^>]+content=["']([^"']*)["'][^>]+name=["']generator["']/i);
1854
+ return m ? m[1] : "";
1855
+ }
1856
+ function detectByUrl(url) {
1857
+ for (const platform of sortedByPriority()) {
1858
+ try {
1859
+ if (platform.detectByUrl(url)) return platform;
1860
+ } catch {
1861
+ }
1862
+ }
1863
+ return null;
1864
+ }
1865
+ function detectByGenerator(html) {
1866
+ const gen = readGenerator(html);
1867
+ if (!gen) return null;
1868
+ for (const platform of sortedByPriority()) {
1869
+ if (platform.detectGenerator && platform.detectGenerator.test(gen)) return platform;
1870
+ }
1871
+ return null;
1872
+ }
1873
+ function detectByHtml(html) {
1874
+ const byGen = detectByGenerator(html);
1875
+ if (byGen) return byGen;
1876
+ for (const platform of sortedByPriority()) {
1877
+ try {
1878
+ if (platform.detectByHtml(html)) return platform;
1879
+ } catch {
1880
+ }
1881
+ }
1882
+ return null;
1883
+ }
1884
+ function detectPlatform(url, html) {
1885
+ const byUrl = detectByUrl(url);
1886
+ if (byUrl) return byUrl;
1887
+ if (html) {
1888
+ const byHtml = detectByHtml(html);
1889
+ if (byHtml) return byHtml;
1890
+ }
1891
+ return framer;
1892
+ }
1893
+ async function detectByDom(page) {
1894
+ try {
1895
+ const signal = await page.evaluate(() => {
1896
+ const html = document.documentElement.outerHTML || "";
1897
+ if (/wix-viewer-model|_wix_|wix-ecom/i.test(html)) return "wix";
1898
+ if (/data-wf-|w-webflow-badge|webflow\.js/i.test(html)) return "webflow";
1899
+ if (/framerusercontent|framercanvas|framerstatic/i.test(html)) return "framer";
1900
+ const srcs = [];
1901
+ const scripts = document.querySelectorAll("script[src]");
1902
+ scripts.forEach((s) => {
1903
+ if (s.src) srcs.push(s.src);
1904
+ });
1905
+ const allSrcs = srcs.join(" ");
1906
+ if (/wixstatic\.com|parastorage\.com/.test(allSrcs)) return "wix";
1907
+ if (/website-files\.com|webflow\.com/.test(allSrcs)) return "webflow";
1908
+ if (/framerusercontent\.com|framerstatic\.com|framer\.app/.test(allSrcs)) return "framer";
1909
+ return "";
1910
+ });
1911
+ if (signal) return getPlatformByName(signal);
1912
+ } catch {
1913
+ }
1914
+ for (const platform of sortedByPriority()) {
1915
+ if (!platform.detectByDom) continue;
1916
+ try {
1917
+ if (await platform.detectByDom(page)) return platform;
1918
+ } catch {
1919
+ }
1920
+ }
1921
+ return null;
1922
+ }
1923
+ function getPlatformByName(name) {
1924
+ const found = PLATFORM_REGISTRY.find((platform) => platform.name === name);
1925
+ return found || framer;
1926
+ }
1927
+ var init_detect = __esm({
1928
+ "src/platforms/detect.ts"() {
1929
+ "use strict";
1930
+ init_registry();
1931
+ init_framer();
1932
+ }
1933
+ });
1934
+
1935
+ // src/platforms/index.ts
1936
+ var platforms_exports = {};
1937
+ __export(platforms_exports, {
1938
+ CATEGORY_LABELS: () => CATEGORY_LABELS,
1939
+ CATEGORY_ORDER: () => CATEGORY_ORDER,
1940
+ PLATFORM_REGISTRY: () => PLATFORM_REGISTRY,
1941
+ detectByDom: () => detectByDom,
1942
+ detectByGenerator: () => detectByGenerator,
1943
+ detectByHtml: () => detectByHtml,
1944
+ detectByUrl: () => detectByUrl,
1945
+ detectPlatform: () => detectPlatform,
1946
+ framer: () => framer,
1947
+ getPlatformByName: () => getPlatformByName,
1948
+ platformsByCategory: () => platformsByCategory,
1949
+ readGenerator: () => readGenerator,
1950
+ sortedByPriority: () => sortedByPriority,
1951
+ webflow: () => webflow,
1952
+ wix: () => wix
1953
+ });
1954
+ var init_platforms = __esm({
1955
+ "src/platforms/index.ts"() {
1956
+ "use strict";
1957
+ init_framer();
1958
+ init_webflow();
1959
+ init_wix();
1960
+ init_registry();
1961
+ init_detect();
1962
+ }
1963
+ });
1964
+
207
1965
  // src/cli/cooking.ts
208
1966
  import chalk2 from "chalk";
209
1967
  async function showLoadingIntro(version) {
@@ -1067,318 +2825,140 @@ function output(line) {
1067
2825
  var _cooking, T, LOG_PALETTE, _li, log, INFO_PALETTE, _ii, info, warn, success;
1068
2826
  var init_logger = __esm({
1069
2827
  "src/logger/index.ts"() {
1070
- "use strict";
1071
- init_theme();
1072
- _cooking = null;
1073
- T = () => (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
1074
- LOG_PALETTE = [
1075
- (s) => chalk4.hex(THEME.primary)(s),
1076
- (s) => chalk4.hex(THEME.primarySoft)(s),
1077
- (s) => chalk4.hex(THEME.secondary)(s),
1078
- (s) => chalk4.hex(THEME.accent)(s),
1079
- (s) => chalk4.hex(THEME.info)(s),
1080
- (s) => chalk4.hex(THEME.text)(s)
1081
- ];
1082
- _li = 0;
1083
- log = (m) => {
1084
- _li++;
1085
- const c = LOG_PALETTE[_li % LOG_PALETTE.length];
1086
- output(
1087
- `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.primary)("[log]")} ${c(trunc(m, 120))}`
1088
- );
1089
- };
1090
- INFO_PALETTE = [
1091
- (s) => chalk4.hex(THEME.info)(s),
1092
- (s) => chalk4.hex(THEME.secondary)(s),
1093
- (s) => chalk4.hex(THEME.primarySoft)(s)
1094
- ];
1095
- _ii = 0;
1096
- info = (m) => {
1097
- _ii++;
1098
- const c = INFO_PALETTE[_ii % INFO_PALETTE.length];
1099
- output(
1100
- `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.info).bold("[info]")} ${c(trunc(m, 120))}`
1101
- );
1102
- };
1103
- warn = (m) => {
1104
- const colors = [
1105
- (s) => chalk4.hex(THEME.warning)(s),
1106
- (s) => chalk4.hex(THEME.primary)(s)
1107
- ];
1108
- const c = colors[Math.floor(Math.random() * colors.length)];
1109
- const line = `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.warning).bold("[warn]")} ${c(trunc(m, 120))}`;
1110
- if (_cooking) _cooking.log(line);
1111
- else console.warn(line);
1112
- };
1113
- success = (m) => {
1114
- const colors = [
1115
- (s) => chalk4.hex(THEME.success)(s),
1116
- (s) => chalk4.hex(THEME.info)(s)
1117
- ];
1118
- const c = colors[Math.floor(Math.random() * colors.length)];
1119
- output(
1120
- `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.success)("[ok]")} ${c(trunc(m, 120))}`
1121
- );
1122
- };
1123
- }
1124
- });
1125
-
1126
- // src/platforms/framer.ts
1127
- var FONT_EXTS, framer;
1128
- var init_framer = __esm({
1129
- "src/platforms/framer.ts"() {
1130
- "use strict";
1131
- FONT_EXTS = [".woff2", ".woff", ".ttf", ".otf"];
1132
- framer = {
1133
- name: "framer",
1134
- displayName: "Framer",
1135
- detectByUrl(url) {
1136
- return /\.framer\.(app|website)|framercanvas\.com/.test(url);
1137
- },
1138
- detectByHtml(html) {
1139
- return html.includes('id="main"') && (html.includes("framerstatic.com") || html.includes("framerusercontent.com"));
1140
- },
1141
- stripDomains: ["events.framer.com", "api.framer.com", "collect.frameranalytics.com"],
1142
- stripSelectors: [
1143
- 'script[src*="events.framer.com"]',
1144
- "#__framer-badge-container",
1145
- "#__framer-badge",
1146
- 'link[href*="canvas-sandbox"]',
1147
- 'script[src*="framer.com/bootstrap"]'
1148
- ],
1149
- stripPatterns: [
1150
- /<div id="__framer-badge-container"[^>]*><\/div>/g,
1151
- /<script>try\{if\(localStorage\.get\("__framer_force_showing_editorbar_since"\)\)[^<]*<\/script>/g
1152
- ],
1153
- hydrationTimeout: 1e4,
1154
- needsHydrationCheck: true,
1155
- mapAssetDir(host, pathname, ext) {
1156
- if (host.includes("framerusercontent.com")) {
1157
- if (pathname.startsWith("/images/")) return "assets/images";
1158
- if (pathname.startsWith("/assets/")) {
1159
- return FONT_EXTS.includes(ext) ? "assets/fonts" : "assets/misc";
1160
- }
1161
- if (pathname.startsWith("/sites/")) {
1162
- if (ext === ".mjs" || ext === ".js") return "scripts/vendor";
1163
- if (ext === ".json") return "data";
1164
- if (ext === ".css") return "styles";
1165
- if (ext === ".framercms") return "data";
1166
- return "assets/misc";
1167
- }
1168
- if (pathname.startsWith("/modules/")) {
1169
- return ext === ".framercms" ? "data" : "scripts/modules";
1170
- }
1171
- return "assets/misc";
1172
- }
1173
- if (host.includes("app.framerstatic.com")) {
1174
- if (ext === ".css") return "styles";
1175
- if (ext === ".mjs" || ext === ".js") return "scripts/vendor";
1176
- if (ext === ".woff2" || ext === ".woff") return "assets/fonts";
1177
- if (ext === ".png" || ext === ".svg") return "assets/images";
1178
- return "assets/misc";
1179
- }
1180
- if (host.includes("framercanvas.com") || host.includes("framer.com")) {
1181
- if (ext === ".mjs" || ext === ".js") return "scripts/vendor";
1182
- if (ext === ".css") return "styles";
1183
- return "assets/misc";
1184
- }
1185
- return null;
1186
- }
1187
- };
1188
- }
1189
- });
1190
-
1191
- // src/platforms/webflow.ts
1192
- var IMG_EXTS, FONT_EXTS2, VIDEO_EXTS, webflow;
1193
- var init_webflow = __esm({
1194
- "src/platforms/webflow.ts"() {
1195
- "use strict";
1196
- IMG_EXTS = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif"];
1197
- FONT_EXTS2 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1198
- VIDEO_EXTS = [".mp4", ".webm", ".ogg"];
1199
- webflow = {
1200
- name: "webflow",
1201
- displayName: "Webflow",
1202
- detectByUrl(url) {
1203
- return /\.webflow\.(io|com)/.test(url);
1204
- },
1205
- detectByHtml(html) {
1206
- return html.includes("data-wf-site") || html.includes("w-webflow-badge") || html.includes("Webflow") || html.includes("webflow.js");
1207
- },
1208
- stripDomains: ["js-agent.newrelic.com", "cdn.heapanalytics.com", "tr.snapchat.com"],
1209
- stripSelectors: [".w-webflow-badge", 'link[href*="editorbar"]'],
1210
- stripPatterns: [
1211
- /<a[^>]*class="[^"]*w-webflow-badge[^"]*"[^>]*>[\s\S]*?<\/a>/g,
1212
- /<a[^>]*href="[^"]*webflow\.com\?utm_campaign=brandjs[^"]*"[^>]*>[\s\S]*?<\/a>/g,
1213
- /<style>[^<]*\.w-webflow-badge[^<]*<\/style>/g,
1214
- /Powered by <a[^>]*href="[^"]*webflow\.com"[^>]*>[^<]*<\/a>/g,
1215
- /<!-- This site was created in Webflow\.[^>]*-->/g,
1216
- /<html([^>]*) data-wf-domain="[^"]*"/g,
1217
- /<html([^>]*) data-wf-page="[^"]*"/g,
1218
- /<html([^>]*) data-wf-site="[^"]*"/g,
1219
- /<html([^>]*) data-wf-status="[^"]*"/g,
1220
- /<meta[^>]*content="Webflow"[^>]*>/g
1221
- ],
1222
- hydrationTimeout: 2e3,
1223
- needsHydrationCheck: false,
1224
- mapAssetDir(host, pathname, ext) {
1225
- if (host.includes("website-files.com") || host.includes("webflow.com") || host.includes("uploads-ssl.webflow.com")) {
1226
- if (VIDEO_EXTS.includes(ext)) return "assets/videos";
1227
- if (pathname.includes("/images/") || IMG_EXTS.includes(ext)) return "assets/images";
1228
- if (pathname.includes("/css/") || ext === ".css") return "styles";
1229
- if (pathname.includes("/js/") || ext === ".js") return "scripts/vendor";
1230
- if (pathname.includes("/gsap/") || pathname.includes("/plugins/")) return "scripts/vendor";
1231
- if (FONT_EXTS2.includes(ext)) return "assets/fonts";
1232
- return "assets/misc";
1233
- }
1234
- if (host.includes("d3e54v103j8qbb.cloudfront.net")) {
1235
- if (ext === ".js") return "scripts/vendor";
1236
- if (ext === ".css") return "styles";
1237
- if (IMG_EXTS.includes(ext)) return "assets/images";
1238
- return "assets/misc";
1239
- }
1240
- return null;
1241
- }
1242
- };
1243
- }
1244
- });
1245
-
1246
- // src/platforms/wix.ts
1247
- var IMG_EXTS2, FONT_EXTS3, VIDEO_EXTS2, wix;
1248
- var init_wix = __esm({
1249
- "src/platforms/wix.ts"() {
1250
- "use strict";
1251
- IMG_EXTS2 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1252
- FONT_EXTS3 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1253
- VIDEO_EXTS2 = [".mp4", ".webm", ".ogg"];
1254
- wix = {
1255
- name: "wix",
1256
- displayName: "Wix",
1257
- detectByUrl(url) {
1258
- return /\.wixsite\.com|\.wix\.com/.test(url);
1259
- },
1260
- detectByHtml(html) {
1261
- return html.includes("wix-viewer-model") || html.includes('id="WIX_ADS"') || html.includes("X-Wix-") || html.includes("Wix.com") || html.includes("wixcode-sdk") || html.includes("data-wix") || html.includes("wix-code") || html.includes("WixCode") || /\bwix\.com\b/.test(html) || /meta\s+name=["']generator["']\s+content=["'][^"']*Wix[^"']*["']/i.test(html) || /_wix_/i.test(html) || /wix-ecom/i.test(html);
1262
- },
1263
- stripDomains: [
1264
- "frog.wix.com",
1265
- "bi.wixapi.com",
1266
- "fed.wixcodescheduler.com",
1267
- "editor.wix.com",
1268
- "panorama.wixapps.net"
1269
- ],
1270
- stripSelectors: ["#WIX_ADS", ".wix-ads", "#wix-badge", "#SCROLL_TO_TOP"],
1271
- stripPatterns: [
1272
- /<div[^>]*class="[^"]*wix-ads[^"]*"[^>]*>[\s\S]*?<\/div>/g
1273
- ],
1274
- hydrationTimeout: 3e3,
1275
- needsHydrationCheck: false,
1276
- mapAssetDir(host, pathname, ext) {
1277
- if (host === "video.wixstatic.com") {
1278
- return "assets/videos";
1279
- }
1280
- if (host.includes("wixstatic.com")) {
1281
- if (VIDEO_EXTS2.includes(ext)) return "assets/videos";
1282
- if (pathname.includes("/images/") || IMG_EXTS2.includes(ext)) return "assets/images";
1283
- if (ext === ".css") return "styles";
1284
- if (ext === ".js") return "scripts/vendor";
1285
- if (FONT_EXTS3.includes(ext)) return "assets/fonts";
1286
- return "assets/misc";
1287
- }
1288
- if (host.includes("parastorage.com")) {
1289
- if (ext === ".js") return "scripts/vendor";
1290
- if (ext === ".css") return "styles";
1291
- if (FONT_EXTS3.includes(ext)) return "assets/fonts";
1292
- if (IMG_EXTS2.includes(ext)) return "assets/images";
1293
- return "assets/misc";
1294
- }
1295
- return null;
1296
- }
2828
+ "use strict";
2829
+ init_theme();
2830
+ _cooking = null;
2831
+ T = () => (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
2832
+ LOG_PALETTE = [
2833
+ (s) => chalk4.hex(THEME.primary)(s),
2834
+ (s) => chalk4.hex(THEME.primarySoft)(s),
2835
+ (s) => chalk4.hex(THEME.secondary)(s),
2836
+ (s) => chalk4.hex(THEME.accent)(s),
2837
+ (s) => chalk4.hex(THEME.info)(s),
2838
+ (s) => chalk4.hex(THEME.text)(s)
2839
+ ];
2840
+ _li = 0;
2841
+ log = (m) => {
2842
+ _li++;
2843
+ const c = LOG_PALETTE[_li % LOG_PALETTE.length];
2844
+ output(
2845
+ `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.primary)("[log]")} ${c(trunc(m, 120))}`
2846
+ );
2847
+ };
2848
+ INFO_PALETTE = [
2849
+ (s) => chalk4.hex(THEME.info)(s),
2850
+ (s) => chalk4.hex(THEME.secondary)(s),
2851
+ (s) => chalk4.hex(THEME.primarySoft)(s)
2852
+ ];
2853
+ _ii = 0;
2854
+ info = (m) => {
2855
+ _ii++;
2856
+ const c = INFO_PALETTE[_ii % INFO_PALETTE.length];
2857
+ output(
2858
+ `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.info).bold("[info]")} ${c(trunc(m, 120))}`
2859
+ );
2860
+ };
2861
+ warn = (m) => {
2862
+ const colors = [
2863
+ (s) => chalk4.hex(THEME.warning)(s),
2864
+ (s) => chalk4.hex(THEME.primary)(s)
2865
+ ];
2866
+ const c = colors[Math.floor(Math.random() * colors.length)];
2867
+ const line = `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.warning).bold("[warn]")} ${c(trunc(m, 120))}`;
2868
+ if (_cooking) _cooking.log(line);
2869
+ else console.warn(line);
2870
+ };
2871
+ success = (m) => {
2872
+ const colors = [
2873
+ (s) => chalk4.hex(THEME.success)(s),
2874
+ (s) => chalk4.hex(THEME.info)(s)
2875
+ ];
2876
+ const c = colors[Math.floor(Math.random() * colors.length)];
2877
+ output(
2878
+ `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.success)("[ok]")} ${c(trunc(m, 120))}`
2879
+ );
1297
2880
  };
1298
2881
  }
1299
2882
  });
1300
2883
 
1301
- // src/platforms/detect.ts
1302
- function detectByUrl(url) {
1303
- for (const platform of ALL_PLATFORMS) {
1304
- if (platform.detectByUrl(url)) return platform;
2884
+ // src/exporter/anti-bot.ts
2885
+ var anti_bot_exports = {};
2886
+ __export(anti_bot_exports, {
2887
+ AntiBotError: () => AntiBotError,
2888
+ detectAntiBotHtml: () => detectAntiBotHtml,
2889
+ detectAntiBotPage: () => detectAntiBotPage,
2890
+ formatAntiBotError: () => formatAntiBotError
2891
+ });
2892
+ function detectAntiBotHtml(html) {
2893
+ if (!html) return null;
2894
+ if (/<title>\s*Just a moment/i.test(html) || /cf-browser-verification|cf-challenge-running|cf_chl_opt|challenge-platform/i.test(html) || /Attention Required!\s*\|\s*Cloudflare/i.test(html) || /Checking if the site connection is secure/i.test(html)) {
2895
+ return "cloudflare";
1305
2896
  }
1306
- return null;
1307
- }
1308
- function detectByHtml(html) {
1309
- for (const platform of ALL_PLATFORMS) {
1310
- if (platform.detectByHtml(html)) return platform;
2897
+ if (/class="g-recaptcha"|h-captcha|hcaptcha\.com|challenges\.cloudflare\.com\/turnstile/i.test(html)) {
2898
+ return "captcha";
1311
2899
  }
1312
2900
  return null;
1313
2901
  }
1314
- function detectPlatform(url, html) {
1315
- const byUrl = detectByUrl(url);
1316
- if (byUrl) return byUrl;
1317
- if (html) {
1318
- const byHtml = detectByHtml(html);
1319
- if (byHtml) return byHtml;
1320
- }
1321
- return framer;
1322
- }
1323
- async function detectByDom(page) {
2902
+ async function detectAntiBotPage(page) {
1324
2903
  try {
1325
- const signal = await page.evaluate(() => {
1326
- const html = document.documentElement.outerHTML || "";
1327
- if (/wix-viewer-model|_wix_|wix-ecom/i.test(html)) return "wix";
1328
- if (/data-wf-|w-webflow-badge|webflow\.js/i.test(html)) return "webflow";
1329
- if (/framerusercontent|framercanvas|framerstatic/i.test(html)) return "framer";
1330
- const srcs = [];
1331
- const scripts = document.querySelectorAll("script[src]");
1332
- scripts.forEach((s) => {
1333
- if (s.src) srcs.push(s.src);
1334
- });
1335
- const allSrcs = srcs.join(" ");
1336
- if (/wixstatic\.com|parastorage\.com/.test(allSrcs)) return "wix";
1337
- if (/website-files\.com|webflow\.com/.test(allSrcs)) return "webflow";
1338
- if (/framerusercontent\.com|framerstatic\.com|framer\.app/.test(allSrcs)) return "framer";
2904
+ const title = await page.title();
2905
+ if (/just a moment/i.test(title) || /attention required/i.test(title)) {
2906
+ return "cloudflare";
2907
+ }
2908
+ const flagged = await page.evaluate(() => {
2909
+ if (document.querySelector('iframe[src*="challenges.cloudflare.com"]')) return "cloudflare";
2910
+ if (document.querySelector(
2911
+ '.g-recaptcha, .h-captcha, iframe[src*="hcaptcha.com"], iframe[src*="recaptcha"]'
2912
+ )) {
2913
+ return "captcha";
2914
+ }
2915
+ const body = document.body ? document.body.innerText || "" : "";
2916
+ const outer = document.documentElement.outerHTML || "";
2917
+ if (body.trim().length < 120 && /cf-chl|cloudflare|cf-browser-verification/i.test(outer)) {
2918
+ return "cloudflare";
2919
+ }
1339
2920
  return "";
1340
2921
  });
1341
- if (signal === "wix") return wix;
1342
- if (signal === "webflow") return webflow;
1343
- if (signal === "framer") return framer;
2922
+ return flagged || null;
1344
2923
  } catch {
2924
+ return null;
1345
2925
  }
1346
- return null;
1347
2926
  }
1348
- function getPlatformByName(name) {
1349
- const map = { framer, webflow, wix };
1350
- return map[name] || framer;
2927
+ function formatAntiBotError(err) {
2928
+ const label = err.type === "cloudflare" ? "Cloudflare (browser challenge)" : err.type === "captcha" ? "a captcha challenge" : "a WAF / bot filter";
2929
+ const lines = [
2930
+ "",
2931
+ ` ${ui.error("\u2717")} ${ui.error.bold("EXPORT BLOCKED")} \u2014 ${err.platform} is protected by ${label}.`,
2932
+ "",
2933
+ ` ${ui.muted("The headless browser could not pass the verification, so nothing was exported.")}`,
2934
+ "",
2935
+ ` ${ui.text.bold(" What you can try:")}`,
2936
+ ` ${ui.muted(" 1.")} Retry in a few minutes \u2014 the challenge often expires.`,
2937
+ ` ${ui.muted(" 2.")} Use an alternative, non-protected canonical URL if one exists.`,
2938
+ ` ${ui.muted(" 3.")} Export from a network/IP the site trusts more.`,
2939
+ "",
2940
+ ` ${ui.muted("No files were written.")} ${ui.muted("(A stealth-browser mode is on the roadmap.)")}`,
2941
+ ""
2942
+ ];
2943
+ return lines.join("\n");
1351
2944
  }
1352
- var ALL_PLATFORMS;
1353
- var init_detect = __esm({
1354
- "src/platforms/detect.ts"() {
1355
- "use strict";
1356
- init_framer();
1357
- init_webflow();
1358
- init_wix();
1359
- ALL_PLATFORMS = [framer, webflow, wix];
1360
- }
1361
- });
1362
-
1363
- // src/platforms/index.ts
1364
- var platforms_exports = {};
1365
- __export(platforms_exports, {
1366
- detectByDom: () => detectByDom,
1367
- detectByHtml: () => detectByHtml,
1368
- detectByUrl: () => detectByUrl,
1369
- detectPlatform: () => detectPlatform,
1370
- framer: () => framer,
1371
- getPlatformByName: () => getPlatformByName,
1372
- webflow: () => webflow,
1373
- wix: () => wix
1374
- });
1375
- var init_platforms = __esm({
1376
- "src/platforms/index.ts"() {
2945
+ var AntiBotError;
2946
+ var init_anti_bot = __esm({
2947
+ "src/exporter/anti-bot.ts"() {
1377
2948
  "use strict";
1378
- init_framer();
1379
- init_webflow();
1380
- init_wix();
1381
- init_detect();
2949
+ init_theme();
2950
+ AntiBotError = class extends Error {
2951
+ type;
2952
+ platform;
2953
+ url;
2954
+ constructor(type, platform, url, message) {
2955
+ super(message || `Anti-bot protection detected (${type})`);
2956
+ this.name = "AntiBotError";
2957
+ this.type = type;
2958
+ this.platform = platform;
2959
+ this.url = url;
2960
+ }
2961
+ };
1382
2962
  }
1383
2963
  });
1384
2964
 
@@ -1443,16 +3023,30 @@ async function launchAndCapture(exporter) {
1443
3023
  log("Platform refined from DOM: " + domDetected.displayName + " (override: " + exporter.platform.name + ")");
1444
3024
  exporter.platform = domDetected;
1445
3025
  }
3026
+ const antiBot = await detectAntiBotPage(exporter.page);
3027
+ if (antiBot) {
3028
+ throw new AntiBotError(antiBot, exporter.platform.displayName, exporter.siteUrl);
3029
+ }
3030
+ if (exporter.platform.preCapture) {
3031
+ try {
3032
+ exporter.cooking?.update("Preparing " + exporter.platform.displayName + " page...");
3033
+ await exporter.platform.preCapture(exporter.page);
3034
+ log("preCapture hook completed");
3035
+ } catch (e) {
3036
+ log("preCapture hook error (continuing): " + e.message);
3037
+ }
3038
+ }
1446
3039
  exporter.cooking?.update("Waiting for " + exporter.platform.displayName + " hydration...");
1447
3040
  if (exporter.platform.needsHydrationCheck) {
1448
- log("Checking for #main element hydration...");
3041
+ const sel = exporter.platform.hydrationSelector || "#main";
3042
+ log("Checking for " + sel + " element hydration...");
1449
3043
  log("Hydration timeout: " + exporter.platform.hydrationTimeout + "ms");
1450
3044
  const timeout = exporter.platform.hydrationTimeout;
1451
3045
  await exporter.page.evaluate(`
1452
3046
  new Promise(function(r) {
1453
3047
  var start = Date.now();
1454
3048
  var tick = function() {
1455
- var m = document.getElementById('main') || document.body;
3049
+ var m = document.querySelector(${JSON.stringify(sel)}) || document.body;
1456
3050
  if (m && m.children.length > 0) setTimeout(r, 2000);
1457
3051
  else if (Date.now() - start > ${timeout}) r();
1458
3052
  else setTimeout(tick, 200);
@@ -1466,27 +3060,38 @@ async function launchAndCapture(exporter) {
1466
3060
  await new Promise((r) => setTimeout(r, exporter.platform.hydrationTimeout));
1467
3061
  success(exporter.platform.displayName + " page rendered");
1468
3062
  }
1469
- exporter.cooking?.update("Scrolling page...");
1470
- log("Starting full-page scroll (step: " + CFG.scrollStep + "px, delay: " + CFG.scrollDelay + "ms)");
1471
- const scrollStep = CFG.scrollStep;
1472
- const scrollDelay = CFG.scrollDelay;
1473
- const pageHeight = await exporter.page.evaluate(`
1474
- Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
1475
- `);
1476
- log("Page height: " + pageHeight + "px (" + Math.ceil(pageHeight / scrollStep) + " scroll steps)");
1477
- await exporter.page.evaluate(`
1478
- new Promise(function(r) {
1479
- var y = 0;
1480
- var max = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
1481
- var step = function() {
1482
- y += ${scrollStep};
1483
- window.scrollTo({ top: y, behavior: 'instant' });
1484
- y < max + 500 ? setTimeout(step, ${scrollDelay}) : (window.scrollTo(0, 0), r());
1485
- };
1486
- step();
1487
- })
1488
- `);
1489
- success("Full-page scroll complete");
3063
+ const scrollStrategy = exporter.platform.scrollStrategy || "standard";
3064
+ if (scrollStrategy === "none") {
3065
+ log("Scroll skipped (scrollStrategy: none)");
3066
+ } else {
3067
+ exporter.cooking?.update("Scrolling page...");
3068
+ log("Starting full-page scroll (step: " + CFG.scrollStep + "px, delay: " + CFG.scrollDelay + "ms)");
3069
+ const scrollStep = CFG.scrollStep;
3070
+ const scrollDelay = CFG.scrollDelay;
3071
+ const pageHeight = await exporter.page.evaluate(`
3072
+ Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
3073
+ `);
3074
+ log("Page height: " + pageHeight + "px (" + Math.ceil(pageHeight / scrollStep) + " scroll steps)");
3075
+ const passes = scrollStrategy === "infinite" ? 3 : 1;
3076
+ for (let pass = 0; pass < passes; pass++) {
3077
+ await exporter.page.evaluate(`
3078
+ new Promise(function(r) {
3079
+ var y = 0;
3080
+ var max = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
3081
+ var step = function() {
3082
+ y += ${scrollStep};
3083
+ window.scrollTo({ top: y, behavior: 'instant' });
3084
+ y < max + 500 ? setTimeout(step, ${scrollDelay}) : (window.scrollTo(0, 0), r());
3085
+ };
3086
+ step();
3087
+ })
3088
+ `);
3089
+ if (scrollStrategy === "infinite" && pass < passes - 1) {
3090
+ await new Promise((r) => setTimeout(r, 1e3));
3091
+ }
3092
+ }
3093
+ success("Full-page scroll complete");
3094
+ }
1490
3095
  exporter.cooking?.update("Waiting for lazy resources...");
1491
3096
  log("Waiting 2s for lazy-loaded resources...");
1492
3097
  await new Promise((r) => setTimeout(r, 2e3));
@@ -1497,6 +3102,19 @@ async function launchAndCapture(exporter) {
1497
3102
  } catch {
1498
3103
  log("Network idle timeout reached (continuing anyway)");
1499
3104
  }
3105
+ if (exporter.platform.captureRenderedDom) {
3106
+ try {
3107
+ const rendered = await exporter.page.evaluate(
3108
+ () => "<!DOCTYPE html>\n" + document.documentElement.outerHTML
3109
+ );
3110
+ if (rendered && rendered.length > 200) {
3111
+ exporter.ssrHTML = rendered;
3112
+ log("Captured rendered DOM as HTML source (" + (rendered.length / 1024).toFixed(1) + " KB)");
3113
+ }
3114
+ } catch (e) {
3115
+ log("Rendered DOM capture failed (keeping SSR HTML): " + e.message);
3116
+ }
3117
+ }
1500
3118
  const totalCaptured = exporter.assets.buffers.size;
1501
3119
  success("Captured " + totalCaptured + " network resources total");
1502
3120
  const cssCount = [...exporter.assets.entries.values()].filter((e) => e.localPath.endsWith(".css")).length;
@@ -1540,6 +3158,7 @@ var init_capture = __esm({
1540
3158
  init_config();
1541
3159
  init_logger();
1542
3160
  init_platforms();
3161
+ init_anti_bot();
1543
3162
  }
1544
3163
  });
1545
3164
 
@@ -1953,12 +3572,36 @@ async function buildOutput(exporter) {
1953
3572
  log(" Pattern removed " + removed + " chars");
1954
3573
  }
1955
3574
  }
3575
+ if (exporter.platform.stripScripts && exporter.platform.stripScripts.length > 0) {
3576
+ log("Applying " + exporter.platform.stripScripts.length + " script-strip patterns...");
3577
+ for (const pattern of exporter.platform.stripScripts) {
3578
+ const before = html.length;
3579
+ html = html.replace(new RegExp(pattern.source, pattern.flags), "");
3580
+ const removed = before - html.length;
3581
+ if (removed > 0) log(" Script pattern removed " + removed + " chars");
3582
+ }
3583
+ }
1956
3584
  success("Platform badges and tracking stripped");
3585
+ if (exporter.platform.postCapture) {
3586
+ try {
3587
+ const before = html.length;
3588
+ html = exporter.platform.postCapture(html, exporter);
3589
+ log("postCapture hook applied (delta " + (html.length - before) + " chars)");
3590
+ } catch (e) {
3591
+ warn("postCapture hook failed: " + e.message);
3592
+ }
3593
+ }
1957
3594
  exporter.cooking?.update("Rewriting asset URLs...");
1958
3595
  log("Rewriting " + exporter.assets.entries.size + " CDN URLs to local paths...");
1959
3596
  const beforeRewrite = html.length;
1960
3597
  html = exporter.assets.rewrite(html, "");
1961
3598
  log("HTML rewrite delta: " + (html.length - beforeRewrite) + " chars");
3599
+ if (exporter.platform.rewriteUrlPatterns && exporter.platform.rewriteUrlPatterns.length > 0) {
3600
+ for (const { from, to } of exporter.platform.rewriteUrlPatterns) {
3601
+ html = html.replace(new RegExp(from.source, from.flags), to);
3602
+ }
3603
+ log("Applied " + exporter.platform.rewriteUrlPatterns.length + " custom URL rewrites");
3604
+ }
1962
3605
  exporter.cooking?.update("Cleaning srcset references...");
1963
3606
  html = stripSrcsetCdnUrls(html);
1964
3607
  log("Cleaned remaining CDN URLs from srcset attributes");
@@ -2695,6 +4338,7 @@ var init_exporter = __esm({
2695
4338
  init_download();
2696
4339
  init_logger();
2697
4340
  init_capture();
4341
+ init_anti_bot();
2698
4342
  init_download2();
2699
4343
  init_output();
2700
4344
  init_summary();
@@ -2771,15 +4415,35 @@ var init_exporter = __esm({
2771
4415
  this.platform = htmlDetected;
2772
4416
  log("Platform refined: " + ui.primary(this.platform.displayName) + " (from HTML analysis)");
2773
4417
  }
2774
- await launchAndCapture(this);
2775
- if (includeSubpages && this.page) {
2776
- await this.crawlSubpages();
4418
+ try {
4419
+ await launchAndCapture(this);
4420
+ if (includeSubpages && this.page) {
4421
+ await this.crawlSubpages();
4422
+ }
4423
+ await closeBrowser(this);
4424
+ this.cooking.update("Downloading assets...");
4425
+ await downloadAll(this);
4426
+ this.cooking.update("Building output...");
4427
+ await buildOutput(this);
4428
+ if (this.platform.postProcess) {
4429
+ this.cooking.update("Running " + this.platform.displayName + " post-processing...");
4430
+ try {
4431
+ await this.platform.postProcess(this);
4432
+ log("postProcess hook completed");
4433
+ } catch (e) {
4434
+ log("postProcess hook error (continuing): " + e.message);
4435
+ }
4436
+ }
4437
+ } catch (e) {
4438
+ this.cooking?.stop();
4439
+ setCooking(null);
4440
+ await closeBrowser(this);
4441
+ if (e instanceof AntiBotError) {
4442
+ await fs5.rm(this.outDir, { recursive: true, force: true }).catch(() => {
4443
+ });
4444
+ }
4445
+ throw e;
2777
4446
  }
2778
- await closeBrowser(this);
2779
- this.cooking.update("Downloading assets...");
2780
- await downloadAll(this);
2781
- this.cooking.update("Building output...");
2782
- await buildOutput(this);
2783
4447
  this.cooking.stop();
2784
4448
  setCooking(null);
2785
4449
  console.log("");
@@ -2850,6 +4514,70 @@ var init_exporter = __esm({
2850
4514
  }
2851
4515
  });
2852
4516
 
4517
+ // src/cli/select-grouped.ts
4518
+ import chalk6 from "chalk";
4519
+ async function selectPlatform(siteUrl) {
4520
+ const grouped = platformsByCategory();
4521
+ const detected = detectByUrl(siteUrl);
4522
+ if (detected) {
4523
+ const choice = await select(
4524
+ "Platform detected",
4525
+ [
4526
+ { label: `Use ${detected.displayName}`, value: USE_DETECTED },
4527
+ { label: "Browse all platforms", value: BROWSE }
4528
+ ],
4529
+ 0,
4530
+ {
4531
+ headerLines: [`URL: ${siteUrl}`, `Detected: ${detected.displayName}`],
4532
+ actions: [{ label: "Modify URL", value: MODIFY_URL }],
4533
+ footer: "tab focus button \xB7 enter select \xB7 mouse hover/click"
4534
+ }
4535
+ );
4536
+ if (choice === MODIFY_URL) return { modifyUrl: true };
4537
+ if (choice === USE_DETECTED) return { platform: detected.name };
4538
+ }
4539
+ let categoryDefault = detected ? Math.max(0, CATEGORY_ORDER.indexOf(detected.category)) : 0;
4540
+ while (true) {
4541
+ const categoryOptions = CATEGORY_ORDER.map((cat2) => ({
4542
+ label: `${CATEGORY_LABELS[cat2]} ${chalk6.gray(`(${grouped[cat2].length})`)}`,
4543
+ value: cat2
4544
+ }));
4545
+ const category = await select("Select a category", categoryOptions, categoryDefault, {
4546
+ headerLines: [`URL: ${siteUrl}`],
4547
+ actions: [{ label: "Modify URL", value: MODIFY_URL }],
4548
+ footer: "tab focus button \xB7 enter next \xB7 mouse hover/click"
4549
+ });
4550
+ if (category === MODIFY_URL) return { modifyUrl: true };
4551
+ const cat = category;
4552
+ categoryDefault = CATEGORY_ORDER.indexOf(cat);
4553
+ const handlers = grouped[cat];
4554
+ const platformOptions = handlers.map((handler) => ({
4555
+ label: detected && handler.name === detected.name ? `${handler.displayName}${chalk6.gray(" (detected)")}` : handler.displayName,
4556
+ value: handler.name
4557
+ }));
4558
+ const platformDefault = detected && detected.category === cat ? Math.max(0, handlers.findIndex((handler) => handler.name === detected.name)) : 0;
4559
+ const platform = await select("Select platform", platformOptions, platformDefault, {
4560
+ headerLines: [`Category: ${CATEGORY_LABELS[cat]}`],
4561
+ actions: [{ label: "Back to categories", value: BACK }],
4562
+ footer: "tab focus button \xB7 enter select \xB7 mouse hover/click"
4563
+ });
4564
+ if (platform === BACK) continue;
4565
+ return { platform };
4566
+ }
4567
+ }
4568
+ var MODIFY_URL, BROWSE, USE_DETECTED, BACK;
4569
+ var init_select_grouped = __esm({
4570
+ "src/cli/select-grouped.ts"() {
4571
+ "use strict";
4572
+ init_select();
4573
+ init_platforms();
4574
+ MODIFY_URL = "__modify_url__";
4575
+ BROWSE = "__browse__";
4576
+ USE_DETECTED = "__use_detected__";
4577
+ BACK = "__back__";
4578
+ }
4579
+ });
4580
+
2853
4581
  // src/cli/setup.ts
2854
4582
  var setup_exports = {};
2855
4583
  __export(setup_exports, {
@@ -2859,7 +4587,7 @@ import readline2 from "readline/promises";
2859
4587
  import { stdin as stdin3, stdout as stdout3 } from "process";
2860
4588
  import path7 from "path";
2861
4589
  import { URL as URL4 } from "url";
2862
- import chalk6 from "chalk";
4590
+ import chalk7 from "chalk";
2863
4591
  function drawHeader(title) {
2864
4592
  const w = maxWidth();
2865
4593
  if (w < 50) {
@@ -2876,7 +4604,7 @@ async function runSetup(legacyMode = false) {
2876
4604
  showBanner();
2877
4605
  console.log(` ${ui.text.bold("Framer Export setup")} ${chip("interactive")}`);
2878
4606
  console.log(
2879
- ` ${ui.muted("Export Framer, Webflow, and Wix sites into a clean local mirror.")}
4607
+ ` ${ui.muted("Export 25+ website platforms into a clean, self-hosted local mirror.")}
2880
4608
  `
2881
4609
  );
2882
4610
  const rl = legacyMode ? readline2.createInterface({ input: stdin3, output: stdout3 }) : null;
@@ -2884,7 +4612,7 @@ async function runSetup(legacyMode = false) {
2884
4612
  if (!legacyMode) {
2885
4613
  return promptInput(question, defaultVal || "", { headerLines });
2886
4614
  }
2887
- const suffix = defaultVal ? chalk6.gray(` (${defaultVal})`) : "";
4615
+ const suffix = defaultVal ? chalk7.gray(` (${defaultVal})`) : "";
2888
4616
  const prompt = ` ${ui.primary("\u25CF")} ${ui.text.bold(question)}${suffix} ${ui.muted(">")} `;
2889
4617
  const answer = await rl.question(prompt);
2890
4618
  return answer.trim() || defaultVal || "";
@@ -2911,7 +4639,7 @@ async function runSetup(legacyMode = false) {
2911
4639
  }
2912
4640
  }
2913
4641
  if (legacyMode) {
2914
- console.log(` ${ui.success("\u2713")} ${ui.success("URL:")} ${chalk6.underline(siteUrl)}
4642
+ console.log(` ${ui.success("\u2713")} ${ui.success("URL:")} ${chalk7.underline(siteUrl)}
2915
4643
  `);
2916
4644
  }
2917
4645
  let platformName = null;
@@ -2919,31 +4647,15 @@ async function runSetup(legacyMode = false) {
2919
4647
  drawHeader("Step 2 : Platform");
2920
4648
  const detected = detectPlatform(siteUrl);
2921
4649
  console.log(` ${ui.info("i")} Auto-detected: ${ui.primary(detected.displayName)}`);
2922
- const platformInput = await ask("Platform (framer/webflow/wix)", detected.name);
2923
- platformName = ["framer", "webflow", "wix"].includes(platformInput) ? platformInput : detected.name;
4650
+ const platformInput = await ask("Platform name", detected.name);
4651
+ const known = PLATFORM_REGISTRY.some((platform) => platform.name === platformInput);
4652
+ platformName = known ? platformInput : detected.name;
2924
4653
  console.log(` ${ui.success("\u2713")} ${ui.success("Platform:")} ${ui.primary(platformName)}
2925
4654
  `);
2926
4655
  } else {
2927
4656
  while (!platformName) {
2928
- const detected = detectPlatform(siteUrl);
2929
- const platforms = [
2930
- {
2931
- label: `Framer${detected.name === "framer" ? chalk6.gray(" (detected)") : ""}`,
2932
- value: "framer"
2933
- },
2934
- {
2935
- label: `Webflow${detected.name === "webflow" ? chalk6.gray(" (detected)") : ""}`,
2936
- value: "webflow"
2937
- },
2938
- { label: `Wix${detected.name === "wix" ? chalk6.gray(" (detected)") : ""}`, value: "wix" }
2939
- ];
2940
- const defaultIdx = ["framer", "webflow", "wix"].indexOf(detected.name);
2941
- const platformChoice = await select("Select platform", platforms, Math.max(0, defaultIdx), {
2942
- headerLines: [`URL: ${siteUrl}`],
2943
- actions: [{ label: "Modify URL", value: "modify-url" }],
2944
- footer: "tab focus button \xB7 mouse hover/click \xB7 enter select"
2945
- });
2946
- if (platformChoice === "modify-url") {
4657
+ const selection = await selectPlatform(siteUrl);
4658
+ if ("modifyUrl" in selection) {
2947
4659
  siteUrl = "";
2948
4660
  urlError = "";
2949
4661
  while (!siteUrl) {
@@ -2962,7 +4674,7 @@ async function runSetup(legacyMode = false) {
2962
4674
  }
2963
4675
  continue;
2964
4676
  }
2965
- platformName = platformChoice;
4677
+ platformName = selection.platform;
2966
4678
  }
2967
4679
  }
2968
4680
  if (!platformName) {
@@ -2973,26 +4685,62 @@ async function runSetup(legacyMode = false) {
2973
4685
  if (!legacyMode) {
2974
4686
  return promptInput(question, defaultVal || "", { headerLines });
2975
4687
  }
2976
- const suffix = defaultVal ? chalk6.gray(` (${defaultVal})`) : "";
4688
+ const suffix = defaultVal ? chalk7.gray(` (${defaultVal})`) : "";
2977
4689
  const prompt = ` ${ui.primary("\u25CF")} ${ui.text.bold(question)}${suffix} ${ui.muted(">")} `;
2978
4690
  const answer = await rl2.question(prompt);
2979
4691
  return answer.trim() || defaultVal || "";
2980
4692
  };
2981
4693
  const derivedName = deriveOutputName(siteUrl, platformName);
2982
- if (legacyMode) drawHeader("Step 3 : Output Directory");
2983
- const outDir = await ask2("Output directory", "./" + derivedName, [
2984
- "Step 3 : Output Directory",
2985
- `URL: ${siteUrl}`,
2986
- `Platform: ${platformName}`
2987
- ]);
4694
+ let outDir = "./" + derivedName;
2988
4695
  if (legacyMode) {
4696
+ drawHeader("Step 3 : Output Directory");
4697
+ outDir = await ask2("Output directory", outDir, [
4698
+ "Step 3 : Output Directory",
4699
+ `URL: ${siteUrl}`,
4700
+ `Platform: ${platformName}`
4701
+ ]);
2989
4702
  console.log(` ${ui.success("\u2713")} ${ui.success("Output:")} ${ui.primary(outDir)}
2990
4703
  `);
4704
+ drawHeader("Step 4 : Options");
4705
+ } else {
4706
+ outDir = await ask2("Output directory", outDir, [
4707
+ "Step 3 : Output Directory",
4708
+ `URL: ${siteUrl}`,
4709
+ `Platform: ${platformName}`
4710
+ ]);
2991
4711
  }
2992
- if (legacyMode) drawHeader("Step 4 : Options");
2993
- let prettyPrint;
2994
- let concurrency;
2995
- let includeSubpages;
4712
+ let prettyPrint = true;
4713
+ let concurrency = 12;
4714
+ let includeSubpages = false;
4715
+ let startExport = false;
4716
+ const summaryRows = () => [
4717
+ ["URL", siteUrl],
4718
+ ["Platform", platformName],
4719
+ ["Output", path7.resolve(outDir)],
4720
+ ["Pretty-print", prettyPrint ? "yes" : "no"],
4721
+ ["Sub-pages", includeSubpages ? "yes" : "no"],
4722
+ ["Concurrency", String(concurrency)]
4723
+ ];
4724
+ const summaryLines = () => summaryRows().map(([label, value]) => `${label}: ${value}`);
4725
+ const printSummary2 = () => {
4726
+ const w = maxWidth();
4727
+ const isSmall = w < 50;
4728
+ console.log("");
4729
+ if (!isSmall) {
4730
+ console.log(boxTop(w));
4731
+ console.log(boxLine(w, ui.text.bold(" Summary")));
4732
+ console.log(boxSep(w));
4733
+ } else {
4734
+ console.log(ui.text.bold(" Summary:"));
4735
+ }
4736
+ for (const [label, value] of summaryRows()) {
4737
+ console.log(boxRow(w, label, value));
4738
+ }
4739
+ if (!isSmall) {
4740
+ console.log(boxBot(w));
4741
+ }
4742
+ console.log("");
4743
+ };
2996
4744
  if (legacyMode) {
2997
4745
  const prettyAnswer = await ask2("Pretty-print JS files? (y/n)", "y");
2998
4746
  prettyPrint = prettyAnswer.toLowerCase().startsWith("y");
@@ -3010,67 +4758,104 @@ async function runSetup(legacyMode = false) {
3010
4758
  concurrency = parseInt(concurrencyAnswer, 10) || 12;
3011
4759
  console.log(` ${ui.success("\u2713")} Concurrency: ${ui.primary(String(concurrency))}
3012
4760
  `);
3013
- } else {
3014
- const prettyVal = await select("Pretty-print JS files?", [
3015
- { label: "Yes", value: "yes" },
3016
- { label: "No", value: "no" }
3017
- ]);
3018
- prettyPrint = prettyVal === "yes";
3019
- const subpagesVal = await select(
3020
- "Export sub-pages?",
3021
- [
3022
- { label: "No", value: "no" },
3023
- { label: "Yes, crawl and export", value: "yes" }
3024
- ],
3025
- 0
3026
- );
3027
- includeSubpages = subpagesVal === "yes";
3028
- const concurrencyVal = await select(
3029
- "Download concurrency",
3030
- [
3031
- { label: "6 (slow connection)", value: "6" },
3032
- { label: "12 (default)", value: "12" },
3033
- { label: "20 (fast connection)", value: "20" }
3034
- ],
3035
- 1
3036
- );
3037
- concurrency = parseInt(concurrencyVal, 10);
3038
- }
3039
- const w = maxWidth();
3040
- const isSmall = w < 50;
3041
- console.log("");
3042
- if (!isSmall) {
3043
- console.log(boxTop(w));
3044
- console.log(boxLine(w, ui.text.bold(" Summary")));
3045
- console.log(boxSep(w));
3046
- } else {
3047
- console.log(ui.text.bold(" Summary:"));
3048
- }
3049
- for (const [label, value] of [
3050
- ["URL", siteUrl],
3051
- ["Platform", platformName],
3052
- ["Output", path7.resolve(outDir)],
3053
- ["Pretty-print", prettyPrint ? "yes" : "no"],
3054
- ["Sub-pages", includeSubpages ? "yes" : "no"],
3055
- ["Concurrency", String(concurrency)]
3056
- ]) {
3057
- console.log(boxRow(w, label, value));
3058
- }
3059
- if (!isSmall) {
3060
- console.log(boxBot(w));
3061
- }
3062
- console.log("");
3063
- let startExport;
3064
- if (legacyMode) {
4761
+ printSummary2();
3065
4762
  const confirm = await ask2("Start export? (y/n)", "y");
3066
4763
  startExport = confirm.toLowerCase().startsWith("y");
3067
4764
  rl2.close();
3068
4765
  } else {
3069
- const confirmVal = await select("Start export?", [
3070
- { label: "Yes, start now", value: "yes" },
3071
- { label: "Cancel", value: "no" }
3072
- ]);
3073
- startExport = confirmVal === "yes";
4766
+ let optionStep = 0;
4767
+ while (true) {
4768
+ while (optionStep < 3) {
4769
+ if (optionStep === 0) {
4770
+ const prettyVal = await select(
4771
+ "Pretty-print JS files?",
4772
+ [
4773
+ { label: "Yes", value: "yes" },
4774
+ { label: "No", value: "no" }
4775
+ ],
4776
+ prettyPrint ? 0 : 1,
4777
+ {
4778
+ headerLines: ["Step 4 : Options"],
4779
+ actions: [{ label: "Previous: output directory", value: "previous" }],
4780
+ footer: "previous go back \xB7 enter next \xB7 mouse hover/click"
4781
+ }
4782
+ );
4783
+ if (prettyVal === "previous") {
4784
+ outDir = await ask2("Output directory", outDir, [
4785
+ "Step 3 : Output Directory",
4786
+ `URL: ${siteUrl}`,
4787
+ `Platform: ${platformName}`
4788
+ ]);
4789
+ optionStep = 0;
4790
+ continue;
4791
+ }
4792
+ prettyPrint = prettyVal === "yes";
4793
+ optionStep = 1;
4794
+ continue;
4795
+ }
4796
+ if (optionStep === 1) {
4797
+ const subpagesVal = await select(
4798
+ "Export sub-pages?",
4799
+ [
4800
+ { label: "No", value: "no" },
4801
+ { label: "Yes, crawl and export", value: "yes" }
4802
+ ],
4803
+ includeSubpages ? 1 : 0,
4804
+ {
4805
+ headerLines: ["Step 4 : Options"],
4806
+ actions: [{ label: "Previous: pretty-print", value: "previous" }],
4807
+ footer: "previous go back \xB7 enter next \xB7 mouse hover/click"
4808
+ }
4809
+ );
4810
+ if (subpagesVal === "previous") {
4811
+ optionStep = 0;
4812
+ continue;
4813
+ }
4814
+ includeSubpages = subpagesVal === "yes";
4815
+ optionStep = 2;
4816
+ continue;
4817
+ }
4818
+ const concurrencyVal = await select(
4819
+ "Download concurrency",
4820
+ [
4821
+ { label: "6 (slow connection)", value: "6" },
4822
+ { label: "12 (default)", value: "12" },
4823
+ { label: "20 (fast connection)", value: "20" }
4824
+ ],
4825
+ concurrency === 6 ? 0 : concurrency === 20 ? 2 : 1,
4826
+ {
4827
+ headerLines: ["Step 4 : Options"],
4828
+ actions: [{ label: "Previous: sub-pages", value: "previous" }],
4829
+ footer: "previous go back \xB7 enter next \xB7 mouse hover/click"
4830
+ }
4831
+ );
4832
+ if (concurrencyVal === "previous") {
4833
+ optionStep = 1;
4834
+ continue;
4835
+ }
4836
+ concurrency = parseInt(concurrencyVal, 10);
4837
+ optionStep = 3;
4838
+ }
4839
+ const confirmVal = await select(
4840
+ "Ready to export?",
4841
+ [
4842
+ { label: "Previous: edit options", value: "previous" },
4843
+ { label: "Next: start now", value: "next" },
4844
+ { label: "Cancel", value: "cancel" }
4845
+ ],
4846
+ 1,
4847
+ {
4848
+ headerLines: ["Summary", ...summaryLines()],
4849
+ footer: "previous edit options \xB7 next start export \xB7 mouse hover/click"
4850
+ }
4851
+ );
4852
+ if (confirmVal === "previous") {
4853
+ optionStep = 2;
4854
+ continue;
4855
+ }
4856
+ startExport = confirmVal === "next";
4857
+ break;
4858
+ }
3074
4859
  }
3075
4860
  if (!startExport) {
3076
4861
  console.log(`
@@ -3083,7 +4868,16 @@ async function runSetup(legacyMode = false) {
3083
4868
  CFG2.concurrency = concurrency;
3084
4869
  const exporter = new FramerExporter(siteUrl, path7.resolve(outDir), platformName);
3085
4870
  exporter.prettyPrint = prettyPrint;
3086
- await exporter.run(includeSubpages);
4871
+ try {
4872
+ await exporter.run(includeSubpages);
4873
+ } catch (e) {
4874
+ const { AntiBotError: AntiBotError2, formatAntiBotError: formatAntiBotError2 } = await Promise.resolve().then(() => (init_anti_bot(), anti_bot_exports));
4875
+ if (e instanceof AntiBotError2) {
4876
+ console.log(formatAntiBotError2(e));
4877
+ process.exit(1);
4878
+ }
4879
+ throw e;
4880
+ }
3087
4881
  }
3088
4882
  var init_setup = __esm({
3089
4883
  "src/cli/setup.ts"() {
@@ -3092,6 +4886,7 @@ var init_setup = __esm({
3092
4886
  init_exporter();
3093
4887
  init_platforms();
3094
4888
  init_select();
4889
+ init_select_grouped();
3095
4890
  init_box();
3096
4891
  init_theme();
3097
4892
  }
@@ -3106,6 +4901,7 @@ import { spawnSync } from "child_process";
3106
4901
  // src/cli/help.ts
3107
4902
  init_banner();
3108
4903
  init_theme();
4904
+ init_platforms();
3109
4905
  function showHelp() {
3110
4906
  showBanner();
3111
4907
  console.log(`${ui.text.bold(" USAGE")} ${chip("cli")}
@@ -3120,7 +4916,7 @@ function showHelp() {
3120
4916
  console.log(ui.text.bold(" OPTIONS\n"));
3121
4917
  const opts = [
3122
4918
  ["--setup", "Launch the interactive setup assistant"],
3123
- ["--platform <p>", "Force platform: framer, webflow, wix"],
4919
+ ["--platform <p>", "Force platform by id (e.g. framer, shopify, notion)"],
3124
4920
  ["--subpages", "Crawl and export sub-pages"],
3125
4921
  ["--legacy-mode", "Use text input instead of arrow selection"],
3126
4922
  ["--help, -h", "Show this help message"]
@@ -3130,13 +4926,10 @@ function showHelp() {
3130
4926
  }
3131
4927
  console.log("");
3132
4928
  console.log(ui.text.bold(" SUPPORTED PLATFORMS\n"));
3133
- const platforms = [
3134
- ["Framer", "Auto-detected via .framer.app / .framer.website URLs"],
3135
- ["Webflow", "Auto-detected via .webflow.io URLs"],
3136
- ["Wix", "Auto-detected via .wixsite.com URLs + HTML analysis"]
3137
- ];
3138
- for (const [name, desc] of platforms) {
3139
- console.log(` ${ui.primary(name.padEnd(12))} ${ui.muted(desc)}`);
4929
+ const grouped = platformsByCategory();
4930
+ for (const cat of CATEGORY_ORDER) {
4931
+ const names = grouped[cat].map((handler) => handler.displayName).join(", ");
4932
+ console.log(` ${ui.primary(CATEGORY_LABELS[cat].padEnd(22))} ${ui.muted(names)}`);
3140
4933
  }
3141
4934
  console.log("");
3142
4935
  console.log(ui.text.bold(" EXAMPLES\n"));
@@ -3285,20 +5078,20 @@ async function main() {
3285
5078
  process.exit(0);
3286
5079
  }
3287
5080
  if (args.includes("--about")) {
3288
- const chalk7 = (await import("chalk")).default;
5081
+ const chalk8 = (await import("chalk")).default;
3289
5082
  showBanner();
3290
5083
  console.log(` ${ui.text.bold("Framer Export")} ${ui.muted(`v${package_default.version}`)}`);
3291
5084
  console.log(` ${ui.text(package_default.description)}
3292
5085
  `);
3293
5086
  console.log(` ${ui.text.bold("Author")} ${ui.primary("Dany (danbenba)")}`);
3294
5087
  console.log(
3295
- ` ${ui.text.bold("Portfolio")} ${chalk7.underline.hex("#FAB283")("https://github.com/danbenba")}`
5088
+ ` ${ui.text.bold("Portfolio")} ${chalk8.underline.hex("#FAB283")("https://github.com/danbenba")}`
3296
5089
  );
3297
5090
  console.log(
3298
- ` ${ui.text.bold("GitHub")} ${chalk7.underline.hex("#FAB283")(package_default.repository.url.replace("git+", "").replace(".git", ""))}`
5091
+ ` ${ui.text.bold("GitHub")} ${chalk8.underline.hex("#FAB283")(package_default.repository.url.replace("git+", "").replace(".git", ""))}`
3299
5092
  );
3300
5093
  console.log(
3301
- ` ${ui.text.bold("npm")} ${chalk7.underline.hex("#FAB283")(`https://www.npmjs.com/package/${package_default.name}`)}`
5094
+ ` ${ui.text.bold("npm")} ${chalk8.underline.hex("#FAB283")(`https://www.npmjs.com/package/${package_default.name}`)}`
3302
5095
  );
3303
5096
  console.log(` ${ui.text.bold("License")} ${ui.success(package_default.license)}`);
3304
5097
  console.log(` ${ui.text.bold("Node")} ${ui.muted(`>=${package_default.engines.node}`)}`);
@@ -3345,12 +5138,17 @@ async function main() {
3345
5138
  includeSubpages
3346
5139
  );
3347
5140
  } catch (e) {
3348
- const chalk7 = (await import("chalk")).default;
5141
+ const { AntiBotError: AntiBotError2, formatAntiBotError: formatAntiBotError2 } = await Promise.resolve().then(() => (init_anti_bot(), anti_bot_exports));
5142
+ if (e instanceof AntiBotError2) {
5143
+ console.log(formatAntiBotError2(e));
5144
+ process.exit(1);
5145
+ }
5146
+ const chalk8 = (await import("chalk")).default;
3349
5147
  console.log(
3350
5148
  `
3351
5149
  ${ui.error("\u2717")} ${ui.error.bold("FAILED:")} ${ui.text(e.message)}`
3352
5150
  );
3353
- console.log(chalk7.gray(e.stack || ""));
5151
+ console.log(chalk8.gray(e.stack || ""));
3354
5152
  process.exit(1);
3355
5153
  }
3356
5154
  }