@reldens/cms 0.50.0 → 0.52.0

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.
@@ -0,0 +1,511 @@
1
+ # Sitemap Generator Guide
2
+
3
+ ## Overview
4
+
5
+ The Sitemap Generator is a CLI tool and service class that automatically generates sitemap.xml files from enabled CMS routes. It supports multi-domain configurations and creates both domain-specific sitemaps and a sitemap index file.
6
+
7
+ ## Key Features
8
+
9
+ - Loads enabled routes from `routes` table
10
+ - Generates sitemap.xml per domain
11
+ - Creates sitemap index pointing to all domain sitemaps
12
+ - Supports optional single-domain generation
13
+ - Uses domain public URL mapping for correct URLs
14
+ - Includes route metadata (lastmod from updated_at)
15
+ - XML generation via template strings with placeholders
16
+
17
+ ## CLI Command Usage
18
+
19
+ ### Basic Usage
20
+
21
+ ```bash
22
+ # Generate sitemaps for all domains
23
+ npx reldens-cms-generate-sitemap
24
+
25
+ # Generate sitemap for specific domain only
26
+ npx reldens-cms-generate-sitemap --domain=example.com
27
+
28
+ # Show help
29
+ npx reldens-cms-generate-sitemap --help
30
+ npx reldens-cms-generate-sitemap -h
31
+ ```
32
+
33
+ ### Options
34
+
35
+ - `--domain=[domain]` - Generate sitemap for specific domain only
36
+ - `--help` or `-h` - Show help message
37
+
38
+ ### Requirements
39
+
40
+ - CMS must be installed (install.lock exists)
41
+ - Database must be accessible
42
+ - Routes entity must be available
43
+ - Generated entities must exist (run `npx reldens-cms-generate-entities` first)
44
+
45
+ ## Generated File Structure
46
+
47
+ ```
48
+ [project-root]/
49
+ ├── public/
50
+ │ ├── sitemap.xml # Sitemap index (all domains)
51
+ │ └── sitemap/
52
+ │ ├── example.com/
53
+ │ │ └── sitemap.xml # Domain-specific sitemap
54
+ │ ├── dev.example.com/
55
+ │ │ └── sitemap.xml
56
+ │ └── another-domain.com/
57
+ │ └── sitemap.xml
58
+ ```
59
+
60
+ ### Sitemap Index Format
61
+
62
+ Located at `public/sitemap.xml`:
63
+
64
+ ```xml
65
+ <?xml version="1.0" encoding="UTF-8"?>
66
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
67
+ <sitemap>
68
+ <loc>https://example.com/sitemap/example.com/sitemap.xml</loc>
69
+ </sitemap>
70
+ <sitemap>
71
+ <loc>https://dev.example.com/sitemap/dev.example.com/sitemap.xml</loc>
72
+ </sitemap>
73
+ </sitemapindex>
74
+ ```
75
+
76
+ ### Domain Sitemap Format
77
+
78
+ Located at `public/sitemap/[domain]/sitemap.xml`:
79
+
80
+ ```xml
81
+ <?xml version="1.0" encoding="UTF-8"?>
82
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
83
+ <url>
84
+ <loc>https://example.com/</loc>
85
+ <lastmod>2025-01-15</lastmod>
86
+ </url>
87
+ <url>
88
+ <loc>https://example.com/about</loc>
89
+ <lastmod>2025-01-14</lastmod>
90
+ </url>
91
+ <url>
92
+ <loc>https://example.com/contact</loc>
93
+ <lastmod>2025-01-13</lastmod>
94
+ </url>
95
+ </urlset>
96
+ ```
97
+
98
+ ## SitemapGenerator Class
99
+
100
+ ### Location
101
+
102
+ `lib/sitemap-generator.js`
103
+
104
+ ### Constructor Parameters
105
+
106
+ ```javascript
107
+ new SitemapGenerator({
108
+ dataServer, // Required - DataServer instance
109
+ projectRoot, // Required - Project root path
110
+ defaultDomain, // Optional - Default domain for routes without domain
111
+ domainMapping, // Optional - Domain to site key mapping
112
+ domainPublicUrlMapping // Optional - Domain to public URL mapping
113
+ })
114
+ ```
115
+
116
+ ### Key Methods
117
+
118
+ **generate(specificDomain = null)**
119
+ - Main entry point for sitemap generation
120
+ - Loads enabled routes from database
121
+ - Filters routes by domain if specified
122
+ - Groups routes by domain
123
+ - Generates sitemap files
124
+ - Generates sitemap index (if no specific domain)
125
+ - Returns boolean success status
126
+
127
+ **loadEnabledRoutes()**
128
+ - Queries `routes` entity with `{enabled: 1}` filter
129
+ - Returns array of route objects or false on error
130
+ - Logs critical errors if routes entity not found
131
+
132
+ **groupRoutesByDomain(routes)**
133
+ - Groups routes by domain field
134
+ - Uses `defaultDomain` for routes without domain
135
+ - Returns object: `{domain: [routes]}`
136
+
137
+ **buildSitemapXml(routes, domain)**
138
+ - Generates sitemap XML string for routes array
139
+ - Builds URL entries with loc and lastmod tags
140
+ - Uses `sc.sanitize()` for XML escaping
141
+ - Uses `sc.formatDate(new Date(route.updated_at), 'Y-m-d')` for date formatting
142
+ - Returns complete XML string with template placeholders replaced
143
+
144
+ **buildSitemapIndexXml(domainRoutes)**
145
+ - Generates sitemap index XML string
146
+ - Creates sitemap entries pointing to domain sitemaps
147
+ - Uses `sc.sanitize()` for XML escaping
148
+ - Returns complete XML string with template placeholders replaced
149
+
150
+ **buildUrl(domain, path)**
151
+ - Constructs full URL from domain and path
152
+ - Uses `domainPublicUrlMapping` for base URL
153
+ - Defaults to `http://[domain]` if no mapping exists
154
+ - Ensures proper slash handling (removes trailing, adds leading)
155
+
156
+ **saveSitemaps(domainRoutes)**
157
+ - Creates domain-specific directories under `public/sitemap/`
158
+ - Writes sitemap.xml for each domain
159
+ - Uses `FileHandler.createFolder()` and `FileHandler.writeFile()`
160
+ - Returns false if any operation fails
161
+
162
+ **saveSitemapIndex(domainRoutes)**
163
+ - Creates `public/sitemap/` directory if needed
164
+ - Writes sitemap index to `public/sitemap.xml`
165
+ - Returns false if operation fails
166
+
167
+ **getSitemapTemplate()**
168
+ - Returns XML template string with `{{URLS}}` placeholder
169
+ - Used by `buildSitemapXml()`
170
+
171
+ **getSitemapIndexTemplate()**
172
+ - Returns XML template string with `{{SITEMAPS}}` placeholder
173
+ - Used by `buildSitemapIndexXml()`
174
+
175
+ ## XML Template Approach
176
+
177
+ The generator uses simple template strings with placeholders instead of file-based templates or raw string concatenation.
178
+
179
+ ### Template Methods
180
+
181
+ ```javascript
182
+ getSitemapTemplate()
183
+ {
184
+ return '<?xml version="1.0" encoding="UTF-8"?>\n'
185
+ +'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
186
+ +'{{URLS}}\n'
187
+ +'</urlset>\n';
188
+ }
189
+
190
+ getSitemapIndexTemplate()
191
+ {
192
+ return '<?xml version="1.0" encoding="UTF-8"?>\n'
193
+ +'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
194
+ +'{{SITEMAPS}}\n'
195
+ +'</sitemapindex>\n';
196
+ }
197
+ ```
198
+
199
+ ### Placeholder Replacement
200
+
201
+ - `{{URLS}}` - Replaced with array of URL entries joined by newlines
202
+ - `{{SITEMAPS}}` - Replaced with array of sitemap entries joined by newlines
203
+
204
+ ### Entry Generation
205
+
206
+ ```javascript
207
+ // URL entry
208
+ let urlEntry = ' <url>\n';
209
+ urlEntry += ' <loc>'+sc.sanitize(this.buildUrl(domain, route.path))+'</loc>\n';
210
+ if(route.updated_at){
211
+ urlEntry += ' <lastmod>'+sc.formatDate(new Date(route.updated_at), 'Y-m-d')+'</lastmod>\n';
212
+ }
213
+ urlEntry += ' </url>';
214
+
215
+ // Sitemap entry
216
+ let sitemapEntry = ' <sitemap>\n';
217
+ sitemapEntry += ' <loc>'+sc.sanitize(this.buildUrl(domain, '/sitemap/'+domain+'/sitemap.xml'))+'</loc>\n';
218
+ sitemapEntry += ' </sitemap>';
219
+ ```
220
+
221
+ ## Multi-Domain Support
222
+
223
+ ### Configuration
224
+
225
+ Multi-domain support relies on Manager configuration passed to SitemapGenerator:
226
+
227
+ ```javascript
228
+ // In Manager
229
+ this.defaultDomain = process.env.RELDENS_DEFAULT_DOMAIN || '';
230
+ this.domainMapping = this.parseDomainMapping(process.env.RELDENS_DOMAIN_MAPPING || '{}');
231
+ this.domainPublicUrlMapping = this.parseDomainMapping(process.env.RELDENS_DOMAIN_PUBLIC_URL_MAPPING || '{}');
232
+
233
+ // Passed to SitemapGenerator
234
+ let sitemapGenerator = new SitemapGenerator({
235
+ dataServer: manager.dataServer,
236
+ projectRoot: this.projectRoot,
237
+ defaultDomain: manager.defaultDomain,
238
+ domainMapping: manager.domainMapping,
239
+ domainPublicUrlMapping: manager.domainPublicUrlMapping
240
+ });
241
+ ```
242
+
243
+ ### Environment Variables
244
+
245
+ ```bash
246
+ # .env
247
+ RELDENS_DEFAULT_DOMAIN=example.com
248
+ RELDENS_DOMAIN_MAPPING={"dev.example.com":"development","staging.example.com":"staging"}
249
+ RELDENS_DOMAIN_PUBLIC_URL_MAPPING={"example.com":"https://www.example.com","dev.example.com":"https://dev.example.com"}
250
+ ```
251
+
252
+ ### Domain Resolution
253
+
254
+ 1. Route has `domain` field → use route.domain
255
+ 2. Route has no domain → use `defaultDomain`
256
+ 3. No defaultDomain → use 'default'
257
+
258
+ ### URL Generation
259
+
260
+ 1. Check `domainPublicUrlMapping` for domain
261
+ 2. Use mapped URL if exists
262
+ 3. Default to `http://[domain]` if no mapping
263
+
264
+ ## Integration with Routes Entity
265
+
266
+ ### Routes Table Structure
267
+
268
+ Required fields for sitemap generation:
269
+
270
+ - `id` - Primary key
271
+ - `path` - Route path (e.g., '/', '/about', '/contact')
272
+ - `enabled` - Boolean flag (1 = enabled, 0 = disabled)
273
+ - `domain` - Optional domain assignment
274
+ - `updated_at` - Optional timestamp for lastmod
275
+
276
+ ### Query
277
+
278
+ ```javascript
279
+ let routes = await routesEntity.load({enabled: 1});
280
+ ```
281
+
282
+ Only enabled routes are included in sitemaps. Disabled routes are excluded.
283
+
284
+ ### Path Format
285
+
286
+ - Paths should start with '/' (added automatically if missing)
287
+ - Full URLs constructed: `https://example.com/path`
288
+
289
+ ## CLI Implementation
290
+
291
+ ### Location
292
+
293
+ `bin/reldens-cms-generate-sitemap.js`
294
+
295
+ ### Class Structure
296
+
297
+ **CmsSitemapGenerator**
298
+ - Constructor: Parses command line arguments
299
+ - `parseArguments()` - Extracts config from process.argv
300
+ - `shouldShowHelp()` - Checks for help flags
301
+ - `showHelp()` - Displays usage information
302
+ - `run()` - Main execution flow
303
+
304
+ ### Execution Flow
305
+
306
+ 1. Check for help flag → show help and exit
307
+ 2. Extract domain parameter (if provided)
308
+ 3. Load .env file from project root
309
+ 4. Detect storage driver from environment
310
+ 5. Load entities using EntitiesLoader
311
+ 6. Create Manager instance with loaded entities
312
+ 7. Verify CMS installation
313
+ 8. Initialize DataServer
314
+ 9. Create SitemapGenerator instance
315
+ 10. Call `generate(domain)` method
316
+ 11. Log success/failure and exit
317
+
318
+ ### Dependencies
319
+
320
+ ```javascript
321
+ const { Manager } = require('../index');
322
+ const { SitemapGenerator } = require('../lib/sitemap-generator');
323
+ const { EntitiesLoader } = require('../lib/entities-loader');
324
+ const { PrismaClientLoader } = require('@reldens/storage');
325
+ const { Logger, sc } = require('@reldens/utils');
326
+ const { FileHandler } = require('@reldens/server-utils');
327
+ const dotenv = require('dotenv');
328
+ ```
329
+
330
+ ## Programmatic Usage
331
+
332
+ ### Basic Example
333
+
334
+ ```javascript
335
+ const { Manager } = require('@reldens/cms');
336
+ const { SitemapGenerator } = require('@reldens/cms/lib/sitemap-generator');
337
+
338
+ let manager = new Manager({projectRoot: process.cwd()});
339
+ await manager.initializeDataServer();
340
+
341
+ let sitemapGenerator = new SitemapGenerator({
342
+ dataServer: manager.dataServer,
343
+ projectRoot: process.cwd(),
344
+ defaultDomain: manager.defaultDomain,
345
+ domainMapping: manager.domainMapping,
346
+ domainPublicUrlMapping: manager.domainPublicUrlMapping
347
+ });
348
+
349
+ // Generate all sitemaps
350
+ let success = await sitemapGenerator.generate();
351
+
352
+ // Generate for specific domain
353
+ let successSingle = await sitemapGenerator.generate('example.com');
354
+ ```
355
+
356
+ ### Custom Integration
357
+
358
+ ```javascript
359
+ // In your application after CMS startup
360
+ let manager = require('./path/to/cms-manager-instance');
361
+ let { SitemapGenerator } = require('@reldens/cms/lib/sitemap-generator');
362
+
363
+ // Generate sitemaps on schedule (e.g., daily cron job)
364
+ async function regenerateSitemaps() {
365
+ let generator = new SitemapGenerator({
366
+ dataServer: manager.dataServer,
367
+ projectRoot: manager.projectRoot,
368
+ defaultDomain: manager.defaultDomain,
369
+ domainMapping: manager.domainMapping,
370
+ domainPublicUrlMapping: manager.domainPublicUrlMapping
371
+ });
372
+
373
+ let success = await generator.generate();
374
+ if(success){
375
+ console.log('Sitemaps regenerated successfully');
376
+ }
377
+ }
378
+
379
+ // Regenerate on route changes
380
+ manager.eventsManager.on('reldens.routeSaved', async () => {
381
+ await regenerateSitemaps();
382
+ });
383
+ ```
384
+
385
+ ## Error Handling
386
+
387
+ ### Common Errors
388
+
389
+ **DataServer not provided**
390
+ ```
391
+ CRITICAL: DataServer is required for sitemap generation.
392
+ ```
393
+ Solution: Ensure DataServer is initialized and passed to constructor
394
+
395
+ **Routes entity not found**
396
+ ```
397
+ CRITICAL: Routes entity not found in dataServer.
398
+ ```
399
+ Solution: Run `npx reldens-cms-generate-entities` to generate entities
400
+
401
+ **Failed to load routes**
402
+ ```
403
+ CRITICAL: Failed to load routes: [error message]
404
+ ```
405
+ Solution: Check database connection and routes table existence
406
+
407
+ **Directory creation failed**
408
+ ```
409
+ CRITICAL: Failed to create sitemap directory: [path]
410
+ ```
411
+ Solution: Check filesystem permissions for public folder
412
+
413
+ **File write failed**
414
+ ```
415
+ CRITICAL: Failed to write sitemap: [path]
416
+ ```
417
+ Solution: Check filesystem permissions and disk space
418
+
419
+ **No enabled routes**
420
+ ```
421
+ WARNING: No enabled routes found for sitemap generation.
422
+ ```
423
+ Not an error - returns true but no files generated
424
+
425
+ **No routes for specific domain**
426
+ ```
427
+ WARNING: No routes found for domain: [domain]
428
+ ```
429
+ Not an error - returns true but no files generated for that domain
430
+
431
+ ### Debug Logging
432
+
433
+ Set environment variable for detailed logging:
434
+
435
+ ```bash
436
+ RELDENS_LOG_LEVEL=9 npx reldens-cms-generate-sitemap
437
+ ```
438
+
439
+ ## Best Practices
440
+
441
+ 1. **Regenerate on Route Changes**: Set up event listeners to regenerate sitemaps when routes are created, updated, or deleted
442
+ 2. **Schedule Regular Generation**: Run CLI command daily or weekly via cron job to ensure sitemaps stay current
443
+ 3. **Use Domain Mapping**: Configure `RELDENS_DOMAIN_PUBLIC_URL_MAPPING` for production URLs (especially for CDN or www subdomain)
444
+ 4. **Submit to Search Engines**: Submit sitemap index URL to Google Search Console and Bing Webmaster Tools
445
+ 5. **Monitor File Sizes**: Large sitemaps (>50k URLs) should be split - consider filtering by category or date
446
+ 6. **Set updated_at Properly**: Ensure routes table has accurate updated_at timestamps for lastmod accuracy
447
+ 7. **Enable Only Public Routes**: Use enabled=1 flag to control which routes appear in sitemaps
448
+
449
+ ## SEO Integration
450
+
451
+ ### Robots.txt
452
+
453
+ Add sitemap reference to robots.txt:
454
+
455
+ ```
456
+ User-agent: *
457
+ Allow: /
458
+
459
+ Sitemap: https://example.com/sitemap.xml
460
+ ```
461
+
462
+ ### Search Console
463
+
464
+ 1. Go to Google Search Console
465
+ 2. Add property for your domain
466
+ 3. Navigate to Sitemaps section
467
+ 4. Submit: `https://example.com/sitemap.xml`
468
+ 5. Monitor indexing status
469
+
470
+ ### Multi-Domain SEO
471
+
472
+ Each domain gets its own sitemap, referenced in the index:
473
+
474
+ ```xml
475
+ <sitemapindex>
476
+ <sitemap>
477
+ <loc>https://example.com/sitemap/example.com/sitemap.xml</loc>
478
+ </sitemap>
479
+ <sitemap>
480
+ <loc>https://blog.example.com/sitemap/blog.example.com/sitemap.xml</loc>
481
+ </sitemap>
482
+ </sitemapindex>
483
+ ```
484
+
485
+ Submit the index URL to search engines, or submit individual domain sitemaps to domain-specific Search Console properties.
486
+
487
+ ## Troubleshooting
488
+
489
+ **Issue**: Sitemaps not updating
490
+ - Solution: Check if routes have `enabled=1` flag
491
+ - Solution: Verify `updated_at` field is being updated on route changes
492
+ - Solution: Delete existing sitemaps and regenerate
493
+
494
+ **Issue**: Wrong URLs in sitemap
495
+ - Solution: Check `RELDENS_DOMAIN_PUBLIC_URL_MAPPING` configuration
496
+ - Solution: Verify route `domain` field matches configured domains
497
+ - Solution: Check `defaultDomain` setting
498
+
499
+ **Issue**: CLI command not found
500
+ - Solution: Run `npm install` to ensure bin scripts are linked
501
+ - Solution: Use `npx reldens-cms-generate-sitemap` instead of direct path
502
+
503
+ **Issue**: Permission denied errors
504
+ - Solution: Check filesystem permissions on public folder
505
+ - Solution: Run command with appropriate user permissions
506
+ - Solution: Ensure public/sitemap directories are writable
507
+
508
+ **Issue**: Empty sitemaps generated
509
+ - Solution: Check if routes exist with `enabled=1`
510
+ - Solution: Verify database connection
511
+ - Solution: Check entity generation completed successfully
package/CLAUDE.md CHANGED
@@ -28,10 +28,16 @@ npx reldens-cms-generate-entities
28
28
  # Update user password via CLI
