@marvalt/digivalt-core 0.2.33 → 0.2.35
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/CHANGELOG.md +16 -0
- package/README.md +1 -0
- package/bin/init.cjs +46 -3
- package/dist/generators/wp-guard.d.ts +11 -0
- package/dist/generators.cjs +625 -506
- package/dist/generators.cjs.map +1 -1
- package/dist/generators.esm.js +625 -506
- package/dist/generators.esm.js.map +1 -1
- package/dist/index.cjs +625 -506
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +625 -506
- package/dist/index.esm.js.map +1 -1
- package/docs/AI-MASTER-PROMPT.md +1 -0
- package/package.json +3 -2
- package/template/DIGIVALT_SETUP.md +4 -0
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
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
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
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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, '&')
|
|
85
|
+
.replace(/</g, '<')
|
|
86
|
+
.replace(/>/g, '>')
|
|
87
|
+
.replace(/"/g, '"');
|
|
88
|
+
}
|
|
89
|
+
function escapeAttr(s) {
|
|
90
|
+
return escapeHtml(s).replace(/'/g, ''');
|
|
124
91
|
}
|
|
125
92
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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, '&')
|
|
415
|
-
.replace(/</g, '<')
|
|
416
|
-
.replace(/>/g, '>')
|
|
417
|
-
.replace(/"/g, '"');
|
|
418
|
-
}
|
|
419
|
-
function escapeAttr(s) {
|
|
420
|
-
return escapeHtml(s).replace(/'/g, ''');
|
|
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
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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
|
|
489
|
-
const
|
|
490
|
-
|
|
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
|
|
493
|
-
const
|
|
494
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
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
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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
|
-
|
|
619
|
-
|
|
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) {
|