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