29
29
  npx reldens-cms-update-password --email=admin@example.com
30
30
 
31
+ # Generate sitemap.xml files from routes
32
+ npx reldens-cms-generate-sitemap
33
+ npx reldens-cms-generate-sitemap --domain=example.com
34
+
31
35
  # Or via npm scripts
32
36
  npm run generate-entities
33
37
  ```
34
38
 
39
+ See `.claude/sitemap-generator-guide.md` for comprehensive sitemap generation documentation.
40
+
35
41
  ## Password Management
36
42
 
37
43
  ### CLI Password Update Command
@@ -719,4 +725,8 @@ See `.claude/advanced-usage-guide.md` for detailed examples.
719
725
 
720
726
  - Main README.md for user documentation
721
727
  - Package.json for dependency versions
728
+ - `.claude/sitemap-generator-guide.md` - Sitemap generation documentation
729
+ - `.claude/password-management-guide.md` - Password management documentation
730
+ - `.claude/templating-system-guide.md` - Template system documentation
731
+ - `.claude/advanced-usage-guide.md` - Advanced customization patterns
722
732
  - License: MIT
@@ -1,5 +1,8 @@
1
1
  <fieldset class="cms-page-route">
2
2
  <legend>Route data</legend>
3
+ {{#originalRouteId}}
4
+ <input type="hidden" name="originalRouteId" value="{{originalRouteId}}"/>
5
+ {{/originalRouteId}}
3
6
  <div class="edit-field">
4
7
  <span class="field-name">Path</span>
5
8
  <span class="field-value">
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  *
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  const { Manager } = require('../index');
10
+ const { PrismaClientLoader } = require('@reldens/storage');
10
11
  const { Logger, sc } = require('@reldens/utils');
11
12
  const { FileHandler } = require('@reldens/server-utils');
12
13
  const readline = require('readline');
@@ -108,7 +109,7 @@ class CmsEntitiesGenerator
108
109
  }
109
110
  let managerConfig = {projectRoot: this.projectRoot};
110
111
  if('prisma' === this.driver){
111
- let prismaClient = await this.loadPrismaClient();
112
+ let prismaClient = PrismaClientLoader.load(this.projectRoot, this.prismaClientPath, null);
112
113
  if(prismaClient){
113
114
  managerConfig.prismaClient = prismaClient;
114
115
  }
@@ -134,34 +135,6 @@ class CmsEntitiesGenerator
134
135
  return true;
135
136
  }
136
137
 
137
- async loadPrismaClient()
138
- {
139
- let clientPath = this.prismaClientPath;
140
- if(!clientPath){
141
- return false;
142
- }
143
- let resolvedPath = clientPath.startsWith('./')
144
- ? FileHandler.joinPaths(process.cwd(), clientPath.substring(2))
145
- : clientPath;
146
- if(!FileHandler.exists(resolvedPath)){
147
- Logger.error('Prisma client not found at: '+resolvedPath);
148
- return false;
149
- }
150
- try {
151
- let PrismaClientModule = require(resolvedPath);
152
- let PrismaClient = PrismaClientModule.PrismaClient || PrismaClientModule.default?.PrismaClient;
153
- if(!PrismaClient){
154
- Logger.error('PrismaClient not found in module: '+resolvedPath);
155
- return false;
156
- }
157
- Logger.debug('Prisma client loaded from: '+resolvedPath);
158
- return new PrismaClient();
159
- } catch (error) {
160
- Logger.error('Failed to load Prisma client from '+resolvedPath+': '+error.message);
161
- return false;
162
- }
163
- }
164
-
165
138
  async confirmOverride()
166
139
  {
167
140
  let rl = readline.createInterface({
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ *
5
+ * Reldens - CMS - Generate Sitemap CLI
6
+ *
7
+ */
8
+
9
+ const { Manager } = require('../index');
10
+ const { SitemapGenerator } = require('../lib/sitemap-generator');
11
+ const { EntitiesLoader } = require('../lib/entities-loader');
12
+ const { PrismaClientLoader } = require('@reldens/storage');
13
+ const { Logger, sc } = require('@reldens/utils');
14
+ const { FileHandler } = require('@reldens/server-utils');
15
+ const dotenv = require('dotenv');
16
+
17
+ class CmsSitemapGenerator
18
+ {
19
+
20
+ constructor()
21
+ {
22
+ this.args = process.argv.slice(2);
23
+ this.projectRoot = process.cwd();
24
+ this.config = {};
25
+ this.parseArguments();
26
+ }
27
+
28
+ parseArguments()
29
+ {
30
+ for(let i = 0; i < this.args.length; i++){
31
+ let arg = this.args[i];
32
+ if(!arg.startsWith('--')){
33
+ continue;
34
+ }
35
+ let equalIndex = arg.indexOf('=');
36
+ if(-1 === equalIndex){
37
+ let flag = arg.substring(2);
38
+ if('help' === flag || 'h' === flag){
39
+ this.config[flag] = true;
40
+ }
41
+ continue;
42
+ }
43
+ this.config[arg.substring(2, equalIndex)] = arg.substring(equalIndex+1);
44
+ }
45
+ }
46
+
47
+ shouldShowHelp()
48
+ {
49
+ return sc.get(this.config, 'help', false) || sc.get(this.config, 'h', false);
50
+ }
51
+
52
+ showHelp()
53
+ {
54
+ Logger.info('');
55
+ Logger.info('Reldens CMS Sitemap Generator');
56
+ Logger.info('=============================');
57
+ Logger.info('');
58
+ Logger.info('Usage: npx reldens-cms-generate-sitemap [options]');
59
+ Logger.info('');
60
+ Logger.info('Options:');
61
+ Logger.info(' --domain=[domain] Generate sitemap for specific domain');
62
+ Logger.info(' --help, -h Show this help message');
63
+ Logger.info('');
64
+ Logger.info('Examples:');
65
+ Logger.info(' npx reldens-cms-generate-sitemap');
66
+ Logger.info(' npx reldens-cms-generate-sitemap --domain=example.com');
67
+ Logger.info(' npx reldens-cms-generate-sitemap --help');
68
+ Logger.info('');
69
+ Logger.info('Note: Generates sitemap.xml files from enabled CMS routes.');
70
+ Logger.info('Note: Files are saved to public/sitemap/[domain]/sitemap.xml');
71
+ Logger.info('Note: A sitemap index is created at public/sitemap.xml');
72
+ Logger.info('');
73
+ }
74
+
75
+ async run()
76
+ {
77
+ if(this.shouldShowHelp()){
78
+ this.showHelp();
79
+ return true;
80
+ }
81
+ let domain = sc.get(this.config, 'domain', null);
82
+ let envFilePath = FileHandler.joinPaths(this.projectRoot, '.env');
83
+ dotenv.config({path: envFilePath});
84
+ let storageDriver = process.env.RELDENS_STORAGE_DRIVER || 'prisma';
85
+ Logger.debug('Using storage driver: '+storageDriver);
86
+ let entitiesLoader = new EntitiesLoader({projectRoot: this.projectRoot});
87
+ let loadedEntities = entitiesLoader.loadEntities(storageDriver);
88
+ if(!loadedEntities || !loadedEntities.rawRegisteredEntities){
89
+ Logger.error('Failed to load entities for driver: '+storageDriver);
90
+ Logger.error('Make sure you have run "npx reldens-cms-generate-entities" first.');
91
+ return false;
92
+ }
93
+ Logger.debug('Loaded entities for driver: '+storageDriver);
94
+ let managerConfig = {
95
+ projectRoot: this.projectRoot,
96
+ rawRegisteredEntities: loadedEntities.rawRegisteredEntities,
97
+ entitiesConfig: loadedEntities.entitiesConfig,
98
+ entitiesTranslations: loadedEntities.entitiesTranslations
99
+ };
100
+ if('prisma' === storageDriver){
101
+ let prismaClient = PrismaClientLoader.load(this.projectRoot, null, null);
102
+ if(prismaClient){
103
+ managerConfig.prismaClient = prismaClient;
104
+ Logger.debug('Prisma client loaded and configured.');
105
+ }
106
+ }
107
+ let manager = new Manager(managerConfig);
108
+ if(!manager.isInstalled()){
109
+ Logger.error('CMS is not installed. Please run installation first.');
110
+ return false;
111
+ }
112
+ Logger.debug('Reldens CMS Manager instance created for sitemap generation.');
113
+ if(!await manager.initializeDataServer()){
114
+ Logger.error('Failed to initialize data server.');
115
+ return false;
116
+ }
117
+ Logger.debug('Data server initialized successfully.');
118
+ let sitemapGenerator = new SitemapGenerator({
119
+ dataServer: manager.dataServer,
120
+ projectRoot: this.projectRoot,
121
+ defaultDomain: manager.defaultDomain,
122
+ domainMapping: manager.domainMapping,
123
+ domainPublicUrlMapping: manager.domainPublicUrlMapping
124
+ });
125
+ if(!await sitemapGenerator.generate(domain)){
126
+ Logger.error('Sitemap generation failed.');
127
+ return false;
128
+ }
129
+ if(domain){
130
+ Logger.info('Sitemap generated successfully for domain: '+domain);
131
+ }
132
+ if(!domain){
133
+ Logger.info('Sitemaps generated successfully for all domains.');
134
+ }
135
+ return true;
136
+ }
137
+
138
+ }
139
+
140
+ let generator = new CmsSitemapGenerator();
141
+ generator.run().then((success) => {
142
+ if(!success){
143
+ process.exit(1);
144
+ }
145
+ process.exit(0);
146
+ }).catch((error) => {
147
+ Logger.critical('Error during sitemap generation: '+error.message);
148
+ process.exit(1);
149
+ });
@@ -8,6 +8,7 @@
8
8
 
9
9
  const { Manager } = require('../index');
10
10
  const { EntitiesLoader } = require('../lib/entities-loader');
11
+ const { PrismaClientLoader } = require('@reldens/storage');
11
12
  const { Logger, sc } = require('@reldens/utils');
12
13
  const { FileHandler, Encryptor } = require('@reldens/server-utils');
13
14
  const dotenv = require('dotenv');
@@ -127,7 +128,7 @@ class CmsPasswordUpdater
127
128
  entitiesTranslations: loadedEntities.entitiesTranslations
128
129
  };
129
130
  if('prisma' === storageDriver){
130
- let prismaClient = this.loadPrismaClient();
131
+ let prismaClient = PrismaClientLoader.load(this.projectRoot, null, null);
131
132
  if(prismaClient){
132
133
  managerConfig.prismaClient = prismaClient;
133
134
  Logger.debug('Prisma client loaded and configured.');
@@ -154,22 +155,6 @@ class CmsPasswordUpdater
154
155
  return true;
155
156
  }
156
157
 
157
- loadPrismaClient()
158
- {
159
- let clientPath = FileHandler.joinPaths(this.projectRoot, 'prisma', 'client');
160
- if(!FileHandler.exists(clientPath)){
161
- Logger.debug('Prisma client path not found: '+clientPath);
162
- return false;
163
- }
164
- try {
165
- let { PrismaClient } = require(clientPath);
166
- return new PrismaClient();
167
- } catch(error) {
168
- Logger.error('Failed to load Prisma client: '+error.message);
169
- return false;
170
- }
171
- }
172
-
173
158
  async promptPassword()
174
159
  {
175
160
  let rl = readline.createInterface({
@@ -52,6 +52,7 @@ class CmsPagesRouteManager
52
52
  if(!event.loadedEntity || !event.loadedEntity.route_id){
53
53
  event.renderedEditProperties.routePath = '';
54
54
  event.renderedEditProperties.routeDomain = '';
55
+ event.renderedEditProperties.originalRouteId = '';
55
56
  return true;
56
57
  }
57
58
  let routesRepository = this.dataServer.getEntity('routes');
@@ -62,6 +63,7 @@ class CmsPagesRouteManager
62
63
  let existingRoute = await routesRepository.loadById(event.loadedEntity.route_id);
63
64
  event.renderedEditProperties.routePath = sc.get(existingRoute, 'path', '');
64
65
  event.renderedEditProperties.routeDomain = sc.get(existingRoute, 'domain', '');
66
+ event.renderedEditProperties.originalRouteId = event.loadedEntity.route_id;
65
67
  return true;
66
68
  }
67
69
 
@@ -90,10 +92,14 @@ class CmsPagesRouteManager
90
92
  domain: sc.get(req.body, 'routeDomain', null)
91
93
  };
92
94
  let routeResult = false;
93
- if(entityData.route_id){
95
+ let originalRouteId = sc.get(req.body, 'originalRouteId', false);
96
+ if(entityData.route_id && originalRouteId && entityData.route_id === originalRouteId){
94
97
  routeResult = await routesRepository.updateById(entityData.route_id, routePatchData);
98
+ if(!routeResult){
99
+ Logger.error('Route could not be updated.', routePatchData);
100
+ }
95
101
  }
96
- if(!routeResult){
102
+ if(!routeResult && !originalRouteId){
97
103
  routeResult = await routesRepository.create(routePatchData);
98
104
  if(routeResult){
99
105
  let pagesRepository = this.dataServer.getEntity('cmsPages');
@@ -104,9 +110,9 @@ class CmsPagesRouteManager
104
110
  }
105
111
  }
106
112
  }
107
- }
108
- if(!routeResult){
109
- Logger.error('Route could not be saved.', routePatchData);
113
+ if(!routeResult){
114
+ Logger.error('Route could not be created.', routePatchData);
115
+ }
110
116
  }
111
117
  return routeResult;
112
118
  }
@@ -0,0 +1,181 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - SitemapGenerator
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+ const { FileHandler } = require('@reldens/server-utils');
9
+
10
+ class SitemapGenerator
11
+ {
12
+
13
+ constructor(props)
14
+ {
15
+ this.dataServer = sc.get(props, 'dataServer', false);
16
+ this.projectRoot = sc.get(props, 'projectRoot', './');
17
+ this.defaultDomain = sc.get(props, 'defaultDomain', '');
18
+ this.domainMapping = sc.get(props, 'domainMapping', {});
19
+ this.domainPublicUrlMapping = sc.get(props, 'domainPublicUrlMapping', {});
20
+ this.sitemapDir = FileHandler.joinPaths(this.projectRoot, 'public', 'sitemap');
21
+ }
22
+
23
+ async generate(specificDomain = null)
24
+ {
25
+ if(!this.dataServer){
26
+ Logger.critical('DataServer is required for sitemap generation.');
27
+ return false;
28
+ }
29
+ let routes = await this.loadEnabledRoutes();
30
+ if(!routes){
31
+ return false;
32
+ }
33
+ if(0 === routes.length){
34
+ Logger.warning('No enabled routes found for sitemap generation.');
35
+ return true;
36
+ }
37
+ let filteredRoutes = routes;
38
+ if(specificDomain){
39
+ filteredRoutes = routes.filter((route) => {
40
+ let routeDomain = route.domain || this.defaultDomain;
41
+ return routeDomain === specificDomain;
42
+ });
43
+ if(0 === filteredRoutes.length){
44
+ Logger.warning('No routes found for domain: '+specificDomain);
45
+ return true;
46
+ }
47
+ }
48
+ let domainRoutes = this.groupRoutesByDomain(filteredRoutes);
49
+ if(!await this.saveSitemaps(domainRoutes)){
50
+ return false;
51
+ }
52
+ if(!specificDomain){
53
+ if(!await this.saveSitemapIndex(domainRoutes)){
54
+ return false;
55
+ }
56
+ }
57
+ Logger.info('Sitemap generation completed successfully.');
58
+ return true;
59
+ }
60
+
61
+ async loadEnabledRoutes()
62
+ {
63
+ let routesEntity = this.dataServer.getEntity('routes');
64
+ if(!routesEntity){
65
+ Logger.critical('Routes entity not found in dataServer.');
66
+ return false;
67
+ }
68
+ try {
69
+ let routes = await routesEntity.load({enabled: 1});
70
+ Logger.debug('Loaded '+routes.length+' enabled routes.');
71
+ return routes;
72
+ } catch(error) {
73
+ Logger.critical('Failed to load routes: '+error.message);
74
+ return false;
75
+ }
76
+ }
77
+
78
+ groupRoutesByDomain(routes)
79
+ {
80
+ let domainRoutes = {};
81
+ for(let route of routes){
82
+ let domain = route.domain || this.defaultDomain || 'default';
83
+ if(!sc.hasOwn(domainRoutes, domain)){
84
+ domainRoutes[domain] = [];
85
+ }
86
+ domainRoutes[domain].push(route);
87
+ }
88
+ return domainRoutes;
89
+ }
90
+
91
+ buildSitemapXml(routes, domain)
92
+ {
93
+ let urlEntries = [];
94
+ for(let route of routes){
95
+ let urlEntry = ' <url>\n';
96
+ urlEntry += ' <loc>'+sc.sanitize(this.buildUrl(domain, route.path))+'</loc>\n';
97
+ if(route.updated_at){
98
+ urlEntry += ' <lastmod>'+sc.formatDate(new Date(route.updated_at), 'Y-m-d')+'</lastmod>\n';
99
+ }
100
+ urlEntry += ' </url>';
101
+ urlEntries.push(urlEntry);
102
+ }
103
+ return this.getSitemapTemplate().replace('{{URLS}}', urlEntries.join('\n'));
104
+ }
105
+
106
+ buildSitemapIndexXml(domainRoutes)
107
+ {
108
+ let sitemapEntries = [];
109
+ for(let domain in domainRoutes){
110
+ let sitemapEntry = ' <sitemap>\n';
111
+ sitemapEntry += ' <loc>'+sc.sanitize(this.buildUrl(domain, '/sitemap/'+domain+'/sitemap.xml'))+'</loc>\n';
112
+ sitemapEntry += ' </sitemap>';
113
+ sitemapEntries.push(sitemapEntry);
114
+ }
115
+ return this.getSitemapIndexTemplate().replace('{{SITEMAPS}}', sitemapEntries.join('\n'));
116
+ }
117
+
118
+ getSitemapTemplate()
119
+ {
120
+ return '<?xml version="1.0" encoding="UTF-8"?>\n'
121
+ +'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
122
+ +'{{URLS}}\n'
123
+ +'</urlset>\n';
124
+ }
125
+
126
+ getSitemapIndexTemplate()
127
+ {
128
+ return '<?xml version="1.0" encoding="UTF-8"?>\n'
129
+ +'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
130
+ +'{{SITEMAPS}}\n'
131
+ +'</sitemapindex>\n';
132
+ }
133
+
134
+ async saveSitemaps(domainRoutes)
135
+ {
136
+ for(let domain in domainRoutes){
137
+ let domainDir = FileHandler.joinPaths(this.sitemapDir, domain);
138
+ if(!FileHandler.createFolder(domainDir)){
139
+ Logger.critical('Failed to create sitemap directory: '+domainDir);
140
+ return false;
141
+ }
142
+ let sitemapPath = FileHandler.joinPaths(domainDir, 'sitemap.xml');
143
+ if(!FileHandler.writeFile(sitemapPath, this.buildSitemapXml(domainRoutes[domain], domain))){
144
+ Logger.critical('Failed to write sitemap: '+sitemapPath);
145
+ return false;
146
+ }
147
+ Logger.info('Sitemap saved: '+sitemapPath);
148
+ }
149
+ return true;
150
+ }
151
+
152
+ async saveSitemapIndex(domainRoutes)
153
+ {
154
+ if(!FileHandler.createFolder(this.sitemapDir)){
155
+ Logger.critical('Failed to create sitemap directory: '+this.sitemapDir);
156
+ return false;
157
+ }
158
+ let indexPath = FileHandler.joinPaths(this.projectRoot, 'public', 'sitemap.xml');
159
+ if(!FileHandler.writeFile(indexPath, this.buildSitemapIndexXml(domainRoutes))){
160
+ Logger.critical('Failed to write sitemap index: '+indexPath);
161
+ return false;
162
+ }
163
+ Logger.info('Sitemap index saved: '+indexPath);
164
+ return true;
165
+ }
166
+
167
+ buildUrl(domain, path)
168
+ {
169
+ let publicUrl = sc.get(this.domainPublicUrlMapping, domain, 'http://'+domain);
170
+ if(publicUrl.endsWith('/')){
171
+ publicUrl = publicUrl.slice(0, -1);
172
+ }
173
+ if(!path.startsWith('/')){
174
+ path = '/'+path;
175
+ }
176
+ return publicUrl+path;
177
+ }
178
+
179
+ }
180
+
181
+ module.exports.SitemapGenerator = SitemapGenerator;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/cms",
3
3
  "scope": "@reldens",
4
- "version": "0.50.0",
4
+ "version": "0.52.0",
5
5
  "description": "Reldens - CMS",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -13,7 +13,8 @@
13
13
  "bin": {
14
14
  "reldens-cms": "bin/reldens-cms.js",
15
15
  "reldens-cms-generate-entities": "bin/reldens-cms-generate-entities.js",
16
- "reldens-cms-update-password": "bin/reldens-cms-update-password.js"
16
+ "reldens-cms-update-password": "bin/reldens-cms-update-password.js",
17
+ "reldens-cms-generate-sitemap": "bin/reldens-cms-generate-sitemap.js"
17
18
  },
18
19
  "repository": {
19
20
  "type": "git",
@@ -34,8 +35,8 @@
34
35
  "url": "https://github.com/damian-pastorini/reldens-cms/issues"
35
36
  },
36
37
  "dependencies": {
37
- "@reldens/server-utils": "^0.43.0",
38
- "@reldens/storage": "^0.85.0",
38
+ "@reldens/server-utils": "^0.44.0",
39
+ "@reldens/storage": "^0.87.0",
39
40
  "@reldens/utils": "^0.54.0",
40
41
  "dotenv": "17.2.3",
41
42
  "mustache": "4.2.0"