framer-export 4.4.2 → 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.2",
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) {
@@ -215,7 +1973,8 @@ async function showLoadingIntro(version) {
215
1973
  const title = renderShinyText(`Framer Export v${version}`, frame, {
216
1974
  baseColor: "#969696",
217
1975
  shineColor: "#ffffff",
218
- shineWidth: 18
1976
+ shineWidth: 18,
1977
+ speed: 1.2
219
1978
  });
220
1979
  const dots = ".".repeat(frame % 4).padEnd(3, " ");
221
1980
  process.stdout.write(
@@ -237,8 +1996,9 @@ function renderShinyText(text, frame, options = {}) {
237
1996
  const baseColor = options.baseColor || "#808080";
238
1997
  const shineColor = options.shineColor || "#ffffff";
239
1998
  const shineWidth = options.shineWidth || SHINE_WIDTH;
1999
+ const speed = options.speed ?? SHINE_SPEED;
240
2000
  const travel = text.length + shineWidth * 2;
241
- const pos = frame * SHINE_SPEED % travel - shineWidth;
2001
+ const pos = frame * speed % travel - shineWidth;
242
2002
  const base = hexToRgb(baseColor);
243
2003
  const shine = hexToRgb(shineColor);
244
2004
  let result = "";
@@ -338,9 +2098,66 @@ function maxWidth() {
338
2098
  return Math.min(process.stdout.columns || 80, 76);
339
2099
  }
340
2100
  function padRight(text, w) {
341
- const visible = stripAnsi(text).length;
342
- if (visible >= w) return text;
343
- return text + " ".repeat(w - visible);
2101
+ const fitted = fitAnsi(text, w);
2102
+ const visible = stripAnsi(fitted).length;
2103
+ if (visible >= w) return fitted;
2104
+ return fitted + " ".repeat(w - visible);
2105
+ }
2106
+ function fitAnsi(text, width) {
2107
+ if (stripAnsi(text).length <= width) return text;
2108
+ if (width <= 2) return ".".repeat(Math.max(0, width));
2109
+ let out = "";
2110
+ let visible = 0;
2111
+ let i = 0;
2112
+ const limit = width - 2;
2113
+ while (i < text.length && visible < limit) {
2114
+ if (text[i] === "\x1B") {
2115
+ const match = text.slice(i).match(/^\x1B\[[0-9;]*[a-zA-Z]/);
2116
+ if (match) {
2117
+ out += match[0];
2118
+ i += match[0].length;
2119
+ continue;
2120
+ }
2121
+ }
2122
+ out += text[i];
2123
+ visible++;
2124
+ i++;
2125
+ }
2126
+ return out + "..\x1B[0m";
2127
+ }
2128
+ function fitValue(value, width) {
2129
+ if (value.length <= width) return value;
2130
+ const pathLike = value.includes("\\") || value.includes("/");
2131
+ if (pathLike) {
2132
+ const shortened = shortenPath(value, width);
2133
+ if (shortened.length <= width) return shortened;
2134
+ }
2135
+ if (width <= 3) return ".".repeat(Math.max(0, width));
2136
+ return value.slice(0, width - 2) + "..";
2137
+ }
2138
+ function shortenPath(value, width) {
2139
+ if (width <= 12) {
2140
+ return width <= 3 ? ".".repeat(Math.max(0, width)) : value.slice(0, width - 2) + "..";
2141
+ }
2142
+ const separator = value.includes("\\") ? "\\" : "/";
2143
+ const parts = value.split(/[\\/]+/).filter(Boolean);
2144
+ const prefix = value.startsWith(separator) ? separator : "";
2145
+ const shortened = parts.map((part, index) => {
2146
+ const isLast = index === parts.length - 1;
2147
+ if (isLast) return part;
2148
+ if (/^[A-Za-z]:$/.test(part)) return part;
2149
+ return part.length > 5 ? part.slice(0, 3) + ".." : part;
2150
+ }).join(separator);
2151
+ const withPrefix = prefix + shortened;
2152
+ if (withPrefix.length <= width) return withPrefix;
2153
+ const last = parts.at(-1) || value;
2154
+ const first = parts.slice(0, -1).map((part) => /^[A-Za-z]:$/.test(part) ? part : part.slice(0, 3) + "..");
2155
+ const compact = prefix + [...first, last].join(separator);
2156
+ if (compact.length <= width) return compact;
2157
+ const tailSpace = Math.max(8, Math.floor(width * 0.45));
2158
+ const headSpace = Math.max(0, width - tailSpace - 4);
2159
+ const middle = compact.slice(0, headSpace) + separator + ".." + separator + compact.slice(-tailSpace);
2160
+ return middle.length <= width ? middle : compact.slice(0, width - 2) + "..";
344
2161
  }
345
2162
  function boxTop(w) {
346
2163
  const inner = w - 4;
@@ -379,14 +2196,10 @@ function panelSep(w) {
379
2196
  function boxRow(w, label, value) {
380
2197
  const inner = w - 4;
381
2198
  const labelPlain = stripAnsi(chalk3.bold(label));
382
- const visible = labelPlain.length + 1 + value.length;
383
- if (visible > inner) {
384
- const avail = inner - labelPlain.length - 2;
385
- const truncated = value.length > avail ? value.slice(0, avail - 1) + ".." : value;
386
- return " " + ui.border("\u2502 ") + chalk3.bold(label) + ": " + ui.primary(truncated) + " ".repeat(Math.max(0, inner - labelPlain.length - 1 - truncated.length)) + ui.border(" \u2502");
387
- }
388
- const right = inner - labelPlain.length - 1 - value.length;
389
- return " " + ui.border("\u2502 ") + chalk3.bold(label) + ": " + ui.primary(value) + " ".repeat(right) + ui.border(" \u2502");
2199
+ const avail = Math.max(0, inner - labelPlain.length - 2);
2200
+ const fittedValue = fitValue(value, avail);
2201
+ const right = Math.max(0, inner - labelPlain.length - 2 - fittedValue.length);
2202
+ return " " + ui.border("\u2502 ") + chalk3.bold(label) + ": " + ui.primary(fittedValue) + " ".repeat(right) + ui.border(" \u2502");
390
2203
  }
391
2204
  var init_box = __esm({
392
2205
  "src/cli/box.ts"() {
@@ -1015,315 +2828,137 @@ var init_logger = __esm({
1015
2828
  "use strict";
1016
2829
  init_theme();
1017
2830
  _cooking = null;
1018
- T = () => (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
1019
- LOG_PALETTE = [
1020
- (s) => chalk4.hex(THEME.primary)(s),
1021
- (s) => chalk4.hex(THEME.primarySoft)(s),
1022
- (s) => chalk4.hex(THEME.secondary)(s),
1023
- (s) => chalk4.hex(THEME.accent)(s),
1024
- (s) => chalk4.hex(THEME.info)(s),
1025
- (s) => chalk4.hex(THEME.text)(s)
1026
- ];
1027
- _li = 0;
1028
- log = (m) => {
1029
- _li++;
1030
- const c = LOG_PALETTE[_li % LOG_PALETTE.length];
1031
- output(
1032
- `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.primary)("[log]")} ${c(trunc(m, 120))}`
1033
- );
1034
- };
1035
- INFO_PALETTE = [
1036
- (s) => chalk4.hex(THEME.info)(s),
1037
- (s) => chalk4.hex(THEME.secondary)(s),
1038
- (s) => chalk4.hex(THEME.primarySoft)(s)
1039
- ];
1040
- _ii = 0;
1041
- info = (m) => {
1042
- _ii++;
1043
- const c = INFO_PALETTE[_ii % INFO_PALETTE.length];
1044
- output(
1045
- `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.info).bold("[info]")} ${c(trunc(m, 120))}`
1046
- );
1047
- };
1048
- warn = (m) => {
1049
- const colors = [
1050
- (s) => chalk4.hex(THEME.warning)(s),
1051
- (s) => chalk4.hex(THEME.primary)(s)
1052
- ];
1053
- const c = colors[Math.floor(Math.random() * colors.length)];
1054
- const line = `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.warning).bold("[warn]")} ${c(trunc(m, 120))}`;
1055
- if (_cooking) _cooking.log(line);
1056
- else console.warn(line);
1057
- };
1058
- success = (m) => {
1059
- const colors = [
1060
- (s) => chalk4.hex(THEME.success)(s),
1061
- (s) => chalk4.hex(THEME.info)(s)
1062
- ];
1063
- const c = colors[Math.floor(Math.random() * colors.length)];
1064
- output(
1065
- `${chalk4.hex(THEME.muted)(`[${T()}]`)} ${chalk4.hex(THEME.success)("[ok]")} ${c(trunc(m, 120))}`
1066
- );
1067
- };
1068
- }
1069
- });
1070
-
1071
- // src/platforms/framer.ts
1072
- var FONT_EXTS, framer;
1073
- var init_framer = __esm({
1074
- "src/platforms/framer.ts"() {
1075
- "use strict";
1076
- FONT_EXTS = [".woff2", ".woff", ".ttf", ".otf"];
1077
- framer = {
1078
- name: "framer",
1079
- displayName: "Framer",
1080
- detectByUrl(url) {
1081
- return /\.framer\.(app|website)|framercanvas\.com/.test(url);
1082
- },
1083
- detectByHtml(html) {
1084
- return html.includes('id="main"') && (html.includes("framerstatic.com") || html.includes("framerusercontent.com"));
1085
- },
1086
- stripDomains: ["events.framer.com", "api.framer.com", "collect.frameranalytics.com"],
1087
- stripSelectors: [
1088
- 'script[src*="events.framer.com"]',
1089
- "#__framer-badge-container",
1090
- "#__framer-badge",
1091
- 'link[href*="canvas-sandbox"]',
1092
- 'script[src*="framer.com/bootstrap"]'
1093
- ],
1094
- stripPatterns: [
1095
- /<div id="__framer-badge-container"[^>]*><\/div>/g,
1096
- /<script>try\{if\(localStorage\.get\("__framer_force_showing_editorbar_since"\)\)[^<]*<\/script>/g
1097
- ],
1098
- hydrationTimeout: 1e4,
1099
- needsHydrationCheck: true,
1100
- mapAssetDir(host, pathname, ext) {
1101
- if (host.includes("framerusercontent.com")) {
1102
- if (pathname.startsWith("/images/")) return "assets/images";
1103
- if (pathname.startsWith("/assets/")) {
1104
- return FONT_EXTS.includes(ext) ? "assets/fonts" : "assets/misc";
1105
- }
1106
- if (pathname.startsWith("/sites/")) {
1107
- if (ext === ".mjs" || ext === ".js") return "scripts/vendor";
1108
- if (ext === ".json") return "data";
1109
- if (ext === ".css") return "styles";
1110
- if (ext === ".framercms") return "data";
1111
- return "assets/misc";
1112
- }
1113
- if (pathname.startsWith("/modules/")) {
1114
- return ext === ".framercms" ? "data" : "scripts/modules";
1115
- }
1116
- return "assets/misc";
1117
- }
1118
- if (host.includes("app.framerstatic.com")) {
1119
- if (ext === ".css") return "styles";
1120
- if (ext === ".mjs" || ext === ".js") return "scripts/vendor";
1121
- if (ext === ".woff2" || ext === ".woff") return "assets/fonts";
1122
- if (ext === ".png" || ext === ".svg") return "assets/images";
1123
- return "assets/misc";
1124
- }
1125
- if (host.includes("framercanvas.com") || host.includes("framer.com")) {
1126
- if (ext === ".mjs" || ext === ".js") return "scripts/vendor";
1127
- if (ext === ".css") return "styles";
1128
- return "assets/misc";
1129
- }
1130
- return null;
1131
- }
1132
- };
1133
- }
1134
- });
1135
-
1136
- // src/platforms/webflow.ts
1137
- var IMG_EXTS, FONT_EXTS2, VIDEO_EXTS, webflow;
1138
- var init_webflow = __esm({
1139
- "src/platforms/webflow.ts"() {
1140
- "use strict";
1141
- IMG_EXTS = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif"];
1142
- FONT_EXTS2 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1143
- VIDEO_EXTS = [".mp4", ".webm", ".ogg"];
1144
- webflow = {
1145
- name: "webflow",
1146
- displayName: "Webflow",
1147
- detectByUrl(url) {
1148
- return /\.webflow\.(io|com)/.test(url);
1149
- },
1150
- detectByHtml(html) {
1151
- return html.includes("data-wf-site") || html.includes("w-webflow-badge") || html.includes("Webflow") || html.includes("webflow.js");
1152
- },
1153
- stripDomains: ["js-agent.newrelic.com", "cdn.heapanalytics.com", "tr.snapchat.com"],
1154
- stripSelectors: [".w-webflow-badge", 'link[href*="editorbar"]'],
1155
- stripPatterns: [
1156
- /<a[^>]*class="[^"]*w-webflow-badge[^"]*"[^>]*>[\s\S]*?<\/a>/g,
1157
- /<a[^>]*href="[^"]*webflow\.com\?utm_campaign=brandjs[^"]*"[^>]*>[\s\S]*?<\/a>/g,
1158
- /<style>[^<]*\.w-webflow-badge[^<]*<\/style>/g,
1159
- /Powered by <a[^>]*href="[^"]*webflow\.com"[^>]*>[^<]*<\/a>/g,
1160
- /<!-- This site was created in Webflow\.[^>]*-->/g,
1161
- /<html([^>]*) data-wf-domain="[^"]*"/g,
1162
- /<html([^>]*) data-wf-page="[^"]*"/g,
1163
- /<html([^>]*) data-wf-site="[^"]*"/g,
1164
- /<html([^>]*) data-wf-status="[^"]*"/g,
1165
- /<meta[^>]*content="Webflow"[^>]*>/g
1166
- ],
1167
- hydrationTimeout: 2e3,
1168
- needsHydrationCheck: false,
1169
- mapAssetDir(host, pathname, ext) {
1170
- if (host.includes("website-files.com") || host.includes("webflow.com") || host.includes("uploads-ssl.webflow.com")) {
1171
- if (VIDEO_EXTS.includes(ext)) return "assets/videos";
1172
- if (pathname.includes("/images/") || IMG_EXTS.includes(ext)) return "assets/images";
1173
- if (pathname.includes("/css/") || ext === ".css") return "styles";
1174
- if (pathname.includes("/js/") || ext === ".js") return "scripts/vendor";
1175
- if (pathname.includes("/gsap/") || pathname.includes("/plugins/")) return "scripts/vendor";
1176
- if (FONT_EXTS2.includes(ext)) return "assets/fonts";
1177
- return "assets/misc";
1178
- }
1179
- if (host.includes("d3e54v103j8qbb.cloudfront.net")) {
1180
- if (ext === ".js") return "scripts/vendor";
1181
- if (ext === ".css") return "styles";
1182
- if (IMG_EXTS.includes(ext)) return "assets/images";
1183
- return "assets/misc";
1184
- }
1185
- return null;
1186
- }
1187
- };
1188
- }
1189
- });
1190
-
1191
- // src/platforms/wix.ts
1192
- var IMG_EXTS2, FONT_EXTS3, VIDEO_EXTS2, wix;
1193
- var init_wix = __esm({
1194
- "src/platforms/wix.ts"() {
1195
- "use strict";
1196
- IMG_EXTS2 = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".avif", ".ico"];
1197
- FONT_EXTS3 = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
1198
- VIDEO_EXTS2 = [".mp4", ".webm", ".ogg"];
1199
- wix = {
1200
- name: "wix",
1201
- displayName: "Wix",
1202
- detectByUrl(url) {
1203
- return /\.wixsite\.com|\.wix\.com/.test(url);
1204
- },
1205
- detectByHtml(html) {
1206
- 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);
1207
- },
1208
- stripDomains: [
1209
- "frog.wix.com",
1210
- "bi.wixapi.com",
1211
- "fed.wixcodescheduler.com",
1212
- "editor.wix.com",
1213
- "panorama.wixapps.net"
1214
- ],
1215
- stripSelectors: ["#WIX_ADS", ".wix-ads", "#wix-badge", "#SCROLL_TO_TOP"],
1216
- stripPatterns: [
1217
- /<div[^>]*class="[^"]*wix-ads[^"]*"[^>]*>[\s\S]*?<\/div>/g
1218
- ],
1219
- hydrationTimeout: 3e3,
1220
- needsHydrationCheck: false,
1221
- mapAssetDir(host, pathname, ext) {
1222
- if (host === "video.wixstatic.com") {
1223
- return "assets/videos";
1224
- }
1225
- if (host.includes("wixstatic.com")) {
1226
- if (VIDEO_EXTS2.includes(ext)) return "assets/videos";
1227
- if (pathname.includes("/images/") || IMG_EXTS2.includes(ext)) return "assets/images";
1228
- if (ext === ".css") return "styles";
1229
- if (ext === ".js") return "scripts/vendor";
1230
- if (FONT_EXTS3.includes(ext)) return "assets/fonts";
1231
- return "assets/misc";
1232
- }
1233
- if (host.includes("parastorage.com")) {
1234
- if (ext === ".js") return "scripts/vendor";
1235
- if (ext === ".css") return "styles";
1236
- if (FONT_EXTS3.includes(ext)) return "assets/fonts";
1237
- if (IMG_EXTS2.includes(ext)) return "assets/images";
1238
- return "assets/misc";
1239
- }
1240
- return null;
1241
- }
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
+ );
1242
2880
  };
1243
2881
  }
1244
2882
  });
1245
2883
 
1246
- // src/platforms/detect.ts
1247
- function detectByUrl(url) {
1248
- for (const platform of ALL_PLATFORMS) {
1249
- 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";
1250
2896
  }
1251
- return null;
1252
- }
1253
- function detectByHtml(html) {
1254
- for (const platform of ALL_PLATFORMS) {
1255
- 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";
1256
2899
  }
1257
2900
  return null;
1258
2901
  }
1259
- function detectPlatform(url, html) {
1260
- const byUrl = detectByUrl(url);
1261
- if (byUrl) return byUrl;
1262
- if (html) {
1263
- const byHtml = detectByHtml(html);
1264
- if (byHtml) return byHtml;
1265
- }
1266
- return framer;
1267
- }
1268
- async function detectByDom(page) {
2902
+ async function detectAntiBotPage(page) {
1269
2903
  try {
1270
- const signal = await page.evaluate(() => {
1271
- const html = document.documentElement.outerHTML || "";
1272
- if (/wix-viewer-model|_wix_|wix-ecom/i.test(html)) return "wix";
1273
- if (/data-wf-|w-webflow-badge|webflow\.js/i.test(html)) return "webflow";
1274
- if (/framerusercontent|framercanvas|framerstatic/i.test(html)) return "framer";
1275
- const srcs = [];
1276
- const scripts = document.querySelectorAll("script[src]");
1277
- scripts.forEach((s) => {
1278
- if (s.src) srcs.push(s.src);
1279
- });
1280
- const allSrcs = srcs.join(" ");
1281
- if (/wixstatic\.com|parastorage\.com/.test(allSrcs)) return "wix";
1282
- if (/website-files\.com|webflow\.com/.test(allSrcs)) return "webflow";
1283
- 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
+ }
1284
2920
  return "";
1285
2921
  });
1286
- if (signal === "wix") return wix;
1287
- if (signal === "webflow") return webflow;
1288
- if (signal === "framer") return framer;
2922
+ return flagged || null;
1289
2923
  } catch {
2924
+ return null;
1290
2925
  }
1291
- return null;
1292
2926
  }
1293
- function getPlatformByName(name) {
1294
- const map = { framer, webflow, wix };
1295
- 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");
1296
2944
  }
1297
- var ALL_PLATFORMS;
1298
- var init_detect = __esm({
1299
- "src/platforms/detect.ts"() {
1300
- "use strict";
1301
- init_framer();
1302
- init_webflow();
1303
- init_wix();
1304
- ALL_PLATFORMS = [framer, webflow, wix];
1305
- }
1306
- });
1307
-
1308
- // src/platforms/index.ts
1309
- var platforms_exports = {};
1310
- __export(platforms_exports, {
1311
- detectByDom: () => detectByDom,
1312
- detectByHtml: () => detectByHtml,
1313
- detectByUrl: () => detectByUrl,
1314
- detectPlatform: () => detectPlatform,
1315
- framer: () => framer,
1316
- getPlatformByName: () => getPlatformByName,
1317
- webflow: () => webflow,
1318
- wix: () => wix
1319
- });
1320
- var init_platforms = __esm({
1321
- "src/platforms/index.ts"() {
2945
+ var AntiBotError;
2946
+ var init_anti_bot = __esm({
2947
+ "src/exporter/anti-bot.ts"() {
1322
2948
  "use strict";
1323
- init_framer();
1324
- init_webflow();
1325
- init_wix();
1326
- 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
+ };
1327
2962
  }
1328
2963
  });
1329
2964
 
@@ -1388,16 +3023,30 @@ async function launchAndCapture(exporter) {
1388
3023
  log("Platform refined from DOM: " + domDetected.displayName + " (override: " + exporter.platform.name + ")");
1389
3024
  exporter.platform = domDetected;
1390
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
+ }
1391
3039
  exporter.cooking?.update("Waiting for " + exporter.platform.displayName + " hydration...");
1392
3040
  if (exporter.platform.needsHydrationCheck) {
1393
- log("Checking for #main element hydration...");
3041
+ const sel = exporter.platform.hydrationSelector || "#main";
3042
+ log("Checking for " + sel + " element hydration...");
1394
3043
  log("Hydration timeout: " + exporter.platform.hydrationTimeout + "ms");
1395
3044
  const timeout = exporter.platform.hydrationTimeout;
1396
3045
  await exporter.page.evaluate(`
1397
3046
  new Promise(function(r) {
1398
3047
  var start = Date.now();
1399
3048
  var tick = function() {
1400
- var m = document.getElementById('main') || document.body;
3049
+ var m = document.querySelector(${JSON.stringify(sel)}) || document.body;
1401
3050
  if (m && m.children.length > 0) setTimeout(r, 2000);
1402
3051
  else if (Date.now() - start > ${timeout}) r();
1403
3052
  else setTimeout(tick, 200);
@@ -1411,27 +3060,38 @@ async function launchAndCapture(exporter) {
1411
3060
  await new Promise((r) => setTimeout(r, exporter.platform.hydrationTimeout));
1412
3061
  success(exporter.platform.displayName + " page rendered");
1413
3062
  }
1414
- exporter.cooking?.update("Scrolling page...");
1415
- log("Starting full-page scroll (step: " + CFG.scrollStep + "px, delay: " + CFG.scrollDelay + "ms)");
1416
- const scrollStep = CFG.scrollStep;
1417
- const scrollDelay = CFG.scrollDelay;
1418
- const pageHeight = await exporter.page.evaluate(`
1419
- Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
1420
- `);
1421
- log("Page height: " + pageHeight + "px (" + Math.ceil(pageHeight / scrollStep) + " scroll steps)");
1422
- await exporter.page.evaluate(`
1423
- new Promise(function(r) {
1424
- var y = 0;
1425
- var max = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
1426
- var step = function() {
1427
- y += ${scrollStep};
1428
- window.scrollTo({ top: y, behavior: 'instant' });
1429
- y < max + 500 ? setTimeout(step, ${scrollDelay}) : (window.scrollTo(0, 0), r());
1430
- };
1431
- step();
1432
- })
1433
- `);
1434
- 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
+ }
1435
3095
  exporter.cooking?.update("Waiting for lazy resources...");
1436
3096
  log("Waiting 2s for lazy-loaded resources...");
1437
3097
  await new Promise((r) => setTimeout(r, 2e3));
@@ -1442,6 +3102,19 @@ async function launchAndCapture(exporter) {
1442
3102
  } catch {
1443
3103
  log("Network idle timeout reached (continuing anyway)");
1444
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
+ }
1445
3118
  const totalCaptured = exporter.assets.buffers.size;
1446
3119
  success("Captured " + totalCaptured + " network resources total");
1447
3120
  const cssCount = [...exporter.assets.entries.values()].filter((e) => e.localPath.endsWith(".css")).length;
@@ -1485,6 +3158,7 @@ var init_capture = __esm({
1485
3158
  init_config();
1486
3159
  init_logger();
1487
3160
  init_platforms();
3161
+ init_anti_bot();
1488
3162
  }
1489
3163
  });
1490
3164
 
@@ -1680,6 +3354,9 @@ const ANSI = {
1680
3354
  border: '\\x1b[38;2;72;72;72m',
1681
3355
  success: '\\x1b[38;2;127;216;143m',
1682
3356
  info: '\\x1b[38;2;86;182;194m',
3357
+ accent: '\\x1b[38;2;157;124;216m',
3358
+ warning: '\\x1b[38;2;245;167;66m',
3359
+ command: '\\x1b[38;2;255;192;159m',
1683
3360
  error: '\\x1b[38;2;224;108;117m',
1684
3361
  };
1685
3362
 
@@ -1687,10 +3364,32 @@ function color(name, value) {
1687
3364
  return ANSI[name] + value + ANSI.reset;
1688
3365
  }
1689
3366
 
1690
- function frameLine(label, value) {
1691
- const text = ' ' + label.padEnd(10) + value;
1692
- const pad = Math.max(0, 56 - text.length);
1693
- console.log(' ' + color('border', '\u2502 ') + color('muted', label.padEnd(10)) + color('text', value) + ' '.repeat(pad) + color('border', ' \u2502'));
3367
+ function fitValue(value, width) {
3368
+ const text = String(value);
3369
+ if (text.length <= width) return text;
3370
+ if ((text.includes('\\\\') || text.includes('/')) && width > 12) {
3371
+ const separator = text.includes('\\\\') ? '\\\\' : '/';
3372
+ const prefix = text.startsWith(separator) ? separator : '';
3373
+ const parts = text.split(/[\\\\/]+/).filter(Boolean);
3374
+ const compact = prefix + parts.map((part, index) => {
3375
+ if (index === parts.length - 1) return part;
3376
+ if (/^[A-Za-z]:$/.test(part)) return part;
3377
+ return part.length > 5 ? part.slice(0, 3) + '..' : part;
3378
+ }).join(separator);
3379
+ if (compact.length <= width) return compact;
3380
+ const tail = Math.max(8, Math.floor(width * 0.45));
3381
+ const head = Math.max(0, width - tail - 4);
3382
+ return compact.slice(0, head) + separator + '..' + separator + compact.slice(-tail);
3383
+ }
3384
+ return width <= 3 ? '.'.repeat(Math.max(0, width)) : text.slice(0, width - 2) + '..';
3385
+ }
3386
+
3387
+ function frameLine(label, value, colorName = 'text') {
3388
+ const labelText = label.padEnd(10);
3389
+ const valueWidth = Math.max(0, 56 - labelText.length);
3390
+ const shown = fitValue(value, valueWidth);
3391
+ const pad = Math.max(0, 56 - labelText.length - shown.length);
3392
+ console.log(' ' + color('border', '\u2502 ') + color('muted', labelText) + color(colorName, shown) + ' '.repeat(pad) + color('border', ' \u2502'));
1694
3393
  }
1695
3394
 
1696
3395
  function drawServerUi() {
@@ -1698,10 +3397,10 @@ function drawServerUi() {
1698
3397
  console.log(' ' + color('border', '\u256D\u2500' + '\u2500'.repeat(56) + '\u2500\u256E'));
1699
3398
  console.log(' ' + color('border', '\u2502 ') + color('text', ' Framer Export preview server') + ' '.repeat(26) + color('border', ' \u2502'));
1700
3399
  console.log(' ' + color('border', '\u251C\u2500' + '\u2500'.repeat(56) + '\u2500\u2524'));
1701
- frameLine('URL', 'http://localhost:' + PORT);
1702
- frameLine('Root', ROOT);
1703
- frameLine('Run', 'node serve.js');
1704
- frameLine('Mode', 'static mirror + SPA fallback');
3400
+ frameLine('URL', 'http://localhost:' + PORT, 'primary');
3401
+ frameLine('Root', ROOT, 'soft');
3402
+ frameLine('Run', 'node serve.js', 'command');
3403
+ frameLine('Mode', 'static mirror + SPA fallback', 'accent');
1705
3404
  console.log(' ' + color('border', '\u2570\u2500' + '\u2500'.repeat(56) + '\u2500\u256F'));
1706
3405
  console.log('');
1707
3406
  console.log(' ' + color('success', 'ready') + color('muted', ' press Ctrl+C to stop') + '\\n');
@@ -1710,11 +3409,13 @@ function drawServerUi() {
1710
3409
  server.listen(PORT, () => {
1711
3410
  const frames = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
1712
3411
  const steps = ['Preparing server', 'Checking exported files', 'Binding local port', 'Serving static mirror'];
3412
+ const stepColors = ['text', 'info', 'warning', 'success'];
1713
3413
  let i = 0;
1714
3414
  const timer = setInterval(() => {
1715
- const step = steps[Math.min(steps.length - 1, Math.floor(i / 6))];
3415
+ const stepIndex = Math.min(steps.length - 1, Math.floor(i / 6));
3416
+ const step = steps[stepIndex];
1716
3417
  const dots = color('error', '.'.repeat(i % 4).padEnd(3, ' '));
1717
- process.stdout.write('\\r\\x1B[2K ' + color('primary', frames[i % frames.length]) + ' ' + color('text', step) + dots);
3418
+ process.stdout.write('\\r\\x1B[2K ' + color('primary', frames[i % frames.length]) + ' ' + color(stepColors[stepIndex], step) + dots);
1718
3419
  i++;
1719
3420
  }, 80);
1720
3421
 
@@ -1871,12 +3572,36 @@ async function buildOutput(exporter) {
1871
3572
  log(" Pattern removed " + removed + " chars");
1872
3573
  }
1873
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
+ }
1874
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
+ }
1875
3594
  exporter.cooking?.update("Rewriting asset URLs...");
1876
3595
  log("Rewriting " + exporter.assets.entries.size + " CDN URLs to local paths...");
1877
3596
  const beforeRewrite = html.length;
1878
3597
  html = exporter.assets.rewrite(html, "");
1879
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
+ }
1880
3605
  exporter.cooking?.update("Cleaning srcset references...");
1881
3606
  html = stripSrcsetCdnUrls(html);
1882
3607
  log("Cleaned remaining CDN URLs from srcset attributes");
@@ -2172,7 +3897,7 @@ function buildServeCommand(exporter) {
2172
3897
  return `cd ${path5.basename(exporter.outDir)} && node serve.js`;
2173
3898
  }
2174
3899
  async function runExportCompletePrompt(exporter, serveCommand) {
2175
- let copyServeCommand = false;
3900
+ let copyServeCommand = true;
2176
3901
  while (true) {
2177
3902
  const action = await select(
2178
3903
  "Export complete",
@@ -2613,6 +4338,7 @@ var init_exporter = __esm({
2613
4338
  init_download();
2614
4339
  init_logger();
2615
4340
  init_capture();
4341
+ init_anti_bot();
2616
4342
  init_download2();
2617
4343
  init_output();
2618
4344
  init_summary();
@@ -2689,15 +4415,35 @@ var init_exporter = __esm({
2689
4415
  this.platform = htmlDetected;
2690
4416
  log("Platform refined: " + ui.primary(this.platform.displayName) + " (from HTML analysis)");
2691
4417
  }
2692
- await launchAndCapture(this);
2693
- if (includeSubpages && this.page) {
2694
- 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;
2695
4446
  }
2696
- await closeBrowser(this);
2697
- this.cooking.update("Downloading assets...");
2698
- await downloadAll(this);
2699
- this.cooking.update("Building output...");
2700
- await buildOutput(this);
2701
4447
  this.cooking.stop();
2702
4448
  setCooking(null);
2703
4449
  console.log("");
@@ -2768,6 +4514,70 @@ var init_exporter = __esm({
2768
4514
  }
2769
4515
  });
2770
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
+
2771
4581
  // src/cli/setup.ts
2772
4582
  var setup_exports = {};
2773
4583
  __export(setup_exports, {
@@ -2777,7 +4587,7 @@ import readline2 from "readline/promises";
2777
4587
  import { stdin as stdin3, stdout as stdout3 } from "process";
2778
4588
  import path7 from "path";
2779
4589
  import { URL as URL4 } from "url";
2780
- import chalk6 from "chalk";
4590
+ import chalk7 from "chalk";
2781
4591
  function drawHeader(title) {
2782
4592
  const w = maxWidth();
2783
4593
  if (w < 50) {
@@ -2794,7 +4604,7 @@ async function runSetup(legacyMode = false) {
2794
4604
  showBanner();
2795
4605
  console.log(` ${ui.text.bold("Framer Export setup")} ${chip("interactive")}`);
2796
4606
  console.log(
2797
- ` ${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.")}
2798
4608
  `
2799
4609
  );
2800
4610
  const rl = legacyMode ? readline2.createInterface({ input: stdin3, output: stdout3 }) : null;
@@ -2802,7 +4612,7 @@ async function runSetup(legacyMode = false) {
2802
4612
  if (!legacyMode) {
2803
4613
  return promptInput(question, defaultVal || "", { headerLines });
2804
4614
  }
2805
- const suffix = defaultVal ? chalk6.gray(` (${defaultVal})`) : "";
4615
+ const suffix = defaultVal ? chalk7.gray(` (${defaultVal})`) : "";
2806
4616
  const prompt = ` ${ui.primary("\u25CF")} ${ui.text.bold(question)}${suffix} ${ui.muted(">")} `;
2807
4617
  const answer = await rl.question(prompt);
2808
4618
  return answer.trim() || defaultVal || "";
@@ -2829,7 +4639,7 @@ async function runSetup(legacyMode = false) {
2829
4639
  }
2830
4640
  }
2831
4641
  if (legacyMode) {
2832
- console.log(` ${ui.success("\u2713")} ${ui.success("URL:")} ${chalk6.underline(siteUrl)}
4642
+ console.log(` ${ui.success("\u2713")} ${ui.success("URL:")} ${chalk7.underline(siteUrl)}
2833
4643
  `);
2834
4644
  }
2835
4645
  let platformName = null;
@@ -2837,31 +4647,15 @@ async function runSetup(legacyMode = false) {
2837
4647
  drawHeader("Step 2 : Platform");
2838
4648
  const detected = detectPlatform(siteUrl);
2839
4649
  console.log(` ${ui.info("i")} Auto-detected: ${ui.primary(detected.displayName)}`);
2840
- const platformInput = await ask("Platform (framer/webflow/wix)", detected.name);
2841
- 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;
2842
4653
  console.log(` ${ui.success("\u2713")} ${ui.success("Platform:")} ${ui.primary(platformName)}
2843
4654
  `);
2844
4655
  } else {
2845
4656
  while (!platformName) {
2846
- const detected = detectPlatform(siteUrl);
2847
- const platforms = [
2848
- {
2849
- label: `Framer${detected.name === "framer" ? chalk6.gray(" (detected)") : ""}`,
2850
- value: "framer"
2851
- },
2852
- {
2853
- label: `Webflow${detected.name === "webflow" ? chalk6.gray(" (detected)") : ""}`,
2854
- value: "webflow"
2855
- },
2856
- { label: `Wix${detected.name === "wix" ? chalk6.gray(" (detected)") : ""}`, value: "wix" }
2857
- ];
2858
- const defaultIdx = ["framer", "webflow", "wix"].indexOf(detected.name);
2859
- const platformChoice = await select("Select platform", platforms, Math.max(0, defaultIdx), {
2860
- headerLines: [`URL: ${siteUrl}`],
2861
- actions: [{ label: "Modify URL", value: "modify-url" }],
2862
- footer: "tab focus button \xB7 mouse hover/click \xB7 enter select"
2863
- });
2864
- if (platformChoice === "modify-url") {
4657
+ const selection = await selectPlatform(siteUrl);
4658
+ if ("modifyUrl" in selection) {
2865
4659
  siteUrl = "";
2866
4660
  urlError = "";
2867
4661
  while (!siteUrl) {
@@ -2880,7 +4674,7 @@ async function runSetup(legacyMode = false) {
2880
4674
  }
2881
4675
  continue;
2882
4676
  }
2883
- platformName = platformChoice;
4677
+ platformName = selection.platform;
2884
4678
  }
2885
4679
  }
2886
4680
  if (!platformName) {
@@ -2891,26 +4685,62 @@ async function runSetup(legacyMode = false) {
2891
4685
  if (!legacyMode) {
2892
4686
  return promptInput(question, defaultVal || "", { headerLines });
2893
4687
  }
2894
- const suffix = defaultVal ? chalk6.gray(` (${defaultVal})`) : "";
4688
+ const suffix = defaultVal ? chalk7.gray(` (${defaultVal})`) : "";
2895
4689
  const prompt = ` ${ui.primary("\u25CF")} ${ui.text.bold(question)}${suffix} ${ui.muted(">")} `;
2896
4690
  const answer = await rl2.question(prompt);
2897
4691
  return answer.trim() || defaultVal || "";
2898
4692
  };
2899
4693
  const derivedName = deriveOutputName(siteUrl, platformName);
2900
- if (legacyMode) drawHeader("Step 3 : Output Directory");
2901
- const outDir = await ask2("Output directory", "./" + derivedName, [
2902
- "Step 3 : Output Directory",
2903
- `URL: ${siteUrl}`,
2904
- `Platform: ${platformName}`
2905
- ]);
4694
+ let outDir = "./" + derivedName;
2906
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
+ ]);
2907
4702
  console.log(` ${ui.success("\u2713")} ${ui.success("Output:")} ${ui.primary(outDir)}
2908
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
+ ]);
2909
4711
  }
2910
- if (legacyMode) drawHeader("Step 4 : Options");
2911
- let prettyPrint;
2912
- let concurrency;
2913
- 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
+ };
2914
4744
  if (legacyMode) {
2915
4745
  const prettyAnswer = await ask2("Pretty-print JS files? (y/n)", "y");
2916
4746
  prettyPrint = prettyAnswer.toLowerCase().startsWith("y");
@@ -2928,67 +4758,104 @@ async function runSetup(legacyMode = false) {
2928
4758
  concurrency = parseInt(concurrencyAnswer, 10) || 12;
2929
4759
  console.log(` ${ui.success("\u2713")} Concurrency: ${ui.primary(String(concurrency))}
2930
4760
  `);
2931
- } else {
2932
- const prettyVal = await select("Pretty-print JS files?", [
2933
- { label: "Yes", value: "yes" },
2934
- { label: "No", value: "no" }
2935
- ]);
2936
- prettyPrint = prettyVal === "yes";
2937
- const subpagesVal = await select(
2938
- "Export sub-pages?",
2939
- [
2940
- { label: "No", value: "no" },
2941
- { label: "Yes, crawl and export", value: "yes" }
2942
- ],
2943
- 0
2944
- );
2945
- includeSubpages = subpagesVal === "yes";
2946
- const concurrencyVal = await select(
2947
- "Download concurrency",
2948
- [
2949
- { label: "6 (slow connection)", value: "6" },
2950
- { label: "12 (default)", value: "12" },
2951
- { label: "20 (fast connection)", value: "20" }
2952
- ],
2953
- 1
2954
- );
2955
- concurrency = parseInt(concurrencyVal, 10);
2956
- }
2957
- const w = maxWidth();
2958
- const isSmall = w < 50;
2959
- console.log("");
2960
- if (!isSmall) {
2961
- console.log(boxTop(w));
2962
- console.log(boxLine(w, ui.text.bold(" Summary")));
2963
- console.log(boxSep(w));
2964
- } else {
2965
- console.log(ui.text.bold(" Summary:"));
2966
- }
2967
- for (const [label, value] of [
2968
- ["URL", siteUrl],
2969
- ["Platform", platformName],
2970
- ["Output", path7.resolve(outDir)],
2971
- ["Pretty-print", prettyPrint ? "yes" : "no"],
2972
- ["Sub-pages", includeSubpages ? "yes" : "no"],
2973
- ["Concurrency", String(concurrency)]
2974
- ]) {
2975
- console.log(boxRow(w, label, value));
2976
- }
2977
- if (!isSmall) {
2978
- console.log(boxBot(w));
2979
- }
2980
- console.log("");
2981
- let startExport;
2982
- if (legacyMode) {
4761
+ printSummary2();
2983
4762
  const confirm = await ask2("Start export? (y/n)", "y");
2984
4763
  startExport = confirm.toLowerCase().startsWith("y");
2985
4764
  rl2.close();
2986
4765
  } else {
2987
- const confirmVal = await select("Start export?", [
2988
- { label: "Yes, start now", value: "yes" },
2989
- { label: "Cancel", value: "no" }
2990
- ]);
2991
- 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
+ }
2992
4859
  }
2993
4860
  if (!startExport) {
2994
4861
  console.log(`
@@ -3001,7 +4868,16 @@ async function runSetup(legacyMode = false) {
3001
4868
  CFG2.concurrency = concurrency;
3002
4869
  const exporter = new FramerExporter(siteUrl, path7.resolve(outDir), platformName);
3003
4870
  exporter.prettyPrint = prettyPrint;
3004
- 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
+ }
3005
4881
  }
3006
4882
  var init_setup = __esm({
3007
4883
  "src/cli/setup.ts"() {
@@ -3010,6 +4886,7 @@ var init_setup = __esm({
3010
4886
  init_exporter();
3011
4887
  init_platforms();
3012
4888
  init_select();
4889
+ init_select_grouped();
3013
4890
  init_box();
3014
4891
  init_theme();
3015
4892
  }
@@ -3024,6 +4901,7 @@ import { spawnSync } from "child_process";
3024
4901
  // src/cli/help.ts
3025
4902
  init_banner();
3026
4903
  init_theme();
4904
+ init_platforms();
3027
4905
  function showHelp() {
3028
4906
  showBanner();
3029
4907
  console.log(`${ui.text.bold(" USAGE")} ${chip("cli")}
@@ -3038,7 +4916,7 @@ function showHelp() {
3038
4916
  console.log(ui.text.bold(" OPTIONS\n"));
3039
4917
  const opts = [
3040
4918
  ["--setup", "Launch the interactive setup assistant"],
3041
- ["--platform <p>", "Force platform: framer, webflow, wix"],
4919
+ ["--platform <p>", "Force platform by id (e.g. framer, shopify, notion)"],
3042
4920
  ["--subpages", "Crawl and export sub-pages"],
3043
4921
  ["--legacy-mode", "Use text input instead of arrow selection"],
3044
4922
  ["--help, -h", "Show this help message"]
@@ -3048,13 +4926,10 @@ function showHelp() {
3048
4926
  }
3049
4927
  console.log("");
3050
4928
  console.log(ui.text.bold(" SUPPORTED PLATFORMS\n"));
3051
- const platforms = [
3052
- ["Framer", "Auto-detected via .framer.app / .framer.website URLs"],
3053
- ["Webflow", "Auto-detected via .webflow.io URLs"],
3054
- ["Wix", "Auto-detected via .wixsite.com URLs + HTML analysis"]
3055
- ];
3056
- for (const [name, desc] of platforms) {
3057
- 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)}`);
3058
4933
  }
3059
4934
  console.log("");
3060
4935
  console.log(ui.text.bold(" EXAMPLES\n"));
@@ -3203,20 +5078,20 @@ async function main() {
3203
5078
  process.exit(0);
3204
5079
  }
3205
5080
  if (args.includes("--about")) {
3206
- const chalk7 = (await import("chalk")).default;
5081
+ const chalk8 = (await import("chalk")).default;
3207
5082
  showBanner();
3208
5083
  console.log(` ${ui.text.bold("Framer Export")} ${ui.muted(`v${package_default.version}`)}`);
3209
5084
  console.log(` ${ui.text(package_default.description)}
3210
5085
  `);
3211
5086
  console.log(` ${ui.text.bold("Author")} ${ui.primary("Dany (danbenba)")}`);
3212
5087
  console.log(
3213
- ` ${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")}`
3214
5089
  );
3215
5090
  console.log(
3216
- ` ${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", ""))}`
3217
5092
  );
3218
5093
  console.log(
3219
- ` ${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}`)}`
3220
5095
  );
3221
5096
  console.log(` ${ui.text.bold("License")} ${ui.success(package_default.license)}`);
3222
5097
  console.log(` ${ui.text.bold("Node")} ${ui.muted(`>=${package_default.engines.node}`)}`);
@@ -3263,12 +5138,17 @@ async function main() {
3263
5138
  includeSubpages
3264
5139
  );
3265
5140
  } catch (e) {
3266
- 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;
3267
5147
  console.log(
3268
5148
  `
3269
5149
  ${ui.error("\u2717")} ${ui.error.bold("FAILED:")} ${ui.text(e.message)}`
3270
5150
  );
3271
- console.log(chalk7.gray(e.stack || ""));
5151
+ console.log(chalk8.gray(e.stack || ""));
3272
5152
  process.exit(1);
3273
5153
  }
3274
5154
  }