@marvalt/digivalt-core 0.2.9 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.12] - 2026-04-13
9
+
10
+ ### Added
11
+ - `GLOBAL-SHORTCODES.md`: consolidated shortcode reference covering DigiValt core form/popup shortcodes (`gf_embed`, `popupbtn_gf`, `popupbtn_mautic`, aliases) plus `@marvalt/shcoder` member/testimonial/event shortcodes and legacy event aliases.
12
+ - `docs/LOVABLE-MASTER-PROMPT.md`: promoted to a tool-agnostic AI master prompt (Cursor / Claude Code / Antigravity / Lovable) for full local WordPress-control scaffolds with explicit AI-vs-human responsibilities and first-run `build` + `dev` expectations.
13
+ - `docs/AI-LOCAL-WORDPRESS-BOOTSTRAP.md`: new local-first runbook with minimum env matrix, first-run commands, artifact checks, and common recovery steps.
14
+ - Template starter fixtures for Lovable-first app bootstraps:
15
+ - `template/public/wordpress-data.json`
16
+ - `template/public/gravity-forms-data.json`
17
+ - `template/src/data/mautic-data.json`
18
+ These placeholder files are safe defaults that prevent first-run runtime failures before real generated exports are available.
19
+ - Cross-links added from package README and `digivalt-landing/DIGIVALT_SETUP.md` to the new unified AI docs.
20
+
21
+ ## [0.2.11] - 2026-04-03
22
+
23
+ ### Added
24
+ - **`generateSitemapPaths` / `generateSitemapPathsSafe`** ([`src/generators/sitemap-paths.ts`](src/generators/sitemap-paths.ts)): build-time merge of **`public/sitemap-static-paths.json`**, WP **`navigation_menu`** URLs, and CPT/post **`link`** pathnames from **`wordpress-data.json`** (via **`config.post_types`** keys, with fallback scanning). Writes **`public/sitemap-paths.json`**. Host filter uses CMS URL from env; **`generateSitemapPathsSafe`** falls back to **`/`** on error.
25
+ - **Template + landing:** **`public/sitemap-static-paths.json`**; **`scripts/generate.ts`** runs sitemap refresh when **`VITE_IS_LOVABLE=true`** or after a failed generate try/catch.
26
+ - **`npm run smoke:sitemap`:** fixture test for merged paths.
27
+
28
+ ### Changed
29
+ - **`generateAllData`:** runs **`generateSitemapPathsSafe`** after successful static generation.
30
+ - **Docs:** `DIGIVALT_SETUP.md`, `SEO-PERFORMANCE.md`, repo **`AGENTS.md`** (sitemap conventions).
31
+
32
+ ## [0.2.10] - 2026-04-03
33
+
34
+ ### Changed
35
+ - **Template `scripts/deploy-secrets.js`:** **`VITE_AUTH_MODE`** and **`VITE_DEFAULT_MAX_ITEMS`** are excluded from bulk Cloudflare upload (set locally or in the Pages dashboard). **`NODE_ENV`** was already excluded. Excluded keys log a dedicated skip reason.
36
+
8
37
  ## [0.2.9] - 2026-04-03
9
38
 
10
39
  ### Added
package/README.md CHANGED
@@ -55,6 +55,14 @@ When to rerun it:
55
55
  - after upgrading `@marvalt/digivalt-core` when scaffolded build wiring changes
56
56
  - before publishing a new app if you want a quick preflight of the DigiVAlt setup
57
57
 
58
+ **Sitemap:** Each build regenerates **`public/sitemap-paths.json`** from **`public/sitemap-static-paths.json`** (paths you add for SPA-only routes) plus WordPress menu and CPT links in **`wordpress-data.json`**. See **[docs/SEO-PERFORMANCE.md](./docs/SEO-PERFORMANCE.md)**.
59
+
60
+ **Google Reviews static data:** `generateAllData()` also fetches `vibune-google-reviews` and writes **`public/google-reviews-data.json`**. By default it derives the endpoint from `WORDPRESS_API_URL` / `VITE_WORDPRESS_API_URL` and calls `/wp-json/vibune-google-reviews/v1/reviews`. Override with `GOOGLE_REVIEWS_API_URL` / `VITE_GOOGLE_REVIEWS_API_URL` when needed.
61
+
62
+ **AI builders (Cursor / Claude Code / Antigravity / Lovable):**
63
+ - Tool-agnostic master prompt: **[docs/LOVABLE-MASTER-PROMPT.md](./docs/LOVABLE-MASTER-PROMPT.md)**
64
+ - Local-first human runbook: **[docs/AI-LOCAL-WORDPRESS-BOOTSTRAP.md](./docs/AI-LOCAL-WORDPRESS-BOOTSTRAP.md)**
65
+
58
66
  ## Developing this package
