@marvalt/digivalt-core 0.2.34 → 0.2.36

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,11 +1,11 @@
1
1
  import dotenv from 'dotenv';
2
+ import fs from 'fs';
2
3
  import path from 'path';
3
4
  import { generateWordPressData as generateWordPressData$1, generateGravityFormsData as generateGravityFormsData$1 } from '@marvalt/wadapter/generators';
4
5
  import { generateMauticData as generateMauticData$1 } from '@marvalt/madapter/generators';
5
6
  import { generatePhplistData as generatePhplistData$1 } from '@marvalt/plister/generators';
6
7
  import { generateSuiteCRMData as generateSuiteCRMData$1 } from '@marvalt/sadapter';
7
8
  import { mkdir, writeFile } from 'fs/promises';
8
- import fs from 'fs';
9
9
 
10
10
  const DEFAULT_ENABLED_POST_TYPES = 'posts,pages';
11
11
  /** Integration tokens in VITE_ENABLED_POST_TYPES that are not WordPress CPTs. */
@@ -31,169 +31,553 @@ function getWordPressPostTypes() {
31
31
  .map((pt) => POST_TYPE_ALIASES[pt] || pt);
32
32
  }
33
33
 
34
- async function generateWordPressData(options) {
35
- const outputPath = options?.outputPath || './public/wordpress-data.json';
36
- const apiUrl = process.env.WORDPRESS_API_URL || process.env.VITE_WORDPRESS_API_URL;
37
- const username = process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME;
38
- const password = process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD;
39
- const cloudflareWorkerUrl = process.env.CLOUDFLARE_WORKER_URL || process.env.VITE_CLOUDFLARE_WORKER_URL;
40
- const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
41
- const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
42
- if (!apiUrl) {
43
- console.error('❌ Missing WordPress API URL. Set WORDPRESS_API_URL or VITE_WORDPRESS_API_URL');
44
- return false;
45
- }
46
- const localDev = process.env.VITE_LOCAL_DEVELOPMENT === 'true';
47
- const requestedAuth = process.env.VITE_AUTH_MODE;
48
- const hasProxy = !!cloudflareWorkerUrl;
49
- const authMode = localDev ? 'direct' : (requestedAuth === 'cloudflare_proxy' && hasProxy ? 'cloudflare_proxy' : 'direct');
50
- const config = {
51
- authMode,
52
- cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? cloudflareWorkerUrl : undefined,
53
- apiUrl,
54
- username,
55
- password,
56
- cfAccessClientId,
57
- cfAccessClientSecret,
58
- };
59
- const postTypes = getWordPressPostTypes();
60
- const frontendId = process.env.VITE_FRONTEND_ID || process.env.FRONTEND_ID || undefined;
61
- const frontendName = process.env.VITE_FRONTEND_NAME || process.env.FRONTEND_NAME || 'Default Frontend';
62
- try {
63
- const result = await generateWordPressData$1({
64
- ...config,
65
- outputPath,
66
- postTypes,
67
- ...(frontendId && { frontendId }),
68
- frontendName,
69
- includeEmbedded: true,
70
- maxItems: parseInt(process.env.VITE_DEFAULT_MAX_ITEMS || '200', 10),
71
- });
72
- console.log(`📁 WordPress data written to: ${path.join(process.cwd(), outputPath)}`);
73
- return true;
74
- }
75
- catch (e) {
76
- console.error('❌ WordPress generation failed:', e.message);
77
- return false;
78
- }
34
+ function normalizePathname(pathname) {
35
+ const decoded = decodeURIComponent(pathname);
36
+ if (!decoded || decoded === '/')
37
+ return '/';
38
+ const stripped = decoded.replace(/\/+$/, '');
39
+ return stripped === '' ? '/' : stripped;
79
40
  }
80
-
81
- async function generateGravityFormsData(options) {
82
- const outputPath = options?.outputPath || './public/gravity-forms-data.json';
83
- const wpApiUrl = process.env.WORDPRESS_API_URL || process.env.VITE_WORDPRESS_API_URL;
84
- const gfApiUrl = process.env.GRAVITY_FORMS_API_URL || process.env.VITE_GRAVITY_FORMS_API_URL;
85
- const username = process.env.GRAVITY_FORMS_USERNAME || process.env.VITE_GRAVITY_FORMS_USERNAME ||
86
- process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME || '';
87
- const password = process.env.GRAVITY_FORMS_PASSWORD || process.env.VITE_GRAVITY_FORMS_PASSWORD ||
88
- process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD || '';
89
- const cloudflareWorkerUrl = process.env.CLOUDFLARE_WORKER_URL || process.env.VITE_CLOUDFLARE_WORKER_URL;
90
- const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
91
- const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
92
- const apiUrl = gfApiUrl || (wpApiUrl ? `${wpApiUrl}/wp-json/gf-api/v1` : undefined);
93
- if (!apiUrl) {
94
- console.error('❌ Missing WordPress/Gravity Forms API URL.');
95
- return false;
96
- }
97
- const localDev = process.env.VITE_LOCAL_DEVELOPMENT === 'true';
98
- const requestedAuth = process.env.VITE_AUTH_MODE;
99
- const hasProxy = !!cloudflareWorkerUrl;
100
- const authMode = localDev ? 'direct' : (requestedAuth === 'cloudflare_proxy' && hasProxy ? 'cloudflare_proxy' : 'direct');
101
- const config = {
102
- authMode,
103
- cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? cloudflareWorkerUrl : undefined,
104
- apiUrl,
105
- username,
106
- password,
107
- cfAccessClientId,
108
- cfAccessClientSecret,
109
- useCustomEndpoint: true,
110
- };
111
- try {
112
- const result = await generateGravityFormsData$1({
113
- ...config,
114
- outputPath,
115
- includeInactive: false,
116
- });
117
- console.log(`📁 Gravity Forms data written to: ${path.join(process.cwd(), outputPath)}`);
118
- return true;
41
+ function joinPath(prefix, slug) {
42
+ const p = prefix.replace(/\/+$/, '');
43
+ const s = slug.replace(/^\/+/, '');
44
+ if (!p || p === '/')
45
+ return normalizePathname(`/${s}`);
46
+ return normalizePathname(`${p.startsWith('/') ? p : `/${p}`}/${s}`);
47
+ }
48
+ function stripHtml(html) {
49
+ return html
50
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
51
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
52
+ .replace(/<[^>]+>/g, ' ')
53
+ .replace(/\s+/g, ' ')
54
+ .trim();
55
+ }
56
+ function sanitizeBodyHtml(html) {
57
+ return html
58
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
59
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
60
+ .replace(/\son\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
61
+ }
62
+ function renderedText(value) {
63
+ if (typeof value === 'string')
64
+ return stripHtml(value);
65
+ if (value && typeof value === 'object' && 'rendered' in value) {
66
+ const r = value.rendered;
67
+ if (typeof r === 'string')
68
+ return stripHtml(r);
119
69
  }
120
- catch (e) {
121
- console.error('❌ Gravity Forms generation failed:', e.message);
122
- return false;
70
+ return '';
71
+ }
72
+ function renderedHtml(value) {
73
+ if (typeof value === 'string')
74
+ return value;
75
+ if (value && typeof value === 'object' && 'rendered' in value) {
76
+ const r = value.rendered;
77
+ if (typeof r === 'string')
78
+ return r;
123
79
  }
80
+ return '';
81
+ }
82
+ function escapeHtml(s) {
83
+ return s
84
+ .replace(/&/g, '&amp;')
85
+ .replace(/</g, '&lt;')
86
+ .replace(/>/g, '&gt;')
87
+ .replace(/"/g, '&quot;');
88
+ }
89
+ function escapeAttr(s) {
90
+ return escapeHtml(s).replace(/'/g, '&#39;');
124
91
  }
125
92
 
126
- async function generateMauticData(options) {
127
- const outputPath = options?.outputPath || './src/data/mautic-data.json';
128
- const mauticUrl = process.env.MAUTIC_URL || process.env.VITE_MAUTIC_URL;
129
- const mauticProxyUrl = process.env.MAUTIC_PROXY_URL || process.env.VITE_MAUTIC_PROXY_URL;
130
- const clientId = process.env.MAUTIC_API_PUBLIC_KEY || process.env.VITE_MAUTIC_API_PUBLIC_KEY;
131
- const clientSecret = process.env.MAUTIC_API_SECRET_KEY || process.env.VITE_MAUTIC_API_SECRET_KEY;
132
- const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
133
- const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
134
- if (!mauticUrl) {
135
- console.log('⚠️ Mautic URL not configured. Skipping Mautic data generation.');
136
- return true;
137
- }
138
- try {
139
- const hasProxy = !!mauticProxyUrl;
140
- const authMode = hasProxy ? 'cloudflare_proxy' : 'direct';
141
- const result = await generateMauticData$1({
142
- authMode,
143
- apiUrl: mauticUrl,
144
- cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? mauticProxyUrl : undefined,
145
- clientId: clientId,
146
- clientSecret: clientSecret,
147
- cfAccessClientId,
148
- cfAccessClientSecret,
149
- outputPath,
150
- includeInactive: false,
151
- });
152
- console.log(`📁 Mautic data written to: ${path.join(process.cwd(), outputPath)}`);
153
- return true;
93
+ const SITEMAP_URL_SHARD_SIZE = 45000;
94
+ function parseRoutePrefixes(raw) {
95
+ const out = {};
96
+ const src = (raw ?? process.env.VITE_SEO_ROUTE_PREFIXES ?? '').trim();
97
+ if (!src)
98
+ return out;
99
+ for (const part of src.split(',')) {
100
+ const trimmed = part.trim();
101
+ if (!trimmed)
102
+ continue;
103
+ const idx = trimmed.indexOf(':');
104
+ if (idx <= 0)
105
+ continue;
106
+ const key = trimmed.slice(0, idx).trim();
107
+ const prefix = trimmed.slice(idx + 1).trim();
108
+ if (key)
109
+ out[key] = prefix || '/';
154
110
  }
155
- catch (e) {
156
- console.warn(`⚠️ Mautic generation failed (build continues): ${e.message}`);
111
+ return out;
112
+ }
113
+ function isPublishedEntity(item) {
114
+ const s = item.status;
115
+ if (s === undefined || s === null || s === '')
157
116
  return true;
158
- }
117
+ return s === 'publish';
159
118
  }
160
-
161
- function parseSubscribePageIds(raw) {
162
- if (!raw?.trim())
163
- return [];
164
- return raw
165
- .split(',')
166
- .map((s) => parseInt(s.trim(), 10))
167
- .filter((n) => !Number.isNaN(n));
119
+ function isNoindexEntity(item) {
120
+ return item.digivalt_seo?.robots_index === false;
168
121
  }
169
- async function generatePhplistData(options) {
170
- const outputPath = options?.outputPath || './public/phplist-data.json';
171
- const phplistUrl = process.env.PHPLIST_URL || process.env.VITE_PHPLIST_URL;
172
- const listIdRaw = process.env.PHPLIST_LIST_ID || process.env.VITE_PHPLIST_LIST_ID;
173
- const apiUser = process.env.PHPLIST_API_USER || process.env.VITE_PHPLIST_API_USER;
174
- const apiPassword = process.env.PHPLIST_API_PASSWORD || process.env.VITE_PHPLIST_API_PASSWORD;
175
- const proxyUrl = process.env.PHPLIST_PROXY_URL || process.env.VITE_PHPLIST_PROXY_URL;
176
- const subscribePageIds = parseSubscribePageIds(process.env.PHPLIST_SUBSCRIBE_PAGE_IDS || process.env.VITE_PHPLIST_SUBSCRIBE_PAGE_IDS);
177
- const newsletterRouteBase = process.env.VITE_PHPLIST_NEWSLETTER_ROUTE_BASE || '/newsletters';
178
- const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
179
- const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
180
- const restApiEndpoint = process.env.PHPLIST_API_ENDPOINT || process.env.VITE_PHPLIST_API_ENDPOINT;
181
- const publicBaseUrl = process.env.PHPLIST_PUBLIC_BASE_URL || process.env.VITE_PHPLIST_PUBLIC_BASE_URL;
182
- const apiSecret = process.env.PHPLIST_API_SECRET || process.env.VITE_PHPLIST_API_SECRET;
183
- const subscribeStrategy = process.env.PHPLIST_SUBSCRIBE_STRATEGY || process.env.VITE_PHPLIST_SUBSCRIBE_STRATEGY;
184
- const subscribePageId = process.env.PHPLIST_SUBSCRIBE_PAGE_ID || process.env.VITE_PHPLIST_SUBSCRIBE_PAGE_ID;
185
- const apiModeRaw = process.env.PHPLIST_API_MODE || process.env.VITE_PHPLIST_API_MODE;
186
- const apiMode = apiModeRaw === 'rest_v2' || apiModeRaw === 'v2'
187
- ? 'rest_v2'
188
- : apiModeRaw === 'restapi' || apiModeRaw === 'plugin' || restApiEndpoint
189
- ? 'restapi'
190
- : undefined;
191
- if (!phplistUrl) {
192
- console.log('⚠️ PHPList URL not configured. Skipping PHPList data generation.');
193
- return true;
122
+ function entityLastmod(item) {
123
+ const v = item.modified_gmt || item.modified || item.date_gmt || item.date;
124
+ return typeof v === 'string' && v.trim() ? v.trim() : undefined;
125
+ }
126
+ function entityImage(item) {
127
+ const v = item.digivalt_seo?.og_image ||
128
+ item.featured_media_cloudflare_url ||
129
+ item.featured_media_url;
130
+ return typeof v === 'string' && v.trim() ? v.trim() : undefined;
131
+ }
132
+ function getFrontPageRef(data) {
133
+ const fp = data.front_page;
134
+ const envSlug = (process.env.VITE_SEO_FRONT_PAGE_SLUG || '').trim();
135
+ let slug;
136
+ let id;
137
+ if (fp && typeof fp === 'object') {
138
+ const rec = fp;
139
+ if (typeof rec.slug === 'string' && rec.slug.trim())
140
+ slug = rec.slug.trim();
141
+ if (typeof rec.id === 'number')
142
+ id = rec.id;
143
+ else if (typeof rec.id === 'string')
144
+ id = Number(rec.id) || undefined;
194
145
  }
195
- const listId = parseInt(listIdRaw || '0', 10);
196
- if (!listId || Number.isNaN(listId)) {
146
+ if (envSlug)
147
+ slug = envSlug;
148
+ return { slug, id };
149
+ }
150
+ function getFrontPageSlug(data) {
151
+ return getFrontPageRef(data).slug;
152
+ }
153
+ function asRecord(item) {
154
+ if (!item || typeof item !== 'object')
155
+ return null;
156
+ return item;
157
+ }
158
+ function newsletterRouteBase() {
159
+ const raw = process.env.VITE_PHPLIST_NEWSLETTER_ROUTE_BASE || '/newsletters';
160
+ return normalizePathname(raw.startsWith('/') ? raw : `/${raw}`);
161
+ }
162
+ function collectSitemapUrls(args) {
163
+ const prefixes = args.prefixes ?? parseRoutePrefixes();
164
+ const front = args.wpData ? getFrontPageRef(args.wpData) : {};
165
+ const byPath = new Map();
166
+ const add = (entry) => {
167
+ const path = normalizePathname(entry.path);
168
+ if (!path)
169
+ return;
170
+ const prev = byPath.get(path);
171
+ if (!prev) {
172
+ byPath.set(path, { ...entry, path });
173
+ return;
174
+ }
175
+ byPath.set(path, {
176
+ path,
177
+ lastmod: entry.lastmod || prev.lastmod,
178
+ image: entry.image || prev.image,
179
+ });
180
+ };
181
+ add({ path: '/' });
182
+ for (const p of args.staticPaths) {
183
+ add({ path: p });
184
+ }
185
+ const pages = args.wpData && Array.isArray(args.wpData.pages) ? args.wpData.pages : [];
186
+ for (const raw of pages) {
187
+ const item = asRecord(raw);
188
+ if (!item || !isPublishedEntity(item) || isNoindexEntity(item))
189
+ continue;
190
+ const slug = typeof item.slug === 'string' ? item.slug.trim() : '';
191
+ if (!slug)
192
+ continue;
193
+ const isFront = (front.slug && slug === front.slug) ||
194
+ (typeof front.id === 'number' && typeof item.id === 'number' && item.id === front.id);
195
+ const path = isFront ? '/' : joinPath('/', slug);
196
+ add({
197
+ path,
198
+ lastmod: entityLastmod(item),
199
+ image: entityImage(item),
200
+ });
201
+ }
202
+ if (args.wpData) {
203
+ for (const [type, prefix] of Object.entries(prefixes)) {
204
+ if (type === 'pages' || type === 'page')
205
+ continue;
206
+ const arr = args.wpData[type];
207
+ if (!Array.isArray(arr))
208
+ continue;
209
+ for (const raw of arr) {
210
+ const item = asRecord(raw);
211
+ if (!item || !isPublishedEntity(item) || isNoindexEntity(item))
212
+ continue;
213
+ const slug = typeof item.slug === 'string' ? item.slug.trim() : '';
214
+ if (!slug)
215
+ continue;
216
+ add({
217
+ path: joinPath(prefix, slug),
218
+ lastmod: entityLastmod(item),
219
+ image: entityImage(item),
220
+ });
221
+ }
222
+ }
223
+ }
224
+ const newsletters = args.phplistData && Array.isArray(args.phplistData.newsletters)
225
+ ? args.phplistData.newsletters
226
+ : [];
227
+ const nBase = newsletterRouteBase();
228
+ for (const raw of newsletters) {
229
+ if (!raw || typeof raw !== 'object')
230
+ continue;
231
+ const rec = raw;
232
+ if (typeof rec.slug !== 'string' || !rec.slug.trim())
233
+ continue;
234
+ add({
235
+ path: joinPath(nBase, rec.slug.trim()),
236
+ lastmod: typeof rec.sent_at === 'string' ? rec.sent_at : undefined,
237
+ });
238
+ }
239
+ const known = new Set(byPath.keys());
240
+ const considerMenu = (items) => {
241
+ if (!Array.isArray(items))
242
+ return;
243
+ for (const item of items) {
244
+ if (!item || typeof item !== 'object')
245
+ continue;
246
+ const rec = item;
247
+ if (typeof rec.url === 'string') {
248
+ const path = menuUrlToFrontendPath(rec.url, front.slug);
249
+ if (path && known.has(path)) {
250
+ add({ path });
251
+ }
252
+ }
253
+ considerMenu(rec.children);
254
+ }
255
+ };
256
+ if (args.wpData) {
257
+ const header = args.wpData.navigation_menu;
258
+ const footer = args.wpData.footer_navigation_menu;
259
+ considerMenu(header?.items);
260
+ considerMenu(footer?.items);
261
+ }
262
+ const sorted = [...byPath.values()].sort((a, b) => {
263
+ if (a.path === '/')
264
+ return -1;
265
+ if (b.path === '/')
266
+ return 1;
267
+ return a.path.localeCompare(b.path);
268
+ });
269
+ return sorted;
270
+ }
271
+ function menuUrlToFrontendPath(urlStr, frontSlug) {
272
+ const t = urlStr.trim();
273
+ if (!t || t === '#' || t.toLowerCase().startsWith('mailto:') || t.toLowerCase().startsWith('tel:')) {
274
+ return null;
275
+ }
276
+ let pathname;
277
+ if (t.startsWith('/')) {
278
+ pathname = normalizePathname(t.split('?')[0]);
279
+ }
280
+ else if (/^https?:\/\//i.test(t)) {
281
+ try {
282
+ pathname = normalizePathname(new URL(t).pathname);
283
+ }
284
+ catch {
285
+ return null;
286
+ }
287
+ }
288
+ else {
289
+ return null;
290
+ }
291
+ if (pathname === '/')
292
+ return '/';
293
+ const slug = pathname.replace(/^\//, '');
294
+ if (frontSlug && slug === frontSlug)
295
+ return '/';
296
+ return pathname;
297
+ }
298
+
299
+ const SHOWN_SLUGS = 5;
300
+ function pageSlug(page) {
301
+ if (!page || typeof page !== 'object')
302
+ return '';
303
+ const slug = page.slug;
304
+ return typeof slug === 'string' ? slug.trim() : '';
305
+ }
306
+ function pageId(page) {
307
+ if (!page || typeof page !== 'object')
308
+ return undefined;
309
+ const id = page.id;
310
+ if (typeof id === 'number' && Number.isFinite(id))
311
+ return id;
312
+ if (typeof id === 'string' && id.trim()) {
313
+ const n = Number(id);
314
+ return Number.isFinite(n) ? n : undefined;
315
+ }
316
+ return undefined;
317
+ }
318
+ function isEmptyChrome(value) {
319
+ if (value == null)
320
+ return true;
321
+ if (typeof value === 'string')
322
+ return value.trim() === '';
323
+ if (Array.isArray(value))
324
+ return value.length === 0;
325
+ if (typeof value === 'object')
326
+ return Object.keys(value).length === 0;
327
+ return false;
328
+ }
329
+ function receivedSlugs(pages) {
330
+ const slugs = pages.map(pageSlug).filter(Boolean);
331
+ const shown = slugs.slice(0, SHOWN_SLUGS);
332
+ if (slugs.length > SHOWN_SLUGS)
333
+ shown.push('...');
334
+ return shown;
335
+ }
336
+ function reject(pages, expectedSlug) {
337
+ return {
338
+ ok: false,
339
+ expectedSlug,
340
+ pageCount: pages.length,
341
+ receivedSlugs: receivedSlugs(pages),
342
+ };
343
+ }
344
+ /** Reject a WordPress payload that cannot be the app's front page. */
345
+ function checkWordPressPayload(payload) {
346
+ if (process.env.VITE_IS_LOVABLE === 'true') {
347
+ return { ok: true, warnEmptyChrome: false };
348
+ }
349
+ if (!payload || typeof payload !== 'object') {
350
+ return { ok: false, pageCount: 0, receivedSlugs: [] };
351
+ }
352
+ const data = payload;
353
+ const pages = Array.isArray(data.pages) ? data.pages : null;
354
+ const envSlug = (process.env.VITE_SEO_FRONT_PAGE_SLUG || '').trim();
355
+ const ref = getFrontPageRef(data);
356
+ if (!pages || pages.length === 0) {
357
+ return reject([], envSlug || ref.slug);
358
+ }
359
+ if (envSlug) {
360
+ if (!pages.some((page) => pageSlug(page) === envSlug)) {
361
+ return reject(pages, envSlug);
362
+ }
363
+ }
364
+ else if (ref.slug || typeof ref.id === 'number') {
365
+ const matches = pages.some((page) => {
366
+ if (ref.slug && pageSlug(page) === ref.slug)
367
+ return true;
368
+ return typeof ref.id === 'number' && pageId(page) === ref.id;
369
+ });
370
+ if (!matches)
371
+ return reject(pages, ref.slug);
372
+ }
373
+ return {
374
+ ok: true,
375
+ warnEmptyChrome: isEmptyChrome(data.site_settings) || isEmptyChrome(data.navigation_menu),
376
+ };
377
+ }
378
+
379
+ async function generateWordPressData(options) {
380
+ const outputPath = options?.outputPath || './public/wordpress-data.json';
381
+ const apiUrl = process.env.WORDPRESS_API_URL || process.env.VITE_WORDPRESS_API_URL;
382
+ const username = process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME;
383
+ const password = process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD;
384
+ const cloudflareWorkerUrl = process.env.CLOUDFLARE_WORKER_URL || process.env.VITE_CLOUDFLARE_WORKER_URL;
385
+ const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
386
+ const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
387
+ if (!apiUrl) {
388
+ console.error('❌ Missing WordPress API URL. Set WORDPRESS_API_URL or VITE_WORDPRESS_API_URL');
389
+ return false;
390
+ }
391
+ const localDev = process.env.VITE_LOCAL_DEVELOPMENT === 'true';
392
+ const requestedAuth = process.env.VITE_AUTH_MODE;
393
+ const hasProxy = !!cloudflareWorkerUrl;
394
+ const authMode = localDev ? 'direct' : (requestedAuth === 'cloudflare_proxy' && hasProxy ? 'cloudflare_proxy' : 'direct');
395
+ const config = {
396
+ authMode,
397
+ cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? cloudflareWorkerUrl : undefined,
398
+ apiUrl,
399
+ username,
400
+ password,
401
+ cfAccessClientId,
402
+ cfAccessClientSecret,
403
+ };
404
+ const postTypes = getWordPressPostTypes();
405
+ const frontendId = process.env.VITE_FRONTEND_ID || process.env.FRONTEND_ID || undefined;
406
+ const frontendName = process.env.VITE_FRONTEND_NAME || process.env.FRONTEND_NAME || 'Default Frontend';
407
+ const absPath = path.join(process.cwd(), outputPath);
408
+ let previous = null;
409
+ try {
410
+ if (fs.existsSync(absPath))
411
+ previous = fs.readFileSync(absPath);
412
+ }
413
+ catch (e) {
414
+ console.error('❌ Could not read existing WordPress data before generation:', e.message);
415
+ return false;
416
+ }
417
+ try {
418
+ const result = await generateWordPressData$1({
419
+ ...config,
420
+ outputPath,
421
+ postTypes,
422
+ ...(frontendId && { frontendId }),
423
+ frontendName,
424
+ includeEmbedded: true,
425
+ maxItems: parseInt(process.env.VITE_DEFAULT_MAX_ITEMS || '200', 10),
426
+ });
427
+ const check = checkWordPressPayload(result);
428
+ if (check.ok === false) {
429
+ console.error(formatRejection(check, revertWordPressFile(absPath, previous)));
430
+ return false;
431
+ }
432
+ if (check.warnEmptyChrome) {
433
+ console.warn('⚠️ WordPress payload is missing site_settings or navigation_menu. wp-custom/v1 may have been unreachable.');
434
+ }
435
+ console.log(`📁 WordPress data written to: ${absPath}`);
436
+ return true;
437
+ }
438
+ catch (e) {
439
+ const hadPrevious = revertWordPressFile(absPath, previous);
440
+ console.error('❌ WordPress generation failed:', e.message);
441
+ console.error(restoreMessage(hadPrevious));
442
+ return false;
443
+ }
444
+ }
445
+ function revertWordPressFile(absPath, previous) {
446
+ if (previous) {
447
+ fs.writeFileSync(absPath, previous);
448
+ return true;
449
+ }
450
+ if (fs.existsSync(absPath))
451
+ fs.unlinkSync(absPath);
452
+ return false;
453
+ }
454
+ function restoreMessage(hadPrevious) {
455
+ return hadPrevious
456
+ ? 'Restored previous wordpress-data.json.'
457
+ : 'No previous wordpress-data.json to restore; removed the generated file.';
458
+ }
459
+ function formatRejection(check, hadPrevious) {
460
+ const expected = check.expectedSlug || '(none)';
461
+ const slugs = check.receivedSlugs.length ? check.receivedSlugs.join(', ') : '(none)';
462
+ return `❌ WordPress payload rejected: expected front page "${expected}", received ${check.pageCount} page(s): ${slugs}. ${restoreMessage(hadPrevious)}`;
463
+ }
464
+
465
+ async function generateGravityFormsData(options) {
466
+ const outputPath = options?.outputPath || './public/gravity-forms-data.json';
467
+ const wpApiUrl = process.env.WORDPRESS_API_URL || process.env.VITE_WORDPRESS_API_URL;
468
+ const gfApiUrl = process.env.GRAVITY_FORMS_API_URL || process.env.VITE_GRAVITY_FORMS_API_URL;
469
+ const username = process.env.GRAVITY_FORMS_USERNAME || process.env.VITE_GRAVITY_FORMS_USERNAME ||
470
+ process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME || '';
471
+ const password = process.env.GRAVITY_FORMS_PASSWORD || process.env.VITE_GRAVITY_FORMS_PASSWORD ||
472
+ process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD || '';
473
+ const cloudflareWorkerUrl = process.env.CLOUDFLARE_WORKER_URL || process.env.VITE_CLOUDFLARE_WORKER_URL;
474
+ const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
475
+ const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
476
+ const apiUrl = gfApiUrl || (wpApiUrl ? `${wpApiUrl}/wp-json/gf-api/v1` : undefined);
477
+ if (!apiUrl) {
478
+ console.error('❌ Missing WordPress/Gravity Forms API URL.');
479
+ return false;
480
+ }
481
+ const localDev = process.env.VITE_LOCAL_DEVELOPMENT === 'true';
482
+ const requestedAuth = process.env.VITE_AUTH_MODE;
483
+ const hasProxy = !!cloudflareWorkerUrl;
484
+ const authMode = localDev ? 'direct' : (requestedAuth === 'cloudflare_proxy' && hasProxy ? 'cloudflare_proxy' : 'direct');
485
+ const config = {
486
+ authMode,
487
+ cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? cloudflareWorkerUrl : undefined,
488
+ apiUrl,
489
+ username,
490
+ password,
491
+ cfAccessClientId,
492
+ cfAccessClientSecret,
493
+ useCustomEndpoint: true,
494
+ };
495
+ try {
496
+ const result = await generateGravityFormsData$1({
497
+ ...config,
498
+ outputPath,
499
+ includeInactive: false,
500
+ });
501
+ console.log(`📁 Gravity Forms data written to: ${path.join(process.cwd(), outputPath)}`);
502
+ return true;
503
+ }
504
+ catch (e) {
505
+ console.error('❌ Gravity Forms generation failed:', e.message);
506
+ return false;
507
+ }
508
+ }
509
+
510
+ async function generateMauticData(options) {
511
+ const outputPath = options?.outputPath || './src/data/mautic-data.json';
512
+ const mauticUrl = process.env.MAUTIC_URL || process.env.VITE_MAUTIC_URL;
513
+ const mauticProxyUrl = process.env.MAUTIC_PROXY_URL || process.env.VITE_MAUTIC_PROXY_URL;
514
+ const clientId = process.env.MAUTIC_API_PUBLIC_KEY || process.env.VITE_MAUTIC_API_PUBLIC_KEY;
515
+ const clientSecret = process.env.MAUTIC_API_SECRET_KEY || process.env.VITE_MAUTIC_API_SECRET_KEY;
516
+ const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
517
+ const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
518
+ if (!mauticUrl) {
519
+ console.log('⚠️ Mautic URL not configured. Skipping Mautic data generation.');
520
+ return true;
521
+ }
522
+ try {
523
+ const hasProxy = !!mauticProxyUrl;
524
+ const authMode = hasProxy ? 'cloudflare_proxy' : 'direct';
525
+ const result = await generateMauticData$1({
526
+ authMode,
527
+ apiUrl: mauticUrl,
528
+ cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? mauticProxyUrl : undefined,
529
+ clientId: clientId,
530
+ clientSecret: clientSecret,
531
+ cfAccessClientId,
532
+ cfAccessClientSecret,
533
+ outputPath,
534
+ includeInactive: false,
535
+ });
536
+ console.log(`📁 Mautic data written to: ${path.join(process.cwd(), outputPath)}`);
537
+ return true;
538
+ }
539
+ catch (e) {
540
+ console.warn(`⚠️ Mautic generation failed (build continues): ${e.message}`);
541
+ return true;
542
+ }
543
+ }
544
+
545
+ function parseSubscribePageIds(raw) {
546
+ if (!raw?.trim())
547
+ return [];
548
+ return raw
549
+ .split(',')
550
+ .map((s) => parseInt(s.trim(), 10))
551
+ .filter((n) => !Number.isNaN(n));
552
+ }
553
+ async function generatePhplistData(options) {
554
+ const outputPath = options?.outputPath || './public/phplist-data.json';
555
+ const phplistUrl = process.env.PHPLIST_URL || process.env.VITE_PHPLIST_URL;
556
+ const listIdRaw = process.env.PHPLIST_LIST_ID || process.env.VITE_PHPLIST_LIST_ID;
557
+ const apiUser = process.env.PHPLIST_API_USER || process.env.VITE_PHPLIST_API_USER;
558
+ const apiPassword = process.env.PHPLIST_API_PASSWORD || process.env.VITE_PHPLIST_API_PASSWORD;
559
+ const proxyUrl = process.env.PHPLIST_PROXY_URL || process.env.VITE_PHPLIST_PROXY_URL;
560
+ const subscribePageIds = parseSubscribePageIds(process.env.PHPLIST_SUBSCRIBE_PAGE_IDS || process.env.VITE_PHPLIST_SUBSCRIBE_PAGE_IDS);
561
+ const newsletterRouteBase = process.env.VITE_PHPLIST_NEWSLETTER_ROUTE_BASE || '/newsletters';
562
+ const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
563
+ const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
564
+ const restApiEndpoint = process.env.PHPLIST_API_ENDPOINT || process.env.VITE_PHPLIST_API_ENDPOINT;
565
+ const publicBaseUrl = process.env.PHPLIST_PUBLIC_BASE_URL || process.env.VITE_PHPLIST_PUBLIC_BASE_URL;
566
+ const apiSecret = process.env.PHPLIST_API_SECRET || process.env.VITE_PHPLIST_API_SECRET;
567
+ const subscribeStrategy = process.env.PHPLIST_SUBSCRIBE_STRATEGY || process.env.VITE_PHPLIST_SUBSCRIBE_STRATEGY;
568
+ const subscribePageId = process.env.PHPLIST_SUBSCRIBE_PAGE_ID || process.env.VITE_PHPLIST_SUBSCRIBE_PAGE_ID;
569
+ const apiModeRaw = process.env.PHPLIST_API_MODE || process.env.VITE_PHPLIST_API_MODE;
570
+ const apiMode = apiModeRaw === 'rest_v2' || apiModeRaw === 'v2'
571
+ ? 'rest_v2'
572
+ : apiModeRaw === 'restapi' || apiModeRaw === 'plugin' || restApiEndpoint
573
+ ? 'restapi'
574
+ : undefined;
575
+ if (!phplistUrl) {
576
+ console.log('⚠️ PHPList URL not configured. Skipping PHPList data generation.');
577
+ return true;
578
+ }
579
+ const listId = parseInt(listIdRaw || '0', 10);
580
+ if (!listId || Number.isNaN(listId)) {
197
581
  console.warn('⚠️ PHPList list ID not configured. Skipping PHPList data generation.');
198
582
  return true;
199
583
  }
@@ -254,376 +638,111 @@ async function generateSuiteCRMData(options) {
254
638
  clientId: process.env.VITE_SUITECRM_CLIENT_ID,
255
639
  clientSecret: process.env.VITE_SUITECRM_CLIENT_SECRET,
256
640
  username: process.env.VITE_SUITECRM_USERNAME,
257
- password: process.env.VITE_SUITECRM_PASSWORD,
258
- },
259
- accessFunctionPath: '/api/fetch-with-access',
260
- outputPath,
261
- modules: ['Leads', 'Contacts', 'Accounts'],
262
- includeFields: true,
263
- includeRelationships: false,
264
- });
265
- console.log(`📁 SuiteCRM data written to: ${path.join(process.cwd(), outputPath)}`);
266
- return true;
267
- }
268
- catch (e) {
269
- console.warn(`⚠️ SuiteCRM generation failed (build continues): ${e.message}`);
270
- return true;
271
- }
272
- }
273
-
274
- function trimTrailingSlash(value) {
275
- return value.replace(/\/+$/, '');
276
- }
277
- function resolveEndpointUrl() {
278
- const explicit = process.env.GOOGLE_REVIEWS_API_URL ||
279
- process.env.VITE_GOOGLE_REVIEWS_API_URL ||
280
- '';
281
- if (explicit) {
282
- return explicit;
283
- }
284
- const wpApiBase = process.env.WORDPRESS_API_URL ||
285
- process.env.VITE_WORDPRESS_API_URL ||
286
- '';
287
- if (!wpApiBase) {
288
- return '';
289
- }
290
- const normalizedBase = trimTrailingSlash(wpApiBase);
291
- if (normalizedBase.endsWith('/wp-json')) {
292
- return `${normalizedBase}/vibune-google-reviews/v1/reviews`;
293
- }
294
- return `${normalizedBase}/wp-json/vibune-google-reviews/v1/reviews`;
295
- }
296
- function buildHeaders() {
297
- const headers = {
298
- Accept: 'application/json',
299
- };
300
- const username = process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME || '';
301
- const password = process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD || '';
302
- if (username && password) {
303
- headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
304
- }
305
- const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID || '';
306
- const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET || '';
307
- if (cfAccessClientId && cfAccessClientSecret) {
308
- headers['CF-Access-Client-Id'] = cfAccessClientId;
309
- headers['CF-Access-Client-Secret'] = cfAccessClientSecret;
310
- }
311
- return headers;
312
- }
313
- function hasExpectedShape(payload) {
314
- if (!payload || typeof payload !== 'object') {
315
- return false;
316
- }
317
- const obj = payload;
318
- return Array.isArray(obj.reviews) && typeof obj.business === 'object' && obj.business !== null;
319
- }
320
- function truncateBody(body, max = 1200) {
321
- if (body.length <= max) {
322
- return body;
323
- }
324
- return `${body.slice(0, max)}...`;
325
- }
326
- async function generateGoogleReviewsData(options) {
327
- if (!isGoogleReviewsEnabled()) {
328
- console.log('⚠️ Skipping Google Reviews data generation (google_reviews not in VITE_ENABLED_POST_TYPES).');
329
- return true;
330
- }
331
- const outputPath = options?.outputPath || './public/google-reviews-data.json';
332
- const endpointUrl = resolveEndpointUrl();
333
- if (!endpointUrl) {
334
- console.error('❌ Missing Google Reviews endpoint. Set GOOGLE_REVIEWS_API_URL or VITE_GOOGLE_REVIEWS_API_URL, or configure WORDPRESS_API_URL/VITE_WORDPRESS_API_URL.');
335
- return false;
336
- }
337
- try {
338
- const response = await fetch(endpointUrl, {
339
- method: 'GET',
340
- headers: buildHeaders(),
341
- });
342
- if (!response.ok) {
343
- const body = truncateBody(await response.text());
344
- console.error(`❌ Google Reviews generation failed (HTTP ${response.status}): ${body}`);
345
- return false;
346
- }
347
- const payload = (await response.json());
348
- if (!hasExpectedShape(payload)) {
349
- console.error('❌ Google Reviews generation failed: endpoint returned unexpected payload shape.');
350
- return false;
351
- }
352
- const outputFilePath = path.join(process.cwd(), outputPath);
353
- await mkdir(path.dirname(outputFilePath), { recursive: true });
354
- await writeFile(outputFilePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
355
- console.log(`📁 Google Reviews data written to: ${outputFilePath}`);
356
- return true;
357
- }
358
- catch (e) {
359
- console.error('❌ Google Reviews generation failed:', e.message);
360
- return false;
361
- }
362
- }
363
-
364
- function normalizePathname(pathname) {
365
- const decoded = decodeURIComponent(pathname);
366
- if (!decoded || decoded === '/')
367
- return '/';
368
- const stripped = decoded.replace(/\/+$/, '');
369
- return stripped === '' ? '/' : stripped;
370
- }
371
- function joinPath(prefix, slug) {
372
- const p = prefix.replace(/\/+$/, '');
373
- const s = slug.replace(/^\/+/, '');
374
- if (!p || p === '/')
375
- return normalizePathname(`/${s}`);
376
- return normalizePathname(`${p.startsWith('/') ? p : `/${p}`}/${s}`);
377
- }
378
- function stripHtml(html) {
379
- return html
380
- .replace(/<script[\s\S]*?<\/script>/gi, ' ')
381
- .replace(/<style[\s\S]*?<\/style>/gi, ' ')
382
- .replace(/<[^>]+>/g, ' ')
383
- .replace(/\s+/g, ' ')
384
- .trim();
385
- }
386
- function sanitizeBodyHtml(html) {
387
- return html
388
- .replace(/<script[\s\S]*?<\/script>/gi, '')
389
- .replace(/<style[\s\S]*?<\/style>/gi, '')
390
- .replace(/\son\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
391
- }
392
- function renderedText(value) {
393
- if (typeof value === 'string')
394
- return stripHtml(value);
395
- if (value && typeof value === 'object' && 'rendered' in value) {
396
- const r = value.rendered;
397
- if (typeof r === 'string')
398
- return stripHtml(r);
399
- }
400
- return '';
401
- }
402
- function renderedHtml(value) {
403
- if (typeof value === 'string')
404
- return value;
405
- if (value && typeof value === 'object' && 'rendered' in value) {
406
- const r = value.rendered;
407
- if (typeof r === 'string')
408
- return r;
409
- }
410
- return '';
411
- }
412
- function escapeHtml(s) {
413
- return s
414
- .replace(/&/g, '&amp;')
415
- .replace(/</g, '&lt;')
416
- .replace(/>/g, '&gt;')
417
- .replace(/"/g, '&quot;');
418
- }
419
- function escapeAttr(s) {
420
- return escapeHtml(s).replace(/'/g, '&#39;');
421
- }
422
-
423
- const SITEMAP_URL_SHARD_SIZE = 45000;
424
- function parseRoutePrefixes(raw) {
425
- const out = {};
426
- const src = (raw ?? process.env.VITE_SEO_ROUTE_PREFIXES ?? '').trim();
427
- if (!src)
428
- return out;
429
- for (const part of src.split(',')) {
430
- const trimmed = part.trim();
431
- if (!trimmed)
432
- continue;
433
- const idx = trimmed.indexOf(':');
434
- if (idx <= 0)
435
- continue;
436
- const key = trimmed.slice(0, idx).trim();
437
- const prefix = trimmed.slice(idx + 1).trim();
438
- if (key)
439
- out[key] = prefix || '/';
641
+ password: process.env.VITE_SUITECRM_PASSWORD,
642
+ },
643
+ accessFunctionPath: '/api/fetch-with-access',
644
+ outputPath,
645
+ modules: ['Leads', 'Contacts', 'Accounts'],
646
+ includeFields: true,
647
+ includeRelationships: false,
648
+ });
649
+ console.log(`📁 SuiteCRM data written to: ${path.join(process.cwd(), outputPath)}`);
650
+ return true;
440
651
  }
441
- return out;
442
- }
443
- function isPublishedEntity(item) {
444
- const s = item.status;
445
- if (s === undefined || s === null || s === '')
652
+ catch (e) {
653
+ console.warn(`⚠️ SuiteCRM generation failed (build continues): ${e.message}`);
446
654
  return true;
447
- return s === 'publish';
448
- }
449
- function isNoindexEntity(item) {
450
- return item.digivalt_seo?.robots_index === false;
451
- }
452
- function entityLastmod(item) {
453
- const v = item.modified_gmt || item.modified || item.date_gmt || item.date;
454
- return typeof v === 'string' && v.trim() ? v.trim() : undefined;
455
- }
456
- function entityImage(item) {
457
- const v = item.digivalt_seo?.og_image ||
458
- item.featured_media_cloudflare_url ||
459
- item.featured_media_url;
460
- return typeof v === 'string' && v.trim() ? v.trim() : undefined;
461
- }
462
- function getFrontPageRef(data) {
463
- const fp = data.front_page;
464
- const envSlug = (process.env.VITE_SEO_FRONT_PAGE_SLUG || '').trim();
465
- let slug;
466
- let id;
467
- if (fp && typeof fp === 'object') {
468
- const rec = fp;
469
- if (typeof rec.slug === 'string' && rec.slug.trim())
470
- slug = rec.slug.trim();
471
- if (typeof rec.id === 'number')
472
- id = rec.id;
473
- else if (typeof rec.id === 'string')
474
- id = Number(rec.id) || undefined;
475
655
  }
476
- if (envSlug)
477
- slug = envSlug;
478
- return { slug, id };
479
656
  }
480
- function getFrontPageSlug(data) {
481
- return getFrontPageRef(data).slug;
482
- }
483
- function asRecord(item) {
484
- if (!item || typeof item !== 'object')
485
- return null;
486
- return item;
657
+
658
+ function trimTrailingSlash(value) {
659
+ return value.replace(/\/+$/, '');
487
660
  }
488
- function newsletterRouteBase() {
489
- const raw = process.env.VITE_PHPLIST_NEWSLETTER_ROUTE_BASE || '/newsletters';
490
- return normalizePathname(raw.startsWith('/') ? raw : `/${raw}`);
661
+ function resolveEndpointUrl() {
662
+ const explicit = process.env.GOOGLE_REVIEWS_API_URL ||
663
+ process.env.VITE_GOOGLE_REVIEWS_API_URL ||
664
+ '';
665
+ if (explicit) {
666
+ return explicit;
667
+ }
668
+ const wpApiBase = process.env.WORDPRESS_API_URL ||
669
+ process.env.VITE_WORDPRESS_API_URL ||
670
+ '';
671
+ if (!wpApiBase) {
672
+ return '';
673
+ }
674
+ const normalizedBase = trimTrailingSlash(wpApiBase);
675
+ if (normalizedBase.endsWith('/wp-json')) {
676
+ return `${normalizedBase}/vibune-google-reviews/v1/reviews`;
677
+ }
678
+ return `${normalizedBase}/wp-json/vibune-google-reviews/v1/reviews`;
491
679
  }
492
- function collectSitemapUrls(args) {
493
- const prefixes = args.prefixes ?? parseRoutePrefixes();
494
- const front = args.wpData ? getFrontPageRef(args.wpData) : {};
495
- const byPath = new Map();
496
- const add = (entry) => {
497
- const path = normalizePathname(entry.path);
498
- if (!path)
499
- return;
500
- const prev = byPath.get(path);
501
- if (!prev) {
502
- byPath.set(path, { ...entry, path });
503
- return;
504
- }
505
- byPath.set(path, {
506
- path,
507
- lastmod: entry.lastmod || prev.lastmod,
508
- image: entry.image || prev.image,
509
- });
680
+ function buildHeaders() {
681
+ const headers = {
682
+ Accept: 'application/json',
510
683
  };
511
- add({ path: '/' });
512
- for (const p of args.staticPaths) {
513
- add({ path: p });
514
- }
515
- const pages = args.wpData && Array.isArray(args.wpData.pages) ? args.wpData.pages : [];
516
- for (const raw of pages) {
517
- const item = asRecord(raw);
518
- if (!item || !isPublishedEntity(item) || isNoindexEntity(item))
519
- continue;
520
- const slug = typeof item.slug === 'string' ? item.slug.trim() : '';
521
- if (!slug)
522
- continue;
523
- const isFront = (front.slug && slug === front.slug) ||
524
- (typeof front.id === 'number' && typeof item.id === 'number' && item.id === front.id);
525
- const path = isFront ? '/' : joinPath('/', slug);
526
- add({
527
- path,
528
- lastmod: entityLastmod(item),
529
- image: entityImage(item),
530
- });
684
+ const username = process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME || '';
685
+ const password = process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD || '';
686
+ if (username && password) {
687
+ headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
531
688
  }
532
- if (args.wpData) {
533
- for (const [type, prefix] of Object.entries(prefixes)) {
534
- if (type === 'pages' || type === 'page')
535
- continue;
536
- const arr = args.wpData[type];
537
- if (!Array.isArray(arr))
538
- continue;
539
- for (const raw of arr) {
540
- const item = asRecord(raw);
541
- if (!item || !isPublishedEntity(item) || isNoindexEntity(item))
542
- continue;
543
- const slug = typeof item.slug === 'string' ? item.slug.trim() : '';
544
- if (!slug)
545
- continue;
546
- add({
547
- path: joinPath(prefix, slug),
548
- lastmod: entityLastmod(item),
549
- image: entityImage(item),
550
- });
551
- }
552
- }
689
+ const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID || '';
690
+ const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET || '';
691
+ if (cfAccessClientId && cfAccessClientSecret) {
692
+ headers['CF-Access-Client-Id'] = cfAccessClientId;
693
+ headers['CF-Access-Client-Secret'] = cfAccessClientSecret;
553
694
  }
554
- const newsletters = args.phplistData && Array.isArray(args.phplistData.newsletters)
555
- ? args.phplistData.newsletters
556
- : [];
557
- const nBase = newsletterRouteBase();
558
- for (const raw of newsletters) {
559
- if (!raw || typeof raw !== 'object')
560
- continue;
561
- const rec = raw;
562
- if (typeof rec.slug !== 'string' || !rec.slug.trim())
563
- continue;
564
- add({
565
- path: joinPath(nBase, rec.slug.trim()),
566
- lastmod: typeof rec.sent_at === 'string' ? rec.sent_at : undefined,
567
- });
695
+ return headers;
696
+ }
697
+ function hasExpectedShape(payload) {
698
+ if (!payload || typeof payload !== 'object') {
699
+ return false;
568
700
  }
569
- const known = new Set(byPath.keys());
570
- const considerMenu = (items) => {
571
- if (!Array.isArray(items))
572
- return;
573
- for (const item of items) {
574
- if (!item || typeof item !== 'object')
575
- continue;
576
- const rec = item;
577
- if (typeof rec.url === 'string') {
578
- const path = menuUrlToFrontendPath(rec.url, front.slug);
579
- if (path && known.has(path)) {
580
- add({ path });
581
- }
582
- }
583
- considerMenu(rec.children);
584
- }
585
- };
586
- if (args.wpData) {
587
- const header = args.wpData.navigation_menu;
588
- const footer = args.wpData.footer_navigation_menu;
589
- considerMenu(header?.items);
590
- considerMenu(footer?.items);
701
+ const obj = payload;
702
+ return Array.isArray(obj.reviews) && typeof obj.business === 'object' && obj.business !== null;
703
+ }
704
+ function truncateBody(body, max = 1200) {
705
+ if (body.length <= max) {
706
+ return body;
591
707
  }
592
- const sorted = [...byPath.values()].sort((a, b) => {
593
- if (a.path === '/')
594
- return -1;
595
- if (b.path === '/')
596
- return 1;
597
- return a.path.localeCompare(b.path);
598
- });
599
- return sorted;
708
+ return `${body.slice(0, max)}...`;
600
709
  }
601
- function menuUrlToFrontendPath(urlStr, frontSlug) {
602
- const t = urlStr.trim();
603
- if (!t || t === '#' || t.toLowerCase().startsWith('mailto:') || t.toLowerCase().startsWith('tel:')) {
604
- return null;
710
+ async function generateGoogleReviewsData(options) {
711
+ if (!isGoogleReviewsEnabled()) {
712
+ console.log('⚠️ Skipping Google Reviews data generation (google_reviews not in VITE_ENABLED_POST_TYPES).');
713
+ return true;
605
714
  }
606
- let pathname;
607
- if (t.startsWith('/')) {
608
- pathname = normalizePathname(t.split('?')[0]);
715
+ const outputPath = options?.outputPath || './public/google-reviews-data.json';
716
+ const endpointUrl = resolveEndpointUrl();
717
+ if (!endpointUrl) {
718
+ console.error('❌ Missing Google Reviews endpoint. Set GOOGLE_REVIEWS_API_URL or VITE_GOOGLE_REVIEWS_API_URL, or configure WORDPRESS_API_URL/VITE_WORDPRESS_API_URL.');
719
+ return false;
609
720
  }
610
- else if (/^https?:\/\//i.test(t)) {
611
- try {
612
- pathname = normalizePathname(new URL(t).pathname);
721
+ try {
722
+ const response = await fetch(endpointUrl, {
723
+ method: 'GET',
724
+ headers: buildHeaders(),
725
+ });
726
+ if (!response.ok) {
727
+ const body = truncateBody(await response.text());
728
+ console.error(`❌ Google Reviews generation failed (HTTP ${response.status}): ${body}`);
729
+ return false;
613
730
  }
614
- catch {
615
- return null;
731
+ const payload = (await response.json());
732
+ if (!hasExpectedShape(payload)) {
733
+ console.error('❌ Google Reviews generation failed: endpoint returned unexpected payload shape.');
734
+ return false;
616
735
  }
736
+ const outputFilePath = path.join(process.cwd(), outputPath);
737
+ await mkdir(path.dirname(outputFilePath), { recursive: true });
738
+ await writeFile(outputFilePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
739
+ console.log(`📁 Google Reviews data written to: ${outputFilePath}`);
740
+ return true;
617
741
  }
618
- else {
619
- return null;
742
+ catch (e) {
743
+ console.error('❌ Google Reviews generation failed:', e.message);
744
+ return false;
620
745
  }
621
- if (pathname === '/')
622
- return '/';
623
- const slug = pathname.replace(/^\//, '');
624
- if (frontSlug && slug === frontSlug)
625
- return '/';
626
- return pathname;
627
746
  }
628
747
 
629
748
  function parseStaticPathsJson(raw) {