@marvalt/digivalt-core 0.2.24 → 0.2.25

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.esm.js CHANGED
@@ -1,1828 +1,11 @@
1
1
  import dotenv from 'dotenv';
2
- import path, { join, dirname } from 'path';
3
- import fs, { writeFileSync } from 'fs';
2
+ import path from 'path';
3
+ import { generateWordPressData as generateWordPressData$1, generateGravityFormsData as generateGravityFormsData$1 } from '@marvalt/wadapter/generators';
4
+ import { generateMauticData as generateMauticData$1 } from '@marvalt/madapter/generators';
5
+ import { generatePhplistData as generatePhplistData$1 } from '@marvalt/plister/generators';
4
6
  import { generateSuiteCRMData as generateSuiteCRMData$1 } from '@marvalt/sadapter';
5
7
  import { mkdir, writeFile } from 'fs/promises';
6
-
7
- /**
8
- * @license GPL-3.0-or-later
9
- *
10
- * This file is part of the MarVAlt Open SDK.
11
- * Copyright (c) 2025 Vibune Pty Ltd.
12
- *
13
- * This program is free software: you can redistribute it and/or modify
14
- * it under the terms of the GNU General Public License as published by
15
- * the Free Software Foundation, either version 3 of the License, or
16
- * (at your option) any later version.
17
- *
18
- * This program is distributed in the hope that it will be useful,
19
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
21
- * See the GNU General Public License for more details.
22
- */
23
- class WordPressClient {
24
- constructor(config) {
25
- this.config = config;
26
- }
27
- async makeRequest(endpoint, params = {}, options = {}) {
28
- const baseUrl = this.getBaseUrl();
29
- const queryString = new URLSearchParams();
30
- Object.entries(params).forEach(([key, value]) => {
31
- if (value !== undefined && value !== null) {
32
- if (Array.isArray(value)) {
33
- value.forEach(v => queryString.append(key, v.toString()));
34
- }
35
- else {
36
- queryString.append(key, value.toString());
37
- }
38
- }
39
- });
40
- // Construct URL based on auth mode
41
- let url;
42
- if (this.config.authMode === 'cloudflare_proxy') {
43
- // For proxy modes, pass the full REST path including /wp-json as query parameter
44
- const fullEndpoint = `/wp-json${endpoint}?${queryString.toString()}`;
45
- url = `${baseUrl}?endpoint=${encodeURIComponent(fullEndpoint)}`;
46
- }
47
- else {
48
- // For direct mode, construct the full WordPress API URL
49
- url = `${baseUrl}/wp-json${endpoint}?${queryString.toString()}`;
50
- }
51
- const headers = {
52
- 'Content-Type': 'application/json',
53
- };
54
- // Add authentication based on mode
55
- if (this.config.authMode === 'cloudflare_proxy') ;
56
- else {
57
- // Direct mode - use basic auth
58
- if (this.config.username && this.config.password) {
59
- const credentials = btoa(`${this.config.username}:${this.config.password}`);
60
- headers.Authorization = `Basic ${credentials}`;
61
- }
62
- // Add Cloudflare Access Service Token headers if provided (for CF Access protected APIs)
63
- if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
64
- headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
65
- headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
66
- console.log('🔐 Added CF Access Service Token headers to WordPress request');
67
- console.log('🔐 CF-Access-Client-Id:', this.config.cfAccessClientId.substring(0, 8) + '...');
68
- console.log('🔐 CF-Access-Client-Secret:', this.config.cfAccessClientSecret.substring(0, 8) + '... (hidden)');
69
- }
70
- else {
71
- console.log('⚠️ CF Access Service Token NOT provided - requests may be blocked by Zero Trust');
72
- console.log(' cfAccessClientId present:', !!this.config.cfAccessClientId);
73
- console.log(' cfAccessClientSecret present:', !!this.config.cfAccessClientSecret);
74
- }
75
- }
76
- console.log('🔍 WordPress API Request Debug:', {
77
- url,
78
- method: options.method || 'GET',
79
- authMode: this.config.authMode,
80
- headers: Object.keys(headers),
81
- hasBasicAuth: !!headers.Authorization,
82
- hasCfAccess: !!(headers['CF-Access-Client-Id'] && headers['CF-Access-Client-Secret']),
83
- });
84
- const response = await fetch(url, {
85
- method: options.method || 'GET',
86
- headers,
87
- body: options.body,
88
- signal: AbortSignal.timeout(this.config.timeout || 30000),
89
- ...options,
90
- });
91
- if (!response.ok) {
92
- console.error('❌ WordPress API Error:', {
93
- status: response.status,
94
- statusText: response.statusText,
95
- url,
96
- headers: Object.fromEntries(response.headers.entries()),
97
- });
98
- throw new Error(`WordPress API request failed: ${response.status} ${response.statusText}`);
99
- }
100
- const responseText = await response.text();
101
- console.log('🔍 WordPress API Response:', {
102
- status: response.status,
103
- responseLength: responseText.length,
104
- responsePreview: responseText.substring(0, 200),
105
- });
106
- if (!responseText) {
107
- // Return empty array for empty responses (common for endpoints with no data)
108
- return [];
109
- }
110
- try {
111
- return JSON.parse(responseText);
112
- }
113
- catch (error) {
114
- const message = error instanceof Error ? error.message : String(error);
115
- console.error('❌ JSON Parse Error:', {
116
- error: message,
117
- responseText: responseText.substring(0, 500),
118
- });
119
- throw new Error(`Failed to parse JSON response: ${message}`);
120
- }
121
- }
122
- getBaseUrl() {
123
- switch (this.config.authMode) {
124
- case 'cloudflare_proxy':
125
- return this.config.cloudflareWorkerUrl || '';
126
- default:
127
- return this.config.apiUrl || '';
128
- }
129
- }
130
- /**
131
- * Fetch posts with automatic pagination support
132
- * @param params Query parameters including per_page for total items to fetch
133
- * @returns All posts up to the limit, fetched across multiple pages if needed
134
- */
135
- async getPosts(params = {}) {
136
- return this.fetchPaginated('/wp/v2/posts', params);
137
- }
138
- async getPost(id) {
139
- return this.makeRequest(`/wp/v2/posts/${id}`);
140
- }
141
- /**
142
- * Fetch pages with automatic pagination support
143
- * @param params Query parameters including per_page for total items to fetch
144
- * @returns All pages up to the limit, fetched across multiple pages if needed
145
- */
146
- async getPages(params = {}) {
147
- return this.fetchPaginated('/wp/v2/pages', params);
148
- }
149
- async getPage(id) {
150
- return this.makeRequest(`/wp/v2/pages/${id}`);
151
- }
152
- /**
153
- * Fetch media with automatic pagination support
154
- * @param params Query parameters including per_page for total items to fetch
155
- * @returns All media items up to the limit, fetched across multiple pages if needed
156
- */
157
- async getMedia(params = {}) {
158
- return this.fetchPaginated('/wp/v2/media', params);
159
- }
160
- async getMediaItem(id) {
161
- return this.makeRequest(`/wp/v2/media/${id}`);
162
- }
163
- /**
164
- * Fetch categories with automatic pagination support
165
- * @param params Query parameters including per_page for total items to fetch
166
- * @returns All categories up to the limit, fetched across multiple pages if needed
167
- */
168
- async getCategories(params = {}) {
169
- return this.fetchPaginated('/wp/v2/categories', params);
170
- }
171
- /**
172
- * Fetch tags with automatic pagination support
173
- * @param params Query parameters including per_page for total items to fetch
174
- * @returns All tags up to the limit, fetched across multiple pages if needed
175
- */
176
- async getTags(params = {}) {
177
- return this.fetchPaginated('/wp/v2/tags', params);
178
- }
179
- /**
180
- * Fetch data with automatic pagination
181
- * Handles fetching all items up to the requested limit by making multiple API calls if needed
182
- * @param endpoint The API endpoint to fetch from
183
- * @param params Query parameters including per_page for total items to fetch
184
- * @returns Array of all items fetched across all pages
185
- */
186
- async fetchPaginated(endpoint, params = {}) {
187
- const requestedTotal = params.per_page || 100;
188
- const itemsPerPage = Math.min(requestedTotal, 100); // WordPress max is typically 100
189
- const maxPages = Math.ceil(requestedTotal / itemsPerPage);
190
- let allItems = [];
191
- let currentPage = 1;
192
- let hasMorePages = true;
193
- console.log(`🔄 Fetching ${endpoint} with pagination: ${requestedTotal} items requested (${maxPages} pages of ${itemsPerPage})`);
194
- while (hasMorePages && currentPage <= maxPages && allItems.length < requestedTotal) {
195
- const pageParams = {
196
- ...params,
197
- per_page: itemsPerPage,
198
- page: currentPage,
199
- };
200
- console.log(`📄 Fetching page ${currentPage}/${maxPages} for ${endpoint}...`);
201
- try {
202
- const pageData = await this.makeRequest(endpoint, pageParams);
203
- if (!pageData || pageData.length === 0) {
204
- console.log(`✓ No more data on page ${currentPage}, stopping pagination`);
205
- hasMorePages = false;
206
- break;
207
- }
208
- allItems = allItems.concat(pageData);
209
- console.log(`✓ Page ${currentPage}: fetched ${pageData.length} items (total: ${allItems.length})`);
210
- // Stop if we got fewer items than requested per page (last page)
211
- if (pageData.length < itemsPerPage) {
212
- console.log(`✓ Reached last page (partial results)`);
213
- hasMorePages = false;
214
- }
215
- currentPage++;
216
- }
217
- catch (error) {
218
- console.error(`❌ Error fetching page ${currentPage}:`, error);
219
- // Stop pagination on error but return what we have so far
220
- hasMorePages = false;
221
- }
222
- }
223
- console.log(`✅ Pagination complete for ${endpoint}: ${allItems.length} total items fetched`);
224
- // Trim to exact requested amount if we fetched more
225
- return allItems.slice(0, requestedTotal);
226
- }
227
- // Gravity Forms endpoints
228
- async getGravityForms() {
229
- return this.makeRequest('/gf-api/v1/forms');
230
- }
231
- async getGravityForm(formId) {
232
- return this.makeRequest(`/gf-api/v1/forms/${formId}/config`);
233
- }
234
- async submitGravityForm(formId, entry) {
235
- return this.makeRequest(`/gf-api/v1/forms/${formId}/submit`, {}, {
236
- method: 'POST',
237
- body: JSON.stringify(entry),
238
- });
239
- }
240
- // Static data endpoints
241
- async getStaticDataNonce() {
242
- return this.makeRequest('/static-data/v1/nonce');
243
- }
244
- /**
245
- * Fetch a custom post type with automatic pagination support
246
- * @param postType The custom post type slug (e.g., 'chapter-member', 'events')
247
- * @param params Query parameters including per_page for total items to fetch
248
- * @returns All items of the custom post type up to the limit
249
- */
250
- async getCustomPostType(postType, params = {}) {
251
- return this.fetchPaginated(`/wp/v2/${postType}`, params);
252
- }
253
- async getAllData(params = {}) {
254
- const [posts, pages, media, categories, tags] = await Promise.all([
255
- this.getPosts(params),
256
- this.getPages(params),
257
- this.getMedia(params),
258
- this.getCategories(params),
259
- this.getTags(params),
260
- ]);
261
- return { posts, pages, media, categories, tags };
262
- }
263
- /**
264
- * Fetch blocks for a specific page from the custom blocks endpoint
265
- * @param pageId Page ID
266
- * @returns Blocks data from /wp-custom/v1/pages/{id}/blocks
267
- */
268
- async getPageBlocks(pageId) {
269
- return this.makeRequest(`/wp-custom/v1/pages/${pageId}/blocks`);
270
- }
271
- /**
272
- * Fetch WordPress Settings from custom endpoint
273
- * @returns Settings including front page information and site branding
274
- */
275
- /**
276
- * Fetch header template part blocks from custom endpoint
277
- * @param slug Template part slug (default: 'header')
278
- * @param area Template part area (default: 'header')
279
- * @returns Header blocks data
280
- */
281
- async getHeader(slug = 'header', area = 'header') {
282
- return this.makeRequest(`/wp-custom/v1/header`, { slug, area });
283
- }
284
- /**
285
- * Fetch all navigation menus
286
- * @returns Array of menu objects
287
- */
288
- async getMenus() {
289
- return this.makeRequest('/wp-custom/v1/menus');
290
- }
291
- /**
292
- * Fetch a specific navigation menu with its items
293
- * Handles both menu term IDs and wp_navigation post IDs
294
- * @param menuId Menu ID (can be menu term ID or wp_navigation post ID)
295
- * @returns Menu data with hierarchical items
296
- */
297
- async getMenu(menuId) {
298
- console.log(`🌐 [WordPressClient] Fetching menu ID ${menuId}...`);
299
- try {
300
- // First try the custom endpoint (handles both menu terms and wp_navigation posts)
301
- console.log(` → Trying endpoint: /wp-custom/v1/menus/${menuId}`);
302
- const result = await this.makeRequest(`/wp-custom/v1/menus/${menuId}`);
303
- console.log(` ✅ Successfully fetched menu ID ${menuId} as menu term: ${result.name}`);
304
- return result;
305
- }
306
- catch (error) {
307
- const errorStatus = error.status || (error.response?.status) || 'unknown';
308
- const errorMessage = error.message || 'Unknown error';
309
- console.log(` ❌ Menu term fetch failed (status: ${errorStatus}): ${errorMessage}`);
310
- // If 404, try fetching as wp_navigation post
311
- if (errorStatus === 404 || errorMessage?.includes('404')) {
312
- console.log(` → Menu ID ${menuId} not found as menu term, trying as wp_navigation post...`);
313
- // Try custom endpoint first
314
- try {
315
- console.log(` → Trying navigation post endpoint: /wp-custom/v1/navigation/${menuId}`);
316
- const navPost = await this.makeRequest(`/wp-custom/v1/navigation/${menuId}`);
317
- console.log(` ✅ Navigation post fetched:`, {
318
- id: navPost.id,
319
- hasMenu: !!navPost.menu,
320
- menuTermId: navPost.menu_term_id,
321
- });
322
- // If navigation post has menu data (either from menu_term_id or extracted from blocks)
323
- if (navPost.menu) {
324
- console.log(` ✅ Returning menu from navigation post: ${navPost.menu.name} (${navPost.menu.items?.length || 0} items)`);
325
- return navPost.menu;
326
- }
327
- // If navigation post has a menu_term_id but no menu data, fetch that menu
328
- if (navPost.menu_term_id) {
329
- console.log(` → Navigation post has menu_term_id ${navPost.menu_term_id}, fetching menu...`);
330
- const menu = await this.makeRequest(`/wp-custom/v1/menus/${navPost.menu_term_id}`);
331
- console.log(` ✅ Fetched menu from menu_term_id ${navPost.menu_term_id}: ${menu.name}`);
332
- return menu;
333
- }
334
- else {
335
- console.warn(` ⚠️ Navigation post ${menuId} has no menu_term_id and no menu data extracted from blocks`);
336
- throw new Error(`Navigation post ${menuId} has no menu_term_id and no menu data`);
337
- }
338
- }
339
- catch (navError) {
340
- const navErrorStatus = navError.status || (navError.response?.status) || 'unknown';
341
- const navErrorMessage = navError.message || 'Unknown error';
342
- console.log(` ❌ Navigation post fetch failed (status: ${navErrorStatus}): ${navErrorMessage}`);
343
- // If custom endpoint fails, try WordPress native REST API (WordPress 6.3+)
344
- if (navErrorStatus === 404 || navErrorMessage?.includes('404')) {
345
- console.log(` → Custom navigation endpoint not found, trying WordPress native REST API...`);
346
- try {
347
- console.log(` → Trying WordPress native REST API: /wp/v2/navigation/${menuId}`);
348
- // WordPress 6.3+ has native REST API for wp_navigation posts
349
- const navPost = await this.makeRequest(`/wp/v2/navigation/${menuId}`);
350
- console.log(` ✅ WordPress native navigation post fetched:`, {
351
- id: navPost.id,
352
- title: navPost.title?.rendered,
353
- hasContent: !!navPost.content?.raw,
354
- });
355
- // Parse blocks from content to find menu reference
356
- // The content.raw contains the block JSON
357
- if (navPost.content?.raw) {
358
- try {
359
- const blocks = JSON.parse(navPost.content.raw);
360
- console.log(` → Parsed ${Array.isArray(blocks) ? blocks.length : 'unknown'} blocks from navigation post`);
361
- // Look for menu reference in blocks (this would need to be done server-side)
362
- // For now, throw error to indicate menu extraction requires server-side processing
363
- console.log(`ℹ️ Found wp_navigation post ${menuId}, but menu extraction requires server-side processing`);
364
- throw new Error(`Menu extraction from wp_navigation post ${menuId} requires server-side processing`);
365
- }
366
- catch (parseError) {
367
- console.log(`⚠️ Could not parse navigation post content`);
368
- throw new Error(`Could not parse navigation post content for menu ID ${menuId}`);
369
- }
370
- }
371
- else {
372
- throw new Error(`wp_navigation post ${menuId} has no content to parse`);
373
- }
374
- }
375
- catch (wpError) {
376
- const wpErrorStatus = wpError.status || (wpError.response?.status) || 'unknown';
377
- const wpErrorMessage = wpError.message || 'Unknown error';
378
- console.error(` ❌ WordPress native REST API also failed (status: ${wpErrorStatus}): ${wpErrorMessage}`);
379
- console.error(` Full WordPress API error:`, wpError);
380
- // Re-throw to ensure error is propagated
381
- throw new Error(`Could not fetch menu ID ${menuId} via any method: ${wpErrorMessage}`);
382
- }
383
- }
384
- else {
385
- console.error(` ❌ Navigation post fetch failed with non-404 error, re-throwing...`);
386
- throw navError; // Re-throw navigation error
387
- }
388
- }
389
- }
390
- else {
391
- console.error(` ❌ Menu term fetch failed with non-404 error (status: ${errorStatus}), re-throwing...`);
392
- throw error; // Re-throw original error
393
- }
394
- }
395
- }
396
- async getFooter(slug = 'footer', area = 'footer') {
397
- return this.makeRequest(`/wp-custom/v1/footer`, { slug, area });
398
- }
399
- async getSettings() {
400
- return this.makeRequest('/wp-custom/v1/settings');
401
- }
402
- }
403
-
404
- let document;
405
- let offset;
406
- let output;
407
- let stack;
408
- const tokenizer = /<!--\s+(\/)?wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+({(?:(?=([^}]+|}+(?=})|(?!}\s+\/?-->)[^])*)\5|[^]*?)}\s+)?(\/)?-->/g;
409
- function Block(blockName, attrs, innerBlocks, innerHTML, innerContent) {
410
- return {
411
- blockName,
412
- attrs,
413
- innerBlocks,
414
- innerHTML,
415
- innerContent
416
- };
417
- }
418
- function Freeform(innerHTML) {
419
- return Block(null, {}, [], innerHTML, [innerHTML]);
420
- }
421
- function Frame(block, tokenStart, tokenLength, prevOffset, leadingHtmlStart) {
422
- return {
423
- block,
424
- tokenStart,
425
- tokenLength,
426
- prevOffset: prevOffset || tokenStart + tokenLength,
427
- leadingHtmlStart
428
- };
429
- }
430
- const parse = (doc) => {
431
- document = doc;
432
- offset = 0;
433
- output = [];
434
- stack = [];
435
- tokenizer.lastIndex = 0;
436
- do {
437
- } while (proceed());
438
- return output;
439
- };
440
- function proceed() {
441
- const stackDepth = stack.length;
442
- const next = nextToken();
443
- const [tokenType, blockName, attrs, startOffset, tokenLength] = next;
444
- const leadingHtmlStart = startOffset > offset ? offset : null;
445
- switch (tokenType) {
446
- case "no-more-tokens":
447
- if (0 === stackDepth) {
448
- addFreeform();
449
- return false;
450
- }
451
- if (1 === stackDepth) {
452
- addBlockFromStack();
453
- return false;
454
- }
455
- while (0 < stack.length) {
456
- addBlockFromStack();
457
- }
458
- return false;
459
- case "void-block":
460
- if (0 === stackDepth) {
461
- if (null !== leadingHtmlStart) {
462
- output.push(
463
- Freeform(
464
- document.substr(
465
- leadingHtmlStart,
466
- startOffset - leadingHtmlStart
467
- )
468
- )
469
- );
470
- }
471
- output.push(Block(blockName, attrs, [], "", []));
472
- offset = startOffset + tokenLength;
473
- return true;
474
- }
475
- addInnerBlock(
476
- Block(blockName, attrs, [], "", []),
477
- startOffset,
478
- tokenLength
479
- );
480
- offset = startOffset + tokenLength;
481
- return true;
482
- case "block-opener":
483
- stack.push(
484
- Frame(
485
- Block(blockName, attrs, [], "", []),
486
- startOffset,
487
- tokenLength,
488
- startOffset + tokenLength,
489
- leadingHtmlStart
490
- )
491
- );
492
- offset = startOffset + tokenLength;
493
- return true;
494
- case "block-closer":
495
- if (0 === stackDepth) {
496
- addFreeform();
497
- return false;
498
- }
499
- if (1 === stackDepth) {
500
- addBlockFromStack(startOffset);
501
- offset = startOffset + tokenLength;
502
- return true;
503
- }
504
- const stackTop = stack.pop();
505
- const html = document.substr(
506
- stackTop.prevOffset,
507
- startOffset - stackTop.prevOffset
508
- );
509
- stackTop.block.innerHTML += html;
510
- stackTop.block.innerContent.push(html);
511
- stackTop.prevOffset = startOffset + tokenLength;
512
- addInnerBlock(
513
- stackTop.block,
514
- stackTop.tokenStart,
515
- stackTop.tokenLength,
516
- startOffset + tokenLength
517
- );
518
- offset = startOffset + tokenLength;
519
- return true;
520
- default:
521
- addFreeform();
522
- return false;
523
- }
524
- }
525
- function parseJSON(input) {
526
- try {
527
- return JSON.parse(input);
528
- } catch (e) {
529
- return null;
530
- }
531
- }
532
- function nextToken() {
533
- const matches = tokenizer.exec(document);
534
- if (null === matches) {
535
- return ["no-more-tokens", "", null, 0, 0];
536
- }
537
- const startedAt = matches.index;
538
- const [
539
- match,
540
- closerMatch,
541
- namespaceMatch,
542
- nameMatch,
543
- attrsMatch,
544
- ,
545
- voidMatch
546
- ] = matches;
547
- const length = match.length;
548
- const isCloser = !!closerMatch;
549
- const isVoid = !!voidMatch;
550
- const namespace = namespaceMatch || "core/";
551
- const name = namespace + nameMatch;
552
- const hasAttrs = !!attrsMatch;
553
- const attrs = hasAttrs ? parseJSON(attrsMatch) : {};
554
- if (isVoid) {
555
- return ["void-block", name, attrs, startedAt, length];
556
- }
557
- if (isCloser) {
558
- return ["block-closer", name, null, startedAt, length];
559
- }
560
- return ["block-opener", name, attrs, startedAt, length];
561
- }
562
- function addFreeform(rawLength) {
563
- const length = document.length - offset;
564
- if (0 === length) {
565
- return;
566
- }
567
- output.push(Freeform(document.substr(offset, length)));
568
- }
569
- function addInnerBlock(block, tokenStart, tokenLength, lastOffset) {
570
- const parent = stack[stack.length - 1];
571
- parent.block.innerBlocks.push(block);
572
- const html = document.substr(
573
- parent.prevOffset,
574
- tokenStart - parent.prevOffset
575
- );
576
- if (html) {
577
- parent.block.innerHTML += html;
578
- parent.block.innerContent.push(html);
579
- }
580
- parent.block.innerContent.push(null);
581
- parent.prevOffset = lastOffset ? lastOffset : tokenStart + tokenLength;
582
- }
583
- function addBlockFromStack(endOffset) {
584
- const { block, leadingHtmlStart, prevOffset, tokenStart } = stack.pop();
585
- const html = endOffset ? document.substr(prevOffset, endOffset - prevOffset) : document.substr(prevOffset);
586
- if (html) {
587
- block.innerHTML += html;
588
- block.innerContent.push(html);
589
- }
590
- if (null !== leadingHtmlStart) {
591
- output.push(
592
- Freeform(
593
- document.substr(
594
- leadingHtmlStart,
595
- tokenStart - leadingHtmlStart
596
- )
597
- )
598
- );
599
- }
600
- output.push(block);
601
- }
602
-
603
- /**
604
- * @license GPL-3.0-or-later
605
- *
606
- * This file is part of the MarVAlt Open SDK.
607
- * Copyright (c) 2025 Vibune Pty Ltd.
608
- *
609
- * This program is free software: you can redistribute it and/or modify
610
- * it under the terms of the GNU General Public License as published by
611
- * the Free Software Foundation, either version 3 of the License, or
612
- * (at your option) any later version.
613
- *
614
- * This program is distributed in the hope that it will be useful,
615
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
616
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
617
- * See the GNU General Public License for more details.
618
- */
619
- // WordPress Static Data Generator
620
- /**
621
- * Extract menu items from a navigation block's innerHTML (rendered HTML)
622
- * WordPress renders navigation blocks, so menu items are in innerHTML as HTML
623
- * This function properly handles submenus by parsing the nested <ul> structure
624
- */
625
- function extractMenuFromNavigationHTML(innerHTML) {
626
- if (!innerHTML)
627
- return null;
628
- // Find the main navigation container <ul>
629
- const mainNavMatch = innerHTML.match(/<ul[^>]*class="[^"]*wp-block-navigation__container[^"]*"[^>]*>([\s\S]*?)<\/ul>/i);
630
- if (!mainNavMatch) {
631
- return null;
632
- }
633
- const mainNavContent = mainNavMatch[1];
634
- const items = [];
635
- let globalOrder = 0;
636
- /**
637
- * Parse a <li> element and extract menu item data
638
- * Handles both regular links and submenus
639
- */
640
- function parseMenuItem(liElement, parentId = 0, order = 0) {
641
- // Extract the link
642
- 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);
643
- if (!linkMatch) {
644
- return null;
645
- }
646
- const url = linkMatch[1].trim();
647
- const title = linkMatch[2].trim();
648
- if (!title) {
649
- return null;
650
- }
651
- // Normalize URL
652
- let finalUrl = url;
653
- try {
654
- const urlObj = new URL(url, 'http://dummy.com'); // Use dummy base for relative URLs
655
- const isAbsoluteUrl = url.startsWith('http://') || url.startsWith('https://');
656
- if (isAbsoluteUrl) {
657
- const pathname = urlObj.pathname;
658
- const isExternal = pathname === '' || pathname === '/' ||
659
- !urlObj.hostname.includes('vibunedemos.com') &&
660
- !urlObj.hostname.includes('localhost') &&
661
- !urlObj.hostname.includes('127.0.0.1');
662
- if (isExternal) {
663
- finalUrl = url;
664
- }
665
- else {
666
- finalUrl = pathname + urlObj.search + urlObj.hash;
667
- if (!finalUrl.startsWith('/')) {
668
- finalUrl = `/${finalUrl}`;
669
- }
670
- if (finalUrl !== '/' && finalUrl.endsWith('/')) {
671
- finalUrl = finalUrl.slice(0, -1);
672
- }
673
- }
674
- }
675
- else {
676
- if (!finalUrl.startsWith('/')) {
677
- finalUrl = `/${finalUrl}`;
678
- }
679
- if (finalUrl !== '/' && finalUrl.endsWith('/')) {
680
- finalUrl = finalUrl.slice(0, -1);
681
- }
682
- }
683
- }
684
- catch (e) {
685
- if (!finalUrl.startsWith('http://') && !finalUrl.startsWith('https://') && !finalUrl.startsWith('/')) {
686
- finalUrl = `/${finalUrl}`;
687
- }
688
- if (finalUrl !== '/' && finalUrl.endsWith('/')) {
689
- finalUrl = finalUrl.slice(0, -1);
690
- }
691
- }
692
- const itemId = globalOrder + 1;
693
- globalOrder++;
694
- const menuItem = {
695
- id: itemId,
696
- title: title,
697
- url: finalUrl,
698
- type: 'custom',
699
- object_id: 0,
700
- object: 'custom',
701
- parent: parentId,
702
- menu_order: order,
703
- };
704
- // Check if this <li> contains a submenu (<ul class="wp-block-navigation__submenu-container">)
705
- const submenuMatch = liElement.match(/<ul[^>]*class="[^"]*wp-block-navigation__submenu-container[^"]*"[^>]*>([\s\S]*?)<\/ul>/i);
706
- if (submenuMatch) {
707
- const submenuContent = submenuMatch[1];
708
- const children = [];
709
- let childOrder = 0;
710
- // Extract all <li> elements from the submenu
711
- const submenuLiRegex = /<li[^>]*class="[^"]*wp-block-navigation-item[^"]*"[^>]*>([\s\S]*?)<\/li>/gi;
712
- let submenuLiMatch;
713
- while ((submenuLiMatch = submenuLiRegex.exec(submenuContent)) !== null) {
714
- const childResult = parseMenuItem(submenuLiMatch[1], itemId, childOrder++);
715
- if (childResult) {
716
- children.push(childResult.item);
717
- globalOrder = childResult.nextOrder;
718
- }
719
- }
720
- if (children.length > 0) {
721
- menuItem.children = children;
722
- }
723
- }
724
- return { item: menuItem, nextOrder: globalOrder };
725
- }
726
- // Extract all top-level <li> elements from the main navigation
727
- // Find all <li> tags and extract complete items, filtering by position
728
- const liRegex = /<li[^>]*>/gi;
729
- const allLiMatches = [];
730
- // First pass: find all <li> tags and their matching </li>
731
- let match;
732
- while ((match = liRegex.exec(mainNavContent)) !== null) {
733
- const liStart = match.index;
734
- let depth = 0;
735
- let j = liStart;
736
- while (j < mainNavContent.length) {
737
- if (mainNavContent.substr(j, 3) === '<li' && /[>\s\n\t]/.test(mainNavContent[j + 3] || '')) {
738
- depth++;
739
- }
740
- else if (mainNavContent.substr(j, 5) === '</li>') {
741
- depth--;
742
- if (depth === 0) {
743
- allLiMatches.push({ start: liStart, end: j + 5 });
744
- break;
745
- }
746
- }
747
- j++;
748
- }
749
- }
750
- // Second pass: filter to only top-level items (not inside submenu containers)
751
- // Build a map of submenu container ranges
752
- const submenuRanges = [];
753
- const submenuContainerRegex = /<ul[^>]*class="[^"]*wp-block-navigation__submenu-container[^"]*"[^>]*>/gi;
754
- let submenuMatch;
755
- // Find all submenu containers and their matching </ul> tags
756
- while ((submenuMatch = submenuContainerRegex.exec(mainNavContent)) !== null) {
757
- const submenuStart = submenuMatch.index;
758
- const submenuUlEnd = mainNavContent.indexOf('>', submenuStart);
759
- if (submenuUlEnd === -1)
760
- continue;
761
- // Find matching </ul> by tracking depth
762
- let ulDepth = 0;
763
- let checkPos = submenuUlEnd + 1;
764
- let submenuEnd = -1;
765
- while (checkPos < mainNavContent.length) {
766
- const ulOpen = mainNavContent.indexOf('<ul', checkPos);
767
- const ulClose = mainNavContent.indexOf('</ul>', checkPos);
768
- if (ulOpen !== -1 && (ulClose === -1 || ulOpen < ulClose)) {
769
- ulDepth++;
770
- checkPos = ulOpen + 3;
771
- }
772
- else if (ulClose !== -1) {
773
- ulDepth--;
774
- if (ulDepth === 0) {
775
- submenuEnd = ulClose + 5;
776
- break;
777
- }
778
- checkPos = ulClose + 5;
779
- }
780
- else {
781
- break;
782
- }
783
- }
784
- if (submenuEnd !== -1) {
785
- submenuRanges.push({ start: submenuStart, end: submenuEnd });
786
- }
787
- }
788
- // Filter <li> elements: exclude only those that are children inside submenu containers
789
- // An <li> is a child (inside submenu) if: submenu's <ul> starts before <li> AND submenu's </ul> ends after <li> ends
790
- // An <li> is a parent (contains submenu) if: <li> starts before submenu's <ul> (we want to include these)
791
- for (const { start, end } of allLiMatches) {
792
- // Check if this <li> is a child inside any submenu container
793
- let isChildItem = false;
794
- for (const range of submenuRanges) {
795
- // If submenu <ul> starts before <li> AND submenu </ul> ends after <li> ends, then <li> is a child
796
- if (range.start < start && range.end > end) {
797
- isChildItem = true;
798
- break;
799
- }
800
- }
801
- // Include all items except children (children will be extracted by parseMenuItem as part of their parent)
802
- if (!isChildItem) {
803
- const item = mainNavContent.substring(start, end);
804
- if (item.includes('wp-block-navigation-item')) {
805
- const result = parseMenuItem(item, 0, items.length);
806
- if (result) {
807
- items.push(result.item);
808
- globalOrder = result.nextOrder;
809
- }
810
- }
811
- }
812
- }
813
- if (items.length === 0) {
814
- return null;
815
- }
816
- return {
817
- id: 0, // No menu ID for inline menus
818
- name: 'Navigation Menu',
819
- slug: 'navigation-menu',
820
- items: items,
821
- };
822
- }
823
- /**
824
- * Find a menu by name (checks both slug and name fields, case-insensitive)
825
- * @param menus Array of menu objects
826
- * @param menuNamePattern Name or slug pattern to match (case-insensitive)
827
- * @returns Menu object if found, null otherwise
828
- */
829
- async function findMenuByName(client, menuNamePattern) {
830
- try {
831
- console.log(`🔍 Fetching all menus to search for "${menuNamePattern}"...`);
832
- const allMenus = await client.getMenus();
833
- console.log(`📋 Found ${allMenus.length} menu(s):`, allMenus.map((m) => `${m.name} (slug: ${m.slug})`).join(', '));
834
- const patternLower = menuNamePattern.toLowerCase();
835
- // Find menu by slug or name (case-insensitive)
836
- const foundMenu = allMenus.find((menu) => {
837
- const slugMatch = menu.slug?.toLowerCase() === patternLower;
838
- const nameMatch = menu.name?.toLowerCase() === patternLower;
839
- // Also check if slug or name contains the pattern (for variations like "main-menu" vs "mainmenu")
840
- const slugNormalized = menu.slug?.toLowerCase().replace(/[^a-z0-9]/g, '');
841
- const nameNormalized = menu.name?.toLowerCase().replace(/[^a-z0-9]/g, '');
842
- const patternNormalized = patternLower.replace(/[^a-z0-9]/g, '');
843
- const slugContains = slugNormalized === patternNormalized;
844
- const nameContains = nameNormalized === patternNormalized;
845
- return slugMatch || nameMatch || slugContains || nameContains;
846
- });
847
- if (foundMenu) {
848
- console.log(`✅ Found matching menu: ${foundMenu.name} (ID: ${foundMenu.id}, slug: ${foundMenu.slug})`);
849
- // Fetch the full menu with items
850
- const fullMenu = await client.getMenu(foundMenu.id);
851
- return fullMenu;
852
- }
853
- console.log(`ℹ️ No menu found matching pattern "${menuNamePattern}"`);
854
- return null;
855
- }
856
- catch (error) {
857
- console.warn(`⚠️ Error finding menu by name "${menuNamePattern}":`, error);
858
- return null;
859
- }
860
- }
861
- /**
862
- * Extract menu items from a navigation block's innerBlocks
863
- * Navigation blocks can have menu items stored as core/navigation-link blocks
864
- */
865
- function extractMenuFromNavigationBlock(navBlock) {
866
- if (!navBlock) {
867
- return null;
868
- }
869
- // First, try to extract from innerBlocks if available
870
- if (navBlock.innerBlocks && navBlock.innerBlocks.length > 0) {
871
- const extractMenuItems = (blocks, parentId = 0, order = 0) => {
872
- const items = [];
873
- let currentOrder = order;
874
- for (const block of blocks) {
875
- if (block.name === 'core/navigation-link' || block.name === 'core/page-list' || block.name === 'core/navigation-submenu') {
876
- const attrs = block.attributes || {};
877
- const label = attrs.label || attrs.text || '';
878
- const url = attrs.url || attrs.href || '';
879
- const kind = attrs.kind || attrs.type || 'custom';
880
- const id = attrs.id || attrs.objectId || currentOrder;
881
- const menuItem = {
882
- id: id,
883
- title: label,
884
- url: url,
885
- type: kind,
886
- object_id: attrs.objectId || 0,
887
- object: attrs.type || 'custom',
888
- parent: parentId,
889
- menu_order: currentOrder++,
890
- };
891
- // Check for submenu items (children)
892
- if (block.innerBlocks && block.innerBlocks.length > 0) {
893
- const children = extractMenuItems(block.innerBlocks, id, 0);
894
- if (children.length > 0) {
895
- menuItem.children = children;
896
- }
897
- }
898
- items.push(menuItem);
899
- }
900
- else if (block.innerBlocks && block.innerBlocks.length > 0) {
901
- // Recursively search in nested blocks
902
- const nestedItems = extractMenuItems(block.innerBlocks, parentId, currentOrder);
903
- items.push(...nestedItems);
904
- currentOrder += nestedItems.length;
905
- }
906
- }
907
- return items;
908
- };
909
- const items = extractMenuItems(navBlock.innerBlocks);
910
- if (items.length > 0) {
911
- return {
912
- id: 0, // No menu ID for inline menus
913
- name: 'Navigation Menu',
914
- slug: 'navigation-menu',
915
- items: items,
916
- };
917
- }
918
- }
919
- // Fallback: Try to extract from innerHTML if innerBlocks are empty
920
- if (navBlock.innerHTML) {
921
- return extractMenuFromNavigationHTML(navBlock.innerHTML);
922
- }
923
- return null;
924
- }
925
- class WordPressGenerator {
926
- constructor(config) {
927
- this.config = config;
928
- }
929
- async generateStaticData() {
930
- const client = new WordPressClient(this.config);
931
- console.log('🚀 Starting WordPress static data generation...');
932
- const frontendId = this.config.frontendId ?? 'default-frontend';
933
- const frontendName = this.config.frontendName || 'Default Frontend';
934
- const postTypes = this.config.postTypes || ['posts', 'pages'];
935
- // Use 'view' context - wp-custom-api-endpoints plugin adds blocks automatically.
936
- // Frontends: when app provides frontendId, request Default + app frontend (OR); otherwise no filter (single-frontend backend).
937
- // Use a custom slug-based param to avoid colliding with WordPress core taxonomy term ID filters.
938
- const fetchParams = {
939
- per_page: this.config.maxItems || 100,
940
- _embed: true,
941
- context: 'view', // Plugin adds blocks in 'view' context
942
- };
943
- if (this.config.frontendId && this.config.frontendId.trim() !== '') {
944
- fetchParams.frontend_slugs = 'default,' + this.config.frontendId.trim();
945
- }
946
- console.log(`📋 Fetching post types: ${postTypes.join(', ')}`);
947
- console.log(`⚙️ Settings: maxItems=${fetchParams.per_page}, includeEmbedded=${fetchParams._embed}`);
948
- // Fetch WordPress Settings (Reading Settings + Site Branding + Theme Styles)
949
- let frontPage;
950
- let settingsPageId;
951
- let siteSettings;
952
- let themeStyles;
953
- let header;
954
- let footer;
955
- let menuData;
956
- let menus = {}; // Declare menus outside try block so it's accessible later
957
- try {
958
- console.log('🔍 Fetching WordPress Settings...');
959
- const settings = await client.getSettings();
960
- if (settings.front_page) {
961
- // WordPress has a page set as front page in Reading Settings
962
- frontPage = settings.front_page;
963
- console.log(`✅ Front page determined from Reading Settings: ${frontPage.slug} (ID: ${frontPage.id})`);
964
- }
965
- else if (settings.show_on_front === 'page' && settings.page_on_front > 0) {
966
- // Page is set but not returned in front_page (might be in pages list)
967
- settingsPageId = settings.page_on_front;
968
- console.log(`📋 Front page ID ${settingsPageId} set in Reading Settings, will check fetched pages`);
969
- }
970
- // Extract site branding settings
971
- if (settings.logo || settings.site_icon || settings.site_name || settings.site_description) {
972
- siteSettings = {
973
- ...(settings.logo && { logo: settings.logo }),
974
- ...(settings.site_icon && { site_icon: settings.site_icon }),
975
- ...(settings.site_name && { site_name: settings.site_name }),
976
- ...(settings.site_description && { site_description: settings.site_description }),
977
- };
978
- if (siteSettings.logo) {
979
- console.log(`✅ Site logo found: ${siteSettings.logo.cloudflare_url || siteSettings.logo.url}`);
980
- }
981
- if (siteSettings.site_icon) {
982
- console.log(`✅ Site icon found: ${siteSettings.site_icon.cloudflare_url || siteSettings.site_icon.url}`);
983
- }
984
- if (siteSettings.site_name) {
985
- console.log(`✅ Site name: ${siteSettings.site_name}`);
986
- }
987
- }
988
- // Extract theme styles
989
- if (settings.theme_styles) {
990
- themeStyles = settings.theme_styles;
991
- console.log(`✅ Theme styles found:`, {
992
- hasTypography: !!themeStyles.typography,
993
- hasColors: !!themeStyles.colors,
994
- hasLayout: !!themeStyles.layout,
995
- });
996
- }
997
- else {
998
- console.log('⚠️ No theme_styles found in settings response');
999
- }
1000
- // Fetch header template part and extract menu
1001
- console.log('🔍 Fetching Header template part...');
1002
- try {
1003
- header = await client.getHeader('header', 'header');
1004
- if (header && header.blocks && header.blocks.length > 0) {
1005
- console.log(`✅ Header found: ${header.blocks_count} blocks`);
1006
- // Find navigation block and extract menu ID
1007
- const findNavigationBlock = (blocks) => {
1008
- for (const block of blocks) {
1009
- if (block.name === 'core/navigation') {
1010
- return block;
1011
- }
1012
- if (block.innerBlocks && block.innerBlocks.length > 0) {
1013
- const found = findNavigationBlock(block.innerBlocks);
1014
- if (found)
1015
- return found;
1016
- }
1017
- }
1018
- return null;
1019
- };
1020
- const navBlock = findNavigationBlock(header.blocks);
1021
- if (navBlock && navBlock.attributes) {
1022
- // Debug: Log all navigation block attributes
1023
- console.log('🔍 Navigation block attributes:', JSON.stringify(navBlock.attributes, null, 2));
1024
- console.log('🔍 Navigation block innerBlocks:', navBlock.innerBlocks ? `${navBlock.innerBlocks.length} blocks` : 'none');
1025
- // WordPress stores menu ID in 'ref' attribute (term_id of the menu)
1026
- // Also check for other possible attribute names
1027
- const menuId = navBlock.attributes['ref'] ||
1028
- navBlock.attributes['menuId'] ||
1029
- navBlock.attributes['menu'] ||
1030
- navBlock.attributes['navigationMenuId'] ||
1031
- navBlock.attributes['menuRef'];
1032
- if (menuId) {
1033
- try {
1034
- console.log(`🔍 Found navigation block with menu ID: ${menuId}, fetching menu data...`);
1035
- menuData = await client.getMenu(parseInt(menuId));
1036
- console.log(`✅ Menu data fetched: ${menuData.items?.length || 0} items`);
1037
- console.log('📋 Menu data structure:', JSON.stringify({
1038
- id: menuData.id,
1039
- name: menuData.name,
1040
- itemsCount: menuData.items?.length || 0,
1041
- firstItem: menuData.items?.[0] ? {
1042
- id: menuData.items[0].id,
1043
- title: menuData.items[0].title,
1044
- url: menuData.items[0].url
1045
- } : null
1046
- }, null, 2));
1047
- }
1048
- catch (error) {
1049
- console.warn(`⚠️ Could not fetch menu data for menu ID ${menuId}:`, error);
1050
- // Fallback: Try to find menu by name (mainmenu, main-menu, Main Menu, etc.)
1051
- console.log('🔍 Menu fetch failed, attempting to find menu by name "mainmenu"...');
1052
- try {
1053
- const namedMenu = await findMenuByName(client, 'mainmenu');
1054
- if (namedMenu && namedMenu.items && namedMenu.items.length > 0) {
1055
- menuData = namedMenu;
1056
- console.log(`✅ Found menu by name "mainmenu": ${namedMenu.name} (${namedMenu.items.length} items)`);
1057
- }
1058
- else {
1059
- console.log('ℹ️ No menu found with name "mainmenu"');
1060
- // Try extracting from Navigation block HTML as last resort
1061
- console.log('🔍 Attempting to extract navigation from Navigation block innerHTML...');
1062
- try {
1063
- const extractedMenu = extractMenuFromNavigationBlock(navBlock);
1064
- if (extractedMenu && extractedMenu.items && extractedMenu.items.length > 0) {
1065
- menuData = extractedMenu;
1066
- console.log(`✅ Extracted navigation from Navigation block: ${extractedMenu.items.length} items`);
1067
- }
1068
- }
1069
- catch (extractError) {
1070
- console.warn('⚠️ Error extracting navigation from Navigation block:', extractError);
1071
- }
1072
- }
1073
- }
1074
- catch (nameError) {
1075
- console.warn('⚠️ Error finding menu by name:', nameError);
1076
- }
1077
- }
1078
- }
1079
- else {
1080
- console.log('ℹ️ Navigation block found but no menu ID attribute found');
1081
- console.log('🔍 Available attributes:', Object.keys(navBlock.attributes));
1082
- // Priority 2: Try to find menu by name (mainmenu, main-menu, Main Menu, etc.)
1083
- console.log('🔍 Attempting to find menu by name "mainmenu"...');
1084
- try {
1085
- const namedMenu = await findMenuByName(client, 'mainmenu');
1086
- if (namedMenu && namedMenu.items && namedMenu.items.length > 0) {
1087
- menuData = namedMenu;
1088
- console.log(`✅ Found menu by name "mainmenu": ${namedMenu.name} (${namedMenu.items.length} items)`);
1089
- }
1090
- else {
1091
- console.log('ℹ️ No menu found with name "mainmenu"');
1092
- // In block themes, navigation items may be stored directly in the Navigation block's innerBlocks
1093
- // Try to extract from innerBlocks (this is how block themes work)
1094
- console.log('🔍 Attempting to extract navigation from Navigation block innerBlocks...');
1095
- try {
1096
- const extractedMenu = extractMenuFromNavigationBlock(navBlock);
1097
- if (extractedMenu && extractedMenu.items && extractedMenu.items.length > 0) {
1098
- menuData = extractedMenu;
1099
- console.log(`✅ Extracted navigation from Navigation block: ${extractedMenu.items.length} items`);
1100
- console.log('📋 Navigation items:', extractedMenu.items.map((item) => `${item.title} (${item.url})`).join(', '));
1101
- console.log('💡 TIP: For better control, create a WordPress menu (Appearance > Menus) named "mainmenu" and assign it to the Navigation block.');
1102
- }
1103
- else {
1104
- console.log('⚠️ WARNING: No navigation found in Navigation block. The frontend will fall back to page hierarchy.');
1105
- console.log('💡 TIP: Add navigation items to the Navigation block in the Site Editor, or create a menu named "mainmenu" in WordPress (Appearance > Menus).');
1106
- menuData = null;
1107
- }
1108
- }
1109
- catch (error) {
1110
- console.warn('⚠️ Error extracting navigation from Navigation block:', error);
1111
- console.log('⚠️ WARNING: The frontend will fall back to page hierarchy.');
1112
- menuData = null;
1113
- }
1114
- }
1115
- }
1116
- catch (error) {
1117
- console.warn('⚠️ Error finding menu by name:', error);
1118
- console.log('ℹ️ Skipping HTML extraction - frontend will use page hierarchy fallback');
1119
- menuData = null; // Don't use extracted menu - let frontend handle fallback
1120
- }
1121
- }
1122
- }
1123
- else {
1124
- console.log('ℹ️ No navigation block found in header');
1125
- }
1126
- }
1127
- else {
1128
- console.log('ℹ️ No header template part found');
1129
- }
1130
- }
1131
- catch (error) {
1132
- console.warn('⚠️ Could not fetch header template part:', error);
1133
- }
1134
- // Fetch footer template part
1135
- console.log('🔍 Fetching Footer template part...');
1136
- try {
1137
- footer = await client.getFooter('footer', 'footer');
1138
- if (footer && footer.blocks && footer.blocks.length > 0) {
1139
- console.log(`✅ Footer found: ${footer.blocks_count} blocks`);
1140
- }
1141
- else {
1142
- console.log('⚠️ Footer template part is empty or not found');
1143
- }
1144
- }
1145
- catch (error) {
1146
- console.log('⚠️ Failed to fetch footer:', error instanceof Error ? error.message : 'Unknown error');
1147
- }
1148
- // Extract all menu IDs from footer blocks and fetch them for static data
1149
- const allMenuIds = new Set();
1150
- if (menuData && menuData.id) {
1151
- allMenuIds.add(menuData.id); // Add header menu
1152
- console.log(`📋 Added header menu ID ${menuData.id} (${menuData.name}) to menu list`);
1153
- }
1154
- if (footer && footer.blocks) {
1155
- let navigationBlockCount = 0;
1156
- const findNavigationBlocks = (blocks, depth = 0) => {
1157
- for (const block of blocks) {
1158
- if (block.name === 'core/navigation') {
1159
- navigationBlockCount++;
1160
- console.log(`🔍 Found navigation block at depth ${depth}:`, {
1161
- blockName: block.name,
1162
- hasAttributes: !!block.attributes,
1163
- attributes: block.attributes ? Object.keys(block.attributes) : [],
1164
- });
1165
- if (block.attributes) {
1166
- const menuId = block.attributes['ref'] ||
1167
- block.attributes['menuId'] ||
1168
- block.attributes['menu'] ||
1169
- block.attributes['navigationMenuId'] ||
1170
- block.attributes['menuRef'];
1171
- if (menuId) {
1172
- const menuIdNum = parseInt(String(menuId));
1173
- allMenuIds.add(menuIdNum);
1174
- console.log(` ✅ Extracted menu ID ${menuIdNum} from attribute:`, {
1175
- ref: block.attributes['ref'],
1176
- menuId: block.attributes['menuId'],
1177
- menu: block.attributes['menu'],
1178
- navigationMenuId: block.attributes['navigationMenuId'],
1179
- menuRef: block.attributes['menuRef'],
1180
- extracted: menuIdNum,
1181
- });
1182
- }
1183
- else {
1184
- console.warn(` ⚠️ Navigation block found but no menu ID attribute detected. Available attributes:`, Object.keys(block.attributes));
1185
- }
1186
- }
1187
- else {
1188
- console.warn(` ⚠️ Navigation block found but has no attributes`);
1189
- }
1190
- }
1191
- if (block.innerBlocks && block.innerBlocks.length > 0) {
1192
- findNavigationBlocks(block.innerBlocks, depth + 1);
1193
- }
1194
- }
1195
- };
1196
- findNavigationBlocks(footer.blocks);
1197
- console.log(`📊 Navigation block extraction summary:`, {
1198
- navigationBlocksFound: navigationBlockCount,
1199
- uniqueMenuIds: allMenuIds.size,
1200
- menuIds: Array.from(allMenuIds),
1201
- });
1202
- }
1203
- else {
1204
- console.log(`ℹ️ No footer blocks found or footer is empty`);
1205
- }
1206
- // Fetch all menus and store them in a menus object keyed by ID
1207
- menus = {}; // Reset menus object (already declared above)
1208
- if (menuData && menuData.id) {
1209
- menus[menuData.id] = menuData;
1210
- console.log(`✅ Added header menu to menus object: ID ${menuData.id} (${menuData.name})`);
1211
- }
1212
- let fetchAttempts = 0;
1213
- let fetchSuccesses = 0;
1214
- let fetchFailures = 0;
1215
- for (const menuId of allMenuIds) {
1216
- if (!menus[menuId]) {
1217
- fetchAttempts++;
1218
- console.log(`🔍 Attempting to fetch menu ID ${menuId}... (attempt ${fetchAttempts}/${allMenuIds.size})`);
1219
- try {
1220
- const fetchedMenu = await client.getMenu(menuId);
1221
- // Validate menu structure
1222
- if (!fetchedMenu) {
1223
- console.error(`❌ Menu ID ${menuId}: Fetched menu is null or undefined`);
1224
- fetchFailures++;
1225
- continue;
1226
- }
1227
- if (!fetchedMenu.id) {
1228
- console.error(`❌ Menu ID ${menuId}: Fetched menu missing 'id' property:`, fetchedMenu);
1229
- fetchFailures++;
1230
- continue;
1231
- }
1232
- if (!fetchedMenu.name) {
1233
- console.warn(`⚠️ Menu ID ${menuId}: Fetched menu missing 'name' property:`, fetchedMenu);
1234
- }
1235
- if (!fetchedMenu.items) {
1236
- console.warn(`⚠️ Menu ID ${menuId}: Fetched menu missing 'items' property:`, fetchedMenu);
1237
- }
1238
- else if (!Array.isArray(fetchedMenu.items)) {
1239
- console.error(`❌ Menu ID ${menuId}: Fetched menu 'items' is not an array:`, typeof fetchedMenu.items);
1240
- fetchFailures++;
1241
- continue;
1242
- }
1243
- // Store menu even if items array is empty (for debugging)
1244
- menus[menuId] = fetchedMenu;
1245
- if (fetchedMenu.items && fetchedMenu.items.length > 0) {
1246
- fetchSuccesses++;
1247
- console.log(`✅ Fetched menu ID ${menuId}: ${fetchedMenu.name} (${fetchedMenu.items.length} items)`);
1248
- }
1249
- else {
1250
- fetchSuccesses++; // Still count as success, just empty
1251
- console.warn(`⚠️ Menu ID ${menuId}: ${fetchedMenu.name} fetched but has no items`);
1252
- }
1253
- }
1254
- catch (error) {
1255
- fetchFailures++;
1256
- const errorDetails = {
1257
- message: error.message || 'Unknown error',
1258
- status: error.status || 'unknown',
1259
- };
1260
- // Try to extract more error details
1261
- if (error.response) {
1262
- errorDetails.responseStatus = error.response.status;
1263
- errorDetails.responseStatusText = error.response.statusText;
1264
- }
1265
- if (error.body) {
1266
- try {
1267
- errorDetails.body = typeof error.body === 'string' ? error.body : JSON.stringify(error.body);
1268
- }
1269
- catch (e) {
1270
- errorDetails.body = 'Could not stringify error body';
1271
- }
1272
- }
1273
- console.error(`❌ Could not fetch menu ID ${menuId}:`, errorDetails);
1274
- console.error(` Full error object:`, error);
1275
- }
1276
- }
1277
- else {
1278
- console.log(`ℹ️ Menu ID ${menuId} already in menus object, skipping fetch`);
1279
- }
1280
- }
1281
- // Log menu fetching summary
1282
- console.log(`📊 Menu fetching summary:`, {
1283
- totalMenuIds: allMenuIds.size,
1284
- fetchAttempts,
1285
- fetchSuccesses,
1286
- fetchFailures,
1287
- menusInObject: Object.keys(menus).length,
1288
- menuIdsInObject: Object.keys(menus).map(id => parseInt(id)),
1289
- menuNames: Object.values(menus).map((m) => m.name || 'unnamed').join(', '),
1290
- });
1291
- // Debug: Dump all WordPress theme data in development
1292
- if (process.env.NODE_ENV === 'development') {
1293
- const debugData = {
1294
- timestamp: new Date().toISOString(),
1295
- settings_response: settings,
1296
- theme_styles_extracted: themeStyles,
1297
- theme_styles_typography: themeStyles?.typography,
1298
- theme_styles_colors: themeStyles?.colors,
1299
- theme_styles_layout: themeStyles?.layout,
1300
- theme_styles_palette: themeStyles?.theme_palette,
1301
- };
1302
- // Use same directory as output file (public/)
1303
- const outputFullPath = join(process.cwd(), this.config.outputPath);
1304
- const outputDir = dirname(outputFullPath);
1305
- const debugPath = join(outputDir, 'wp-theme-data.json');
1306
- try {
1307
- writeFileSync(debugPath, JSON.stringify(debugData, null, 2), 'utf-8');
1308
- console.log(`📊 Debug: WordPress theme data dumped to ${debugPath}`);
1309
- }
1310
- catch (error) {
1311
- console.warn('⚠️ Could not write debug file:', error);
1312
- }
1313
- }
1314
- }
1315
- catch (error) {
1316
- console.warn('⚠️ Could not fetch WordPress settings, will use fallback logic:', error);
1317
- }
1318
- // Fetch standard post types
1319
- const data = await client.getAllData(fetchParams);
1320
- // Determine front page if not already set from settings
1321
- if (!frontPage) {
1322
- if (settingsPageId) {
1323
- // Try to find the page by ID from Reading Settings
1324
- const settingsPage = (data.pages || []).find((p) => p.id === settingsPageId);
1325
- if (settingsPage) {
1326
- const title = typeof settingsPage.title === 'string'
1327
- ? settingsPage.title
1328
- : (settingsPage.title?.rendered || 'Frontpage');
1329
- frontPage = {
1330
- id: settingsPage.id,
1331
- slug: settingsPage.slug,
1332
- title,
1333
- };
1334
- if (frontPage) {
1335
- console.log(`✅ Front page determined from Reading Settings (ID match): ${frontPage.slug} (ID: ${frontPage.id})`);
1336
- }
1337
- }
1338
- }
1339
- // Fallback: look for page with slug "frontpage"
1340
- if (!frontPage) {
1341
- const frontpagePage = (data.pages || []).find((p) => p.slug === 'frontpage');
1342
- if (frontpagePage) {
1343
- const title = typeof frontpagePage.title === 'string'
1344
- ? frontpagePage.title
1345
- : (frontpagePage.title?.rendered || 'Frontpage');
1346
- frontPage = {
1347
- id: frontpagePage.id,
1348
- slug: frontpagePage.slug,
1349
- title,
1350
- };
1351
- if (frontPage) {
1352
- console.log(`✅ Front page determined from slug "frontpage": ${frontPage.slug} (ID: ${frontPage.id})`);
1353
- }
1354
- }
1355
- else {
1356
- console.log('⚠️ No front page found (neither in Reading Settings nor slug "frontpage")');
1357
- }
1358
- }
1359
- }
1360
- // Create the static data structure
1361
- const staticData = {
1362
- generated_at: new Date().toISOString(),
1363
- frontend_id: frontendId,
1364
- frontend_name: frontendName,
1365
- ...(frontPage && { front_page: frontPage }),
1366
- ...(siteSettings && { site_settings: siteSettings }),
1367
- ...(header && { header: header }),
1368
- ...(menuData && { navigation_menu: menuData }),
1369
- menus: menus, // Always include menus object, even if empty, for debugging
1370
- ...(footer && { footer: footer }),
1371
- ...(themeStyles && { theme_styles: themeStyles }),
1372
- config: {
1373
- frontend_id: frontendId,
1374
- frontend_name: frontendName,
1375
- post_types: postTypes.reduce((acc, postType) => {
1376
- acc[postType] = {
1377
- max_items: this.config.maxItems || 100,
1378
- include_embedded: this.config.includeEmbedded || true,
1379
- orderby: 'date',
1380
- order: 'desc',
1381
- };
1382
- return acc;
1383
- }, {}),
1384
- },
1385
- // Map standard post types
1386
- posts: data.posts,
1387
- pages: await Promise.all((data.pages || []).map(async (p) => {
1388
- // Always use custom blocks endpoint to ensure we get the latest blocks with complete Cloudflare URLs
1389
- // The custom endpoint (/wp-custom/v1/pages/{id}/blocks) is more reliable and always has the latest data
1390
- let blocks = [];
1391
- try {
1392
- console.log(`🌐 Fetching blocks from custom endpoint for page "${p.slug}" (ID: ${p.id})...`);
1393
- const blocksResponse = await client.getPageBlocks(p.id);
1394
- console.log(`📦 Blocks response for "${p.slug}":`, blocksResponse ? `has blocks=${Array.isArray(blocksResponse.blocks) ? blocksResponse.blocks.length : 'N/A'}` : 'null');
1395
- if (blocksResponse && Array.isArray(blocksResponse.blocks) && blocksResponse.blocks.length > 0) {
1396
- blocks = blocksResponse.blocks;
1397
- console.log(`✅ Fetched ${blocks.length} blocks for page "${p.slug}" (ID: ${p.id}) from custom endpoint`);
1398
- }
1399
- else {
1400
- // Fallback: try blocks from API response if custom endpoint is empty
1401
- const blocksFromResponse = Array.isArray(p.blocks) ? p.blocks : [];
1402
- if (blocksFromResponse.length > 0) {
1403
- blocks = blocksFromResponse;
1404
- console.log(`⚠️ Custom endpoint returned no blocks, using ${blocks.length} blocks from API response for page "${p.slug}" (ID: ${p.id})`);
1405
- }
1406
- else {
1407
- console.warn(`⚠️ Custom endpoint returned no blocks for page "${p.slug}" (ID: ${p.id})`);
1408
- // Last resort: try parsing from content.raw (but this won't work in 'view' context)
1409
- const raw = p?.content?.raw || '';
1410
- if (raw) {
1411
- try {
1412
- blocks = parse(raw);
1413
- if (blocks.length > 0) {
1414
- console.log(`✅ Parsed ${blocks.length} blocks for page "${p.slug}" (ID: ${p.id}) from content.raw`);
1415
- }
1416
- }
1417
- catch (parseError) {
1418
- console.warn(`⚠️ Failed to parse blocks from content.raw for page "${p.slug}" (ID: ${p.id}):`, parseError);
1419
- }
1420
- }
1421
- else {
1422
- console.warn(`⚠️ Page "${p.slug}" (ID: ${p.id}) has no blocks and no content.raw. Plugin may not be active or working.`);
1423
- }
1424
- }
1425
- }
1426
- }
1427
- catch (e) {
1428
- console.warn(`⚠️ Failed to fetch blocks from custom endpoint for page "${p.slug}" (ID: ${p.id}):`, e.message);
1429
- // Fallback: try blocks from API response
1430
- const blocksFromResponse = Array.isArray(p.blocks) ? p.blocks : [];
1431
- if (blocksFromResponse.length > 0) {
1432
- blocks = blocksFromResponse;
1433
- console.log(`⚠️ Using ${blocks.length} blocks from API response as fallback for page "${p.slug}" (ID: ${p.id})`);
1434
- }
1435
- else {
1436
- // Last resort: try parsing from content.raw
1437
- try {
1438
- const raw = p?.content?.raw || '';
1439
- if (raw) {
1440
- blocks = parse(raw);
1441
- if (blocks.length > 0) {
1442
- console.log(`✅ Parsed ${blocks.length} blocks for page "${p.slug}" (ID: ${p.id}) from content.raw (fallback)`);
1443
- }
1444
- }
1445
- }
1446
- catch (parseError) {
1447
- // Silent fail - we've already logged the warning
1448
- }
1449
- }
1450
- }
1451
- return { ...p, blocks };
1452
- })),
1453
- media: data.media,
1454
- categories: data.categories,
1455
- tags: data.tags,
1456
- };
1457
- // Fetch custom post types (anything beyond the standard posts, pages, media, categories, tags)
1458
- const standardTypes = ['posts', 'pages', 'media', 'categories', 'tags'];
1459
- const customPostTypes = postTypes.filter(pt => !standardTypes.includes(pt));
1460
- if (customPostTypes.length > 0) {
1461
- console.log(`📦 Fetching custom post types: ${customPostTypes.join(', ')}`);
1462
- for (const postType of customPostTypes) {
1463
- try {
1464
- console.log(`\n🔍 Fetching custom post type: ${postType}`);
1465
- const customData = await client.getCustomPostType(postType, fetchParams);
1466
- staticData[postType] = customData;
1467
- console.log(`✅ ${postType}: ${customData.length} items fetched`);
1468
- }
1469
- catch (error) {
1470
- const msg = error?.message || String(error);
1471
- const status = error?.response?.status ?? error?.status;
1472
- console.warn(`⚠️ Failed to fetch custom post type '${postType}':`, msg);
1473
- if (status)
1474
- console.warn(` HTTP status: ${status}`);
1475
- if (error?.response?.statusText)
1476
- console.warn(` Status text: ${error.response.statusText}`);
1477
- staticData[postType] = [];
1478
- }
1479
- }
1480
- // Enrich CPT items that have featured_media with Cloudflare URL from media list
1481
- // (API may return WordPress URLs in featured_image_urls; media has cloudflare_image_standardized)
1482
- const mediaById = new Map();
1483
- (data.media || []).forEach((m) => {
1484
- const url = m.cloudflare_image_standardized || m.cloudflare_url;
1485
- if (url)
1486
- mediaById.set(m.id, url);
1487
- });
1488
- if (mediaById.size > 0) {
1489
- for (const postType of customPostTypes) {
1490
- const items = staticData[postType];
1491
- if (!Array.isArray(items))
1492
- continue;
1493
- let enriched = 0;
1494
- items.forEach((item) => {
1495
- const fid = item.featured_media ? Number(item.featured_media) : 0;
1496
- if (fid && mediaById.has(fid)) {
1497
- item.featured_media_cloudflare_url = mediaById.get(fid);
1498
- enriched++;
1499
- }
1500
- });
1501
- if (enriched > 0) {
1502
- console.log(`✅ Enriched ${enriched} ${postType} with featured_media_cloudflare_url`);
1503
- }
1504
- }
1505
- }
1506
- }
1507
- // Write to file
1508
- this.writeStaticData(staticData);
1509
- console.log('\n✅ WordPress static data generation completed');
1510
- console.log(`📊 Standard data: ${data.posts.length} posts, ${data.pages.length} pages, ${data.media.length} media`);
1511
- if (customPostTypes.length > 0) {
1512
- const customCounts = customPostTypes.map(pt => `${pt}: ${staticData[pt]?.length || 0}`).join(', ');
1513
- console.log(`📊 Custom data: ${customCounts}`);
1514
- }
1515
- return staticData;
1516
- }
1517
- writeStaticData(data) {
1518
- try {
1519
- const outputPath = join(process.cwd(), this.config.outputPath);
1520
- writeFileSync(outputPath, JSON.stringify(data, null, 2));
1521
- console.log(`📁 Static data written to: ${outputPath}`);
1522
- }
1523
- catch (error) {
1524
- console.error('❌ Error writing static data:', error);
1525
- throw error;
1526
- }
1527
- }
1528
- async generatePostsOnly() {
1529
- const client = new WordPressClient(this.config);
1530
- return client.getPosts({ per_page: 100 });
1531
- }
1532
- async generatePagesOnly() {
1533
- const client = new WordPressClient(this.config);
1534
- return client.getPages({ per_page: 100 });
1535
- }
1536
- async generateMediaOnly() {
1537
- const client = new WordPressClient(this.config);
1538
- return client.getMedia({ per_page: 100 });
1539
- }
1540
- }
1541
- // Convenience function for easy usage
1542
- async function generateWordPressData$1(config) {
1543
- const generator = new WordPressGenerator(config);
1544
- return generator.generateStaticData();
1545
- }
1546
-
1547
- /**
1548
- * @license GPL-3.0-or-later
1549
- *
1550
- * This file is part of the MarVAlt Open SDK.
1551
- * Copyright (c) 2025 Vibune Pty Ltd.
1552
- *
1553
- * This program is free software: you can redistribute it and/or modify
1554
- * it under the terms of the GNU General Public License as published by
1555
- * the Free Software Foundation, either version 3 of the License, or
1556
- * (at your option) any later version.
1557
- *
1558
- * This program is distributed in the hope that it will be useful,
1559
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
1560
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1561
- * See the GNU General Public License for more details.
1562
- */
1563
- class GravityFormsClient {
1564
- constructor(config) {
1565
- this.config = config;
1566
- // Determine mode: direct for generators (Node.js), proxy for browser
1567
- const hasCredentials = !!(config.username && config.password && config.username.length > 0 && config.password.length > 0);
1568
- this.useDirectMode = config.authMode === 'direct' && hasCredentials;
1569
- // Debug logging for Node.js environments (generators)
1570
- if (typeof process !== 'undefined' && process.env) {
1571
- console.log('🔍 GravityFormsClient Configuration:', {
1572
- authMode: config.authMode,
1573
- hasUsername: !!config.username,
1574
- hasPassword: !!config.password,
1575
- hasApiUrl: !!config.apiUrl,
1576
- useDirectMode: this.useDirectMode,
1577
- });
1578
- }
1579
- // Convention-based: Always use /api/gravity-forms-submit unless explicitly overridden
1580
- this.proxyEndpoint = config.proxyEndpoint || '/api/gravity-forms-submit';
1581
- }
1582
- async makeRequest(endpoint, options = {}) {
1583
- let url;
1584
- const headers = {
1585
- 'Content-Type': 'application/json',
1586
- ...options.headers,
1587
- };
1588
- if (this.useDirectMode) {
1589
- // Direct mode: Call WordPress API directly (for generators/Node.js)
1590
- url = `${this.config.apiUrl}${endpoint}`;
1591
- // Add Basic Auth
1592
- if (this.config.username && this.config.password) {
1593
- const credentials = btoa(`${this.config.username}:${this.config.password}`);
1594
- headers['Authorization'] = `Basic ${credentials}`;
1595
- }
1596
- // Add Cloudflare Access Service Token headers if provided (for CF Access protected APIs)
1597
- if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
1598
- headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
1599
- headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
1600
- console.log('🔐 Added CF Access Service Token headers to Gravity Forms request');
1601
- console.log('🔐 CF-Access-Client-Id:', this.config.cfAccessClientId.substring(0, 8) + '...');
1602
- console.log('🔐 CF-Access-Client-Secret:', this.config.cfAccessClientSecret.substring(0, 8) + '... (hidden)');
1603
- }
1604
- else {
1605
- console.log('⚠️ CF Access Service Token NOT provided - requests may be blocked by Zero Trust');
1606
- console.log(' cfAccessClientId present:', !!this.config.cfAccessClientId);
1607
- console.log(' cfAccessClientSecret present:', !!this.config.cfAccessClientSecret);
1608
- }
1609
- }
1610
- else {
1611
- // Proxy mode: Use Pages Function (for browser)
1612
- const proxyEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
1613
- url = `${this.proxyEndpoint}?endpoint=${encodeURIComponent(proxyEndpoint)}`;
1614
- }
1615
- const response = await fetch(url, {
1616
- ...options,
1617
- headers,
1618
- signal: AbortSignal.timeout(this.config.timeout || 30000),
1619
- });
1620
- const responseText = await response.text();
1621
- if (!response.ok) {
1622
- // Try to parse error response for validation details
1623
- let errorDetails = null;
1624
- try {
1625
- if (responseText) {
1626
- errorDetails = JSON.parse(responseText);
1627
- }
1628
- }
1629
- catch (e) {
1630
- // Ignore parse errors, use generic error
1631
- }
1632
- // Provide helpful error message for 404 (plugin not installed/activated)
1633
- if (response.status === 404) {
1634
- const errorMsg = `Gravity Forms API request failed: ${response.status} ${response.statusText}\n` +
1635
- `The endpoint ${url} was not found.\n` +
1636
- `This usually means the 'gravity-forms-api-endpoint' plugin is not installed or activated on your WordPress site.\n` +
1637
- `Please verify:\n` +
1638
- ` 1. The plugin is installed in /wp-content/plugins/gravity-forms-api-endpoint/\n` +
1639
- ` 2. The plugin is activated in WordPress admin\n` +
1640
- ` 3. The endpoint is accessible: ${this.config.apiUrl}/health`;
1641
- throw new Error(errorMsg);
1642
- }
1643
- // For 400 errors, include validation details if available
1644
- // WordPress REST API formats WP_Error as: { code, message, data: { status, validation_errors? } }
1645
- if (response.status === 400 && errorDetails) {
1646
- // Debug logging to see the actual error structure
1647
- if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production') {
1648
- console.log('🔍 GravityFormsClient 400 error details:', JSON.stringify(errorDetails, null, 2));
1649
- }
1650
- const error = new Error(errorDetails.message || `Gravity Forms API request failed: ${response.status} ${response.statusText}`);
1651
- error.data = errorDetails;
1652
- error.code = errorDetails.code;
1653
- // Extract validation_errors from WordPress REST API format
1654
- if (errorDetails.data?.validation_errors) {
1655
- error.validation_errors = errorDetails.data.validation_errors;
1656
- }
1657
- throw error;
1658
- }
1659
- throw new Error(`Gravity Forms API request failed: ${response.status} ${response.statusText}`);
1660
- }
1661
- if (!responseText) {
1662
- return [];
1663
- }
1664
- try {
1665
- return JSON.parse(responseText);
1666
- }
1667
- catch (error) {
1668
- console.error('❌ Gravity Forms JSON Parse Error:', {
1669
- error: error.message,
1670
- responseText: responseText.substring(0, 500),
1671
- });
1672
- throw new Error(`Failed to parse JSON response: ${error.message}`);
1673
- }
1674
- }
1675
- async getForm(id) {
1676
- // Always use custom gf-api/v1 endpoint (from custom plugin)
1677
- const endpoint = `/forms/${id}`;
1678
- return this.makeRequest(endpoint);
1679
- }
1680
- async getForms() {
1681
- // Always use custom gf-api/v1 endpoint (from custom plugin)
1682
- const endpoint = '/forms';
1683
- return this.makeRequest(endpoint);
1684
- }
1685
- async getFormConfig(id) {
1686
- // Always use custom gf-api/v1 endpoint (from custom plugin)
1687
- const endpoint = `/forms/${id}/config`;
1688
- return this.makeRequest(endpoint);
1689
- }
1690
- async submitForm(formId, submission) {
1691
- // Always use custom gf-api/v1 submit endpoint (from custom plugin)
1692
- const endpoint = `/forms/${formId}/submit`;
1693
- // Debug logging
1694
- console.log('🔍 GravityFormsClient.submitForm called:', {
1695
- formId,
1696
- field_values: submission.field_values,
1697
- field_values_stringified: JSON.stringify(submission.field_values),
1698
- has_headers: !!submission.headers,
1699
- });
1700
- // Merge custom headers (e.g., Turnstile token) with default headers
1701
- const headers = {
1702
- 'Content-Type': 'application/json',
1703
- ...(submission.headers || {}),
1704
- };
1705
- return this.makeRequest(endpoint, {
1706
- method: 'POST',
1707
- headers,
1708
- body: JSON.stringify(submission.field_values),
1709
- });
1710
- }
1711
- async getHealth() {
1712
- const endpoint = '/health';
1713
- return this.makeRequest(endpoint);
1714
- }
1715
- }
1716
-
1717
- /**
1718
- * @license GPL-3.0-or-later
1719
- *
1720
- * This file is part of the MarVAlt Open SDK.
1721
- * Copyright (c) 2025 Vibune Pty Ltd.
1722
- *
1723
- * This program is free software: you can redistribute it and/or modify
1724
- * it under the terms of the GNU General Public License as published by
1725
- * the Free Software Foundation, either version 3 of the License, or
1726
- * (at your option) any later version.
1727
- *
1728
- * This program is distributed in the hope that it will be useful,
1729
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
1730
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1731
- * See the GNU General Public License for more details.
1732
- */
1733
- // Gravity Forms Static Data Generator
1734
- class GravityFormsGenerator {
1735
- constructor(config) {
1736
- this.config = config;
1737
- }
1738
- async generateStaticData() {
1739
- const client = new GravityFormsClient(this.config);
1740
- console.log('🚀 Starting Gravity Forms static data generation...');
1741
- let forms = [];
1742
- if (this.config.formIds && this.config.formIds.length > 0) {
1743
- // Generate specific forms
1744
- for (const formId of this.config.formIds) {
1745
- try {
1746
- const form = await client.getFormConfig(formId);
1747
- if (this.config.includeInactive || form.is_active) {
1748
- forms.push(form);
1749
- }
1750
- }
1751
- catch (error) {
1752
- console.warn(`⚠️ Could not fetch form ${formId}:`, error);
1753
- }
1754
- }
1755
- }
1756
- else {
1757
- // Generate all forms
1758
- const formsList = await client.getForms();
1759
- console.log('🔍 Gravity Forms Response:', {
1760
- formsType: typeof formsList,
1761
- isArray: Array.isArray(formsList),
1762
- formsLength: formsList?.length,
1763
- formsPreview: formsList ? JSON.stringify(formsList).substring(0, 200) : 'null',
1764
- });
1765
- let formsMetadata = [];
1766
- if (Array.isArray(formsList)) {
1767
- formsMetadata = formsList;
1768
- }
1769
- else if (formsList && typeof formsList === 'object' && Array.isArray(formsList.forms)) {
1770
- formsMetadata = formsList.forms;
1771
- }
1772
- else if (formsList) {
1773
- formsMetadata = [formsList];
1774
- }
1775
- if (!this.config.includeInactive) {
1776
- formsMetadata = formsMetadata.filter(form => form.is_active === '1' || form.is_active === true);
1777
- }
1778
- // Fetch full form configuration for each form (schema lives under /config)
1779
- console.log(`🔄 Fetching full data for ${formsMetadata.length} forms...`);
1780
- forms = [];
1781
- for (const formMetadata of formsMetadata) {
1782
- try {
1783
- const formId = Number(formMetadata.id);
1784
- const fullForm = await client.getFormConfig(formId);
1785
- forms.push(fullForm);
1786
- console.log(`✅ Fetched form: ${fullForm.title} (${fullForm.fields?.length || 0} fields)`);
1787
- }
1788
- catch (error) {
1789
- console.error(`❌ Failed to fetch form ${formMetadata.id} config:`, error);
1790
- throw error;
1791
- }
1792
- }
1793
- }
1794
- const staticData = {
1795
- generated_at: new Date().toISOString(),
1796
- total_forms: forms.length,
1797
- forms,
1798
- };
1799
- // Write to file
1800
- this.writeStaticData(staticData);
1801
- console.log('✅ Gravity Forms static data generation completed');
1802
- console.log(`📊 Generated data: ${forms.length} forms`);
1803
- return staticData;
1804
- }
1805
- writeStaticData(data) {
1806
- try {
1807
- const outputPath = join(process.cwd(), this.config.outputPath);
1808
- writeFileSync(outputPath, JSON.stringify(data, null, 2));
1809
- console.log(`📁 Static data written to: ${outputPath}`);
1810
- }
1811
- catch (error) {
1812
- console.error('❌ Error writing static data:', error);
1813
- throw error;
1814
- }
1815
- }
1816
- async generateFormConfig(formId) {
1817
- const client = new GravityFormsClient(this.config);
1818
- return client.getFormConfig(formId);
1819
- }
1820
- }
1821
- // Convenience function for easy usage
1822
- async function generateGravityFormsData$1(config) {
1823
- const generator = new GravityFormsGenerator(config);
1824
- return generator.generateStaticData();
1825
- }
8
+ import fs from 'fs';
1826
9
 
