@see-ms/converter 0.1.1 → 0.1.3

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/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/converter.ts
2
2
  import pc3 from "picocolors";
3
- import fs4 from "fs-extra";
3
+ import path11 from "path";
4
+ import fs9 from "fs-extra";
4
5
 
5
6
  // src/filesystem.ts
6
7
  import fs from "fs-extra";
@@ -319,9 +320,86 @@ ${styles}`, "utf-8");
319
320
  }
320
321
  }
321
322
 
322
- // src/boilerplate.ts
323
+ // src/editor-integration.ts
323
324
  import fs3 from "fs-extra";
324
325
  import path4 from "path";
326
+ async function createEditorPlugin(outputDir) {
327
+ const pluginsDir = path4.join(outputDir, "plugins");
328
+ await fs3.ensureDir(pluginsDir);
329
+ const pluginContent = `/**
330
+ * CMS Editor Overlay Plugin
331
+ * Loads the inline editor when ?preview=true
332
+ */
333
+
334
+ export default defineNuxtPlugin(() => {
335
+ // Only run on client side
336
+ if (process.server) return;
337
+
338
+ // Check for preview mode
339
+ const params = new URLSearchParams(window.location.search);
340
+
341
+ if (params.get('preview') === 'true') {
342
+ // Dynamically import the editor
343
+ import('@see-ms/editor-overlay').then(({ initEditor, createToolbar }) => {
344
+ const editor = initEditor({
345
+ apiEndpoint: '/api/cms/save',
346
+ richText: true,
347
+ });
348
+
349
+ editor.enable();
350
+
351
+ const toolbar = createToolbar(editor);
352
+ document.body.appendChild(toolbar);
353
+ });
354
+ }
355
+ });
356
+ `;
357
+ const pluginPath = path4.join(pluginsDir, "cms-editor.client.ts");
358
+ await fs3.writeFile(pluginPath, pluginContent, "utf-8");
359
+ }
360
+ async function addEditorDependency(outputDir) {
361
+ const packageJsonPath = path4.join(outputDir, "package.json");
362
+ if (await fs3.pathExists(packageJsonPath)) {
363
+ const packageJson = await fs3.readJson(packageJsonPath);
364
+ if (!packageJson.dependencies) {
365
+ packageJson.dependencies = {};
366
+ }
367
+ packageJson.dependencies["@see-ms/editor-overlay"] = "^0.1.1";
368
+ await fs3.writeJson(packageJsonPath, packageJson, { spaces: 2 });
369
+ }
370
+ }
371
+ async function createSaveEndpoint(outputDir) {
372
+ const serverDir = path4.join(outputDir, "server", "api", "cms");
373
+ await fs3.ensureDir(serverDir);
374
+ const endpointContent = `/**
375
+ * API endpoint for saving CMS changes
376
+ */
377
+
378
+ export default defineEventHandler(async (event) => {
379
+ const body = await readBody(event);
380
+
381
+ // TODO: Implement actual saving to Strapi
382
+ // For now, just log the changes
383
+ console.log('CMS changes:', body);
384
+
385
+ // In production, this would:
386
+ // 1. Validate the changes
387
+ // 2. Send to Strapi API
388
+ // 3. Return success/error
389
+
390
+ return {
391
+ success: true,
392
+ message: 'Changes saved (demo mode)',
393
+ };
394
+ });
395
+ `;
396
+ const endpointPath = path4.join(serverDir, "save.post.ts");
397
+ await fs3.writeFile(endpointPath, endpointContent, "utf-8");
398
+ }
399
+
400
+ // src/boilerplate.ts
401
+ import fs4 from "fs-extra";
402
+ import path5 from "path";
325
403
  import { execSync as execSync2 } from "child_process";
326
404
  import pc2 from "picocolors";
327
405
  function isGitHubURL(source) {
@@ -331,8 +409,8 @@ async function cloneFromGitHub(repoUrl, outputDir) {
331
409
  console.log(pc2.blue(" Cloning from GitHub..."));
332
410
  try {
333
411
  execSync2(`git clone ${repoUrl} ${outputDir}`, { stdio: "inherit" });
334
- const gitDir = path4.join(outputDir, ".git");
335
- await fs3.remove(gitDir);
412
+ const gitDir = path5.join(outputDir, ".git");
413
+ await fs4.remove(gitDir);
336
414
  console.log(pc2.green(" \u2713 Boilerplate cloned successfully"));
337
415
  } catch (error) {
338
416
  throw new Error(`Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`);
@@ -340,13 +418,13 @@ async function cloneFromGitHub(repoUrl, outputDir) {
340
418
  }
341
419
  async function copyFromLocal(sourcePath, outputDir) {
342
420
  console.log(pc2.blue(" Copying from local path..."));
343
- const sourceExists = await fs3.pathExists(sourcePath);
421
+ const sourceExists = await fs4.pathExists(sourcePath);
344
422
  if (!sourceExists) {
345
423
  throw new Error(`Local boilerplate not found: ${sourcePath}`);
346
424
  }
347
- await fs3.copy(sourcePath, outputDir, {
425
+ await fs4.copy(sourcePath, outputDir, {
348
426
  filter: (src) => {
349
- const name = path4.basename(src);
427
+ const name = path5.basename(src);
350
428
  return !["node_modules", ".nuxt", ".output", ".git", "dist"].includes(name);
351
429
  }
352
430
  });
@@ -355,25 +433,25 @@ async function copyFromLocal(sourcePath, outputDir) {
355
433
  async function setupBoilerplate(boilerplateSource, outputDir) {
356
434
  if (!boilerplateSource) {
357
435
  console.log(pc2.blue("\n\u{1F4E6} Creating minimal Nuxt structure..."));
358
- await fs3.ensureDir(outputDir);
359
- await fs3.ensureDir(path4.join(outputDir, "pages"));
360
- await fs3.ensureDir(path4.join(outputDir, "assets"));
361
- await fs3.ensureDir(path4.join(outputDir, "public"));
362
- await fs3.ensureDir(path4.join(outputDir, "utils"));
363
- const configPath = path4.join(outputDir, "nuxt.config.ts");
364
- const configExists = await fs3.pathExists(configPath);
436
+ await fs4.ensureDir(outputDir);
437
+ await fs4.ensureDir(path5.join(outputDir, "pages"));
438
+ await fs4.ensureDir(path5.join(outputDir, "assets"));
439
+ await fs4.ensureDir(path5.join(outputDir, "public"));
440
+ await fs4.ensureDir(path5.join(outputDir, "utils"));
441
+ const configPath = path5.join(outputDir, "nuxt.config.ts");
442
+ const configExists = await fs4.pathExists(configPath);
365
443
  if (!configExists) {
366
444
  const basicConfig = `export default defineNuxtConfig({
367
445
  devtools: { enabled: true },
368
446
  css: [],
369
447
  })
