@marvalt/wadapter 2.3.44 → 2.3.46

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.
@@ -287,11 +287,107 @@ class WordPressClient {
287
287
  }
288
288
  /**
289
289
  * Fetch a specific navigation menu with its items
290
- * @param menuId Menu ID
290
+ * Handles both menu term IDs and wp_navigation post IDs
291
+ * @param menuId Menu ID (can be menu term ID or wp_navigation post ID)
291
292
  * @returns Menu data with hierarchical items
292
293
  */
293
294
  async getMenu(menuId) {
294
- return this.makeRequest(`/wp-custom/v1/menus/${menuId}`);
295
+ console.log(`🌐 [WordPressClient] Fetching menu ID ${menuId}...`);
296
+ try {
297
+ // First try the custom endpoint (handles both menu terms and wp_navigation posts)
298
+ console.log(` → Trying endpoint: /wp-custom/v1/menus/${menuId}`);
299
+ const result = await this.makeRequest(`/wp-custom/v1/menus/${menuId}`);
300
+ console.log(` ✅ Successfully fetched menu ID ${menuId} as menu term: ${result.name}`);
301
+ return result;
302
+ }
303
+ catch (error) {
304
+ const errorStatus = error.status || (error.response?.status) || 'unknown';
305
+ const errorMessage = error.message || 'Unknown error';
306
+ console.log(` ❌ Menu term fetch failed (status: ${errorStatus}): ${errorMessage}`);
307
+ // If 404, try fetching as wp_navigation post
308
+ if (errorStatus === 404 || errorMessage?.includes('404')) {
309
+ console.log(` → Menu ID ${menuId} not found as menu term, trying as wp_navigation post...`);
310
+ // Try custom endpoint first
311
+ try {
312
+ console.log(` → Trying navigation post endpoint: /wp-custom/v1/navigation/${menuId}`);
313
+ const navPost = await this.makeRequest(`/wp-custom/v1/navigation/${menuId}`);
314
+ console.log(` ✅ Navigation post fetched:`, {
315
+ id: navPost.id,
316
+ hasMenu: !!navPost.menu,
317
+ menuTermId: navPost.menu_term_id,
318
+ });
319
+ if (navPost.menu) {
320
+ console.log(` ✅ Returning menu from navigation post: ${navPost.menu.name}`);
321
+ return navPost.menu;
322
+ }
323
+ // If navigation post has a menu_term_id, fetch that menu
324
+ if (navPost.menu_term_id) {
325
+ console.log(` → Navigation post has menu_term_id ${navPost.menu_term_id}, fetching menu...`);
326
+ const menu = await this.makeRequest(`/wp-custom/v1/menus/${navPost.menu_term_id}`);
327
+ console.log(` ✅ Fetched menu from menu_term_id ${navPost.menu_term_id}: ${menu.name}`);
328
+ return menu;
329
+ }
330
+ else {
331
+ console.warn(` âš ī¸ Navigation post ${menuId} has no menu_term_id and no menu data`);
332
+ throw new Error(`Navigation post ${menuId} has no menu_term_id and no menu data`);
333
+ }
334
+ }
335
+ catch (navError) {
336
+ const navErrorStatus = navError.status || (navError.response?.status) || 'unknown';
337
+ const navErrorMessage = navError.message || 'Unknown error';
338
+ console.log(` ❌ Navigation post fetch failed (status: ${navErrorStatus}): ${navErrorMessage}`);
339
+ // If custom endpoint fails, try WordPress native REST API (WordPress 6.3+)
340
+ if (navErrorStatus === 404 || navErrorMessage?.includes('404')) {
341
+ console.log(` → Custom navigation endpoint not found, trying WordPress native REST API...`);
342
+ try {
343
+ console.log(` → Trying WordPress native REST API: /wp/v2/navigation/${menuId}`);
344
+ // WordPress 6.3+ has native REST API for wp_navigation posts
345
+ const navPost = await this.makeRequest(`/wp/v2/navigation/${menuId}`);
346
+ console.log(` ✅ WordPress native navigation post fetched:`, {
347
+ id: navPost.id,
348
+ title: navPost.title?.rendered,
349
+ hasContent: !!navPost.content?.raw,
350
+ });
351
+ // Parse blocks from content to find menu reference
352
+ // The content.raw contains the block JSON
353
+ if (navPost.content?.raw) {
354
+ try {
355
+ const blocks = JSON.parse(navPost.content.raw);
356
+ console.log(` → Parsed ${Array.isArray(blocks) ? blocks.length : 'unknown'} blocks from navigation post`);
357
+ // Look for menu reference in blocks (this would need to be done server-side)
358
+ // For now, throw error to indicate menu extraction requires server-side processing
359
+ console.log(`â„šī¸ Found wp_navigation post ${menuId}, but menu extraction requires server-side processing`);
360
+ throw new Error(`Menu extraction from wp_navigation post ${menuId} requires server-side processing`);
361
+ }
362
+ catch (parseError) {
363
+ console.log(`âš ī¸ Could not parse navigation post content`);
364
+ throw new Error(`Could not parse navigation post content for menu ID ${menuId}`);
365
+ }
366
+ }
367
+ else {
368
+ throw new Error(`wp_navigation post ${menuId} has no content to parse`);
369
+ }
370
+ }
371
+ catch (wpError) {
372
+ const wpErrorStatus = wpError.status || (wpError.response?.status) || 'unknown';
373
+ const wpErrorMessage = wpError.message || 'Unknown error';
374
+ console.error(` ❌ WordPress native REST API also failed (status: ${wpErrorStatus}): ${wpErrorMessage}`);
375
+ console.error(` Full WordPress API error:`, wpError);
376
+ // Re-throw to ensure error is propagated
377
+ throw new Error(`Could not fetch menu ID ${menuId} via any method: ${wpErrorMessage}`);
378
+ }
379
+ }
380
+ else {
381
+ console.error(` ❌ Navigation post fetch failed with non-404 error, re-throwing...`);
382
+ throw navError; // Re-throw navigation error
383
+ }
384
+ }
385
+ }
386
+ else {
387
+ console.error(` ❌ Menu term fetch failed with non-404 error (status: ${errorStatus}), re-throwing...`);
388
+ throw error; // Re-throw original error
389
+ }
390
+ }
295
391
  }