1827
10
  const DEFAULT_ENABLED_POST_TYPES = 'posts,pages';
1828
11
  /** Integration tokens in VITE_ENABLED_POST_TYPES that are not WordPress CPTs. */
@@ -1940,464 +123,6 @@ async function generateGravityFormsData(options) {
1940
123
  }
1941
124
  }
1942
125
 
1943
- /**
1944
- * @license GPL-3.0-or-later
1945
- *
1946
- * This file is part of the MarVAlt Open SDK.
1947
- * Copyright (c) 2025 Vibune Pty Ltd.
1948
- *
1949
- * This program is free software: you can redistribute it and/or modify
1950
- * it under the terms of the GNU General Public License as published by
1951
- * the Free Software Foundation, either version 3 of the License, or
1952
- * (at your option) any later version.
1953
- *
1954
- * This program is distributed in the hope that it will be useful,
1955
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
1956
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1957
- * See the GNU General Public License for more details.
1958
- */
1959
- class MauticClient {
1960
- constructor(config) {
1961
- this.config = config;
1962
- // Convention-based: Always use /api/mautic-submit unless explicitly overridden
1963
- this.proxyEndpoint = config.proxyEndpoint || '/api/mautic-submit';
1964
- }
1965
- async makeRequest(endpoint, options = {}) {
1966
- // Always use proxy endpoint (convention-based)
1967
- const proxyEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
1968
- const url = `${this.proxyEndpoint}?endpoint=${encodeURIComponent(proxyEndpoint)}`;
1969
- const isFormSubmission = endpoint.startsWith('/form/submit') ||
1970
- (endpoint.startsWith('/forms/') && endpoint.includes('/submit'));
1971
- const headers = {
1972
- ...options.headers,
1973
- };
1974
- // Default JSON only for non-form submissions when a body exists and no explicit content-type provided
1975
- const hasBody = options.body != null && options.body !== undefined;
1976
- if (!isFormSubmission && hasBody && !('Content-Type' in headers)) {
1977
- headers['Content-Type'] = 'application/json';
1978
- }
1979
- try {
1980
- const response = await fetch(url, {
1981
- ...options,
1982
- headers,
1983
- });
1984
- // Determine if this was a form submission based on the original endpoint
1985
- const wasFormSubmission = endpoint.startsWith('/form/submit') ||
1986
- (endpoint.startsWith('/forms/') && endpoint.includes('/submit'));
1987
- if (!response.ok) {
1988
- // For form submissions, some Mautic setups respond with an HTML 404 after a successful submit.
1989
- // Do not treat this as an error; let the frontend handle success/redirect messaging.
1990
- const isRedirect = response.status >= 300 && response.status < 400;
1991
- const isHtmlLike = (response.headers.get('content-type') || '').includes('text/html');
1992
- if (!(wasFormSubmission && (isRedirect || response.status === 404 || isHtmlLike))) {
1993
- throw new Error(`Mautic API request failed: ${response.status} ${response.statusText}`);
1994
- }
1995
- }
1996
- const responseText = await response.text();
1997
- // Handle empty responses
1998
- if (!responseText) {
1999
- return [];
2000
- }
2001
- // For Mautic form submissions, the response may be HTML. Try JSON first, fallback to text.
2002
- try {
2003
- return JSON.parse(responseText);
2004
- }
2005
- catch {
2006
- return responseText;
2007
- }
2008
- }
2009
- catch (error) {
2010
- const message = error instanceof Error ? error.message : String(error);
2011
- console.error('Mautic API request error:', message);
2012
- throw new Error(message);
2013
- }
2014
- }
2015
- /**
2016
- * Submit a form to Mautic
2017
- */
2018
- async submitForm(formId, submission) {
2019
- // Endpoint should be just the path, without query string (formId is in the body)
2020
- const endpoint = `/form/submit`;
2021
- // Encode as application/x-www-form-urlencoded to match Mautic expectations
2022
- const formParams = new URLSearchParams();
2023
- // Add fields
2024
- Object.entries(submission.fields || {}).forEach(([alias, value]) => {
2025
- if (Array.isArray(value)) {
2026
- value.forEach((v) => formParams.append(`mauticform[${alias}]`, String(v)));
2027
- }
2028
- else if (typeof value === 'boolean') {
2029
- formParams.append(`mauticform[${alias}]`, value ? '1' : '0');
2030
- }
2031
- else if (value != null) {
2032
- formParams.append(`mauticform[${alias}]`, String(value));
2033
- }
2034
- });
2035
- // Required hidden fields
2036
- formParams.append('mauticform[formId]', String(formId));
2037
- // Prevent Mautic from attempting a redirect; frontend will handle any redirect
2038
- formParams.append('mauticform[return]', '');
2039
- // Mautic submit flag similar to native form button name/value
2040
- formParams.append('mauticform[submit]', '1');
2041
- // Include formName only if provided (derived from cachedHtml)
2042
- if (submission.formName) {
2043
- formParams.append('mauticform[formName]', submission.formName);
2044
- }
2045
- // Merge custom headers (e.g., Turnstile token) with default headers
2046
- const headers = {
2047
- 'Content-Type': 'application/x-www-form-urlencoded',
2048
- ...(submission.headers || {}),
2049
- };
2050
- return this.makeRequest(endpoint, {
2051
- method: 'POST',
2052
- headers,
2053
- body: formParams.toString(),
2054
- });
2055
- }
2056
- /**
2057
- * Create a new contact in Mautic
2058
- */
2059
- async createContact(contact) {
2060
- return this.makeRequest('/contacts/new', {
2061
- method: 'POST',
2062
- body: JSON.stringify(contact),
2063
- });
2064
- }
2065
- /**
2066
- * Update an existing contact in Mautic
2067
- */
2068
- async updateContact(contactId, contact) {
2069
- return this.makeRequest(`/contacts/${contactId}/edit`, {
2070
- method: 'PATCH',
2071
- body: JSON.stringify(contact),
2072
- });
2073
- }
2074
- /**
2075
- * Get a contact by email
2076
- */
2077
- async getContactByEmail(email) {
2078
- return this.makeRequest(`/contacts?search=email:${encodeURIComponent(email)}`);
2079
- }
2080
- /**
2081
- * Get all forms from Mautic
2082
- */
2083
- async getForms() {
2084
- const response = await this.makeRequest('/forms');
2085
- return response.forms || [];
2086
- }
2087
- /**
2088
- * Get a specific form by ID
2089
- */
2090
- async getForm(formId) {
2091
- return this.makeRequest(`/forms/${formId}`);
2092
- }
2093
- /**
2094
- * Track an event in Mautic
2095
- */
2096
- async trackEvent(eventName, eventData = {}) {
2097
- return this.makeRequest('/events', {
2098
- method: 'POST',
2099
- body: JSON.stringify({
2100
- eventName,
2101
- eventData,
2102
- }),
2103
- });
2104
- }
2105
- /**
2106
- * Add tags to a contact
2107
- */
2108
- async addTagToContact(contactId, tags) {
2109
- return this.makeRequest(`/contacts/${contactId}/tags`, {
2110
- method: 'POST',
2111
- body: JSON.stringify({ tags }),
2112
- });
2113
- }
2114
- /**
2115
- * Remove tags from a contact
2116
- */
2117
- async removeTagFromContact(contactId, tags) {
2118
- return this.makeRequest(`/contacts/${contactId}/tags`, {
2119
- method: 'DELETE',
2120
- body: JSON.stringify({ tags }),
2121
- });
2122
- }
2123
- /**
2124
- * Get contact tags
2125
- */
2126
- async getContactTags(contactId) {
2127
- return this.makeRequest(`/contacts/${contactId}/tags`);
2128
- }
2129
- /**
2130
- * Get all tags
2131
- */
2132
- async getTags() {
2133
- return this.makeRequest('/tags');
2134
- }
2135
- /**
2136
- * Get segments
2137
- */
2138
- async getSegments() {
2139
- return this.makeRequest('/segments');
2140
- }
2141
- /**
2142
- * Add contact to segment
2143
- */
2144
- async addContactToSegment(contactId, segmentId) {
2145
- return this.makeRequest(`/contacts/${contactId}/segments`, {
2146
- method: 'POST',
2147
- body: JSON.stringify({ segmentId }),
2148
- });
2149
- }
2150
- /**
2151
- * Remove contact from segment
2152
- */
2153
- async removeContactFromSegment(contactId, segmentId) {
2154
- return this.makeRequest(`/contacts/${contactId}/segments`, {
2155
- method: 'DELETE',
2156
- body: JSON.stringify({ segmentId }),
2157
- });
2158
- }
2159
- }
2160
-
2161
- /**
2162
- * @license GPL-3.0-or-later
2163
- *
2164
- * This file is part of the MarVAlt Open SDK.
2165
- * Copyright (c) 2025 Vibune Pty Ltd.
2166
- *
2167
- * This program is free software: you can redistribute it and/or modify
2168
- * it under the terms of the GNU General Public License as published by
2169
- * the Free Software Foundation, either version 3 of the License, or
2170
- * (at your option) any later version.
2171
- *
2172
- * This program is distributed in the hope that it will be useful,
2173
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
2174
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
2175
- * See the GNU General Public License for more details.
2176
- */
2177
- class MauticGenerator {
2178
- constructor(config) {
2179
- this.cachedToken = null;
2180
- this.config = config;
2181
- // Generator uses direct OAuth2 calls, not the client
2182
- // Client is only used for proxy mode (cloudflare_proxy)
2183
- this.client = new MauticClient({
2184
- apiUrl: config.apiUrl,
2185
- proxyEndpoint: config.cloudflareWorkerUrl,
2186
- timeout: config.timeout,
2187
- retries: config.retries,
2188
- });
2189
- }
2190
- /**
2191
- * Get OAuth2 token for direct mode API calls
2192
- */
2193
- async getOAuth2Token() {
2194
- if (this.cachedToken && this.cachedToken.expires_at > Date.now() + 300000) {
2195
- console.log('🔑 Using cached OAuth2 token');
2196
- return this.cachedToken.access_token;
2197
- }
2198
- if (!this.config.clientId || !this.config.clientSecret) {
2199
- throw new Error('OAuth2 credentials (clientId, clientSecret) required for direct mode');
2200
- }
2201
- console.log('🔑 Fetching new OAuth2 token...');
2202
- const tokenUrl = `${this.config.apiUrl}/oauth/v2/token`;
2203
- const body = new URLSearchParams({
2204
- grant_type: 'client_credentials',
2205
- client_id: this.config.clientId,
2206
- client_secret: this.config.clientSecret,
2207
- });
2208
- const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
2209
- if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
2210
- headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
2211
- headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
2212
- console.log('🔐 Added CF Access headers to OAuth2 token request');
2213
- }
2214
- const resp = await fetch(tokenUrl, { method: 'POST', headers, body: body.toString() });
2215
- if (!resp.ok) {
2216
- const errText = await resp.text();
2217
- throw new Error(`OAuth2 token failed: ${resp.status} ${errText}`);
2218
- }
2219
- const data = await resp.json();
2220
- this.cachedToken = {
2221
- access_token: data.access_token,
2222
- expires_at: Date.now() + (data.expires_in * 1000),
2223
- };
2224
- console.log('✅ OAuth2 token cached');
2225
- return this.cachedToken.access_token;
2226
- }
2227
- /**
2228
- * Generate static data for Mautic forms
2229
- */
2230
- async generateStaticData() {
2231
- console.log('🔄 Generating Mautic forms JSON data...');
2232
- try {
2233
- const allForms = await this.fetchMauticFormsList();
2234
- console.log(`📝 Found ${allForms.length} forms`);
2235
- const forms = [];
2236
- for (const formSummary of allForms) {
2237
- const formId = formSummary.id?.toString();
2238
- if (!formId) {
2239
- console.warn(`⚠️ Form summary missing ID:`, formSummary);
2240
- continue;
2241
- }
2242
- try {
2243
- const formDetails = await this.fetchMauticForm(formId);
2244
- if (!formDetails) {
2245
- console.warn(`⚠️ No form details returned for form ${formId}`);
2246
- continue;
2247
- }
2248
- // Extract the form data with proper structure
2249
- const formData = {
2250
- id: formId,
2251
- name: formDetails.name || formSummary.name || `Form ${formId}`,
2252
- description: formDetails.description,
2253
- isPublished: formDetails.isPublished || formSummary.isPublished || false,
2254
- // Include post-submission behavior configuration
2255
- postAction: formDetails.postAction || 'message',
2256
- postActionProperty: formDetails.postActionProperty || 'Thank you for your submission!',
2257
- // Include other important form properties
2258
- formType: formDetails.formType || 'standalone',
2259
- // Extract fields with validation and properties
2260
- fields: (formDetails.fields || []).map((field) => ({
2261
- id: field.id,
2262
- label: field.label,
2263
- alias: field.alias,
2264
- type: field.type,
2265
- isRequired: field.isRequired || false,
2266
- validationMessage: field.validationMessage,
2267
- defaultValue: field.defaultValue,
2268
- properties: {
2269
- placeholder: field.properties?.placeholder,
2270
- cssClass: field.properties?.cssClass,
2271
- validation: field.properties?.validation,
2272
- options: field.properties?.options,
2273
- helpText: field.properties?.helpText,
2274
- size: field.properties?.size,
2275
- ...field.properties
2276
- }
2277
- })),
2278
- // Extract actions
2279
- actions: (formDetails.actions || []).map((action) => ({
2280
- id: action.id,
2281
- name: action.name,
2282
- type: action.type,
2283
- properties: action.properties || {}
2284
- })),
2285
- // Include styling and behavior
2286
- cssClass: formDetails.cssClass,
2287
- submitAction: formDetails.submitAction,
2288
- // Keep the details for reference
2289
- ...formSummary
2290
- };
2291
- forms.push(formData);
2292
- console.log(`✅ Processed form: ${formData.name} (ID: ${formId})`);
2293
- }
2294
- catch (error) {
2295
- const message = error instanceof Error ? error.message : String(error);
2296
- console.error(`❌ Error processing form ${formId}:`, message);
2297
- // Continue with other forms
2298
- }
2299
- }
2300
- const staticData = {
2301
- generated_at: new Date().toISOString(),
2302
- total_forms: forms.length,
2303
- forms: forms.map((form) => ({
2304
- id: parseInt(form.id),
2305
- name: form.name,
2306
- alias: String(form.alias ?? (form.name || '').toLowerCase().replace(/\s+/g, '-')),
2307
- description: form.description,
2308
- isPublished: form.isPublished,
2309
- fields: form.fields,
2310
- actions: form.actions,
2311
- cssClass: form.cssClass,
2312
- submitAction: form.submitAction,
2313
- postAction: form.postAction,
2314
- postActionProperty: form.postActionProperty,
2315
- formType: form.formType,
2316
- }))
2317
- };
2318
- console.log(`✅ Generated static data for ${forms.length} forms`);
2319
- return staticData;
2320
- }
2321
- catch (error) {
2322
- const message = error instanceof Error ? error.message : String(error);
2323
- console.error('❌ Error generating Mautic static data:', message);
2324
- throw new Error(message);
2325
- }
2326
- }
2327
- /**
2328
- * Write static data to file
2329
- */
2330
- async writeStaticData(staticData) {
2331
- const outputPath = path.resolve(this.config.outputPath);
2332
- const outputDir = path.dirname(outputPath);
2333
- // Ensure output directory exists
2334
- if (!fs.existsSync(outputDir)) {
2335
- fs.mkdirSync(outputDir, { recursive: true });
2336
- }
2337
- // Write the static data
2338
- fs.writeFileSync(outputPath, JSON.stringify(staticData, null, 2));
2339
- console.log(`📁 Static data written to: ${outputPath}`);
2340
- }
2341
- /**
2342
- * Generate and write static data
2343
- */
2344
- async generateAndWrite() {
2345
- const staticData = await this.generateStaticData();
2346
- await this.writeStaticData(staticData);
2347
- return staticData;
2348
- }
2349
- /**
2350
- * Fetch list of forms from Mautic
2351
- */
2352
- async fetchMauticFormsList() {
2353
- if (this.config.authMode === 'cloudflare_proxy') {
2354
- const forms = await this.client.getForms();
2355
- return Array.isArray(forms) ? forms : [];
2356
- }
2357
- // Direct mode with OAuth2
2358
- const token = await this.getOAuth2Token();
2359
- const url = `${this.config.apiUrl}/api/forms`;
2360
- const headers = { 'Authorization': `Bearer ${token}` };
2361
- if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
2362
- headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
2363
- headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
2364
- }
2365
- const resp = await fetch(url, { headers });
2366
- if (!resp.ok)
2367
- throw new Error(`Failed to fetch forms: ${resp.status}`);
2368
- const data = await resp.json();
2369
- return Array.isArray(data.forms) ? data.forms : [];
2370
- }
2371
- /**
2372
- * Fetch individual form details from Mautic
2373
- */
2374
- async fetchMauticForm(formId) {
2375
- if (this.config.authMode === 'cloudflare_proxy') {
2376
- return this.client.getForm(parseInt(formId, 10));
2377
- }
2378
- // Direct mode with OAuth2
2379
- const token = await this.getOAuth2Token();
2380
- const url = `${this.config.apiUrl}/api/forms/${formId}`;
2381
- const headers = { 'Authorization': `Bearer ${token}` };
2382
- if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
2383
- headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
2384
- headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
2385
- }
2386
- const resp = await fetch(url, { headers });
2387
- if (!resp.ok)
2388
- throw new Error(`Failed to fetch form ${formId}: ${resp.status}`);
2389
- const data = await resp.json();
2390
- return data.form || data;
2391
- }
2392
- }
2393
- /**
2394
- * Generate Mautic static data with configuration
2395
- */
2396
- async function generateMauticData$1(config) {
2397
- const generator = new MauticGenerator(config);
2398
- return generator.generateAndWrite();
2399
- }
2400
-
2401
126
  async function generateMauticData(options) {
2402
127
  const outputPath = options?.outputPath || './src/data/mautic-data.json';
2403
128
  const mauticUrl = process.env.MAUTIC_URL || process.env.VITE_MAUTIC_URL;
@@ -2433,255 +158,6 @@ async function generateMauticData(options) {
2433
158
  }
2434
159
  }