370
448
  `;
371
- await fs3.writeFile(configPath, basicConfig, "utf-8");
449
+ await fs4.writeFile(configPath, basicConfig, "utf-8");
372
450
  }
373
451
  console.log(pc2.green(" \u2713 Structure created"));
374
452
  return;
375
453
  }
376
- const outputExists = await fs3.pathExists(outputDir);
454
+ const outputExists = await fs4.pathExists(outputDir);
377
455
  if (outputExists) {
378
456
  throw new Error(`Output directory already exists: ${outputDir}. Please choose a different path or remove it first.`);
379
457
  }
@@ -385,6 +463,652 @@ async function setupBoilerplate(boilerplateSource, outputDir) {
385
463
  }
386
464
  }
387
465
 
466
+ // src/manifest.ts
467
+ import fs6 from "fs-extra";
468
+ import path7 from "path";
469
+
470
+ // src/detector.ts
471
+ import * as cheerio2 from "cheerio";
472
+ import fs5 from "fs-extra";
473
+ import path6 from "path";
474
+ function cleanClassName(className) {
475
+ return className.split(" ").filter((cls) => !cls.startsWith("c-") && !cls.startsWith("w-")).filter((cls) => cls.length > 0).join(" ");
476
+ }
477
+ function getPrimaryClass(classAttr) {
478
+ if (!classAttr) return null;
479
+ const cleaned = cleanClassName(classAttr);
480
+ const classes = cleaned.split(" ").filter((c) => c.length > 0);
481
+ if (classes.length === 0) return null;
482
+ const original = classes[0];
483
+ return {
484
+ selector: original,
485
+ // Keep original with dashes for CSS selector
486
+ fieldName: original.replace(/-/g, "_")
487
+ // Normalize for field name
488
+ };
489
+ }
490
+ function getContextModifier(_$, $el) {
491
+ let $current = $el.parent();
492
+ let depth = 0;
493
+ while ($current.length > 0 && depth < 5) {
494
+ const classes = $current.attr("class");
495
+ if (classes) {
496
+ const ccClass = classes.split(" ").find((c) => c.startsWith("cc-"));
497
+ if (ccClass) {
498
+ return ccClass.replace("cc-", "").replace(/-/g, "_");
499
+ }
500
+ }
501
+ $current = $current.parent();
502
+ depth++;
503
+ }
504
+ return null;
505
+ }
506
+ function isDecorativeImage(_$, $img) {
507
+ const $parent = $img.parent();
508
+ const parentClass = $parent.attr("class") || "";
509
+ const decorativePatterns = [
510
+ "nav",
511
+ "logo",
512
+ "icon",
513
+ "arrow",
514
+ "button",
515
+ "quote",
516
+ "pagination",
517
+ "footer",
518
+ "link"
519
+ ];
520
+ return decorativePatterns.some(
521
+ (pattern) => parentClass.includes(pattern) || parentClass.includes(`${pattern}_`)
522
+ );
523
+ }
524
+ function isInsideButton($, el) {
525
+ const $el = $(el);
526
+ const $button = $el.closest("button, a, NuxtLink, .c_button, .c_icon_button");
527
+ return $button.length > 0;
528
+ }
529
+ function extractTemplateFromVue(vueContent) {
530
+ const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
531
+ if (!templateMatch) {
532
+ return "";
533
+ }
534
+ return templateMatch[1];
535
+ }
536
+ function detectEditableFields(templateHtml) {
537
+ const $ = cheerio2.load(templateHtml);
538
+ const detectedFields = {};
539
+ const detectedCollections = {};
540
+ const collectionElements = /* @__PURE__ */ new Set();
541
+ const processedCollectionClasses = /* @__PURE__ */ new Set();
542
+ const potentialCollections = /* @__PURE__ */ new Map();
543
+ $("[class]").each((_, el) => {
544
+ const primaryClass = getPrimaryClass($(el).attr("class"));
545
+ if (primaryClass && (primaryClass.fieldName.includes("card") || primaryClass.fieldName.includes("item") || primaryClass.fieldName.includes("post") || primaryClass.fieldName.includes("feature")) && !primaryClass.fieldName.includes("image") && !primaryClass.fieldName.includes("inner")) {
546
+ if (!potentialCollections.has(primaryClass.fieldName)) {
547
+ potentialCollections.set(primaryClass.fieldName, []);
548
+ }
549
+ potentialCollections.get(primaryClass.fieldName)?.push(el);
550
+ }
551
+ });
552
+ potentialCollections.forEach((elements, className) => {
553
+ if (elements.length >= 2) {
554
+ const $first = $(elements[0]);
555
+ const collectionFields = {};
556
+ processedCollectionClasses.add(className);
557
+ elements.forEach((el) => {
558
+ collectionElements.add(el);
559
+ $(el).find("*").each((_, child) => {
560
+ collectionElements.add(child);
561
+ });
562
+ });
563
+ const collectionClassInfo = getPrimaryClass($(elements[0]).attr("class"));
564
+ const collectionSelector = collectionClassInfo ? `.${collectionClassInfo.selector}` : `.${className}`;
565
+ $first.find("img").each((_, img) => {
566
+ if (isInsideButton($, img)) return;
567
+ const $img = $(img);
568
+ const $parent = $img.parent();
569
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
570
+ if (parentClassInfo && parentClassInfo.fieldName.includes("image")) {
571
+ collectionFields.image = `.${parentClassInfo.selector}`;
572
+ return false;
573
+ }
574
+ });
575
+ $first.find("div").each((_, el) => {
576
+ const classInfo = getPrimaryClass($(el).attr("class"));
577
+ if (classInfo && classInfo.fieldName.includes("tag") && !classInfo.fieldName.includes("container")) {
578
+ collectionFields.tag = `.${classInfo.selector}`;
579
+ return false;
580
+ }
581
+ });
582
+ $first.find("h1, h2, h3, h4, h5, h6").first().each((_, el) => {
583
+ const classInfo = getPrimaryClass($(el).attr("class"));
584
+ if (classInfo) {
585
+ collectionFields.title = `.${classInfo.selector}`;
586
+ }
587
+ });
588
+ $first.find("p").first().each((_, el) => {
589
+ const classInfo = getPrimaryClass($(el).attr("class"));
590
+ if (classInfo) {
591
+ collectionFields.description = `.${classInfo.selector}`;
592
+ }
593
+ });
594
+ $first.find("a, NuxtLink").not(".c_button, .c_icon_button").each((_, el) => {
595
+ const $link = $(el);
596
+ const linkText = $link.text().trim();
597
+ if (linkText) {
598
+ const classInfo = getPrimaryClass($link.attr("class"));
599
+ collectionFields.link = classInfo ? `.${classInfo.selector}` : "a";
600
+ return false;
601
+ }
602
+ });
603
+ if (Object.keys(collectionFields).length > 0) {
604
+ let collectionName = className;
605
+ if (!collectionName.endsWith("s")) {
606
+ collectionName += "s";
607
+ }
608
+ detectedCollections[collectionName] = {
609
+ selector: collectionSelector,
610
+ fields: collectionFields
611
+ };
612
+ }
613
+ }
614
+ });
615
+ const $body = $("body");
616
+ $body.find("h1, h2, h3, h4, h5, h6").each((index, el) => {
617
+ if (collectionElements.has(el)) return;
618
+ const $el = $(el);
619
+ const text = $el.text().trim();
620
+ const classInfo = getPrimaryClass($el.attr("class"));
621
+ if (text) {
622
+ let fieldName;
623
+ let selector;
624
+ if (classInfo && !classInfo.fieldName.startsWith("heading_")) {
625
+ fieldName = classInfo.fieldName;
626
+ selector = `.${classInfo.selector}`;
627
+ } else {
628
+ const $parent = $el.closest('[class*="header"], [class*="hero"], [class*="cta"]').first();
629
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
630
+ const modifier = getContextModifier($, $el);
631
+ if (parentClassInfo) {
632
+ fieldName = modifier ? `${modifier}_${parentClassInfo.fieldName}` : parentClassInfo.fieldName;
633
+ selector = classInfo ? `.${classInfo.selector}` : `.${parentClassInfo.selector}`;
634
+ } else if (modifier) {
635
+ fieldName = `${modifier}_heading`;
636
+ selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
637
+ } else {
638
+ fieldName = `heading_${index}`;
639
+ selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
640
+ }
641
+ }
642
+ detectedFields[fieldName] = {
643
+ selector,
644
+ type: "plain",
645
+ editable: true
646
+ };
647
+ }
648
+ });
649
+ $body.find("p").each((_index, el) => {
650
+ if (collectionElements.has(el)) return;
651
+ const $el = $(el);
652
+ const text = $el.text().trim();
653
+ const classInfo = getPrimaryClass($el.attr("class"));
654
+ if (text && text.length > 20 && classInfo) {
655
+ const hasFormatting = $el.find("strong, em, b, i, a, NuxtLink").length > 0;
656
+ detectedFields[classInfo.fieldName] = {
657
+ selector: `.${classInfo.selector}`,
658
+ type: hasFormatting ? "rich" : "plain",
659
+ editable: true
660
+ };
661
+ }
662
+ });
663
+ $body.find("img").each((_index, el) => {
664
+ if (collectionElements.has(el)) return;
665
+ if (isInsideButton($, el)) return;
666
+ const $el = $(el);
667
+ if (isDecorativeImage($, $el)) return;
668
+ const $parent = $el.parent();
669
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
670
+ if (parentClassInfo) {
671
+ const fieldName = parentClassInfo.fieldName.includes("image") ? parentClassInfo.fieldName : `${parentClassInfo.fieldName}_image`;
672
+ detectedFields[fieldName] = {
673
+ selector: `.${parentClassInfo.selector}`,
674
+ type: "image",
675
+ editable: true
676
+ };
677
+ }
678
+ });
679
+ $body.find("NuxtLink.c_button, a.c_button, .c_button").each((_index, el) => {
680
+ if (collectionElements.has(el)) return;
681
+ const $el = $(el);
682
+ const text = $el.contents().filter(function() {
683
+ return this.type === "text" || this.type === "tag" && this.name === "div";
684
+ }).first().text().trim();
685
+ if (text && text.length > 2) {
686
+ const $parent = $el.closest('[class*="cta"]').first();
687
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
688
+ const fieldName = parentClassInfo ? `${parentClassInfo.fieldName}_button_text` : "button_text";
689
+ detectedFields[fieldName] = {
690
+ selector: `.c_button`,
691
+ type: "plain",
692
+ editable: true
693
+ };
694
+ }
695
+ });
696
+ return {
697
+ fields: detectedFields,
698
+ collections: detectedCollections
699
+ };
700
+ }
701
+ async function analyzeVuePages(pagesDir) {
702
+ const results = {};
703
+ const vueFiles = await fs5.readdir(pagesDir);
704
+ for (const file of vueFiles) {
705
+ if (file.endsWith(".vue")) {
706
+ const filePath = path6.join(pagesDir, file);
707
+ const content = await fs5.readFile(filePath, "utf-8");
708
+ const template = extractTemplateFromVue(content);
709
+ if (template) {
710
+ const pageName = file.replace(".vue", "");
711
+ results[pageName] = detectEditableFields(template);
712
+ }
713
+ }
714
+ }
715
+ return results;
716
+ }
717
+
718
+ // src/manifest.ts
719
+ async function generateManifest(pagesDir) {
720
+ const analyzed = await analyzeVuePages(pagesDir);
721
+ const pages = {};
722
+ for (const [pageName, detection] of Object.entries(analyzed)) {
723
+ pages[pageName] = {
724
+ fields: detection.fields,
725
+ collections: detection.collections,
726
+ meta: {
727
+ route: pageName === "index" ? "/" : `/${pageName}`
728
+ }
729
+ };
730
+ }
731
+ const manifest = {
732
+ version: "1.0",
733
+ pages
734
+ };
735
+ return manifest;
736
+ }
737
+ async function writeManifest(outputDir, manifest) {
738
+ const manifestPath = path7.join(outputDir, "cms-manifest.json");
739
+ await fs6.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
740
+ }
741
+
742
+ // src/transformer.ts
743
+ function mapFieldTypeToStrapi(fieldType) {
744
+ const typeMap = {
745
+ plain: "string",
746
+ rich: "richtext",
747
+ html: "richtext",
748
+ image: "media",
749
+ link: "string",
750
+ email: "email",
751
+ phone: "string"
752
+ };
753
+ return typeMap[fieldType] || "string";
754
+ }
755
+ function pluralize(word) {
756
+ if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
757
+ return word + "es";
758
+ }
759
+ if (word.endsWith("y") && word.length > 1) {
760
+ const secondLast = word[word.length - 2];
761
+ if (!"aeiou".includes(secondLast.toLowerCase())) {
762
+ return word.slice(0, -1) + "ies";
763
+ }
764
+ }
765
+ return word + "s";
766
+ }
767
+ function pageToStrapiSchema(pageName, fields) {
768
+ const attributes = {};
769
+ for (const [fieldName, field] of Object.entries(fields)) {
770
+ attributes[fieldName] = {
771
+ type: mapFieldTypeToStrapi(field.type),
772
+ required: field.required || false
773
+ };
774
+ if (field.default) {
775
+ attributes[fieldName].default = field.default;
776
+ }
777
+ }
778
+ const displayName = pageName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
779
+ const kebabCaseName = pageName;
780
+ const pluralName = pluralize(kebabCaseName);
781
+ return {
782
+ kind: "singleType",
783
+ collectionName: kebabCaseName,
784
+ info: {
785
+ singularName: kebabCaseName,
786
+ pluralName,
787
+ displayName
788
+ },
789
+ options: {
790
+ draftAndPublish: true
791
+ },
792
+ attributes
793
+ };
794
+ }
795
+ function collectionToStrapiSchema(collectionName, collection) {
796
+ const attributes = {};
797
+ for (const [fieldName, _selector] of Object.entries(collection.fields)) {
798
+ let type = "string";
799
+ if (fieldName === "image" || fieldName.includes("image")) {
800
+ type = "media";
801
+ } else if (fieldName === "description" || fieldName === "content") {
802
+ type = "richtext";
803
+ } else if (fieldName === "link" || fieldName === "url") {
804
+ type = "string";
805
+ } else if (fieldName === "title" || fieldName === "tag") {
806
+ type = "string";
807
+ }
808
+ attributes[fieldName] = {
809
+ type
810
+ };
811
+ }
812
+ const displayName = collectionName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
813
+ const kebabCaseName = collectionName.replace(/_/g, "-");
814
+ const singularName = kebabCaseName.endsWith("s") ? kebabCaseName.slice(0, -1) : kebabCaseName;
815
+ return {
816
+ kind: "collectionType",
817
+ collectionName: kebabCaseName,
818
+ info: {
819
+ singularName,
820
+ pluralName: kebabCaseName,
821
+ displayName
822
+ },
823
+ options: {
824
+ draftAndPublish: true
825
+ },
826
+ attributes
827
+ };
828
+ }
829
+ function manifestToSchemas(manifest) {
830
+ const schemas = {};
831
+ for (const [pageName, page] of Object.entries(manifest.pages)) {
832
+ if (page.fields && Object.keys(page.fields).length > 0) {
833
+ schemas[pageName] = pageToStrapiSchema(pageName, page.fields);
834
+ }
835
+ if (page.collections) {
836
+ for (const [collectionName, collection] of Object.entries(page.collections)) {
837
+ schemas[collectionName] = collectionToStrapiSchema(collectionName, collection);
838
+ }
839
+ }
840
+ }
841
+ return schemas;
842
+ }
843
+
844
+ // src/schema-writer.ts
845
+ import fs7 from "fs-extra";
846
+ import path8 from "path";
847
+ async function writeStrapiSchema(outputDir, name, schema) {
848
+ const schemasDir = path8.join(outputDir, "cms-schemas");
849
+ await fs7.ensureDir(schemasDir);
850
+ const schemaPath = path8.join(schemasDir, `${name}.json`);
851
+ await fs7.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
852
+ }
853
+ async function writeAllSchemas(outputDir, schemas) {
854
+ for (const [name, schema] of Object.entries(schemas)) {
855
+ await writeStrapiSchema(outputDir, name, schema);
856
+ }
857
+ }
858
+ async function createStrapiReadme(outputDir) {
859
+ const readmePath = path8.join(outputDir, "cms-schemas", "README.md");
860
+ const content = `# CMS Schemas
861
+
862
+ Auto-generated Strapi content type schemas from your Webflow export.
863
+
864
+ ## What's in this folder?
865
+
866
+ Each \`.json\` file is a Strapi content type schema:
867
+
868
+ - **Pages** (single types) - Unique pages like \`index.json\`, \`about.json\`
869
+ - **Collections** (collection types) - Repeating content like \`portfolio_cards.json\`
870
+
871
+ ## How to use with Strapi
872
+
873
+ ### Option 1: Manual Setup (Recommended for learning)
874
+
875
+ 1. Start your Strapi project
876
+ 2. In Strapi admin, go to **Content-Type Builder**
877
+ 3. Create each content type manually using these schemas as reference
878
+ 4. Match the field names and types
879
+
880
+ ### Option 2: Automated Setup (Advanced)
881
+
882
+ Copy schemas to your Strapi project structure:
883
+
884
+ \`\`\`bash
885
+ # For each schema file, create the Strapi directory structure
886
+ # Example for index.json (single type):
887
+ mkdir -p strapi/src/api/index/content-types/index
888
+ cp cms-schemas/index.json strapi/src/api/index/content-types/index/schema.json
889
+
890
+ # Example for portfolio_cards.json (collection type):
891
+ mkdir -p strapi/src/api/portfolio-cards/content-types/portfolio-card
892
+ cp cms-schemas/portfolio_cards.json strapi/src/api/portfolio-cards/content-types/portfolio-card/schema.json
893
+ \`\`\`
894
+
895
+ Then restart Strapi - it will auto-create the content types.
896
+
897
+ ## Schema Structure
898
+
899
+ Each schema defines:
900
+ - \`kind\`: "singleType" (unique page) or "collectionType" (repeating)
901
+ - \`attributes\`: Fields and their types (string, richtext, media, etc.)
902
+ - \`displayName\`: How it appears in Strapi admin
903
+
904
+ ## Field Types
905
+
906
+ - \`string\` - Plain text
907
+ - \`richtext\` - Formatted text with HTML
908
+ - \`media\` - Image uploads
909
+
910
+ ## Next Steps
911
+
912
+ 1. Set up a Strapi project: \`npx create-strapi-app@latest my-strapi\`
913
+ 2. Use these schemas to create content types
914
+ 3. Populate content in Strapi admin
915
+ 4. Connect your Nuxt app to Strapi API
916
+
917
+ ## API Usage in Nuxt
918
+
919
+ Once Strapi is running with these content types:
920
+
921
+ \`\`\`typescript
922
+ // Fetch single type (e.g., home page)
923
+ const { data } = await $fetch('http://localhost:1337/api/index')
924
+
925
+ // Fetch collection type (e.g., portfolio cards)
926
+ const { data } = await $fetch('http://localhost:1337/api/portfolio-cards')
927
+ \`\`\`
928
+ `;
929
+ await fs7.writeFile(readmePath, content, "utf-8");
930
+ }
931
+
932
+ // src/content-extractor.ts
933
+ import * as cheerio3 from "cheerio";
934
+ import path9 from "path";
935
+ function extractContentFromHTML(html, _pageName, pageManifest) {
936
+ const $ = cheerio3.load(html);
937
+ const content = {
938
+ fields: {},
939
+ collections: {}
940
+ };
941
+ if (pageManifest.fields) {
942
+ for (const [fieldName, field] of Object.entries(pageManifest.fields)) {
943
+ const selector = field.selector;
944
+ const element = $(selector).first();
945
+ if (element.length > 0) {
946
+ if (field.type === "image") {
947
+ const src = element.attr("src") || element.find("img").attr("src") || "";
948
+ content.fields[fieldName] = src;
949
+ } else {
950
+ const text = element.text().trim();
951
+ content.fields[fieldName] = text;
952
+ }
953
+ }
954
+ }
955
+ }
956
+ if (pageManifest.collections) {
957
+ for (const [collectionName, collection] of Object.entries(pageManifest.collections)) {
958
+ const items = [];
959
+ const collectionElements = $(collection.selector);
960
+ collectionElements.each((_, elem) => {
961
+ const item = {};
962
+ const $elem = $(elem);
963
+ for (const [fieldName, fieldSelector] of Object.entries(collection.fields)) {
964
+ const fieldElement = $elem.find(fieldSelector).first();
965
+ if (fieldElement.length > 0) {
966
+ if (fieldName === "image" || fieldName.includes("image")) {
967
+ const src = fieldElement.attr("src") || fieldElement.find("img").attr("src") || "";
968
+ item[fieldName] = src;
969
+ } else if (fieldName === "link" || fieldName === "url") {
970
+ const href = fieldElement.attr("href") || "";
971
+ item[fieldName] = href;
972
+ } else {
973
+ const text = fieldElement.text().trim();
974
+ item[fieldName] = text;
975
+ }
976
+ }
977
+ }
978
+ if (Object.keys(item).length > 0) {
979
+ items.push(item);
980
+ }
981
+ });
982
+ if (items.length > 0) {
983
+ content.collections[collectionName] = items;
984
+ }
985
+ }
986
+ }
987
+ return content;
988
+ }
989
+ function extractAllContent(htmlFiles, manifest) {
990
+ const extractedContent = {
991
+ pages: {}
992
+ };
993
+ for (const [pageName, pageManifest] of Object.entries(manifest.pages)) {
994
+ const html = htmlFiles.get(pageName);
995
+ if (html) {
996
+ const content = extractContentFromHTML(html, pageName, pageManifest);
997
+ extractedContent.pages[pageName] = content;
998
+ }
999
+ }
1000
+ return extractedContent;
1001
+ }
1002
+ function normalizeImagePath(imageSrc) {
1003
+ if (!imageSrc) return "";
1004
+ if (imageSrc.startsWith("/")) return imageSrc;
1005
+ const filename = path9.basename(imageSrc);
1006
+ if (imageSrc.includes("images/")) {
1007
+ return `/images/${filename}`;
1008
+ }
1009
+ return `/${filename}`;
1010
+ }
1011
+ function formatForStrapi(extracted) {
1012
+ const seedData = {};
1013
+ for (const [pageName, content] of Object.entries(extracted.pages)) {
1014
+ if (Object.keys(content.fields).length > 0) {
1015
+ const formattedFields = {};
1016
+ for (const [fieldName, value] of Object.entries(content.fields)) {
1017
+ if (fieldName.includes("image") || fieldName.includes("bg")) {
1018
+ formattedFields[fieldName] = normalizeImagePath(value);
1019
+ } else {
1020
+ formattedFields[fieldName] = value;
1021
+ }
1022
+ }
1023
+ seedData[pageName] = formattedFields;
1024
+ }
1025
+ for (const [collectionName, items] of Object.entries(content.collections)) {
1026
+ const formattedItems = items.map((item) => {
1027
+ const formattedItem = {};
1028
+ for (const [fieldName, value] of Object.entries(item)) {
1029
+ if (fieldName === "image" || fieldName.includes("image")) {
1030
+ formattedItem[fieldName] = normalizeImagePath(value);
1031
+ } else {
1032
+ formattedItem[fieldName] = value;
1033
+ }
1034
+ }
1035
+ return formattedItem;
1036
+ });
1037
+ seedData[collectionName] = formattedItems;
1038
+ }
1039
+ }
1040
+ return seedData;
1041
+ }
1042
+
1043
+ // src/seed-writer.ts
1044
+ import fs8 from "fs-extra";
1045
+ import path10 from "path";
1046
+ async function writeSeedData(outputDir, seedData) {
1047
+ const seedDir = path10.join(outputDir, "cms-seed");
1048
+ await fs8.ensureDir(seedDir);
1049
+ const seedPath = path10.join(seedDir, "seed-data.json");
1050
+ await fs8.writeJson(seedPath, seedData, { spaces: 2 });
1051
+ }
1052
+ async function createSeedReadme(outputDir) {
1053
+ const readmePath = path10.join(outputDir, "cms-seed", "README.md");
1054
+ const content = `# CMS Seed Data
1055
+
1056
+ Auto-extracted content from your Webflow export, ready to seed into Strapi.
1057
+
1058
+ ## What's in this folder?
1059
+
1060
+ \`seed-data.json\` contains the actual content extracted from your HTML:
1061
+ - **Single types** - Page-specific content (homepage, about page, etc.)
1062
+ - **Collection types** - Repeating items (portfolio cards, team members, etc.)
1063
+
1064
+ ## Structure
1065
+
1066
+ \`\`\`json
1067
+ {
1068
+ "index": {
1069
+ "hero_heading_container": "Actual heading from HTML",
1070
+ "hero_bg_image": "/images/hero.jpg",
1071
+ ...
1072
+ },
1073
+ "portfolio_cards": [
1074
+ {
1075
+ "image": "/images/card1.jpg",
1076
+ "tag": "Technology",
1077
+ "description": "Card description"
1078
+ }
1079
+ ]
1080
+ }
1081
+ \`\`\`
1082
+
1083
+ ## How to Seed Strapi
1084
+
1085
+ ### Option 1: Manual Entry
1086
+ 1. Open Strapi admin panel
1087
+ 2. Go to Content Manager
1088
+ 3. Create entries using the data from \`seed-data.json\`
1089
+
1090
+ ### Option 2: Automated Seeding (Coming Soon)
1091
+ We'll provide a seeding script that:
1092
+ 1. Uploads images to Strapi media library
1093
+ 2. Creates content entries via Strapi API
1094
+ 3. Handles relationships between content types
1095
+
1096
+ ## Image Paths
1097
+
1098
+ Image paths in the seed data reference files in your Nuxt \`public/\` directory:
1099
+ - \`/images/hero.jpg\` \u2192 \`public/images/hero.jpg\`
1100
+
1101
+ When seeding Strapi, these images will be uploaded to Strapi's media library.
1102
+
1103
+ ## Next Steps
1104
+
1105
+ 1. Review the extracted data for accuracy
1106
+ 2. Set up your Strapi instance with the schemas from \`cms-schemas/\`
1107
+ 3. Use this seed data to populate your CMS
1108
+ `;
1109
+ await fs8.writeFile(readmePath, content, "utf-8");
1110
+ }
1111
+
388
1112
  // src/converter.ts
