@marvalt/digivalt-core 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +3 -1
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/sitemap-paths.d.ts +11 -0
- package/dist/generators.cjs +213 -0
- package/dist/generators.cjs.map +1 -1
- package/dist/generators.d.ts +14 -1
- package/dist/generators.esm.js +212 -1
- package/dist/generators.esm.js.map +1 -1
- package/dist/index.cjs +215 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +19 -9
- package/dist/index.esm.js +214 -1
- package/dist/index.esm.js.map +1 -1
- package/docs/SEO-PERFORMANCE.md +9 -1
- package/package.json +3 -2
- package/template/DIGIVALT_SETUP.md +1 -1
- package/template/functions/sitemap.xml.ts +1 -1
- package/template/public/sitemap-static-paths.json +3 -0
- package/template/scripts/deploy-secrets.js +9 -7
- package/template/scripts/generate.ts +9 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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.11] - 2026-04-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`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.
|
|
12
|
+
- **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.
|
|
13
|
+
- **`npm run smoke:sitemap`:** fixture test for merged paths.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **`generateAllData`:** runs **`generateSitemapPathsSafe`** after successful static generation.
|
|
17
|
+
- **Docs:** `DIGIVALT_SETUP.md`, `SEO-PERFORMANCE.md`, repo **`AGENTS.md`** (sitemap conventions).
|
|
18
|
+
|
|
19
|
+
## [0.2.10] - 2026-04-03
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **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.
|
|
23
|
+
|
|
8
24
|
## [0.2.9] - 2026-04-03
|
|
9
25
|
|
|
10
26
|
### Added
|
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ 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
|
+
|
|
58
60
|
## Developing this package
|
|
59
61
|
|
|
60
62
|
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 +68,5 @@ Use `node scripts/deploy-secrets.js` after `digivalt-init`.
|
|
|
66
68
|
The helper now:
|
|
67
69
|
|
|
68
70
|
- uploads only DigiVAlt-related environment variables (including **`VITE_SEO_*`** and server-only **`TURNSTILE_SECRET_KEY`** when present)
|
|
69
|
-
- skips
|
|
71
|
+
- 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
72
|
- fails with a non-zero exit code when Wrangler or project detection fails
|
|
@@ -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>;
|
package/dist/generators.cjs
CHANGED
|
@@ -2453,6 +2453,216 @@ async function generateSuiteCRMData(options) {
|
|
|
2453
2453
|
}
|
|
2454
2454
|
}
|
|
2455
2455
|
|
|
2456
|
+
const SITEMAP_SKIP_TOP_LEVEL_KEYS = new Set([
|
|
2457
|
+
'config',
|
|
2458
|
+
'site_settings',
|
|
2459
|
+
'header',
|
|
2460
|
+
'footer',
|
|
2461
|
+
'theme_styles',
|
|
2462
|
+
'navigation_menu',
|
|
2463
|
+
'menus',
|
|
2464
|
+
'categories',
|
|
2465
|
+
'tags',
|
|
2466
|
+
'generated_at',
|
|
2467
|
+
'frontend_id',
|
|
2468
|
+
'frontend_name',
|
|
2469
|
+
'front_page',
|
|
2470
|
+
]);
|
|
2471
|
+
function normalizePathname(pathname) {
|
|
2472
|
+
const decoded = decodeURIComponent(pathname);
|
|
2473
|
+
if (!decoded || decoded === '/')
|
|
2474
|
+
return '/';
|
|
2475
|
+
const stripped = decoded.replace(/\/+$/, '');
|
|
2476
|
+
return stripped === '' ? '/' : stripped;
|
|
2477
|
+
}
|
|
2478
|
+
function getAllowedHosts() {
|
|
2479
|
+
const hosts = new Set();
|
|
2480
|
+
const api = process.env.VITE_WORDPRESS_API_URL || process.env.WORDPRESS_API_URL;
|
|
2481
|
+
if (api) {
|
|
2482
|
+
try {
|
|
2483
|
+
hosts.add(new URL(api).hostname);
|
|
2484
|
+
}
|
|
2485
|
+
catch {
|
|
2486
|
+
/* ignore */
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
return hosts;
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* When CMS host is unknown, accept any http(s) URL so local/offline merges still produce paths.
|
|
2493
|
+
*/
|
|
2494
|
+
function urlToPath(urlStr, allowedHosts) {
|
|
2495
|
+
const t = urlStr.trim();
|
|
2496
|
+
if (!t || t === '#' || t.toLowerCase().startsWith('mailto:') || t.toLowerCase().startsWith('tel:')) {
|
|
2497
|
+
return null;
|
|
2498
|
+
}
|
|
2499
|
+
if (!/^https?:\/\//i.test(t)) {
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2502
|
+
let u;
|
|
2503
|
+
try {
|
|
2504
|
+
u = new URL(t);
|
|
2505
|
+
}
|
|
2506
|
+
catch {
|
|
2507
|
+
return null;
|
|
2508
|
+
}
|
|
2509
|
+
if (allowedHosts.size > 0 && !allowedHosts.has(u.hostname)) {
|
|
2510
|
+
return null;
|
|
2511
|
+
}
|
|
2512
|
+
return normalizePathname(u.pathname);
|
|
2513
|
+
}
|
|
2514
|
+
function parsePathsJson(raw) {
|
|
2515
|
+
try {
|
|
2516
|
+
const data = JSON.parse(raw);
|
|
2517
|
+
if (Array.isArray(data)) {
|
|
2518
|
+
return data.filter((p) => typeof p === 'string' && p.startsWith('/'));
|
|
2519
|
+
}
|
|
2520
|
+
if (data && typeof data === 'object' && Array.isArray(data.paths)) {
|
|
2521
|
+
return data.paths.filter((p) => typeof p === 'string' && p.startsWith('/'));
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
catch {
|
|
2525
|
+
/* ignore */
|
|
2526
|
+
}
|
|
2527
|
+
return [];
|
|
2528
|
+
}
|
|
2529
|
+
function loadStaticPaths(cwd, staticInputPath, allowedHosts) {
|
|
2530
|
+
const full = path.join(cwd, staticInputPath);
|
|
2531
|
+
if (!fs.existsSync(full)) {
|
|
2532
|
+
return [];
|
|
2533
|
+
}
|
|
2534
|
+
const raw = fs.readFileSync(full, 'utf8');
|
|
2535
|
+
const paths = parsePathsJson(raw);
|
|
2536
|
+
const out = [];
|
|
2537
|
+
for (const p of paths) {
|
|
2538
|
+
if (p.startsWith('http://') || p.startsWith('https://')) {
|
|
2539
|
+
const normalized = urlToPath(p, allowedHosts);
|
|
2540
|
+
if (normalized)
|
|
2541
|
+
out.push(normalized);
|
|
2542
|
+
}
|
|
2543
|
+
else {
|
|
2544
|
+
out.push(normalizePathname(p));
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return out;
|
|
2548
|
+
}
|
|
2549
|
+
function walkMenuItems(items, allowedHosts, out) {
|
|
2550
|
+
if (!Array.isArray(items))
|
|
2551
|
+
return;
|
|
2552
|
+
for (const item of items) {
|
|
2553
|
+
if (!item || typeof item !== 'object')
|
|
2554
|
+
continue;
|
|
2555
|
+
const rec = item;
|
|
2556
|
+
if (typeof rec.url === 'string') {
|
|
2557
|
+
const p = urlToPath(rec.url, allowedHosts);
|
|
2558
|
+
if (p)
|
|
2559
|
+
out.add(p);
|
|
2560
|
+
}
|
|
2561
|
+
if (Array.isArray(rec.children)) {
|
|
2562
|
+
walkMenuItems(rec.children, allowedHosts, out);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function collectLinksFromArray(arr, allowedHosts, out) {
|
|
2567
|
+
for (const item of arr) {
|
|
2568
|
+
if (!item || typeof item !== 'object')
|
|
2569
|
+
continue;
|
|
2570
|
+
const link = item.link;
|
|
2571
|
+
if (typeof link === 'string') {
|
|
2572
|
+
const p = urlToPath(link, allowedHosts);
|
|
2573
|
+
if (p)
|
|
2574
|
+
out.add(p);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
function collectPathsFromWpData(data, allowedHosts) {
|
|
2579
|
+
const out = new Set();
|
|
2580
|
+
const menu = data.navigation_menu;
|
|
2581
|
+
if (menu?.items) {
|
|
2582
|
+
walkMenuItems(menu.items, allowedHosts, out);
|
|
2583
|
+
}
|
|
2584
|
+
const config = data.config;
|
|
2585
|
+
const postTypeKeys = config?.post_types && typeof config.post_types === 'object'
|
|
2586
|
+
? Object.keys(config.post_types)
|
|
2587
|
+
: null;
|
|
2588
|
+
if (postTypeKeys && postTypeKeys.length > 0) {
|
|
2589
|
+
for (const key of postTypeKeys) {
|
|
2590
|
+
const arr = data[key];
|
|
2591
|
+
if (Array.isArray(arr)) {
|
|
2592
|
+
collectLinksFromArray(arr, allowedHosts, out);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
else {
|
|
2597
|
+
for (const [key, value] of Object.entries(data)) {
|
|
2598
|
+
if (SITEMAP_SKIP_TOP_LEVEL_KEYS.has(key))
|
|
2599
|
+
continue;
|
|
2600
|
+
if (Array.isArray(value)) {
|
|
2601
|
+
collectLinksFromArray(value, allowedHosts, out);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
return out;
|
|
2606
|
+
}
|
|
2607
|
+
function writeFallback(cwd, outputPath) {
|
|
2608
|
+
const full = path.join(cwd, outputPath);
|
|
2609
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
2610
|
+
fs.writeFileSync(full, `${JSON.stringify({ paths: ['/'] }, null, 2)}\n`, 'utf8');
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Merge sitemap-static-paths.json, navigation_menu URLs, and CPT/post links from wordpress-data.json.
|
|
2614
|
+
*/
|
|
2615
|
+
async function generateSitemapPaths(options = {}) {
|
|
2616
|
+
dotenv.config();
|
|
2617
|
+
dotenv.config({ path: '.env.local' });
|
|
2618
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2619
|
+
const wpDataPath = options.wpDataPath ?? 'public/wordpress-data.json';
|
|
2620
|
+
const staticInputPath = options.staticInputPath ?? 'public/sitemap-static-paths.json';
|
|
2621
|
+
const outputPath = options.outputPath ?? 'public/sitemap-paths.json';
|
|
2622
|
+
const allowedHosts = getAllowedHosts();
|
|
2623
|
+
const merged = new Set();
|
|
2624
|
+
for (const p of loadStaticPaths(cwd, staticInputPath, allowedHosts)) {
|
|
2625
|
+
merged.add(normalizePathname(p));
|
|
2626
|
+
}
|
|
2627
|
+
const wpFull = path.join(cwd, wpDataPath);
|
|
2628
|
+
if (fs.existsSync(wpFull)) {
|
|
2629
|
+
try {
|
|
2630
|
+
const raw = fs.readFileSync(wpFull, 'utf8');
|
|
2631
|
+
const data = JSON.parse(raw);
|
|
2632
|
+
const fromWp = collectPathsFromWpData(data, allowedHosts);
|
|
2633
|
+
for (const p of fromWp) {
|
|
2634
|
+
merged.add(p);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
catch (e) {
|
|
2638
|
+
console.warn('⚠️ Could not parse wordpress-data.json for sitemap paths:', e.message);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
merged.add('/');
|
|
2642
|
+
const sorted = [...merged].sort((a, b) => {
|
|
2643
|
+
if (a === '/')
|
|
2644
|
+
return -1;
|
|
2645
|
+
if (b === '/')
|
|
2646
|
+
return 1;
|
|
2647
|
+
return a.localeCompare(b);
|
|
2648
|
+
});
|
|
2649
|
+
const outFull = path.join(cwd, outputPath);
|
|
2650
|
+
fs.mkdirSync(path.dirname(outFull), { recursive: true });
|
|
2651
|
+
fs.writeFileSync(outFull, `${JSON.stringify({ paths: sorted }, null, 2)}\n`, 'utf8');
|
|
2652
|
+
console.log(`📍 Sitemap paths written (${sorted.length} URLs): ${outFull}`);
|
|
2653
|
+
}
|
|
2654
|
+
async function generateSitemapPathsSafe(options = {}) {
|
|
2655
|
+
try {
|
|
2656
|
+
await generateSitemapPaths(options);
|
|
2657
|
+
}
|
|
2658
|
+
catch (e) {
|
|
2659
|
+
console.error('❌ Sitemap path generation failed; writing minimal / only:', e.message);
|
|
2660
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2661
|
+
const outputPath = options.outputPath ?? 'public/sitemap-paths.json';
|
|
2662
|
+
writeFallback(cwd, outputPath);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2456
2666
|
async function generateAllData() {
|
|
2457
2667
|
// Load .env files if they exist (for local development)
|
|
2458
2668
|
dotenv.config();
|
|
@@ -2470,12 +2680,15 @@ async function generateAllData() {
|
|
|
2470
2680
|
console.error('❌ Some generation tasks failed. See above for details.');
|
|
2471
2681
|
process.exit(1);
|
|
2472
2682
|
}
|
|
2683
|
+
await generateSitemapPathsSafe();
|
|
2473
2684
|
console.log('✅ Generation complete!');
|
|
2474
2685
|
}
|
|
2475
2686
|
|
|
2476
2687
|
exports.generateAllData = generateAllData;
|
|
2477
2688
|
exports.generateGravityFormsData = generateGravityFormsData;
|
|
2478
2689
|
exports.generateMauticData = generateMauticData;
|
|
2690
|
+
exports.generateSitemapPaths = generateSitemapPaths;
|
|
2691
|
+
exports.generateSitemapPathsSafe = generateSitemapPathsSafe;
|
|
2479
2692
|
exports.generateSuiteCRMData = generateSuiteCRMData;
|
|
2480
2693
|
exports.generateWordPressData = generateWordPressData;
|
|
2481
2694
|
//# sourceMappingURL=generators.cjs.map
|