2435
160
 
2436
- /** Normalize PHPList base URL to REST API v2 root. */
2437
- function getApiBaseUrl(phplistUrl) {
2438
- const trimmed = phplistUrl.replace(/\/$/, '');
2439
- if (trimmed.endsWith('/api/v2'))
2440
- return trimmed;
2441
- if (trimmed.endsWith('/lists'))
2442
- return `${trimmed}/api/v2`;
2443
- return `${trimmed}/lists/api/v2`;
2444
- }
2445
- function buildViewProxyPath(messageId, uid) {
2446
- const params = new URLSearchParams({ endpoint: 'view', mid: String(messageId) });
2447
- return `/api/phplist-proxy?${params.toString()}`;
2448
- }
2449
-
2450
- let cachedSession = null;
2451
- function buildAccessHeaders(options) {
2452
- const headers = {};
2453
- if (options.cfAccessClientId && options.cfAccessClientSecret) {
2454
- headers['CF-Access-Client-Id'] = options.cfAccessClientId;
2455
- headers['CF-Access-Client-Secret'] = options.cfAccessClientSecret;
2456
- }
2457
- return headers;
2458
- }
2459
- async function createPhplistSession(options) {
2460
- if (cachedSession && cachedSession.expiresAt > Date.now() + 300000) {
2461
- return cachedSession.key;
2462
- }
2463
- const apiBase = getApiBaseUrl(options.apiUrl);
2464
- const headers = {
2465
- 'Content-Type': 'application/json',
2466
- ...buildAccessHeaders(options),
2467
- };
2468
- const response = await fetch(`${apiBase}/sessions`, {
2469
- method: 'POST',
2470
- headers,
2471
- body: JSON.stringify({
2472
- login_name: options.apiUser,
2473
- password: options.apiPassword,
2474
- }),
2475
- });
2476
- if (!response.ok) {
2477
- const errText = await response.text();
2478
- throw new Error(`PHPList session failed: ${response.status} ${errText}`);
2479
- }
2480
- const data = (await response.json());
2481
- const expiryMs = data.expiry ? Date.parse(data.expiry) : Date.now() + 3600000;
2482
- cachedSession = { key: data.key, expiresAt: expiryMs };
2483
- return data.key;
2484
- }
2485
- async function phplistApiFetch(options, path, init = {}) {
2486
- const sessionKey = await createPhplistSession(options);
2487
- const apiBase = getApiBaseUrl(options.apiUrl);
2488
- const url = path.startsWith('http') ? path : `${apiBase}${path.startsWith('/') ? path : `/${path}`}`;
2489
- const headers = {
2490
- 'Content-Type': 'application/json',
2491
- 'php-auth-pw': sessionKey,
2492
- ...init.headers,
2493
- ...buildAccessHeaders(options),
2494
- };
2495
- const response = await fetch(url, { ...init, headers });
2496
- if (!response.ok) {
2497
- const errText = await response.text();
2498
- throw new Error(`PHPList API ${path} failed: ${response.status} ${errText}`);
2499
- }
2500
- if (response.status === 204) {
2501
- return undefined;
2502
- }
2503
- const text = await response.text();
2504
- if (!text)
2505
- return undefined;
2506
- return JSON.parse(text);
2507
- }
2508
-
2509
- /** URL-safe slug from subject line. */
2510
- function slugifySubject(subject) {
2511
- return subject
2512
- .toLowerCase()
2513
- .normalize('NFKD')
2514
- .replace(/[\u0300-\u036f]/g, '')
2515
- .replace(/[^a-z0-9]+/g, '-')
2516
- .replace(/^-+|-+$/g, '')
2517
- .slice(0, 80) || 'newsletter';
2518
- }
2519
- function slugifyListName(name) {
2520
- return slugifySubject(name);
2521
- }
2522
- function buildNewsletterSlug(listSlug, messageId, subject) {
2523
- return `${listSlug}/${messageId}-${slugifySubject(subject)}`;
2524
- }
2525
- function stripHtmlExcerpt(html, maxLength = 200) {
2526
- const text = html
2527
- .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
2528
- .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
2529
- .replace(/<[^>]+>/g, ' ')
2530
- .replace(/\s+/g, ' ')
2531
- .trim();
2532
- if (text.length <= maxLength)
2533
- return text;
2534
- return `${text.slice(0, maxLength).trim()}…`;
2535
- }
2536
-
2537
- async function generatePhplistData$1(config) {
2538
- console.log('🔄 Generating PHPList newsletter JSON data...');
2539
- try {
2540
- const authOptions = {
2541
- apiUrl: config.apiUrl,
2542
- apiUser: config.apiUser,
2543
- apiPassword: config.apiPassword,
2544
- cfAccessClientId: config.cfAccessClientId,
2545
- cfAccessClientSecret: config.cfAccessClientSecret,
2546
- };
2547
- const list = await phplistApiFetch(authOptions, `/lists/${config.listId}`);
2548
- const listSlug = slugifyListName(list.name);
2549
- const existing = readExistingData(config.outputPath);
2550
- const knownIds = new Set(existing?.newsletters.map((n) => n.id) ?? []);
2551
- const lastSentAt = existing?.newsletters.reduce((max, n) => {
2552
- const t = Date.parse(n.sent_at);
2553
- return Number.isNaN(t) ? max : Math.max(max, t);
2554
- }, 0) ?? 0;
2555
- const listMessageIds = await fetchListMessageIds(authOptions, config.listId);
2556
- const listMessageIdSet = new Set(listMessageIds);
2557
- const newItems = [];
2558
- let afterId = 0;
2559
- let hasMore = true;
2560
- while (hasMore) {
2561
- const page = await phplistApiFetch(authOptions, `/campaigns?limit=100&after_id=${afterId || 1}`);
2562
- const items = page.items ?? [];
2563
- if (items.length === 0)
2564
- break;
2565
- for (const summary of items) {
2566
- const id = summary.id;
2567
- if (!id || !listMessageIdSet.has(id) || knownIds.has(id))
2568
- continue;
2569
- if (summary.message_metadata?.status !== 'sent')
2570
- continue;
2571
- const sentAt = summary.message_metadata?.sent;
2572
- if (sentAt && lastSentAt > 0 && Date.parse(sentAt) <= lastSentAt) {
2573
- continue;
2574
- }
2575
- const detail = await phplistApiFetch(authOptions, `/campaigns/${id}`);
2576
- const item = messageToNewsletter(detail, listSlug, config.newsletterRouteBase);
2577
- if (item) {
2578
- newItems.push(item);
2579
- knownIds.add(id);
2580
- }
2581
- }
2582
- const lastId = items[items.length - 1]?.id;
2583
- if (!lastId || items.length < 100) {
2584
- hasMore = false;
2585
- }
2586
- else {
2587
- afterId = lastId;
2588
- const oldestSent = items
2589
- .map((m) => m.message_metadata?.sent)
2590
- .filter(Boolean)
2591
- .map((s) => Date.parse(s))
2592
- .filter((t) => !Number.isNaN(t));
2593
- if (lastSentAt > 0 && oldestSent.length && Math.min(...oldestSent) <= lastSentAt) {
2594
- hasMore = false;
2595
- }
2596
- }
2597
- }
2598
- const mergedNewsletters = [
2599
- ...(existing?.newsletters ?? []),
2600
- ...newItems,
2601
- ].sort((a, b) => Date.parse(b.sent_at) - Date.parse(a.sent_at));
2602
- const subscribePages = await fetchSubscribePages(authOptions, config.subscribePageIds);
2603
- const data = {
2604
- generated_at: new Date().toISOString(),
2605
- list: {
2606
- id: list.id,
2607
- name: list.name,
2608
- slug: listSlug,
2609
- description: list.description,
2610
- },
2611
- subscribe_pages: subscribePages,
2612
- newsletters: mergedNewsletters,
2613
- };
2614
- writeStaticData(config.outputPath, data);
2615
- console.log(`✅ PHPList: ${mergedNewsletters.length} newsletters (${newItems.length} new)`);
2616
- return { success: true, data };
2617
- }
2618
- catch (error) {
2619
- const message = error instanceof Error ? error.message : String(error);
2620
- console.warn(`⚠️ PHPList generation failed: ${message}`);
2621
- return { success: false, error: message };
2622
- }
2623
- }
2624
- async function fetchListMessageIds(authOptions, listId) {
2625
- const response = await phplistApiFetch(authOptions, `/list-messages/list/${listId}/messages`);
2626
- return (response.items ?? []).map((row) => row.message_id).filter(Boolean);
2627
- }
2628
- async function fetchSubscribePages(authOptions, pageIds) {
2629
- const pages = [];
2630
- for (const pageId of pageIds) {
2631
- try {
2632
- const publicPage = await phplistApiFetch(authOptions, `/public/subscribe-pages/${pageId}`);
2633
- pages.push({
2634
- id: pageId,
2635
- title: publicPage.title,
2636
- intro: publicPage.intro,
2637
- attributes: publicPage.attributes ?? [],
2638
- list_ids: publicPage.list_ids,
2639
- });
2640
- }
2641
- catch (e) {
2642
- console.warn(`⚠️ Could not load subscribe page ${pageId}:`, e.message);
2643
- }
2644
- }
2645
- return pages;
2646
- }
2647
- function messageToNewsletter(message, listSlug, _routeBase) {
2648
- const subject = message.message_content?.subject?.trim();
2649
- const html = message.message_content?.text ?? '';
2650
- const sentAt = message.message_metadata?.sent;
2651
- if (!subject || !sentAt || message.message_metadata?.status !== 'sent') {
2652
- return null;
2653
- }
2654
- const slug = buildNewsletterSlug(listSlug, message.id, subject);
2655
- return {
2656
- id: message.id,
2657
- unique_id: message.unique_id,
2658
- slug,
2659
- subject,
2660
- sent_at: sentAt,
2661
- excerpt: stripHtmlExcerpt(html),
2662
- html,
2663
- plain_text: message.message_content?.text_message,
2664
- view_url: buildViewProxyPath(message.id),
2665
- };
2666
- }
2667
- function readExistingData(outputPath) {
2668
- try {
2669
- if (!fs.existsSync(outputPath))
2670
- return null;
2671
- return JSON.parse(fs.readFileSync(outputPath, 'utf8'));
2672
- }
2673
- catch {
2674
- return null;
2675
- }
2676
- }
2677
- function writeStaticData(outputPath, data) {
2678
- const dir = path.dirname(outputPath);
2679
- if (!fs.existsSync(dir)) {
2680
- fs.mkdirSync(dir, { recursive: true });
2681
- }
2682
- fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf8');
2683
- }
2684
-
2685
161
  function parseSubscribePageIds(raw) {
2686
162
  if (!raw?.trim())
2687
163
  return [];
@@ -3120,20 +596,20 @@ async function generateAllData() {
3120
596
  }
3121
597
 
3122
598
  var index = /*#__PURE__*/Object.freeze({
3123
- __proto__: null,
3124
- PSEUDO_POST_TYPES: PSEUDO_POST_TYPES,
3125
- generateAllData: generateAllData,
3126
- generateGoogleReviewsData: generateGoogleReviewsData,
3127
- generateGravityFormsData: generateGravityFormsData,
3128
- generateMauticData: generateMauticData,
3129
- generatePhplistData: generatePhplistData,
3130
- generateSitemapPaths: generateSitemapPaths,
3131
- generateSitemapPathsSafe: generateSitemapPathsSafe,
3132
- generateSuiteCRMData: generateSuiteCRMData,
3133
- generateWordPressData: generateWordPressData,
3134
- getWordPressPostTypes: getWordPressPostTypes,
3135
- isGoogleReviewsEnabled: isGoogleReviewsEnabled,
3136
- parseEnabledPostTypes: parseEnabledPostTypes
599
+ __proto__: null,
600
+ PSEUDO_POST_TYPES: PSEUDO_POST_TYPES,
601
+ generateAllData: generateAllData,
602
+ generateGoogleReviewsData: generateGoogleReviewsData,
603
+ generateGravityFormsData: generateGravityFormsData,
604
+ generateMauticData: generateMauticData,
605
+ generatePhplistData: generatePhplistData,
606
+ generateSitemapPaths: generateSitemapPaths,
607
+ generateSitemapPathsSafe: generateSitemapPathsSafe,
608
+ generateSuiteCRMData: generateSuiteCRMData,
609
+ generateWordPressData: generateWordPressData,
610
+ getWordPressPostTypes: getWordPressPostTypes,
611
+ isGoogleReviewsEnabled: isGoogleReviewsEnabled,
612
+ parseEnabledPostTypes: parseEnabledPostTypes
3137
613
  });
3138
614
 
3139
615
  /**