296
392
  async getFooter(slug = 'footer', area = 'footer') {
297
393
  return this.makeRequest(`/wp-custom/v1/footer`, { slug, area });
@@ -520,86 +616,194 @@ function addBlockFromStack(endOffset) {
520
616
  /**
521
617
  * Extract menu items from a navigation block's innerHTML (rendered HTML)
522
618
  * WordPress renders navigation blocks, so menu items are in innerHTML as HTML
619
+ * This function properly handles submenus by parsing the nested <ul> structure
523
620
  */
524
621
  function extractMenuFromNavigationHTML(innerHTML) {
525
622
  if (!innerHTML)
526
623
  return null;
527
- // Parse HTML to extract menu items from <a> tags
528
- // Look for links inside navigation items: <a class="wp-block-navigation-item__content" href="..."><span class="wp-block-navigation-item__label">...</span></a>
529
- const linkRegex = /<a[^>]+class="[^"]*wp-block-navigation-item__content[^"]*"[^>]+href=["']([^"']+)["'][^>]*>[\s\S]*?<span[^>]*class="[^"]*wp-block-navigation-item__label[^"]*"[^>]*>([^<]+)<\/span>[\s\S]*?<\/a>/gi;
624
+ // Find the main navigation container <ul>
625
+ const mainNavMatch = innerHTML.match(/<ul[^>]*class="[^"]*wp-block-navigation__container[^"]*"[^>]*>([\s\S]*?)<\/ul>/i);
626
+ if (!mainNavMatch) {
627
+ return null;
628
+ }
629
+ const mainNavContent = mainNavMatch[1];
530
630
  const items = [];
531
- let match;
532
- let order = 0;
533
- while ((match = linkRegex.exec(innerHTML)) !== null) {
534
- const url = match[1].trim();
535
- const title = match[2].trim();
536
- if (title && url) {
537
- // Handle URLs: preserve external URLs, convert internal absolute URLs to relative paths
538
- let finalUrl = url;
539
- try {
540
- const urlObj = new URL(url);
541
- // Check if it's an external URL (different origin from WordPress site)
542
- // We'll consider it external if the origin doesn't match common internal patterns
543
- // For now, preserve all absolute URLs that are clearly external (different domain)
544
- // Internal URLs from the same WordPress site will be converted to relative paths
545
- const isAbsoluteUrl = url.startsWith('http://') || url.startsWith('https://');
546
- if (isAbsoluteUrl) {
547
- // Check if pathname is empty or just "/" - likely an external URL
548
- // Or if the domain is clearly different (simple heuristic: check if it contains common external domains)
549
- const pathname = urlObj.pathname;
550
- const isExternal = pathname === '' || pathname === '/' ||
551
- !urlObj.hostname.includes('vibunedemos.com') &&
552
- !urlObj.hostname.includes('localhost') &&
553
- !urlObj.hostname.includes('127.0.0.1');
554
- if (isExternal) {
555
- // Preserve external URLs as-is
556
- finalUrl = url;
557
- }
558
- else {
559
- // Convert internal absolute URLs to relative paths
560
- finalUrl = pathname + urlObj.search + urlObj.hash;
561
- // Ensure relative URL starts with /
562
- if (!finalUrl.startsWith('/')) {
563
- finalUrl = `/${finalUrl}`;
564
- }
565
- // Remove trailing slash (except for root path) to match React Router routes
566
- if (finalUrl !== '/' && finalUrl.endsWith('/')) {
567
- finalUrl = finalUrl.slice(0, -1);
568
- }
569
- }
631
+ let globalOrder = 0;
632
+ /**
633
+ * Parse a <li> element and extract menu item data
634
+ * Handles both regular links and submenus
635
+ */
636
+ function parseMenuItem(liElement, parentId = 0, order = 0) {
637
+ // Extract the link
638
+ const linkMatch = liElement.match(/<a[^>]+class="[^"]*wp-block-navigation-item__content[^"]*"[^>]+href=["']([^"']+)["'][^>]*>[\s\S]*?<span[^>]*class="[^"]*wp-block-navigation-item__label[^"]*"[^>]*>([^<]+)<\/span>[\s\S]*?<\/a>/i);
639
+ if (!linkMatch) {
640
+ return null;
641
+ }
642
+ const url = linkMatch[1].trim();
643
+ const title = linkMatch[2].trim();
644
+ if (!title) {
645
+ return null;
646
+ }
647
+ // Normalize URL
648
+ let finalUrl = url;
649
+ try {
650
+ const urlObj = new URL(url, 'http://dummy.com'); // Use dummy base for relative URLs
651
+ const isAbsoluteUrl = url.startsWith('http://') || url.startsWith('https://');
652
+ if (isAbsoluteUrl) {
653
+ const pathname = urlObj.pathname;
654
+ const isExternal = pathname === '' || pathname === '/' ||
655
+ !urlObj.hostname.includes('vibunedemos.com') &&
656
+ !urlObj.hostname.includes('localhost') &&
657
+ !urlObj.hostname.includes('127.0.0.1');
658
+ if (isExternal) {
659
+ finalUrl = url;
570
660
  }
571
661
  else {
572
- // Already a relative URL, ensure it starts with /
662
+ finalUrl = pathname + urlObj.search + urlObj.hash;
573
663
  if (!finalUrl.startsWith('/')) {
574
664
  finalUrl = `/${finalUrl}`;
575
665
  }
576
- // Remove trailing slash (except for root path) to match React Router routes
577
666
  if (finalUrl !== '/' && finalUrl.endsWith('/')) {
578
667
  finalUrl = finalUrl.slice(0, -1);
579
668
  }
580
669
  }
581
670
  }
582
- catch (e) {
583
- // If URL parsing fails, assume it's already relative or keep as is
584
- // Ensure relative URLs start with /
585
- if (!finalUrl.startsWith('http://') && !finalUrl.startsWith('https://') && !finalUrl.startsWith('/')) {
671
+ else {
672
+ if (!finalUrl.startsWith('/')) {
586
673
  finalUrl = `/${finalUrl}`;
587
674
  }
588
- // Remove trailing slash (except for root path) to match React Router routes
589
675
  if (finalUrl !== '/' && finalUrl.endsWith('/')) {
590
676
  finalUrl = finalUrl.slice(0, -1);
591
677
  }
592
678
  }
593
- items.push({
594
- id: order + 1,
595
- title: title,
596
- url: finalUrl,
597
- type: 'custom',
598
- object_id: 0,
599
- object: 'custom',
600
- parent: 0,
601
- menu_order: order++,
602
- });
679
+ }
680
+ catch (e) {
681
+ if (!finalUrl.startsWith('http://') && !finalUrl.startsWith('https://') && !finalUrl.startsWith('/')) {
682
+ finalUrl = `/${finalUrl}`;
683
+ }
684
+ if (finalUrl !== '/' && finalUrl.endsWith('/')) {
685
+ finalUrl = finalUrl.slice(0, -1);
686
+ }
687
+ }
688
+ const itemId = globalOrder + 1;
689
+ globalOrder++;
690
+ const menuItem = {
691
+ id: itemId,
692
+ title: title,
693
+ url: finalUrl,
694
+ type: 'custom',
695
+ object_id: 0,
696
+ object: 'custom',
697
+ parent: parentId,
698
+ menu_order: order,
699
+ };
700
+ // Check if this <li> contains a submenu (<ul class="wp-block-navigation__submenu-container">)
701
+ const submenuMatch = liElement.match(/<ul[^>]*class="[^"]*wp-block-navigation__submenu-container[^"]*"[^>]*>([\s\S]*?)<\/ul>/i);
702
+ if (submenuMatch) {
703
+ const submenuContent = submenuMatch[1];
704
+ const children = [];
705
+ let childOrder = 0;
706
+ // Extract all <li> elements from the submenu
707
+ const submenuLiRegex = /<li[^>]*class="[^"]*wp-block-navigation-item[^"]*"[^>]*>([\s\S]*?)<\/li>/gi;
708
+ let submenuLiMatch;
709
+ while ((submenuLiMatch = submenuLiRegex.exec(submenuContent)) !== null) {
710
+ const childResult = parseMenuItem(submenuLiMatch[1], itemId, childOrder++);
711
+ if (childResult) {
712
+ children.push(childResult.item);
713
+ globalOrder = childResult.nextOrder;
714
+ }
715
+ }
716
+ if (children.length > 0) {
717
+ menuItem.children = children;
718
+ }
719
+ }
720
+ return { item: menuItem, nextOrder: globalOrder };
721
+ }
722
+ // Extract all top-level <li> elements from the main navigation
723
+ // Find all <li> tags and extract complete items, filtering by position
724
+ const liRegex = /<li[^>]*>/gi;
725
+ const allLiMatches = [];
726
+ // First pass: find all <li> tags and their matching </li>
727
+ let match;
728
+ while ((match = liRegex.exec(mainNavContent)) !== null) {
729
+ const liStart = match.index;
730
+ let depth = 0;
731
+ let j = liStart;
732
+ while (j < mainNavContent.length) {
733
+ if (mainNavContent.substr(j, 3) === '<li' && /[>\s\n\t]/.test(mainNavContent[j + 3] || '')) {
734
+ depth++;
735
+ }
736
+ else if (mainNavContent.substr(j, 5) === '</li>') {
737
+ depth--;
738
+ if (depth === 0) {
739
+ allLiMatches.push({ start: liStart, end: j + 5 });
740
+ break;
741
+ }
742
+ }
743
+ j++;
744
+ }
745
+ }
746
+ // Second pass: filter to only top-level items (not inside submenu containers)
747
+ // Build a map of submenu container ranges
748
+ const submenuRanges = [];
749
+ const submenuContainerRegex = /<ul[^>]*class="[^"]*wp-block-navigation__submenu-container[^"]*"[^>]*>/gi;
750
+ let submenuMatch;
751
+ // Find all submenu containers and their matching </ul> tags
752
+ while ((submenuMatch = submenuContainerRegex.exec(mainNavContent)) !== null) {
753
+ const submenuStart = submenuMatch.index;
754
+ const submenuUlEnd = mainNavContent.indexOf('>', submenuStart);
755
+ if (submenuUlEnd === -1)
756
+ continue;
757
+ // Find matching </ul> by tracking depth
758
+ let ulDepth = 0;
759
+ let checkPos = submenuUlEnd + 1;
760
+ let submenuEnd = -1;
761
+ while (checkPos < mainNavContent.length) {
762
+ const ulOpen = mainNavContent.indexOf('<ul', checkPos);
763
+ const ulClose = mainNavContent.indexOf('</ul>', checkPos);
764
+ if (ulOpen !== -1 && (ulClose === -1 || ulOpen < ulClose)) {
765
+ ulDepth++;
766
+ checkPos = ulOpen + 3;
767
+ }
768
+ else if (ulClose !== -1) {
769
+ ulDepth--;
770
+ if (ulDepth === 0) {
771
+ submenuEnd = ulClose + 5;
772
+ break;
773
+ }
774
+ checkPos = ulClose + 5;
775
+ }
776
+ else {
777
+ break;
778
+ }
779
+ }
780
+ if (submenuEnd !== -1) {
781
+ submenuRanges.push({ start: submenuStart, end: submenuEnd });
782
+ }
783
+ }
784
+ // Filter <li> elements: exclude only those that are children inside submenu containers
785
+ // An <li> is a child (inside submenu) if: submenu's <ul> starts before <li> AND submenu's </ul> ends after <li> ends
786
+ // An <li> is a parent (contains submenu) if: <li> starts before submenu's <ul> (we want to include these)
787
+ for (const { start, end } of allLiMatches) {
788
+ // Check if this <li> is a child inside any submenu container
789
+ let isChildItem = false;
790
+ for (const range of submenuRanges) {
791
+ // If submenu <ul> starts before <li> AND submenu </ul> ends after <li> ends, then <li> is a child
792
+ if (range.start < start && range.end > end) {
793
+ isChildItem = true;
794
+ break;
795
+ }
796
+ }
797
+ // Include all items except children (children will be extracted by parseMenuItem as part of their parent)
798
+ if (!isChildItem) {
799
+ const item = mainNavContent.substring(start, end);
800
+ if (item.includes('wp-block-navigation-item')) {
801
+ const result = parseMenuItem(item, 0, items.length);
802
+ if (result) {
803
+ items.push(result.item);
804
+ globalOrder = result.nextOrder;
805
+ }
806
+ }
603
807
  }
604
808
  }
605
809
  if (items.length === 0) {
@@ -741,6 +945,7 @@ class WordPressGenerator {
741
945
  let header;
742
946
  let footer;
743
947
  let menuData;
948
+ let menus = {}; // Declare menus outside try block so it's accessible later
744
949
  try {
745
950
  console.log('🔍 Fetching WordPress Settings...');
746
951
  const settings = await client.getSettings();
@@ -834,6 +1039,33 @@ class WordPressGenerator {
834
1039
  }
835
1040
  catch (error) {
836
1041
  console.warn(`âš ī¸ Could not fetch menu data for menu ID ${menuId}:`, error);
1042
+ // Fallback: Try to find menu by name (mainmenu, main-menu, Main Menu, etc.)
1043
+ console.log('🔍 Menu fetch failed, attempting to find menu by name "mainmenu"...');
1044
+ try {
1045
+ const namedMenu = await findMenuByName(client, 'mainmenu');
1046
+ if (namedMenu && namedMenu.items && namedMenu.items.length > 0) {
1047
+ menuData = namedMenu;
1048
+ console.log(`✅ Found menu by name "mainmenu": ${namedMenu.name} (${namedMenu.items.length} items)`);
1049
+ }
1050
+ else {
1051
+ console.log('â„šī¸ No menu found with name "mainmenu"');
1052
+ // Try extracting from Navigation block HTML as last resort
1053
+ console.log('🔍 Attempting to extract navigation from Navigation block innerHTML...');
1054
+ try {
1055
+ const extractedMenu = extractMenuFromNavigationBlock(navBlock);
1056
+ if (extractedMenu && extractedMenu.items && extractedMenu.items.length > 0) {
1057
+ menuData = extractedMenu;
1058
+ console.log(`✅ Extracted navigation from Navigation block: ${extractedMenu.items.length} items`);
1059
+ }
1060
+ }
1061
+ catch (extractError) {
1062
+ console.warn('âš ī¸ Error extracting navigation from Navigation block:', extractError);
1063
+ }
1064
+ }
1065
+ }
1066
+ catch (nameError) {
1067
+ console.warn('âš ī¸ Error finding menu by name:', nameError);
1068
+ }
837
1069
  }
838
1070
  }
839
1071
  else {
@@ -905,6 +1137,149 @@ class WordPressGenerator {
905
1137
  catch (error) {
906
1138
  console.log('âš ī¸ Failed to fetch footer:', error instanceof Error ? error.message : 'Unknown error');
907
1139
  }
1140
+ // Extract all menu IDs from footer blocks and fetch them for static data
1141
+ const allMenuIds = new Set();
1142
+ if (menuData && menuData.id) {
1143
+ allMenuIds.add(menuData.id); // Add header menu
1144
+ console.log(`📋 Added header menu ID ${menuData.id} (${menuData.name}) to menu list`);
1145
+ }
1146
+ if (footer && footer.blocks) {
1147
+ let navigationBlockCount = 0;
1148
+ const findNavigationBlocks = (blocks, depth = 0) => {
1149
+ for (const block of blocks) {
1150
+ if (block.name === 'core/navigation') {
1151
+ navigationBlockCount++;
1152
+ console.log(`🔍 Found navigation block at depth ${depth}:`, {
1153
+ blockName: block.name,
1154
+ hasAttributes: !!block.attributes,
1155
+ attributes: block.attributes ? Object.keys(block.attributes) : [],
1156
+ });
1157
+ if (block.attributes) {
1158
+ const menuId = block.attributes['ref'] ||
1159
+ block.attributes['menuId'] ||
1160
+ block.attributes['menu'] ||
1161
+ block.attributes['navigationMenuId'] ||
1162
+ block.attributes['menuRef'];
1163
+ if (menuId) {
1164
+ const menuIdNum = parseInt(String(menuId));
1165
+ allMenuIds.add(menuIdNum);
1166
+ console.log(` ✅ Extracted menu ID ${menuIdNum} from attribute:`, {
1167
+ ref: block.attributes['ref'],
1168
+ menuId: block.attributes['menuId'],
1169
+ menu: block.attributes['menu'],
1170
+ navigationMenuId: block.attributes['navigationMenuId'],
1171
+ menuRef: block.attributes['menuRef'],
1172
+ extracted: menuIdNum,
1173
+ });
1174
+ }
1175
+ else {
1176
+ console.warn(` âš ī¸ Navigation block found but no menu ID attribute detected. Available attributes:`, Object.keys(block.attributes));
1177
+ }
1178
+ }
1179
+ else {
1180
+ console.warn(` âš ī¸ Navigation block found but has no attributes`);
1181
+ }
1182
+ }
1183
+ if (block.innerBlocks && block.innerBlocks.length > 0) {
1184
+ findNavigationBlocks(block.innerBlocks, depth + 1);
1185
+ }
1186
+ }
1187
+ };
1188
+ findNavigationBlocks(footer.blocks);
1189
+ console.log(`📊 Navigation block extraction summary:`, {
1190
+ navigationBlocksFound: navigationBlockCount,
1191
+ uniqueMenuIds: allMenuIds.size,
1192
+ menuIds: Array.from(allMenuIds),
1193
+ });
1194
+ }
1195
+ else {
1196
+ console.log(`â„šī¸ No footer blocks found or footer is empty`);
1197
+ }
1198
+ // Fetch all menus and store them in a menus object keyed by ID
1199
+ menus = {}; // Reset menus object (already declared above)
1200
+ if (menuData && menuData.id) {
1201
+ menus[menuData.id] = menuData;
1202
+ console.log(`✅ Added header menu to menus object: ID ${menuData.id} (${menuData.name})`);
1203
+ }
1204
+ let fetchAttempts = 0;
1205
+ let fetchSuccesses = 0;
1206
+ let fetchFailures = 0;
1207
+ for (const menuId of allMenuIds) {
1208
+ if (!menus[menuId]) {
1209
+ fetchAttempts++;
1210
+ console.log(`🔍 Attempting to fetch menu ID ${menuId}... (attempt ${fetchAttempts}/${allMenuIds.size})`);
1211
+ try {
1212
+ const fetchedMenu = await client.getMenu(menuId);
1213
+ // Validate menu structure
1214
+ if (!fetchedMenu) {
1215
+ console.error(`❌ Menu ID ${menuId}: Fetched menu is null or undefined`);
1216
+ fetchFailures++;
1217
+ continue;
1218
+ }
1219
+ if (!fetchedMenu.id) {
1220
+ console.error(`❌ Menu ID ${menuId}: Fetched menu missing 'id' property:`, fetchedMenu);
1221
+ fetchFailures++;
1222
+ continue;
1223
+ }
1224
+ if (!fetchedMenu.name) {
1225
+ console.warn(`âš ī¸ Menu ID ${menuId}: Fetched menu missing 'name' property:`, fetchedMenu);
1226
+ }
1227
+ if (!fetchedMenu.items) {
1228
+ console.warn(`âš ī¸ Menu ID ${menuId}: Fetched menu missing 'items' property:`, fetchedMenu);
1229
+ }
1230
+ else if (!Array.isArray(fetchedMenu.items)) {
1231
+ console.error(`❌ Menu ID ${menuId}: Fetched menu 'items' is not an array:`, typeof fetchedMenu.items);
1232
+ fetchFailures++;
1233
+ continue;
1234
+ }
1235
+ // Store menu even if items array is empty (for debugging)
1236
+ menus[menuId] = fetchedMenu;
1237
+ if (fetchedMenu.items && fetchedMenu.items.length > 0) {
1238
+ fetchSuccesses++;
1239
+ console.log(`✅ Fetched menu ID ${menuId}: ${fetchedMenu.name} (${fetchedMenu.items.length} items)`);
1240
+ }
1241
+ else {
1242
+ fetchSuccesses++; // Still count as success, just empty
1243
+ console.warn(`âš ī¸ Menu ID ${menuId}: ${fetchedMenu.name} fetched but has no items`);
1244
+ }
1245
+ }
1246
+ catch (error) {
1247
+ fetchFailures++;
1248
+ const errorDetails = {
1249
+ message: error.message || 'Unknown error',
1250
+ status: error.status || 'unknown',
1251
+ };
1252
+ // Try to extract more error details
1253
+ if (error.response) {
1254
+ errorDetails.responseStatus = error.response.status;
1255
+ errorDetails.responseStatusText = error.response.statusText;
1256
+ }
1257
+ if (error.body) {
1258
+ try {
1259
+ errorDetails.body = typeof error.body === 'string' ? error.body : JSON.stringify(error.body);
1260
+ }
1261
+ catch (e) {
1262
+ errorDetails.body = 'Could not stringify error body';
1263
+ }
1264
+ }
1265
+ console.error(`❌ Could not fetch menu ID ${menuId}:`, errorDetails);
1266
+ console.error(` Full error object:`, error);
1267
+ }
1268
+ }
1269
+ else {
1270
+ console.log(`â„šī¸ Menu ID ${menuId} already in menus object, skipping fetch`);
1271
+ }
1272
+ }
1273
+ // Log menu fetching summary
1274
+ console.log(`📊 Menu fetching summary:`, {
1275
+ totalMenuIds: allMenuIds.size,
1276
+ fetchAttempts,
1277
+ fetchSuccesses,
1278
+ fetchFailures,
1279
+ menusInObject: Object.keys(menus).length,
1280
+ menuIdsInObject: Object.keys(menus).map(id => parseInt(id)),
1281
+ menuNames: Object.values(menus).map((m) => m.name || 'unnamed').join(', '),
1282
+ });
908
1283
  // Debug: Dump all WordPress theme data in development
909
1284
  if (process.env.NODE_ENV === 'development') {
910
1285
  const debugData = {
@@ -983,6 +1358,7 @@ class WordPressGenerator {
983
1358
  ...(siteSettings && { site_settings: siteSettings }),
984
1359
  ...(header && { header: header }),
985
1360
  ...(menuData && { navigation_menu: menuData }),
1361
+ menus: menus, // Always include menus object, even if empty, for debugging
986
1362
  ...(footer && { footer: footer }),
987
1363
  ...(themeStyles && { theme_styles: themeStyles }),
988
1364
  config: {