59
67
 
60
68
  This repo uses **`.npmrc`** with `legacy-peer-deps=true` so `react-helmet-async` installs cleanly alongside **React 19**-compatible transitive deps during local builds. Consumers of the published package only need to satisfy peers (`react`, `react-dom`, `react-helmet-async`).
@@ -66,5 +74,5 @@ Use `node scripts/deploy-secrets.js` after `digivalt-init`.
66
74
  The helper now:
67
75
 
68
76
  - uploads only DigiVAlt-related environment variables (including **`VITE_SEO_*`** and server-only **`TURNSTILE_SECRET_KEY`** when present)
69
- - skips known local-only variables such as `VITE_LOCAL_DEVELOPMENT`
77
+ - skips **`NODE_ENV`**, **`VITE_IS_LOVABLE`**, **`VITE_LOCAL_DEVELOPMENT`**, **`VITE_AUTH_MODE`**, and **`VITE_DEFAULT_MAX_ITEMS`** (set those locally or in the Pages dashboard instead of bulk upload)
70
78
  - fails with a non-zero exit code when Wrangler or project detection fails
@@ -9,6 +9,9 @@ VITE_WORDPRESS_API_URL=https://cms.example.com/wp-json
9
9
  VITE_AUTH_MODE=direct
10
10
  VITE_CLOUDFLARE_WORKER_URL=
11
11
  VITE_LOCAL_DEVELOPMENT=false
12
+ # Optional explicit endpoint for vibune-google-reviews static generation.
13
+ # Defaults to <WORDPRESS_API_URL>/vibune-google-reviews/v1/reviews.
14
+ # VITE_GOOGLE_REVIEWS_API_URL=https://cms.example.com/wp-json/vibune-google-reviews/v1/reviews
12
15
 
13
16
  # Multi-frontend (digivalt-multi-frontends)
14
17
  VITE_FRONTEND_ID=my-app
@@ -0,0 +1,5 @@
1
+ interface GenerateGoogleReviewsOptions {
2
+ outputPath?: string;
3
+ }
4
+ export declare function generateGoogleReviewsData(options?: GenerateGoogleReviewsOptions): Promise<boolean>;
5
+ export {};
@@ -2,4 +2,6 @@ export * from './wp';
2
2
  export * from './gf';
3
3
  export * from './mautic';
4
4
  export * from './suitecrm';
5
+ export * from './google-reviews';
6
+ export * from './sitemap-paths';
5
7
  export declare function generateAllData(): Promise<void>;
@@ -0,0 +1,11 @@
1
+ export type GenerateSitemapPathsOptions = {
2
+ cwd?: string;
3
+ wpDataPath?: string;
4
+ staticInputPath?: string;
5
+ outputPath?: string;
6
+ };
7
+ /**
8
+ * Merge sitemap-static-paths.json, navigation_menu URLs, and CPT/post links from wordpress-data.json.
9
+ */
10
+ export declare function generateSitemapPaths(options?: GenerateSitemapPathsOptions): Promise<void>;
11
+ export declare function generateSitemapPathsSafe(options?: GenerateSitemapPathsOptions): Promise<void>;
@@ -4,6 +4,7 @@ var dotenv = require('dotenv');
4
4
  var path = require('path');
5
5
  var fs = require('fs');
6
6
  var sadapter = require('@marvalt/sadapter');
7
+ var promises = require('fs/promises');
7
8
 