389
1113
  async function convertWebflowExport(options) {
390
1114
  const { inputDir, outputDir, boilerplate } = options;
@@ -393,7 +1117,7 @@ async function convertWebflowExport(options) {
393
1117
  console.log(pc3.dim(`Output: ${outputDir}`));
394
1118
  try {
395
1119
  await setupBoilerplate(boilerplate, outputDir);
396
- const inputExists = await fs4.pathExists(inputDir);
1120
+ const inputExists = await fs9.pathExists(inputDir);
397
1121
  if (!inputExists) {
398
1122
  throw new Error(`Input directory not found: ${inputDir}`);
399
1123
  }
@@ -409,10 +1133,17 @@ async function convertWebflowExport(options) {
409
1133
  console.log(pc3.blue("\n\u{1F50D} Finding HTML files..."));
410
1134
  const htmlFiles = await findHTMLFiles(inputDir);
411
1135
  console.log(pc3.green(` \u2713 Found ${htmlFiles.length} HTML files`));
1136
+ const htmlContentMap = /* @__PURE__ */ new Map();
1137
+ for (const htmlFile of htmlFiles) {
1138
+ const html = await readHTMLFile(inputDir, htmlFile);
1139
+ const pageName = htmlFile.replace(".html", "").replace(/\//g, "-");
1140
+ htmlContentMap.set(pageName, html);
1141
+ console.log(pc3.dim(` Stored: ${pageName} from ${htmlFile}`));
1142
+ }
412
1143
  console.log(pc3.blue("\n\u2699\uFE0F Converting HTML to Vue components..."));
413
1144
  let allEmbeddedStyles = "";
414
1145
  for (const htmlFile of htmlFiles) {
415
- const html = await readHTMLFile(inputDir, htmlFile);
1146
+ const html = htmlContentMap.get(htmlFile.replace(".html", "").replace(/\//g, "-"));
416
1147
  const parsed = parseHTML(html, htmlFile);
417
1148
  if (parsed.embeddedStyles) {
418
1149
  allEmbeddedStyles += `
@@ -427,6 +1158,41 @@ ${parsed.embeddedStyles}
427
1158
  console.log(pc3.green(` \u2713 Created ${htmlFile.replace(".html", ".vue")}`));
428
1159
  }
429
1160
  await formatVueFiles(outputDir);
1161
+ console.log(pc3.blue("\n\u{1F50D} Analyzing pages for CMS fields..."));
1162
+ const pagesDir = path11.join(outputDir, "pages");
1163
+ const manifest = await generateManifest(pagesDir);
1164
+ await writeManifest(outputDir, manifest);
1165
+ const totalFields = Object.values(manifest.pages).reduce(
1166
+ (sum, page) => sum + Object.keys(page.fields || {}).length,
1167
+ 0
1168
+ );
1169
+ const totalCollections = Object.values(manifest.pages).reduce(
1170
+ (sum, page) => sum + Object.keys(page.collections || {}).length,
1171
+ 0
1172
+ );
1173
+ console.log(pc3.green(` \u2713 Detected ${totalFields} fields across ${Object.keys(manifest.pages).length} pages`));
1174
+ console.log(pc3.green(` \u2713 Detected ${totalCollections} collections`));
1175
+ console.log(pc3.green(" \u2713 Generated cms-manifest.json"));
1176
+ console.log(pc3.blue("\n\u{1F4DD} Extracting content from HTML..."));
1177
+ console.log(pc3.dim(` HTML map has ${htmlContentMap.size} entries`));
1178
+ console.log(pc3.dim(` Manifest has ${Object.keys(manifest.pages).length} pages`));
1179
+ const extractedContent = extractAllContent(htmlContentMap, manifest);
1180
+ const seedData = formatForStrapi(extractedContent);
1181
+ await writeSeedData(outputDir, seedData);
1182
+ await createSeedReadme(outputDir);
1183
+ const pagesWithContent = Object.keys(seedData).filter((key) => {
1184
+ const data = seedData[key];
1185
+ if (Array.isArray(data)) return data.length > 0;
1186
+ return Object.keys(data).length > 0;
1187
+ }).length;
1188
+ console.log(pc3.green(` \u2713 Extracted content from ${pagesWithContent} pages`));
1189
+ console.log(pc3.green(` \u2713 Generated cms-seed/seed-data.json`));
1190
+ console.log(pc3.blue("\n\u{1F4CB} Generating Strapi schemas..."));
1191
+ const schemas = manifestToSchemas(manifest);
1192
+ await writeAllSchemas(outputDir, schemas);
1193
+ await createStrapiReadme(outputDir);
1194
+ console.log(pc3.green(` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`));
1195
+ console.log(pc3.dim(" View schemas in: cms-schemas/"));
430
1196
  if (allEmbeddedStyles.trim()) {
431
1197
  console.log(pc3.blue("\n\u2728 Writing embedded styles..."));
432
1198
  const dedupedStyles = deduplicateStyles(allEmbeddedStyles);
@@ -441,14 +1207,24 @@ ${parsed.embeddedStyles}
441
1207
  await updateNuxtConfig(outputDir, assets.css);
442
1208
  console.log(pc3.green(" \u2713 Config updated"));
443
1209
  } catch (error) {
444
- console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
1210
+ console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
445
1211
  console.log(pc3.dim(" Please add CSS files manually"));
446
1212
  }
1213
+ console.log(pc3.blue("\n\u{1F3A8} Setting up editor overlay..."));
1214
+ await createEditorPlugin(outputDir);
1215
+ await addEditorDependency(outputDir);
1216
+ await createSaveEndpoint(outputDir);
1217
+ console.log(pc3.green(" \u2713 Editor plugin created"));
1218
+ console.log(pc3.green(" \u2713 Editor dependency added"));
1219
+ console.log(pc3.green(" \u2713 Save endpoint created"));
447
1220
  console.log(pc3.green("\n\u2705 Conversion completed successfully!"));
448
1221
  console.log(pc3.cyan("\n\u{1F4CB} Next steps:"));
449
1222
  console.log(pc3.dim(` 1. cd ${outputDir}`));
450
- console.log(pc3.dim(" 2. pnpm install"));
451
- console.log(pc3.dim(" 3. pnpm dev"));
1223
+ console.log(pc3.dim(" 2. Review cms-manifest.json and cms-seed/seed-data.json"));
1224
+ console.log(pc3.dim(" 3. Set up Strapi and install schemas from cms-schemas/"));
1225
+ console.log(pc3.dim(" 4. Seed Strapi with data from cms-seed/"));
1226
+ console.log(pc3.dim(" 5. pnpm install && pnpm dev"));
1227
+ console.log(pc3.dim(" 6. Visit http://localhost:3000?preview=true to edit inline!"));
452
1228
  } catch (error) {
453
1229
  console.error(pc3.red("\n\u274C Conversion failed:"));
454
1230
  console.error(pc3.red(error instanceof Error ? error.message : String(error)));
@@ -456,31 +1232,16 @@ ${parsed.embeddedStyles}
456
1232
  }
457
1233
  }
458
1234
 
459
- // src/detector.ts
460
- function detectEditableFields(_html) {
461
- throw new Error("Not yet implemented");
462
- }
463
-
464
- // src/manifest.ts
465
- function generateManifest(_data) {
466
- throw new Error("Not yet implemented");
467
- }
468
-
469
1235
  // src/generator.ts
470
1236
  async function generateSchemas(_manifestPath, _cmsType) {
471
1237
  throw new Error("Not yet implemented");
472
1238
  }
473
-
474
- // src/transformer.ts
475
- function manifestToSchema(_manifest, _cmsType) {
476
- throw new Error("Not yet implemented");
477
- }
478
1239
  export {
479
1240
  convertWebflowExport,
480
1241
  detectEditableFields,
481
1242
  generateManifest,
482
1243
  generateSchemas,
483
- manifestToSchema,
1244
+ manifestToSchemas,
484
1245
  setupBoilerplate
485
1246
  };
486
1247
  //# sourceMappingURL=index.mjs.map