@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.
- package/.claude/sitemap-generator-guide.md +511 -0
- package/CLAUDE.md +10 -0
- package/admin/templates/sections/editForm/cms-pages.html +3 -0
- package/bin/reldens-cms-generate-entities.js +3 -30
- package/bin/reldens-cms-generate-sitemap.js +149 -0
- package/bin/reldens-cms-update-password.js +2 -17
- package/lib/cms-pages-route-manager.js +11 -5
- package/lib/sitemap-generator.js +181 -0
- package/package.json +5 -4
|
@@ -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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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.
|
|
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.
|
|
38
|
-
"@reldens/storage": "^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"
|