@marvalt/digivalt-core 0.2.30 → 0.2.33

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/GLOBAL-SHORTCODES.md +7 -7
  3. package/README.md +10 -8
  4. package/bin/env-example-fragments.cjs +6 -0
  5. package/bin/init.cjs +44 -5
  6. package/dist/generators/index.d.ts +3 -0
  7. package/dist/generators/prerender-seo.d.ts +6 -0
  8. package/dist/generators/seo-manifest.d.ts +3 -0
  9. package/dist/generators/seo-urls.d.ts +29 -0
  10. package/dist/generators/sitemap-paths.d.ts +5 -9
  11. package/dist/generators.cjs +1005 -155
  12. package/dist/generators.cjs.map +1 -1
  13. package/dist/generators.d.ts +86 -6
  14. package/dist/generators.esm.js +992 -156
  15. package/dist/generators.esm.js.map +1 -1
  16. package/dist/index.cjs +1019 -156
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +103 -9
  19. package/dist/index.esm.js +1006 -157
  20. package/dist/index.esm.js.map +1 -1
  21. package/dist/registry/index.d.ts +20 -0
  22. package/dist/registry.cjs +1528 -0
  23. package/dist/registry.cjs.map +1 -0
  24. package/dist/registry.d.ts +26 -0
  25. package/dist/registry.esm.js +1523 -0
  26. package/dist/registry.esm.js.map +1 -0
  27. package/dist/seo/DigiValtRouteSeo.d.ts +12 -0
  28. package/dist/seo/DigiValtSeoHead.d.ts +2 -0
  29. package/dist/seo/buildPageJsonLd.d.ts +11 -0
  30. package/dist/seo/html.d.ts +8 -0
  31. package/dist/seo/index.d.ts +7 -0
  32. package/dist/seo/resolvePageSeo.d.ts +17 -0
  33. package/dist/seo/seoTypes.d.ts +57 -0
  34. package/dist/seo.cjs +307 -86
  35. package/dist/seo.cjs.map +1 -1
  36. package/dist/seo.d.ts +105 -2
  37. package/dist/seo.esm.js +300 -87
  38. package/dist/seo.esm.js.map +1 -1
  39. package/dist/static/wpStatic.d.ts +16 -0
  40. package/dist/static.cjs +10 -0
  41. package/dist/static.cjs.map +1 -1
  42. package/dist/static.d.ts +38 -2
  43. package/dist/static.esm.js +10 -1
  44. package/dist/static.esm.js.map +1 -1
  45. package/dist/utils/notifications.d.ts +3 -0
  46. package/docs/AI-LOCAL-WORDPRESS-BOOTSTRAP.md +7 -3
  47. package/docs/AI-MASTER-PROMPT.md +37 -0
  48. package/docs/ENVIRONMENT.md +3 -3
  49. package/docs/README.md +6 -4
  50. package/docs/SEO-AIO.md +59 -0
  51. package/docs/SEO-PERFORMANCE.md +44 -11
  52. package/docs/SHORTCODES.md +4 -5
  53. package/package.json +17 -6
  54. package/template/DIGIVALT_SETUP.md +14 -8
  55. package/template/functions/robots.txt.ts +27 -0
  56. package/template/functions/sitemap.xml.ts +143 -65
  57. package/template/scripts/generate.ts +3 -10
  58. package/template/scripts/prerender-seo.ts +10 -0
  59. package/docs/LOVABLE-MASTER-PROMPT.md +0 -140
package/dist/index.esm.js CHANGED
@@ -361,21 +361,6 @@ async function generateGoogleReviewsData(options) {
361
361
  }
362
362
  }
363
363
 
364
- const SITEMAP_SKIP_TOP_LEVEL_KEYS = new Set([
365
- 'config',
366
- 'site_settings',
367
- 'header',
368
- 'footer',
369
- 'theme_styles',
370
- 'navigation_menu',
371
- 'menus',
372
- 'categories',
373
- 'tags',
374
- 'generated_at',
375
- 'frontend_id',
376
- 'frontend_name',
377
- 'front_page',
378
- ]);
379
364
  function normalizePathname(pathname) {
380
365
  const decoded = decodeURIComponent(pathname);
381
366
  if (!decoded || decoded === '/')
@@ -383,43 +368,265 @@ function normalizePathname(pathname) {
383
368
  const stripped = decoded.replace(/\/+$/, '');
384
369
  return stripped === '' ? '/' : stripped;
385
370
  }
386
- function getAllowedHosts() {
387
- const hosts = new Set();
388
- const api = process.env.VITE_WORDPRESS_API_URL || process.env.WORDPRESS_API_URL;
389
- if (api) {
390
- try {
391
- hosts.add(new URL(api).hostname);
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 || '/';
440
+ }
441
+ return out;
442
+ }
443
+ function isPublishedEntity(item) {
444
+ const s = item.status;
445
+ if (s === undefined || s === null || s === '')
446
+ 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
+ }
476
+ if (envSlug)
477
+ slug = envSlug;
478
+ return { slug, id };
479
+ }
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;
487
+ }
488
+ function newsletterRouteBase() {
489
+ const raw = process.env.VITE_PHPLIST_NEWSLETTER_ROUTE_BASE || '/newsletters';
490
+ return normalizePathname(raw.startsWith('/') ? raw : `/${raw}`);
491
+ }
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;
392
504
  }
393
- catch {
394
- /* ignore */
505
+ byPath.set(path, {
506
+ path,
507
+ lastmod: entry.lastmod || prev.lastmod,
508
+ image: entry.image || prev.image,
509
+ });
510
+ };
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
+ });
531
+ }
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
+ }
553
+ }
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
+ });
568
+ }
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);
395
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);
396
591
  }
397
- return hosts;
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;
398
600
  }