8
9
  /**
9
10
  * @license GPL-3.0-or-later
@@ -2453,6 +2454,302 @@ async function generateSuiteCRMData(options) {
2453
2454
  }
2454
2455
  }
2455
2456
 
2457
+ function trimTrailingSlash(value) {
2458
+ return value.replace(/\/+$/, '');
2459
+ }
2460
+ function resolveEndpointUrl() {
2461
+ const explicit = process.env.GOOGLE_REVIEWS_API_URL ||
2462
+ process.env.VITE_GOOGLE_REVIEWS_API_URL ||
2463
+ '';
2464
+ if (explicit) {
2465
+ return explicit;
2466
+ }
2467
+ const wpApiBase = process.env.WORDPRESS_API_URL ||
2468
+ process.env.VITE_WORDPRESS_API_URL ||
2469
+ '';
2470
+ if (!wpApiBase) {
2471
+ return '';
2472
+ }
2473
+ const normalizedBase = trimTrailingSlash(wpApiBase);
2474
+ if (normalizedBase.endsWith('/wp-json')) {
2475
+ return `${normalizedBase}/vibune-google-reviews/v1/reviews`;
2476
+ }
2477
+ return `${normalizedBase}/wp-json/vibune-google-reviews/v1/reviews`;
2478
+ }
2479
+ function buildHeaders() {
2480
+ const headers = {
2481
+ Accept: 'application/json',
2482
+ };
2483
+ const username = process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME || '';
2484
+ const password = process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD || '';
2485
+ if (username && password) {
2486
+ headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
2487
+ }
2488
+ const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID || '';
2489
+ const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET || '';
2490
+ if (cfAccessClientId && cfAccessClientSecret) {
2491
+ headers['CF-Access-Client-Id'] = cfAccessClientId;
2492
+ headers['CF-Access-Client-Secret'] = cfAccessClientSecret;
2493
+ }
2494
+ return headers;
2495
+ }
2496
+ function hasExpectedShape(payload) {
2497
+ if (!payload || typeof payload !== 'object') {
2498
+ return false;
2499
+ }
2500
+ const obj = payload;
2501
+ return Array.isArray(obj.reviews) && typeof obj.business === 'object' && obj.business !== null;
2502
+ }
2503
+ function truncateBody(body, max = 1200) {
2504
+ if (body.length <= max) {
2505
+ return body;
2506
+ }
2507
+ return `${body.slice(0, max)}...`;
2508
+ }
2509
+ async function generateGoogleReviewsData(options) {
2510
+ const outputPath = options?.outputPath || './public/google-reviews-data.json';
2511
+ const endpointUrl = resolveEndpointUrl();
2512
+ if (!endpointUrl) {
2513
+ console.error('❌ Missing Google Reviews endpoint. Set GOOGLE_REVIEWS_API_URL or VITE_GOOGLE_REVIEWS_API_URL, or configure WORDPRESS_API_URL/VITE_WORDPRESS_API_URL.');
2514
+ return false;
2515
+ }
2516
+ try {
2517
+ const response = await fetch(endpointUrl, {
2518
+ method: 'GET',
2519
+ headers: buildHeaders(),
2520
+ });
2521
+ if (!response.ok) {
2522
+ const body = truncateBody(await response.text());
2523
+ console.error(`❌ Google Reviews generation failed (HTTP ${response.status}): ${body}`);
2524
+ return false;
2525
+ }
2526
+ const payload = (await response.json());
2527
+ if (!hasExpectedShape(payload)) {
2528
+ console.error('❌ Google Reviews generation failed: endpoint returned unexpected payload shape.');
2529
+ return false;
2530
+ }
2531
+ const outputFilePath = path.join(process.cwd(), outputPath);
2532
+ await promises.mkdir(path.dirname(outputFilePath), { recursive: true });
2533
+ await promises.writeFile(outputFilePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
2534
+ console.log(`📁 Google Reviews data written to: ${outputFilePath}`);
2535
+ return true;
2536
+ }
2537
+ catch (e) {
2538
+ console.error('❌ Google Reviews generation failed:', e.message);
2539
+ return false;
2540
+ }
2541
+ }
2542
+
2543
+ const SITEMAP_SKIP_TOP_LEVEL_KEYS = new Set([
2544
+ 'config',
2545
+ 'site_settings',
2546
+ 'header',
2547
+ 'footer',
2548
+ 'theme_styles',
2549
+ 'navigation_menu',
2550
+ 'menus',
2551
+ 'categories',
2552
+ 'tags',
2553
+ 'generated_at',
2554
+ 'frontend_id',
2555
+ 'frontend_name',
2556
+ 'front_page',
2557
+ ]);
2558
+ function normalizePathname(pathname) {
2559
+ const decoded = decodeURIComponent(pathname);
2560
+ if (!decoded || decoded === '/')
2561
+ return '/';
2562
+ const stripped = decoded.replace(/\/+$/, '');
2563
+ return stripped === '' ? '/' : stripped;
2564
+ }
2565
+ function getAllowedHosts() {
2566
+ const hosts = new Set();
2567
+ const api = process.env.VITE_WORDPRESS_API_URL || process.env.WORDPRESS_API_URL;
2568
+ if (api) {
2569
+ try {
2570
+ hosts.add(new URL(api).hostname);
2571
+ }
2572
+ catch {
2573
+ /* ignore */
2574
+ }
2575
+ }
2576
+ return hosts;
2577
+ }
2578
+ /**
2579
+ * When CMS host is unknown, accept any http(s) URL so local/offline merges still produce paths.
2580
+ */
2581
+ function urlToPath(urlStr, allowedHosts) {
2582
+ const t = urlStr.trim();
2583
+ if (!t || t === '#' || t.toLowerCase().startsWith('mailto:') || t.toLowerCase().startsWith('tel:')) {
2584
+ return null;
2585
+ }
2586
+ if (!/^https?:\/\//i.test(t)) {
2587
+ return null;
2588
+ }
2589
+ let u;
2590
+ try {
2591
+ u = new URL(t);
2592
+ }
2593
+ catch {
2594
+ return null;
2595
+ }
2596
+ if (allowedHosts.size > 0 && !allowedHosts.has(u.hostname)) {
2597
+ return null;
2598
+ }
2599
+ return normalizePathname(u.pathname);
2600
+ }
2601
+ function parsePathsJson(raw) {
2602
+ try {
2603
+ const data = JSON.parse(raw);
2604
+ if (Array.isArray(data)) {
2605
+ return data.filter((p) => typeof p === 'string' && p.startsWith('/'));
2606
+ }
2607
+ if (data && typeof data === 'object' && Array.isArray(data.paths)) {
2608
+ return data.paths.filter((p) => typeof p === 'string' && p.startsWith('/'));
2609
+ }
2610
+ }
2611
+ catch {
2612
+ /* ignore */
2613
+ }
2614
+ return [];
2615
+ }
2616
+ function loadStaticPaths(cwd, staticInputPath, allowedHosts) {
2617
+ const full = path.join(cwd, staticInputPath);
2618
+ if (!fs.existsSync(full)) {
2619
+ return [];
2620
+ }
2621
+ const raw = fs.readFileSync(full, 'utf8');
2622
+ const paths = parsePathsJson(raw);
2623
+ const out = [];
2624
+ for (const p of paths) {
2625
+ if (p.startsWith('http://') || p.startsWith('https://')) {
2626
+ const normalized = urlToPath(p, allowedHosts);
2627
+ if (normalized)
2628
+ out.push(normalized);
2629
+ }
2630
+ else {
2631
+ out.push(normalizePathname(p));
2632
+ }
2633
+ }
2634
+ return out;
2635
+ }
2636
+ function walkMenuItems(items, allowedHosts, out) {
2637
+ if (!Array.isArray(items))
2638
+ return;
2639
+ for (const item of items) {
2640
+ if (!item || typeof item !== 'object')
2641
+ continue;
2642
+ const rec = item;
2643
+ if (typeof rec.url === 'string') {
2644
+ const p = urlToPath(rec.url, allowedHosts);
2645
+ if (p)
2646
+ out.add(p);
2647
+ }
2648
+ if (Array.isArray(rec.children)) {
2649
+ walkMenuItems(rec.children, allowedHosts, out);
2650
+ }
2651
+ }
2652
+ }
2653
+ function collectLinksFromArray(arr, allowedHosts, out) {
2654
+ for (const item of arr) {
2655
+ if (!item || typeof item !== 'object')
2656
+ continue;
2657
+ const link = item.link;
2658
+ if (typeof link === 'string') {
2659
+ const p = urlToPath(link, allowedHosts);
2660
+ if (p)
2661
+ out.add(p);
2662
+ }
2663
+ }
2664
+ }
2665
+ function collectPathsFromWpData(data, allowedHosts) {
2666
+ const out = new Set();
2667
+ const menu = data.navigation_menu;
2668
+ if (menu?.items) {
2669
+ walkMenuItems(menu.items, allowedHosts, out);
2670
+ }
2671
+ const config = data.config;
2672
+ const postTypeKeys = config?.post_types && typeof config.post_types === 'object'
2673
+ ? Object.keys(config.post_types)
2674
+ : null;
2675
+ if (postTypeKeys && postTypeKeys.length > 0) {
2676
+ for (const key of postTypeKeys) {
2677
+ const arr = data[key];
2678
+ if (Array.isArray(arr)) {
2679
+ collectLinksFromArray(arr, allowedHosts, out);
2680
+ }
2681
+ }
2682
+ }
2683
+ else {
2684
+ for (const [key, value] of Object.entries(data)) {
2685
+ if (SITEMAP_SKIP_TOP_LEVEL_KEYS.has(key))
2686
+ continue;
2687
+ if (Array.isArray(value)) {
2688
+ collectLinksFromArray(value, allowedHosts, out);
2689
+ }
2690
+ }
2691
+ }
2692
+ return out;
2693
+ }
2694
+ function writeFallback(cwd, outputPath) {
2695
+ const full = path.join(cwd, outputPath);
2696
+ fs.mkdirSync(path.dirname(full), { recursive: true });
2697
+ fs.writeFileSync(full, `${JSON.stringify({ paths: ['/'] }, null, 2)}\n`, 'utf8');
2698
+ }
2699
+ /**
2700
+ * Merge sitemap-static-paths.json, navigation_menu URLs, and CPT/post links from wordpress-data.json.
2701
+ */
2702
+ async function generateSitemapPaths(options = {}) {
2703
+ dotenv.config();
2704
+ dotenv.config({ path: '.env.local' });
2705
+ const cwd = options.cwd ?? process.cwd();
2706
+ const wpDataPath = options.wpDataPath ?? 'public/wordpress-data.json';
2707
+ const staticInputPath = options.staticInputPath ?? 'public/sitemap-static-paths.json';
2708
+ const outputPath = options.outputPath ?? 'public/sitemap-paths.json';
2709
+ const allowedHosts = getAllowedHosts();
2710
+ const merged = new Set();
2711
+ for (const p of loadStaticPaths(cwd, staticInputPath, allowedHosts)) {
2712
+ merged.add(normalizePathname(p));
2713
+ }
2714
+ const wpFull = path.join(cwd, wpDataPath);
2715
+ if (fs.existsSync(wpFull)) {
2716
+ try {
2717
+ const raw = fs.readFileSync(wpFull, 'utf8');
2718
+ const data = JSON.parse(raw);
2719
+ const fromWp = collectPathsFromWpData(data, allowedHosts);
2720
+ for (const p of fromWp) {
2721
+ merged.add(p);
2722
+ }
2723
+ }
2724
+ catch (e) {
2725
+ console.warn('⚠️ Could not parse wordpress-data.json for sitemap paths:', e.message);
2726
+ }
2727
+ }
2728
+ merged.add('/');
2729
+ const sorted = [...merged].sort((a, b) => {
2730
+ if (a === '/')
2731
+ return -1;
2732
+ if (b === '/')
2733
+ return 1;
2734
+ return a.localeCompare(b);
2735
+ });
2736
+ const outFull = path.join(cwd, outputPath);
2737
+ fs.mkdirSync(path.dirname(outFull), { recursive: true });
2738
+ fs.writeFileSync(outFull, `${JSON.stringify({ paths: sorted }, null, 2)}\n`, 'utf8');
2739
+ console.log(`📍 Sitemap paths written (${sorted.length} URLs): ${outFull}`);
2740
+ }
2741
+ async function generateSitemapPathsSafe(options = {}) {
2742
+ try {
2743
+ await generateSitemapPaths(options);
2744
+ }
2745
+ catch (e) {
2746
+ console.error('❌ Sitemap path generation failed; writing minimal / only:', e.message);
2747
+ const cwd = options.cwd ?? process.cwd();
2748
+ const outputPath = options.outputPath ?? 'public/sitemap-paths.json';
2749
+ writeFallback(cwd, outputPath);
2750
+ }
2751
+ }
2752
+
2456
2753
  async function generateAllData() {
2457
2754
  // Load .env files if they exist (for local development)
2458
2755
  dotenv.config();
@@ -2466,16 +2763,21 @@ async function generateAllData() {
2466
2763
  const gfSuccess = await generateGravityFormsData();
2467
2764
  const mauticSuccess = await generateMauticData();
2468
2765
  const suitecrmSuccess = await generateSuiteCRMData();
2469
- if (!wpSuccess || !gfSuccess || !mauticSuccess || !suitecrmSuccess) {
2766
+ const googleReviewsSuccess = await generateGoogleReviewsData();
2767
+ if (!wpSuccess || !gfSuccess || !mauticSuccess || !suitecrmSuccess || !googleReviewsSuccess) {
2470
2768
  console.error('❌ Some generation tasks failed. See above for details.');
2471
2769
  process.exit(1);
2472
2770
  }
2771
+ await generateSitemapPathsSafe();
2473
2772
  console.log('✅ Generation complete!');
2474
2773
  }
2475
2774
 
2476
2775
  exports.generateAllData = generateAllData;
2776
+ exports.generateGoogleReviewsData = generateGoogleReviewsData;
2477
2777
  exports.generateGravityFormsData = generateGravityFormsData;
2478
2778
  exports.generateMauticData = generateMauticData;
2779
+ exports.generateSitemapPaths = generateSitemapPaths;
2780
+ exports.generateSitemapPathsSafe = generateSitemapPathsSafe;
2479
2781
  exports.generateSuiteCRMData = generateSuiteCRMData;
2480
2782
  exports.generateWordPressData = generateWordPressData;
2481
2783
  //# sourceMappingURL=generators.cjs.map