399
- /**
400
- * When CMS host is unknown, accept any http(s) URL so local/offline merges still produce paths.
401
- */
402
- function urlToPath(urlStr, allowedHosts) {
601
+ function menuUrlToFrontendPath(urlStr, frontSlug) {
403
602
  const t = urlStr.trim();
404
603
  if (!t || t === '#' || t.toLowerCase().startsWith('mailto:') || t.toLowerCase().startsWith('tel:')) {
405
604
  return null;
406
605
  }
407
- if (!/^https?:\/\//i.test(t)) {
408
- return null;
409
- }
410
- let u;
411
- try {
412
- u = new URL(t);
606
+ let pathname;
607
+ if (t.startsWith('/')) {
608
+ pathname = normalizePathname(t.split('?')[0]);
413
609
  }
414
- catch {
415
- return null;
610
+ else if (/^https?:\/\//i.test(t)) {
611
+ try {
612
+ pathname = normalizePathname(new URL(t).pathname);
613
+ }
614
+ catch {
615
+ return null;
616
+ }
416
617
  }
417
- if (allowedHosts.size > 0 && !allowedHosts.has(u.hostname)) {
618
+ else {
418
619
  return null;
419
620
  }
420
- return normalizePathname(u.pathname);
621
+ if (pathname === '/')
622
+ return '/';
623
+ const slug = pathname.replace(/^\//, '');
624
+ if (frontSlug && slug === frontSlug)
625
+ return '/';
626
+ return pathname;
421
627
  }
422
- function parsePathsJson(raw) {
628
+
629
+ function parseStaticPathsJson(raw) {
423
630
  try {
424
631
  const data = JSON.parse(raw);
425
632
  if (Array.isArray(data)) {
@@ -434,130 +641,70 @@ function parsePathsJson(raw) {
434
641
  }
435
642
  return [];
436
643
  }
437
- function loadStaticPaths(cwd, staticInputPath, allowedHosts) {
438
- const full = path.join(cwd, staticInputPath);
439
- if (!fs.existsSync(full)) {
440
- return [];
441
- }
442
- const raw = fs.readFileSync(full, 'utf8');
443
- const paths = parsePathsJson(raw);
444
- const out = [];
445
- for (const p of paths) {
446
- if (p.startsWith('http://') || p.startsWith('https://')) {
447
- const normalized = urlToPath(p, allowedHosts);
448
- if (normalized)
449
- out.push(normalized);
450
- }
451
- else {
452
- out.push(normalizePathname(p));
453
- }
454
- }
455
- return out;
456
- }
457
- function walkMenuItems(items, allowedHosts, out) {
458
- if (!Array.isArray(items))
459
- return;
460
- for (const item of items) {
461
- if (!item || typeof item !== 'object')
462
- continue;
463
- const rec = item;
464
- if (typeof rec.url === 'string') {
465
- const p = urlToPath(rec.url, allowedHosts);
466
- if (p)
467
- out.add(p);
468
- }
469
- if (Array.isArray(rec.children)) {
470
- walkMenuItems(rec.children, allowedHosts, out);
644
+ function loadJson$1(full) {
645
+ if (!fs.existsSync(full))
646
+ return undefined;
647
+ try {
648
+ const data = JSON.parse(fs.readFileSync(full, 'utf8'));
649
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
650
+ return data;
471
651
  }
472
652
  }
473
- }
474
- function collectLinksFromArray(arr, allowedHosts, out) {
475
- for (const item of arr) {
476
- if (!item || typeof item !== 'object')
477
- continue;
478
- const link = item.link;
479
- if (typeof link === 'string') {
480
- const p = urlToPath(link, allowedHosts);
481
- if (p)
482
- out.add(p);
483
- }
653
+ catch (e) {
654
+ console.warn('⚠️ Could not parse JSON for sitemap:', full, e.message);
484
655
  }
656
+ return undefined;
485
657
  }
486
- function collectPathsFromWpData(data, allowedHosts) {
487
- const out = new Set();
488
- const menu = data.navigation_menu;
489
- if (menu?.items) {
490
- walkMenuItems(menu.items, allowedHosts, out);
491
- }
492
- const config = data.config;
493
- const postTypeKeys = config?.post_types && typeof config.post_types === 'object'
494
- ? Object.keys(config.post_types)
495
- : null;
496
- if (postTypeKeys && postTypeKeys.length > 0) {
497
- for (const key of postTypeKeys) {
498
- const arr = data[key];
499
- if (Array.isArray(arr)) {
500
- collectLinksFromArray(arr, allowedHosts, out);
501
- }
502
- }
503
- }
504
- else {
505
- for (const [key, value] of Object.entries(data)) {
506
- if (SITEMAP_SKIP_TOP_LEVEL_KEYS.has(key))
507
- continue;
508
- if (Array.isArray(value)) {
509
- collectLinksFromArray(value, allowedHosts, out);
510
- }
511
- }
512
- }
513
- return out;
658
+ function loadStaticPaths(cwd, staticInputPath) {
659
+ const full = path.join(cwd, staticInputPath);
660
+ if (!fs.existsSync(full))
661
+ return [];
662
+ return parseStaticPathsJson(fs.readFileSync(full, 'utf8')).map((p) => normalizePathname(p));
514
663
  }
515
664
  function writeFallback(cwd, outputPath) {
516
665
  const full = path.join(cwd, outputPath);
517
666
  fs.mkdirSync(path.dirname(full), { recursive: true });
518
- fs.writeFileSync(full, `${JSON.stringify({ paths: ['/'] }, null, 2)}\n`, 'utf8');
667
+ const payload = { urls: [{ path: '/' }], paths: ['/'] };
668
+ fs.writeFileSync(full, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
669
+ }
670
+ function writeShard(full, urls) {
671
+ const paths = urls.map((u) => u.path);
672
+ fs.writeFileSync(full, `${JSON.stringify({ urls, paths }, null, 2)}\n`, 'utf8');
519
673
  }
520
674
  /**
521
- * Merge sitemap-static-paths.json, navigation_menu URLs, and CPT/post links from wordpress-data.json.
675
+ * Merge static app routes, WP pages (frontend paths), opt-in CPT prefixes, and PHPList issues.
522
676
  */
523
677
  async function generateSitemapPaths(options = {}) {
524
678
  dotenv.config();
525
679
  dotenv.config({ path: '.env.local' });
526
680
  const cwd = options.cwd ?? process.cwd();
527
681
  const wpDataPath = options.wpDataPath ?? 'public/wordpress-data.json';
682
+ const phplistDataPath = options.phplistDataPath ?? 'public/phplist-data.json';
528
683
  const staticInputPath = options.staticInputPath ?? 'public/sitemap-static-paths.json';
529
684
  const outputPath = options.outputPath ?? 'public/sitemap-paths.json';
530
- const allowedHosts = getAllowedHosts();
531
- const merged = new Set();
532
- for (const p of loadStaticPaths(cwd, staticInputPath, allowedHosts)) {
533
- merged.add(normalizePathname(p));
534
- }
535
- const wpFull = path.join(cwd, wpDataPath);
536
- if (fs.existsSync(wpFull)) {
537
- try {
538
- const raw = fs.readFileSync(wpFull, 'utf8');
539
- const data = JSON.parse(raw);
540
- const fromWp = collectPathsFromWpData(data, allowedHosts);
541
- for (const p of fromWp) {
542
- merged.add(p);
543
- }
544
- }
545
- catch (e) {
546
- console.warn('⚠️ Could not parse wordpress-data.json for sitemap paths:', e.message);
547
- }
548
- }
549
- merged.add('/');
550
- const sorted = [...merged].sort((a, b) => {
551
- if (a === '/')
552
- return -1;
553
- if (b === '/')
554
- return 1;
555
- return a.localeCompare(b);
685
+ const urls = collectSitemapUrls({
686
+ wpData: loadJson$1(path.join(cwd, wpDataPath)),
687
+ phplistData: loadJson$1(path.join(cwd, phplistDataPath)),
688
+ staticPaths: loadStaticPaths(cwd, staticInputPath),
689
+ prefixes: parseRoutePrefixes(),
556
690
  });
557
691
  const outFull = path.join(cwd, outputPath);
558
692
  fs.mkdirSync(path.dirname(outFull), { recursive: true });
559
- fs.writeFileSync(outFull, `${JSON.stringify({ paths: sorted }, null, 2)}\n`, 'utf8');
560
- console.log(`📍 Sitemap paths written (${sorted.length} URLs): ${outFull}`);
693
+ if (urls.length <= SITEMAP_URL_SHARD_SIZE) {
694
+ writeShard(outFull, urls);
695
+ console.log(`📍 Sitemap paths written (${urls.length} URLs): ${outFull}`);
696
+ return;
697
+ }
698
+ const parts = Math.ceil(urls.length / SITEMAP_URL_SHARD_SIZE);
699
+ const dir = path.dirname(outFull);
700
+ const base = path.basename(outFull, path.extname(outFull));
701
+ const ext = path.extname(outFull) || '.json';
702
+ for (let i = 0; i < parts; i++) {
703
+ const slice = urls.slice(i * SITEMAP_URL_SHARD_SIZE, (i + 1) * SITEMAP_URL_SHARD_SIZE);
704
+ writeShard(path.join(dir, `${base}-${i}${ext}`), slice);
705
+ }
706
+ fs.writeFileSync(outFull, `${JSON.stringify({ index: true, parts }, null, 2)}\n`, 'utf8');
707
+ console.log(`📍 Sitemap index written (${urls.length} URLs, ${parts} parts): ${outFull}`);
561
708
  }
562
709
  async function generateSitemapPathsSafe(options = {}) {
563
710
  try {
@@ -571,45 +718,747 @@ async function generateSitemapPathsSafe(options = {}) {
571
718
  }
572
719
  }
573
720
 
721
+ const getImportMetaEnv = () => {
722
+ try {
723
+ return typeof import.meta !== 'undefined' ? import.meta.env : undefined;
724
+ }
725
+ catch {
726
+ return undefined;
727
+ }
728
+ };
729
+ const getEnv = (key) => {
730
+ const viteEnv = getImportMetaEnv();
731
+ if (viteEnv && key in viteEnv) {
732
+ const value = viteEnv[key];
733
+ return value == null ? undefined : String(value);
734
+ }
735
+ if (typeof process !== 'undefined' && process.env && key in process.env) {
736
+ const value = process.env[key];
737
+ return value == null ? undefined : String(value);
738
+ }
739
+ return undefined;
740
+ };
741
+ const isDevEnvironment = () => {
742
+ const viteEnv = getImportMetaEnv();
743
+ if (viteEnv && 'DEV' in viteEnv) {
744
+ return Boolean(viteEnv.DEV);
745
+ }
746
+ return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
747
+ };
748
+
749
+ /**
750
+ * Integration Configuration
751
+ * Centralized configuration for all third-party integrations
752
+ */
753
+ function envBool(key, defaultValue) {
754
+ const v = getEnv(key);
755
+ if (v === undefined || v === '')
756
+ return defaultValue;
757
+ const lower = v.toLowerCase();
758
+ if (lower === 'false' || lower === '0' || lower === 'no')
759
+ return false;
760
+ if (lower === 'true' || lower === '1' || lower === 'yes')
761
+ return true;
762
+ return defaultValue;
763
+ }
764
+ /**
765
+ * Get integration configuration with environment variable fallbacks
766
+ */
767
+ function getIntegrationConfig() {
768
+ isDevEnvironment();
769
+ return {
770
+ wordpress: {
771
+ enabled: true,
772
+ apiUrl: getEnv('VITE_WORDPRESS_API_URL') || '',
773
+ authMode: getEnv('VITE_AUTH_MODE') || 'direct',
774
+ },
775
+ supabase: {
776
+ enabled: getEnv('VITE_AUTH_MODE') === 'supabase_proxy',
777
+ url: getEnv('VITE_SUPABASE_URL') || '',
778
+ anonKey: getEnv('VITE_SUPABASE_ANON_KEY') || '',
779
+ serviceRoleKey: getEnv('VITE_SUPABASE_SERVICE_ROLE_KEY'),
780
+ },
781
+ gravityForms: {
782
+ enabled: !!(getEnv('VITE_CLOUDFLARE_WORKER_URL') || getEnv('VITE_WORDPRESS_API_URL')),
783
+ url: getEnv('VITE_WORDPRESS_API_URL'),
784
+ proxyUrl: getEnv('VITE_CLOUDFLARE_WORKER_URL'),
785
+ consumerKey: getEnv('VITE_GF_CONSUMER_KEY'),
786
+ consumerSecret: getEnv('VITE_GF_CONSUMER_SECRET'),
787
+ forms: {
788
+ 'contact': {
789
+ name: 'Contact Form',
790
+ type: 'contact',
791
+ successMessage: 'Thank you for your message. We\'ll get back to you soon!',
792
+ },
793
+ 'newsletter': {
794
+ name: 'Newsletter Signup',
795
+ type: 'newsletter',
796
+ successMessage: 'Thank you for subscribing to our newsletter!',
797
+ },
798
+ 'support': {
799
+ name: 'Support Request',
800
+ type: 'support',
801
+ successMessage: 'Your support request has been submitted successfully.',
802
+ },
803
+ },
804
+ },
805
+ mautic: {
806
+ enabled: !!(getEnv('VITE_MAUTIC_PROXY_URL') || getEnv('VITE_MAUTIC_URL')),
807
+ url: getEnv('VITE_MAUTIC_URL') || '',
808
+ proxyUrl: getEnv('VITE_MAUTIC_PROXY_URL'),
809
+ forms: {
810
+ '1': {
811
+ name: 'Newsletter Signup',
812
+ type: 'newsletter',
813
+ successMessage: 'Thank you for subscribing to our newsletter!',
814
+ postAction: 'message',
815
+ postActionProperty: 'Thank you for your submission!'
816
+ },
817
+ '2': {
818
+ name: 'Contact Form',
819
+ type: 'contact',
820
+ successMessage: 'Thank you for contacting us! We\'ll get back to you soon.',
821
+ postAction: 'message',
822
+ postActionProperty: 'Thank you for your message!'
823
+ },
824
+ '3': {
825
+ name: 'Lead Capture',
826
+ type: 'lead',
827
+ successMessage: 'Thank you for your interest! We\'ll be in touch.',
828
+ postAction: 'message',
829
+ postActionProperty: 'Thank you for your submission!'
830
+ }
831
+ }
832
+ },
833
+ phplist: {
834
+ enabled: !!(getEnv('VITE_PHPLIST_URL') || getEnv('PHPLIST_URL')),
835
+ url: getEnv('VITE_PHPLIST_URL') || getEnv('PHPLIST_URL'),
836
+ proxyUrl: getEnv('VITE_PHPLIST_PROXY_URL') || getEnv('PHPLIST_PROXY_URL'),
837
+ listId: parseInt(getEnv('VITE_PHPLIST_LIST_ID') || getEnv('PHPLIST_LIST_ID') || '0', 10) || undefined,
838
+ subscribePageIds: (getEnv('VITE_PHPLIST_SUBSCRIBE_PAGE_IDS') || getEnv('PHPLIST_SUBSCRIBE_PAGE_IDS') || '')
839
+ .split(',')
840
+ .map((s) => parseInt(s.trim(), 10))
841
+ .filter((n) => !Number.isNaN(n)),
842
+ newsletterRouteBase: getEnv('VITE_PHPLIST_NEWSLETTER_ROUTE_BASE') || '/newsletters',
843
+ },
844
+ suiteCRM: {
845
+ enabled: !!(getEnv('VITE_SUITECRM_URL') && getEnv('VITE_SUITECRM_CLIENT_ID') && getEnv('VITE_SUITECRM_CLIENT_SECRET')),
846
+ url: getEnv('VITE_SUITECRM_URL'),
847
+ proxyUrl: getEnv('VITE_SUITECRM_PROXY_URL'),
848
+ appId: undefined,
849
+ workerSecret: undefined,
850
+ oauth2: {
851
+ clientId: getEnv('VITE_SUITECRM_CLIENT_ID'),
852
+ clientSecret: getEnv('VITE_SUITECRM_CLIENT_SECRET'),
853
+ username: getEnv('VITE_SUITECRM_USERNAME'),
854
+ password: getEnv('VITE_SUITECRM_PASSWORD'),
855
+ },
856
+ },
857
+ chatwoot: {
858
+ enabled: !!(getEnv('VITE_CHATWOOT_WEBSITE_TOKEN') && getEnv('VITE_CHATWOOT_BASE_URL')),
859
+ websiteToken: getEnv('VITE_CHATWOOT_WEBSITE_TOKEN') || '',
860
+ baseUrl: getEnv('VITE_CHATWOOT_BASE_URL') || '',
861
+ locale: 'en',
862
+ type: 'standard',
863
+ launcherTitle: 'Chat',
864
+ hideMessageBubble: false,
865
+ position: 'right',
866
+ showPopoutButton: false,
867
+ },
868
+ analytics: {
869
+ googleAnalytics: { enabled: false },
870
+ googleTagManager: { enabled: false },
871
+ facebookPixel: { enabled: false },
872
+ linkedinInsight: { enabled: false },
873
+ },
874
+ seo: (() => {
875
+ const opt = (key) => {
876
+ const v = getEnv(key);
877
+ if (v === undefined || v === '')
878
+ return undefined;
879
+ const t = v.trim();
880
+ return t || undefined;
881
+ };
882
+ const siteUrl = (getEnv('VITE_SEO_SITE_URL') || '').replace(/\/+$/, '').trim();
883
+ const siteName = (getEnv('VITE_SEO_SITE_NAME') || '').trim();
884
+ const defaultTitle = (getEnv('VITE_SEO_DEFAULT_TITLE') || '').trim() || siteName || '';
885
+ return {
886
+ defaultTitle,
887
+ defaultDescription: (getEnv('VITE_SEO_DEFAULT_DESCRIPTION') || '').trim(),
888
+ defaultKeywords: opt('VITE_SEO_DEFAULT_KEYWORDS'),
889
+ defaultImage: (getEnv('VITE_SEO_DEFAULT_OG_IMAGE') || '').trim(),
890
+ siteName,
891
+ siteUrl,
892
+ twitterHandle: opt('VITE_SEO_TWITTER_SITE'),
893
+ facebookAppId: opt('VITE_SEO_FACEBOOK_APP_ID'),
894
+ googleSiteVerification: opt('VITE_SEO_GOOGLE_SITE_VERIFICATION'),
895
+ bingSiteVerification: opt('VITE_SEO_BING_SITE_VERIFICATION'),
896
+ };
897
+ })(),
898
+ performance: {
899
+ enableLazyLoading: envBool('VITE_PERF_LAZY_LOADING', true),
900
+ enableImageOptimization: envBool('VITE_PERF_IMAGE_OPTIMIZATION', true),
901
+ enableCodeSplitting: envBool('VITE_PERF_CODE_SPLITTING', true),
902
+ enableServiceWorker: envBool('VITE_PERF_SERVICE_WORKER', false),
903
+ enableCaching: envBool('VITE_PERF_CACHING', true),
904
+ }
905
+ };
906
+ }
907
+ function getSEOConfig() {
908
+ return getIntegrationConfig().seo;
909
+ }
910
+
911
+ /**
912
+ * SEO defaults from env-backed integration config (see `VITE_SEO_*` in docs).
913
+ */
914
+ function getSeoDefaultsFromEnv() {
915
+ const seo = getSEOConfig();
916
+ return {
917
+ siteUrl: seo.siteUrl,
918
+ siteName: seo.siteName,
919
+ defaultTitle: seo.defaultTitle,
920
+ defaultDescription: seo.defaultDescription,
921
+ defaultKeywords: seo.defaultKeywords,
922
+ defaultImage: seo.defaultImage,
923
+ twitterHandle: seo.twitterHandle,
924
+ facebookAppId: seo.facebookAppId,
925
+ googleSiteVerification: seo.googleSiteVerification,
926
+ bingSiteVerification: seo.bingSiteVerification,
927
+ };
928
+ }
929
+
930
+ function firstNonEmpty(...values) {
931
+ for (const v of values) {
932
+ if (typeof v === 'string' && v.trim())
933
+ return v.trim();
934
+ }
935
+ return '';
936
+ }
937
+ function resolvePageSeo(entity, pathname, overrides) {
938
+ const d = getSeoDefaultsFromEnv();
939
+ const seo = entity?.digivalt_seo;
940
+ const title = firstNonEmpty(overrides?.title, seo?.title, renderedText(entity?.title), d.defaultTitle, d.siteName);
941
+ const description = firstNonEmpty(overrides?.description, seo?.description, renderedText(entity?.excerpt), d.defaultDescription);
942
+ const imageUrl = firstNonEmpty(overrides?.imageUrl, seo?.og_image, entity?.featured_media_cloudflare_url, entity?.featured_media_url, d.defaultImage);
943
+ const noindex = overrides?.noindex ?? seo?.robots_index === false;
944
+ const ogType = overrides?.ogType || seo?.og_type || 'website';
945
+ const canonicalUrl = firstNonEmpty(overrides?.canonicalUrl, seo?.canonical) || undefined;
946
+ const robotsFollow = seo?.robots_follow !== false;
947
+ let robots;
948
+ if (noindex && robotsFollow)
949
+ robots = 'noindex, follow';
950
+ else if (noindex)
951
+ robots = 'noindex, nofollow';
952
+ else if (!robotsFollow)
953
+ robots = 'index, nofollow';
954
+ return {
955
+ entity,
956
+ noindex,
957
+ head: {
958
+ pathname,
959
+ title: title || undefined,
960
+ description: description || undefined,
961
+ imageUrl: imageUrl || undefined,
962
+ canonicalUrl,
963
+ ogType,
964
+ noindex,
965
+ robots,
966
+ },
967
+ };
968
+ }
969
+
970
+ const createOrganizationSchema = (data) => ({
971
+ '@context': 'https://schema.org',
972
+ '@type': 'Organization',
973
+ name: data.name,
974
+ url: data.url,
975
+ ...(data.logo && { logo: data.logo }),
976
+ ...(data.description && { description: data.description }),
977
+ ...(data.address && {
978
+ address: {
979
+ '@type': 'PostalAddress',
980
+ ...data.address,
981
+ },
982
+ }),
983
+ ...(data.contactPoint && {
984
+ contactPoint: {
985
+ '@type': 'ContactPoint',
986
+ ...data.contactPoint,
987
+ },
988
+ }),
989
+ });
990
+ const createWebSiteSchema = (data) => ({
991
+ '@context': 'https://schema.org',
992
+ '@type': 'WebSite',
993
+ name: data.name,
994
+ url: data.url,
995
+ ...(data.description && { description: data.description }),
996
+ ...(data.publisher && { publisher: data.publisher }),
997
+ });
998
+ const createArticleSchema = (data) => ({
999
+ '@context': 'https://schema.org',
1000
+ '@type': 'Article',
1001
+ headline: data.headline,
1002
+ description: data.description,
1003
+ ...(data.image && { image: data.image }),
1004
+ author: {
1005
+ '@type': 'Person',
1006
+ name: data.author,
1007
+ },
1008
+ publisher: {
1009
+ '@type': 'Organization',
1010
+ name: data.publisher,
1011
+ },
1012
+ datePublished: data.datePublished,
1013
+ ...(data.dateModified && { dateModified: data.dateModified }),
1014
+ url: data.url,
1015
+ });
1016
+ const createServiceSchema = (data) => ({
1017
+ '@context': 'https://schema.org',
1018
+ '@type': 'Service',
1019
+ name: data.name,
1020
+ description: data.description,
1021
+ provider: {
1022
+ '@type': 'Organization',
1023
+ name: data.provider,
1024
+ },
1025
+ url: data.url,
1026
+ ...(data.areaServed && { areaServed: data.areaServed }),
1027
+ ...(data.serviceType && { serviceType: data.serviceType }),
1028
+ });
1029
+ const createFAQSchema = (faqs) => ({
1030
+ '@context': 'https://schema.org',
1031
+ '@type': 'FAQPage',
1032
+ mainEntity: faqs.map(faq => ({
1033
+ '@type': 'Question',
1034
+ name: faq.question,
1035
+ acceptedAnswer: {
1036
+ '@type': 'Answer',
1037
+ text: faq.answer,
1038
+ },
1039
+ })),
1040
+ });
1041
+
1042
+ function joinAbsoluteUrl(siteUrl, pathname) {
1043
+ const base = siteUrl.replace(/\/+$/, '');
1044
+ if (pathname === '/')
1045
+ return base || pathname;
1046
+ const p = pathname.startsWith('/') ? pathname : `/${pathname}`;
1047
+ return base ? `${base}${p}` : p;
1048
+ }
1049
+ function extraToList(extra) {
1050
+ if (!extra)
1051
+ return [];
1052
+ if (Array.isArray(extra)) {
1053
+ return extra.filter((x) => !!x && typeof x === 'object');
1054
+ }
1055
+ if (typeof extra === 'object')
1056
+ return [extra];
1057
+ return [];
1058
+ }
1059
+ /**
1060
+ * JSON-LD for a route: WebSite + Organization (from env) plus page schema from DigiValt SEO fields.
1061
+ */
1062
+ function buildPageJsonLd(entity, pathname, options) {
1063
+ const d = getSeoDefaultsFromEnv();
1064
+ const seo = entity?.digivalt_seo;
1065
+ const title = options?.title ||
1066
+ seo?.title ||
1067
+ renderedText(entity?.title) ||
1068
+ d.defaultTitle ||
1069
+ d.siteName;
1070
+ const description = options?.description ||
1071
+ seo?.description ||
1072
+ renderedText(entity?.excerpt) ||
1073
+ d.defaultDescription;
1074
+ const image = options?.imageUrl ||
1075
+ seo?.og_image ||
1076
+ entity?.featured_media_cloudflare_url ||
1077
+ entity?.featured_media_url ||
1078
+ d.defaultImage;
1079
+ const url = options?.canonicalUrl ||
1080
+ seo?.canonical ||
1081
+ (d.siteUrl ? joinAbsoluteUrl(d.siteUrl, pathname || '/') : pathname);
1082
+ const out = [];
1083
+ if (d.siteName && d.siteUrl) {
1084
+ out.push(createWebSiteSchema({
1085
+ name: d.siteName,
1086
+ url: d.siteUrl,
1087
+ description: d.defaultDescription || undefined,
1088
+ }));
1089
+ out.push(createOrganizationSchema({
1090
+ name: d.siteName,
1091
+ url: d.siteUrl,
1092
+ logo: d.defaultImage || undefined,
1093
+ description: d.defaultDescription || undefined,
1094
+ }));
1095
+ }
1096
+ const schemaType = seo?.schema_type || 'WebPage';
1097
+ if (schemaType === 'Article') {
1098
+ out.push(createArticleSchema({
1099
+ headline: title,
1100
+ description: description || title,
1101
+ image: image || undefined,
1102
+ author: d.siteName || 'Publisher',
1103
+ publisher: d.siteName || 'Publisher',
1104
+ datePublished: entity?.date_gmt || entity?.date || new Date().toISOString(),
1105
+ dateModified: entity?.modified_gmt || entity?.modified,
1106
+ url,
1107
+ }));
1108
+ }
1109
+ else if (schemaType === 'Service') {
1110
+ out.push(createServiceSchema({
1111
+ name: title,
1112
+ description: description || title,
1113
+ provider: d.siteName || title,
1114
+ url,
1115
+ }));
1116
+ }
1117
+ else if (schemaType === 'FAQPage' || (seo?.faq && seo.faq.length > 0)) {
1118
+ const faqs = (seo?.faq || []).filter((f) => f.question && f.answer);
1119
+ if (faqs.length) {
1120
+ out.push(createFAQSchema(faqs));
1121
+ }
1122
+ else {
1123
+ out.push({
1124
+ '@context': 'https://schema.org',
1125
+ '@type': 'WebPage',
1126
+ name: title,
1127
+ description: description || undefined,
1128
+ url,
1129
+ });
1130
+ }
1131
+ }
1132
+ else {
1133
+ out.push({
1134
+ '@context': 'https://schema.org',
1135
+ '@type': 'WebPage',
1136
+ name: title,
1137
+ description: description || undefined,
1138
+ url,
1139
+ });
1140
+ }
1141
+ out.push(...extraToList(seo?.extra_json_ld));
1142
+ return out;
1143
+ }
1144
+
1145
+ function loadJson(full) {
1146
+ if (!fs.existsSync(full))
1147
+ return undefined;
1148
+ try {
1149
+ const data = JSON.parse(fs.readFileSync(full, 'utf8'));
1150
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
1151
+ return data;
1152
+ }
1153
+ }
1154
+ catch {
1155
+ /* ignore */
1156
+ }
1157
+ return undefined;
1158
+ }
1159
+ function parseStaticPaths(full) {
1160
+ if (!fs.existsSync(full))
1161
+ return [];
1162
+ try {
1163
+ const data = JSON.parse(fs.readFileSync(full, 'utf8'));
1164
+ const list = Array.isArray(data)
1165
+ ? data
1166
+ : data && typeof data === 'object' && Array.isArray(data.paths)
1167
+ ? data.paths
1168
+ : [];
1169
+ return list.filter((p) => typeof p === 'string' && p.startsWith('/')).map(normalizePathname);
1170
+ }
1171
+ catch {
1172
+ return [];
1173
+ }
1174
+ }
1175
+ function asEntity(item) {
1176
+ if (!item || typeof item !== 'object')
1177
+ return undefined;
1178
+ return item;
1179
+ }
1180
+ function findEntityForPath(pathname, wpData, phplistData) {
1181
+ const front = wpData ? getFrontPageRef(wpData) : {};
1182
+ const pages = wpData && Array.isArray(wpData.pages) ? wpData.pages : [];
1183
+ if (pathname === '/') {
1184
+ const hit = pages.find((p) => {
1185
+ const ent = asEntity(p);
1186
+ if (!ent)
1187
+ return false;
1188
+ if (front.slug && ent.slug === front.slug)
1189
+ return true;
1190
+ if (typeof front.id === 'number' && typeof p.id === 'number') {
1191
+ return p.id === front.id;
1192
+ }
1193
+ return false;
1194
+ });
1195
+ return asEntity(hit);
1196
+ }
1197
+ const pageSlug = pathname.replace(/^\//, '');
1198
+ const pageHit = pages.find((p) => asEntity(p)?.slug === pageSlug);
1199
+ if (pageHit)
1200
+ return asEntity(pageHit);
1201
+ const prefixes = parseRoutePrefixes();
1202
+ for (const [type, prefix] of Object.entries(prefixes)) {
1203
+ const normalizedPrefix = normalizePathname(prefix.startsWith('/') ? prefix : `/${prefix}`);
1204
+ if (pathname === normalizedPrefix)
1205
+ continue;
1206
+ if (!pathname.startsWith(`${normalizedPrefix}/`))
1207
+ continue;
1208
+ const slug = pathname.slice(normalizedPrefix.length).replace(/^\//, '');
1209
+ const arr = wpData?.[type];
1210
+ if (!Array.isArray(arr))
1211
+ continue;
1212
+ const hit = arr.find((p) => asEntity(p)?.slug === slug);
1213
+ if (hit)
1214
+ return asEntity(hit);
1215
+ }
1216
+ const nBase = newsletterRouteBase();
1217
+ if (pathname.startsWith(`${nBase}/`) || (nBase !== '/' && pathname.startsWith(`${nBase}/`))) {
1218
+ const slug = nBase === '/' ? pathname.replace(/^\//, '') : pathname.slice(nBase.length).replace(/^\//, '');
1219
+ const newsletters = phplistData && Array.isArray(phplistData.newsletters) ? phplistData.newsletters : [];
1220
+ const hit = newsletters.find((n) => n && typeof n === 'object' && n.slug === slug);
1221
+ if (hit && typeof hit === 'object') {
1222
+ const rec = hit;
1223
+ return {
1224
+ slug: rec.slug,
1225
+ title: { rendered: rec.subject || '' },
1226
+ excerpt: { rendered: rec.excerpt || '' },
1227
+ content: { rendered: rec.html || '' },
1228
+ date: rec.sent_at,
1229
+ modified: rec.sent_at,
1230
+ };
1231
+ }
1232
+ }
1233
+ return undefined;
1234
+ }
1235
+ async function generateSeoManifest(options = {}) {
1236
+ dotenv.config();
1237
+ dotenv.config({ path: '.env.local' });
1238
+ const cwd = options.cwd ?? process.cwd();
1239
+ const wpDataPath = options.wpDataPath ?? 'public/wordpress-data.json';
1240
+ const phplistDataPath = options.phplistDataPath ?? 'public/phplist-data.json';
1241
+ const staticInputPath = options.staticInputPath ?? 'public/sitemap-static-paths.json';
1242
+ const manifestOutputPath = options.manifestOutputPath ?? 'public/seo-manifest.json';
1243
+ const wpData = loadJson(path.join(cwd, wpDataPath));
1244
+ const phplistData = loadJson(path.join(cwd, phplistDataPath));
1245
+ const staticPaths = parseStaticPaths(path.join(cwd, staticInputPath));
1246
+ const urls = collectSitemapUrls({
1247
+ wpData,
1248
+ phplistData,
1249
+ staticPaths,
1250
+ prefixes: parseRoutePrefixes(),
1251
+ });
1252
+ const defaults = getSeoDefaultsFromEnv();
1253
+ const entries = urls.map((u) => {
1254
+ const entity = findEntityForPath(u.path, wpData, phplistData);
1255
+ const resolved = resolvePageSeo(entity, u.path);
1256
+ const jsonLd = buildPageJsonLd(entity, u.path, {
1257
+ title: resolved.head.title,
1258
+ description: resolved.head.description,
1259
+ imageUrl: resolved.head.imageUrl,
1260
+ canonicalUrl: resolved.head.canonicalUrl,
1261
+ });
1262
+ const bodySource = (typeof entity?.content_cloudflare === 'string' && entity.content_cloudflare) ||
1263
+ renderedHtml(entity?.content);
1264
+ return {
1265
+ path: u.path,
1266
+ title: resolved.head.title || defaults.defaultTitle || defaults.siteName || '',
1267
+ description: resolved.head.description || defaults.defaultDescription || '',
1268
+ canonical: resolved.head.canonicalUrl,
1269
+ image: resolved.head.imageUrl || u.image,
1270
+ noindex: resolved.noindex,
1271
+ ogType: resolved.head.ogType,
1272
+ jsonLd,
1273
+ bodyHtml: bodySource ? sanitizeBodyHtml(bodySource) : '',
1274
+ };
1275
+ });
1276
+ const full = path.join(cwd, manifestOutputPath);
1277
+ fs.mkdirSync(path.dirname(full), { recursive: true });
1278
+ fs.writeFileSync(full, `${JSON.stringify({ generated_at: new Date().toISOString(), urls: entries }, null, 2)}\n`, 'utf8');
1279
+ console.log(`📍 SEO manifest written (${entries.length} URLs): ${full}`);
1280
+ }
1281
+ async function generateSeoManifestSafe(options = {}) {
1282
+ try {
1283
+ await generateSeoManifest(options);
1284
+ }
1285
+ catch (e) {
1286
+ console.error('❌ SEO manifest generation failed:', e.message);
1287
+ const cwd = options.cwd ?? process.cwd();
1288
+ const manifestOutputPath = options.manifestOutputPath ?? 'public/seo-manifest.json';
1289
+ const full = path.join(cwd, manifestOutputPath);
1290
+ fs.mkdirSync(path.dirname(full), { recursive: true });
1291
+ fs.writeFileSync(full, `${JSON.stringify({ urls: [] }, null, 2)}\n`, 'utf8');
1292
+ }
1293
+ }
1294
+
1295
+ function upsertMeta(html, attrName, attrValue, content) {
1296
+ const re = new RegExp(`<meta\\s+[^>]*${attrName}=["']${attrValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["'][^>]*>`, 'i');
1297
+ const tag = `<meta ${attrName}="${escapeAttr(attrValue)}" content="${escapeAttr(content)}" />`;
1298
+ if (re.test(html))
1299
+ return html.replace(re, tag);
1300
+ return html.replace(/<\/head>/i, ` ${tag}\n </head>`);
1301
+ }
1302
+ function upsertLink(html, rel, href) {
1303
+ const re = new RegExp(`<link\\s+[^>]*rel=["']${rel}["'][^>]*>`, 'i');
1304
+ const tag = `<link rel="${escapeAttr(rel)}" href="${escapeAttr(href)}" />`;
1305
+ if (re.test(html))
1306
+ return html.replace(re, tag);
1307
+ return html.replace(/<\/head>/i, ` ${tag}\n </head>`);
1308
+ }
1309
+ function injectJsonLd(html, blocks) {
1310
+ if (!blocks?.length)
1311
+ return html;
1312
+ const scripts = blocks
1313
+ .map((b) => `<script type="application/ld+json">${JSON.stringify(b).replace(/</g, '\\u003c')}</script>`)
1314
+ .join('\n ');
1315
+ return html.replace(/<\/head>/i, ` ${scripts}\n </head>`);
1316
+ }
1317
+ function applyEntry(html, entry) {
1318
+ let out = html;
1319
+ if (entry.title) {
1320
+ out = out.replace(/<title>[\s\S]*?<\/title>/i, `<title>${escapeHtml(entry.title)}</title>`);
1321
+ }
1322
+ if (entry.description) {
1323
+ out = upsertMeta(out, 'name', 'description', entry.description);
1324
+ out = upsertMeta(out, 'property', 'og:description', entry.description);
1325
+ }
1326
+ if (entry.title) {
1327
+ out = upsertMeta(out, 'property', 'og:title', entry.title);
1328
+ out = upsertMeta(out, 'name', 'twitter:title', entry.title);
1329
+ }
1330
+ if (entry.canonical) {
1331
+ out = upsertLink(out, 'canonical', entry.canonical);
1332
+ out = upsertMeta(out, 'property', 'og:url', entry.canonical);
1333
+ }
1334
+ if (entry.image) {
1335
+ out = upsertMeta(out, 'property', 'og:image', entry.image);
1336
+ out = upsertMeta(out, 'name', 'twitter:image', entry.image);
1337
+ }
1338
+ if (entry.ogType) {
1339
+ out = upsertMeta(out, 'property', 'og:type', entry.ogType);
1340
+ }
1341
+ if (entry.noindex) {
1342
+ out = upsertMeta(out, 'name', 'robots', 'noindex, nofollow');
1343
+ }
1344
+ out = injectJsonLd(out, entry.jsonLd);
1345
+ const snapshot = `<article data-digivalt-prerender><h1>${escapeHtml(entry.title || '')}</h1>${entry.description ? `<p>${escapeHtml(entry.description)}</p>` : ''}${entry.bodyHtml || ''}</article>`;
1346
+ if (/<div id="root"><\/div>/i.test(out)) {
1347
+ out = out.replace(/<div id="root"><\/div>/i, `<div id="root">${snapshot}</div>`);
1348
+ }
1349
+ else if (/<div id="root">[\s\S]*?<\/div>/i.test(out)) {
1350
+ out = out.replace(/<div id="root">[\s\S]*?<\/div>/i, `<div id="root">${snapshot}</div>`);
1351
+ }
1352
+ return out;
1353
+ }
1354
+ function distFileForPath(distAbs, urlPath) {
1355
+ if (urlPath === '/')
1356
+ return path.join(distAbs, 'index.html');
1357
+ const rel = urlPath.replace(/^\//, '');
1358
+ return path.join(distAbs, rel, 'index.html');
1359
+ }
1360
+ async function prerenderSeoHtml(options = {}) {
1361
+ dotenv.config();
1362
+ dotenv.config({ path: '.env.local' });
1363
+ const cwd = options.cwd ?? process.cwd();
1364
+ const distDir = options.distDir ?? 'dist';
1365
+ const distAbs = path.join(cwd, distDir);
1366
+ const indexFile = path.join(distAbs, 'index.html');
1367
+ if (!fs.existsSync(indexFile)) {
1368
+ console.warn('⚠️ prerenderSeoHtml: dist/index.html missing; skip.');
1369
+ return;
1370
+ }
1371
+ const manifestCandidates = [
1372
+ options.manifestPath ? path.join(cwd, options.manifestPath) : '',
1373
+ path.join(distAbs, 'seo-manifest.json'),
1374
+ path.join(cwd, 'public', 'seo-manifest.json'),
1375
+ ].filter(Boolean);
1376
+ let manifestRaw;
1377
+ for (const p of manifestCandidates) {
1378
+ if (fs.existsSync(p)) {
1379
+ manifestRaw = fs.readFileSync(p, 'utf8');
1380
+ break;
1381
+ }
1382
+ }
1383
+ if (!manifestRaw) {
1384
+ console.warn('⚠️ prerenderSeoHtml: seo-manifest.json missing; skip.');
1385
+ return;
1386
+ }
1387
+ let urls = [];
1388
+ try {
1389
+ const data = JSON.parse(manifestRaw);
1390
+ urls = Array.isArray(data.urls) ? data.urls : [];
1391
+ }
1392
+ catch (e) {
1393
+ console.warn('⚠️ prerenderSeoHtml: invalid manifest:', e.message);
1394
+ return;
1395
+ }
1396
+ const template = fs.readFileSync(indexFile, 'utf8');
1397
+ let written = 0;
1398
+ for (const entry of urls) {
1399
+ if (!entry?.path)
1400
+ continue;
1401
+ const html = applyEntry(template, entry);
1402
+ const dest = distFileForPath(distAbs, entry.path);
1403
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
1404
+ fs.writeFileSync(dest, html, 'utf8');
1405
+ written += 1;
1406
+ }
1407
+ console.log(`📍 Prerendered ${written} HTML snapshots into ${distAbs}`);
1408
+ }
1409
+
574
1410
  async function generateAllData() {
575
- // Load .env files if they exist (for local development)
576
1411
  dotenv.config();
577
1412
  dotenv.config({ path: '.env.local' });
578
1413
  console.log('🚀 Starting DigiVAlt static data generation...');
579
1414
  if (process.env.VITE_IS_LOVABLE === 'true') {
580
- console.log('⚡ Lovable Environment Detected (VITE_IS_LOVABLE). Skipping static data generation to use GitHub synced static files.');
581
- return;
1415
+ console.log('⚡ Fixture mode (`VITE_IS_LOVABLE`) enabled. Skipping remote fetch; refreshing sitemap and SEO manifest from committed JSON.');
582
1416
  }
583
- const wpSuccess = await generateWordPressData();
584
- const gfSuccess = await generateGravityFormsData();
585
- // Optional integrations — warn-only (same as PHPList); do not block deploy
586
- await generateMauticData();
587
- await generatePhplistData();
588
- await generateSuiteCRMData();
589
- await generateGoogleReviewsData();
590
- if (!wpSuccess || !gfSuccess) {
591
- console.error('❌ Required generation failed (WordPress or Gravity Forms). See above for details.');
592
- process.exit(1);
1417
+ else {
1418
+ const wpSuccess = await generateWordPressData();
1419
+ const gfSuccess = await generateGravityFormsData();
1420
+ await generateMauticData();
1421
+ await generatePhplistData();
1422
+ await generateSuiteCRMData();
1423
+ await generateGoogleReviewsData();
1424
+ if (!wpSuccess || !gfSuccess) {
1425
+ console.error('❌ Required generation failed (WordPress or Gravity Forms). See above for details.');
1426
+ process.exit(1);
1427
+ }
593
1428
  }
594
1429
  await generateSitemapPathsSafe();
1430
+ await generateSeoManifestSafe();
595
1431
  console.log('✅ Generation complete!');
596
1432
  }
597
1433
 
598
1434
  var index = /*#__PURE__*/Object.freeze({
599
1435
  __proto__: null,
600
1436
  PSEUDO_POST_TYPES: PSEUDO_POST_TYPES,
1437
+ SITEMAP_URL_SHARD_SIZE: SITEMAP_URL_SHARD_SIZE,
1438
+ collectSitemapUrls: collectSitemapUrls,
1439
+ entityImage: entityImage,
1440
+ entityLastmod: entityLastmod,
601
1441
  generateAllData: generateAllData,
602
1442
  generateGoogleReviewsData: generateGoogleReviewsData,
603
1443
  generateGravityFormsData: generateGravityFormsData,
604
1444
  generateMauticData: generateMauticData,
605
1445
  generatePhplistData: generatePhplistData,
1446
+ generateSeoManifest: generateSeoManifest,
1447
+ generateSeoManifestSafe: generateSeoManifestSafe,
606
1448
  generateSitemapPaths: generateSitemapPaths,
607
1449
  generateSitemapPathsSafe: generateSitemapPathsSafe,
608
1450
  generateSuiteCRMData: generateSuiteCRMData,
609
1451
  generateWordPressData: generateWordPressData,
1452
+ getFrontPageRef: getFrontPageRef,
1453
+ getFrontPageSlug: getFrontPageSlug,
610
1454
  getWordPressPostTypes: getWordPressPostTypes,
611
1455
  isGoogleReviewsEnabled: isGoogleReviewsEnabled,
612
- parseEnabledPostTypes: parseEnabledPostTypes
1456
+ isNoindexEntity: isNoindexEntity,
1457
+ isPublishedEntity: isPublishedEntity,
1458
+ newsletterRouteBase: newsletterRouteBase,
1459
+ parseEnabledPostTypes: parseEnabledPostTypes,
1460
+ parseRoutePrefixes: parseRoutePrefixes,
1461
+ prerenderSeoHtml: prerenderSeoHtml
613
1462
  });
614
1463
 
615
1464
  /**
@@ -620,5 +1469,5 @@ const getDigiValtVersion = () => {
620
1469
  return "0.2.6";
621
1470
  };
622
1471
 
623
- export { PSEUDO_POST_TYPES, generateAllData, generateGoogleReviewsData, generateGravityFormsData, generateMauticData, generatePhplistData, generateSitemapPaths, generateSitemapPathsSafe, generateSuiteCRMData, generateWordPressData, index as generators, getDigiValtVersion, getWordPressPostTypes, isGoogleReviewsEnabled, parseEnabledPostTypes };
1472
+ export { PSEUDO_POST_TYPES, SITEMAP_URL_SHARD_SIZE, collectSitemapUrls, entityImage, entityLastmod, generateAllData, generateGoogleReviewsData, generateGravityFormsData, generateMauticData, generatePhplistData, generateSeoManifest, generateSeoManifestSafe, generateSitemapPaths, generateSitemapPathsSafe, generateSuiteCRMData, generateWordPressData, index as generators, getDigiValtVersion, getFrontPageRef, getFrontPageSlug, getWordPressPostTypes, isGoogleReviewsEnabled, isNoindexEntity, isPublishedEntity, newsletterRouteBase, parseEnabledPostTypes, parseRoutePrefixes, prerenderSeoHtml };
624
1473
  //# sourceMappingURL=index.esm.js.map