@reldens/cms 0.52.0 → 0.53.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.
@@ -9,7 +9,6 @@
9
9
  const { Manager } = require('../index');
10
10
  const { PrismaClientLoader } = require('@reldens/storage');
11
11
  const { Logger, sc } = require('@reldens/utils');
12
- const { FileHandler } = require('@reldens/server-utils');
13
12
  const readline = require('readline');
14
13
 
15
14
  class CmsEntitiesGenerator
@@ -109,6 +109,10 @@
109
109
  <input type="checkbox" name="install-dynamic-forms" id="install-dynamic-forms" checked/>
110
110
  <label for="install-dynamic-forms">Install dynamic forms system</label>
111
111
  </div>
112
+ <div class="input-box input-checkbox install-seo-files">
113
+ <input type="checkbox" name="install-seo-files" id="install-seo-files" checked/>
114
+ <label for="install-seo-files">Install default robots.txt and sitemap.xml</label>
115
+ </div>
112
116
  <div class="input-box submit-container">
113
117
  <input id="install-submit-button" type="submit" value="Install"/>
114
118
  <img class="install-loading hidden" src="/install-assets/img/loading.gif"/>
@@ -248,6 +248,7 @@ class RouterContents
248
248
  let loadedEntities = await entityRepository.load(idsFilter);
249
249
  await this.deleteEntitiesRelatedFiles(driverResource, loadedEntities);
250
250
  let deleteResult = await entityRepository.delete(idsFilter);
251
+ await this.emitEvent('reldens.adminAfterEntityDelete', {driverResource, idProperty, ids});
251
252
  return redirectPath + (deleteResult ? 'success' : 'errorStorageFailure');
252
253
  } catch (error) {
253
254
  return redirectPath + 'errorDeleteFailure';
@@ -119,6 +119,7 @@ class ContentRenderer
119
119
  );
120
120
  let templatePath = this.templateResolver.findTemplatePath(templateName, domain);
121
121
  if(!templatePath){
122
+ Logger.warning('Template not found: "'+templateName+'" for domain: "'+domain+'"');
122
123
  return layoutContent;
123
124
  }
124
125
  let routerKey = route?.router;
@@ -13,7 +13,7 @@ class TemplateCache
13
13
  constructor(props)
14
14
  {
15
15
  this.templatesPath = sc.get(props, 'templatesPath', '');
16
- this.templateExtensions = sc.get(props, 'templateExtensions', ['.html', '.mustache', '.template']);
16
+ this.templateExtensions = sc.get(props, 'templateExtensions', ['.html', '.mustache', '.template', '.txt', '.xml', '.json']);
17
17
  this.defaultDomain = sc.get(props, 'defaultDomain', 'default');
18
18
  this.partialsCache = {};
19
19
  this.domainPartialsCache = new Map();
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  const { FileHandler } = require('@reldens/server-utils');
8
- const { sc } = require('@reldens/utils');
8
+ const { Logger, sc } = require('@reldens/utils');
9
9
 
10
10
  class TemplateResolver
11
11
  {
@@ -13,7 +13,11 @@ class TemplateResolver
13
13
  constructor(props)
14
14
  {
15
15
  this.templatesPath = sc.get(props, 'templatesPath', '');
16
- this.templateExtensions = sc.get(props, 'templateExtensions', ['.html', '.mustache', '.template']);
16
+ this.templateExtensions = sc.get(
17
+ props,
18
+ 'templateExtensions',
19
+ ['.html', '.mustache', '.template', '.txt', '.xml', '.json']
20
+ );
17
21
  this.defaultDomain = sc.get(props, 'defaultDomain', 'default');
18
22
  this.domainMapping = sc.get(props, 'domainMapping', {});
19
23
  this.siteKeyMapping = sc.get(props, 'siteKeyMapping', {});
@@ -75,6 +79,7 @@ class TemplateResolver
75
79
  return templatePath;
76
80
  }
77
81
  }
82
+ Logger.debug('Template file not found: "'+templateName+'" in path: "'+basePath+'"');
78
83
  return false;
79
84
  }
80
85
 
package/lib/frontend.js CHANGED
@@ -33,7 +33,11 @@ class Frontend
33
33
  this.projectRoot = sc.get(props, 'projectRoot', './');
34
34
  this.templatesPath = FileHandler.joinPaths(this.projectRoot, 'templates');
35
35
  this.publicPath = FileHandler.joinPaths(this.projectRoot, 'public');
36
- this.templateExtensions = sc.get(props, 'templateExtensions', ['.html', '.mustache', '.template']);
36
+ this.templateExtensions = sc.get(
37
+ props,
38
+ 'templateExtensions',
39
+ ['.html', '.mustache', '.template', '.txt', '.xml', '.json']
40
+ );
37
41
  this.defaultDomain = sc.get(props, 'defaultDomain', 'default');
38
42
  this.domainMapping = sc.get(props, 'domainMapping', {});
39
43
  this.siteKeyMapping = sc.get(props, 'siteKeyMapping', {});
@@ -95,6 +99,12 @@ class Frontend
95
99
  enableJsonResponse: sc.get(props, 'enableJsonResponse', false),
96
100
  events: this.events
97
101
  });
102
+ this.contentTypeMap = sc.get(props, 'contentTypeMap', {
103
+ '.txt': 'text/plain',
104
+ '.xml': 'application/xml',
105
+ '.json': 'application/json',
106
+ '.html': 'text/html'
107
+ });
98
108
  }
99
109
 
100
110
  async initialize()
@@ -153,6 +163,16 @@ class Frontend
153
163
  return true;
154
164
  }
155
165
 
166
+ getContentTypeFromPath(path)
167
+ {
168
+ let lastDotIndex = path.lastIndexOf('.');
169
+ if(-1 === lastDotIndex){
170
+ return 'text/html';
171
+ }
172
+ let extension = path.substring(lastDotIndex);
173
+ return sc.get(this.contentTypeMap, extension, 'text/html');
174
+ }
175
+
156
176
  async handleRequest(req, res)
157
177
  {
158
178
  try {
@@ -178,10 +198,14 @@ class Frontend
178
198
  if(!originalPath.endsWith('/') && routeFoundWithSlash){
179
199
  return res.redirect(301, originalPath + '/');
180
200
  }
201
+ let contentType = this.getContentTypeFromPath(originalPath);
181
202
  return await this.responseManager.renderWithCacheHandler(
182
203
  async () => await this.contentRenderer.generateRouteContent(route, domain, req),
183
204
  async () => await this.responseManager.renderNotFound(domain, res, req),
184
- async (content) => res.send(content),
205
+ async (content) => {
206
+ res.setHeader('Content-Type', contentType);
207
+ return res.send(content);
208
+ },
185
209
  domain,
186
210
  res,
187
211
  originalPath,
@@ -200,6 +224,7 @@ class Frontend
200
224
  }
201
225
  let entityResult = await this.entityAccessManager.findEntityByPath(originalPath);
202
226
  if(entityResult){
227
+ let contentType = this.getContentTypeFromPath(originalPath);
203
228
  return await this.responseManager.renderWithCacheHandler(
204
229
  async () => await this.contentRenderer.renderWithTemplateContent(
205
230
  entityResult.entity,
@@ -209,7 +234,10 @@ class Frontend
209
234
  null
210
235
  ),
211
236
  async () => await this.responseManager.renderNotFound(domain, res, req),
212
- async (content) => res.send(content),
237
+ async (content) => {
238
+ res.setHeader('Content-Type', contentType);
239
+ return res.send(content);
240
+ },
213
241
  domain,
214
242
  res,
215
243
  originalPath,
@@ -218,12 +246,16 @@ class Frontend
218
246
  }
219
247
  let templatePath = this.templateResolver.findTemplateByPath(originalPath, domain);
220
248
  if(templatePath){
249
+ let contentType = this.getContentTypeFromPath(originalPath);
221
250
  return await this.responseManager.renderWithCacheHandler(
222
251
  async () => await this.contentRenderer.generateTemplateContent(templatePath, domain, req),
223
252
  async () => res.status(500).send('Template error: '+templatePath),
224
- async (content) => res.send(
225
- await this.contentRenderer.renderWithTemplateContent({content}, {}, domain, req, null)
226
- ),
253
+ async (content) => {
254
+ res.setHeader('Content-Type', contentType);
255
+ return res.send(
256
+ await this.contentRenderer.renderWithTemplateContent({content}, {}, domain, req, null)
257
+ );
258
+ },
227
259
  domain,
228
260
  res,
229
261
  originalPath,
package/lib/manager.js CHANGED
@@ -21,6 +21,7 @@ const { Frontend } = require('./frontend');
21
21
  const { CacheManager } = require('./cache/cache-manager');
22
22
  const { TemplateReloader } = require('./template-reloader');
23
23
  const { PasswordEncryptionHandler } = require('./password-encryption-handler');
24
+ const { SitemapLoader } = require('./sitemap-loader');
24
25
  const { EventsManagerSingleton, Logger, sc } = require('@reldens/utils');
25
26
  const { DriversMap } = require('@reldens/storage');
26
27
  const { AppServerFactory, FileHandler, Encryptor } = require('@reldens/server-utils');
@@ -74,7 +75,11 @@ class Manager
74
75
  'domainCdnMapping',
75
76
  sc.toJson(process.env.RELDENS_DOMAIN_CDN_MAPPING, {})
76
77
  );
77
- this.templateExtensions = sc.get(props, 'templateExtensions', ['.html', '.template']);
78
+ this.templateExtensions = sc.get(
79
+ props,
80
+ 'templateExtensions',
81
+ ['.html', '.mustache', '.template', '.txt', '.xml', '.json']
82
+ );
78
83
  this.cache = sc.get(props, 'cache', false);
79
84
  this.reloadTime = sc.get(props, 'reloadTime', 0);
80
85
  this.app = sc.get(props, 'app', false);
@@ -416,6 +421,11 @@ class Manager
416
421
  return false;
417
422
  }
418
423
  }
424
+ this.sitemapLoader = new SitemapLoader({
425
+ dataServer: this.dataServer,
426
+ events: this.events,
427
+ domainMapping: this.domainMapping
428
+ });
419
429
  if(!this.useProvidedServer){
420
430
  await this.appServer.listen(this.config.port);
421
431
  }
@@ -20,7 +20,8 @@ class MySQLInstaller
20
20
  'install-default-homepage': 'default-homepage.sql',
21
21
  'install-default-blocks': 'default-blocks.sql',
22
22
  'install-entity-access': 'default-entity-access.sql',
23
- 'install-dynamic-forms': 'default-forms.sql'
23
+ 'install-dynamic-forms': 'default-forms.sql',
24
+ 'install-seo-files': 'default-sitemaps-and-robots.sql'
24
25
  };
25
26
  }
26
27
 
@@ -0,0 +1,101 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - SitemapLoader
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class SitemapLoader
10
+ {
11
+
12
+ constructor(props)
13
+ {
14
+ this.dataServer = sc.get(props, 'dataServer', false);
15
+ this.routesRepository = this.dataServer?.getEntity('routes');
16
+ this.events = sc.get(props, 'events', false);
17
+ this.domainMapping = sc.get(props, 'domainMapping', {});
18
+ this.listenEvents();
19
+ }
20
+
21
+ listenEvents()
22
+ {
23
+ if(!this.events){
24
+ Logger.error('EventsManager not provided for SitemapLoader.');
25
+ return;
26
+ }
27
+ this.events.on('reldens.afterVariablesCreated', this.loadSitemapData.bind(this));
28
+ }
29
+
30
+ async loadSitemapData(eventData)
31
+ {
32
+ if(!this.dataServer){
33
+ Logger.error('DataServer not provided for SitemapLoader.');
34
+ return;
35
+ }
36
+ if(!this.routesRepository){
37
+ Logger.error('Routes repository not found for SitemapLoader.');
38
+ return;
39
+ }
40
+ let req = eventData.renderContext.req;
41
+ if(!req){
42
+ return;
43
+ }
44
+ if(!req.path){
45
+ return;
46
+ }
47
+ if('/sitemap.xml' !== req.path){
48
+ return;
49
+ }
50
+ if(req.sitemapPages){
51
+ eventData.variables.sitemapPages = req.sitemapPages;
52
+ return;
53
+ }
54
+ let currentDomain = eventData.renderContext.domain;
55
+ let mappedDomain = this.resolveMappedDomain(currentDomain);
56
+ let filters = {
57
+ enabled: 1,
58
+ redirect_url: null,
59
+ redirect_type: null,
60
+ OR: [
61
+ {domain: null},
62
+ {domain: mappedDomain}
63
+ ]
64
+ };
65
+ try {
66
+ let routesWithPages = await this.routesRepository.loadWithRelations(filters, 'cms_pages');
67
+ let sitemapPages = routesWithPages.filter(route => {
68
+ if(!route.cms_pages || 0 === route.cms_pages.length){
69
+ return false;
70
+ }
71
+ let page = route.cms_pages[0];
72
+ if('noindex,nofollow' === page.meta_robots){
73
+ return false;
74
+ }
75
+ return true;
76
+ });
77
+ req.sitemapPages = sitemapPages;
78
+ eventData.variables.sitemapPages = sitemapPages;
79
+ } catch (error) {
80
+ Logger.error('Error loading sitemap data: '+error.message);
81
+ }
82
+ }
83
+
84
+ resolveMappedDomain(currentDomain)
85
+ {
86
+ if(!this.domainMapping){
87
+ return currentDomain;
88
+ }
89
+ if(!sc.isObject(this.domainMapping)){
90
+ return currentDomain;
91
+ }
92
+ let mappedValue = sc.get(this.domainMapping, currentDomain, false);
93
+ if(!mappedValue){
94
+ return currentDomain;
95
+ }
96
+ return mappedValue;
97
+ }
98
+
99
+ }
100
+
101
+ module.exports.SitemapLoader = SitemapLoader;
@@ -234,7 +234,14 @@ class CollectionsTransformer extends CollectionsTransformerBase
234
234
  this.jsonFieldsParser.getJsonFieldsForEntity(tableName)
235
235
  );
236
236
  }
237
- let collectionContent = await this.renderCollectionLoop(loopContent, collectionData, domain, req, systemVariables, enhancedData);
237
+ let collectionContent = await this.renderCollectionLoop(
238
+ loopContent,
239
+ collectionData,
240
+ domain,
241
+ req,
242
+ systemVariables,
243
+ enhancedData
244
+ );
238
245
  finalContent = this.renderEngine.render(
239
246
  1 < paginationData.totalPages
240
247
  ? this.loadPaginationTemplate(paginationOptions.containerName, domain)
@@ -259,7 +266,14 @@ class CollectionsTransformer extends CollectionsTransformerBase
259
266
  queryOptionsJson,
260
267
  relationsString
261
268
  );
262
- finalContent = await this.renderCollectionLoop(loopContent, collectionData, domain, req, systemVariables, enhancedData);
269
+ finalContent = await this.renderCollectionLoop(
270
+ loopContent,
271
+ collectionData,
272
+ domain,
273
+ req,
274
+ systemVariables,
275
+ enhancedData
276
+ );
263
277
  }
264
278
  return template.substring(0, startPos) + finalContent + template.substring(endPos + endMatch[0].length);
265
279
  }
@@ -312,7 +326,14 @@ class CollectionsTransformer extends CollectionsTransformerBase
312
326
  }
313
327
  let renderedContent = '';
314
328
  for(let row of collectionData){
315
- renderedContent += await this.processPartialsInLoop(loopContent, row, domain, req, systemVariables, enhancedData);
329
+ renderedContent += await this.processPartialsInLoop(
330
+ loopContent,
331
+ row,
332
+ domain,
333
+ req,
334
+ systemVariables,
335
+ enhancedData
336
+ );
316
337
  }
317
338
  return renderedContent;
318
339
  }
@@ -351,7 +372,13 @@ class CollectionsTransformer extends CollectionsTransformerBase
351
372
  continue;
352
373
  }
353
374
  if(this.processAllTemplateFunctions){
354
- partialContent = await this.processAllTemplateFunctions(partialContent, domain, req, systemVariables, enhancedData);
375
+ partialContent = await this.processAllTemplateFunctions(
376
+ partialContent,
377
+ domain,
378
+ req,
379
+ systemVariables,
380
+ enhancedData
381
+ );
355
382
  }
356
383
  let renderData = Object.assign({}, enhancedData);
357
384
  if(sc.hasOwn(tag.attributes, 'row')){
@@ -367,9 +394,19 @@ class CollectionsTransformer extends CollectionsTransformerBase
367
394
  +processedContent.substring(tag.end);
368
395
  }
369
396
  if(this.processAllTemplateFunctions){
370
- processedContent = await this.processAllTemplateFunctions(processedContent, domain, req, systemVariables, enhancedData);
397
+ processedContent = await this.processAllTemplateFunctions(
398
+ processedContent,
399
+ domain,
400
+ req,
401
+ systemVariables,
402
+ enhancedData
403
+ );
371
404
  }
372
- return this.renderEngine.render(processedContent, Object.assign({}, enhancedData, {row: rowData}), this.getPartials(domain));
405
+ return this.renderEngine.render(
406
+ processedContent,
407
+ Object.assign({}, enhancedData, {row: rowData}),
408
+ this.getPartials(domain)
409
+ );
373
410
  }
374
411
 
375
412
  getCurrentUrl(req)
@@ -195,11 +195,7 @@ class TemplateEngine
195
195
 
196
196
  buildEnhancedRenderData(originalData, systemVariables, currentEntityData)
197
197
  {
198
- let enhancedData = Object.assign({}, originalData);
199
- enhancedData.currentRequest = systemVariables.currentRequest;
200
- enhancedData.currentRoute = systemVariables.currentRoute;
201
- enhancedData.currentDomain = systemVariables.currentDomain;
202
- enhancedData.systemInfo = systemVariables.systemInfo;
198
+ let enhancedData = Object.assign({}, originalData, systemVariables);
203
199
  if(currentEntityData){
204
200
  enhancedData.currentEntity = currentEntityData;
205
201
  }
@@ -1,5 +1,5 @@
1
1
 
2
- -- Default CMS blocks:
2
+ -- Default CMS blocks
3
3
 
4
4
  REPLACE INTO `cms_blocks` (`name`, `title`, `content`, `enabled`) VALUES
5
5
  ('header-main', 'Main Header', '{{>header}}', 1),
@@ -1,5 +1,5 @@
1
1
 
2
- -- Default entity access rules:
2
+ -- Default entity access rules
3
3
 
4
4
  REPLACE INTO `entities_access` (`entity_name`, `is_public`, `allowed_operations`) VALUES
5
5
  ('cms_pages', TRUE, '["read"]'),
@@ -12,7 +12,6 @@ CREATE TABLE `cms_forms` (
12
12
  UNIQUE INDEX `form_key` (`form_key`) USING BTREE
13
13
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
14
14
 
15
-
16
15
  CREATE TABLE `cms_forms_submitted` (
17
16
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
18
17
  `form_id` INT UNSIGNED NOT NULL,
@@ -1,19 +1,19 @@
1
- -- Default homepage:
2
1
 
3
- -- Create a default route first
4
- REPLACE INTO `routes` (`id`, `path`, `router`, `cache_ttl_seconds`, `enabled`, `created_at`) VALUES (1, '/', 'cmsPages', 3600, 1, NOW());
2
+ -- Default homepage
5
3
 
6
- -- Create a default homepage with route_id reference
7
- REPLACE INTO `cms_pages` (
4
+ INSERT INTO `routes` (`id`, `path`, `router`, `cache_ttl_seconds`, `enabled`, `created_at`) VALUES (NULL, '/', 'cmsPages', 3600, 1, NOW());
5
+ SET @route_id = LAST_INSERT_ID();
6
+
7
+ INSERT INTO `cms_pages` (
8
8
  `id`, `title`, `content`, `template`, `route_id`, `meta_title`, `meta_description`,
9
9
  `canonical_url`, `meta_robots`, `meta_og_title`, `meta_og_description`,
10
10
  `meta_og_image`, `meta_twitter_card_type`, `status`, `locale`, `publish_date`, `expire_date`, `created_at`
11
11
  ) VALUES (
12
- 1,
12
+ NULL,
13
13
  'Home',
14
14
  '<h1>Welcome to Reldens CMS</h1><p>This is your homepage. Edit this content in the admin panel.</p>',
15
15
  NULL,
16
- 1,
16
+ @route_id,
17
17
  'Home - Reldens CMS',
18
18
  'Welcome to Reldens CMS',
19
19
  NULL,
@@ -0,0 +1,44 @@
1
+
2
+ -- Default robots.txt and sitemap.xml
3
+
4
+ INSERT INTO `routes` (`id`, `path`, `router`, `cache_ttl_seconds`, `enabled`, `created_at`) VALUES
5
+ (NULL, '/robots.txt', 'cmsPages', 3600, 1, NOW());
6
+ SET @route_id_robots = LAST_INSERT_ID();
7
+
8
+ INSERT INTO `cms_pages` (
9
+ `id`, `title`, `content`, `template`, `layout`, `route_id`, `meta_robots`,
10
+ `status`, `locale`, `publish_date`, `created_at`
11
+ ) VALUES (
12
+ NULL,
13
+ 'Robots.txt',
14
+ NULL,
15
+ 'robots',
16
+ 'raw',
17
+ @route_id_robots,
18
+ 'noindex,nofollow',
19
+ NULL,
20
+ NULL,
21
+ NOW(),
22
+ NOW()
23
+ );
24
+
25
+ INSERT INTO `routes` (`id`, `path`, `router`, `cache_ttl_seconds`, `enabled`, `domain`, `created_at`) VALUES
26
+ (NULL, '/sitemap.xml', 'cmsPages', 3600, 1, NULL, NOW());
27
+ SET @route_id_sitemap = LAST_INSERT_ID();
28
+
29
+ INSERT INTO `cms_pages` (
30
+ `id`, `title`, `content`, `template`, `layout`, `route_id`, `meta_robots`,
31
+ `status`, `locale`, `publish_date`, `created_at`
32
+ ) VALUES (
33
+ NULL,
34
+ 'Sitemap XML',
35
+ NULL,
36
+ 'sitemap',
37
+ 'raw',
38
+ @route_id_sitemap,
39
+ 'noindex,nofollow',
40
+ NULL,
41
+ NULL,
42
+ NOW(),
43
+ NOW()
44
+ );
@@ -1,5 +1,5 @@
1
1
 
2
- -- Default admin user:
2
+ -- Default admin user
3
3
 
4
4
  REPLACE INTO `users` (`id`, `email`, `username`, `password`, `role_id`, `status`)
5
5
  VALUES (
@@ -1,5 +1,5 @@
1
1
 
2
- -- Default db-users authentication:
2
+ -- Default db-users authentication
3
3
 
4
4
  CREATE TABLE IF NOT EXISTS `users` (
5
5
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/cms",
3
3
  "scope": "@reldens",
4
- "version": "0.52.0",
4
+ "version": "0.53.0",
5
5
  "description": "Reldens - CMS",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -13,8 +13,7 @@
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",
17
- "reldens-cms-generate-sitemap": "bin/reldens-cms-generate-sitemap.js"
16
+ "reldens-cms-update-password": "bin/reldens-cms-update-password.js"
18
17
  },
19
18
  "repository": {
20
19
  "type": "git",
@@ -36,7 +35,7 @@
36
35
  },
37
36
  "dependencies": {
38
37
  "@reldens/server-utils": "^0.44.0",
39
- "@reldens/storage": "^0.87.0",
38
+ "@reldens/storage": "^0.88.0",
40
39
  "@reldens/utils": "^0.54.0",
41
40
  "dotenv": "17.2.3",
42
41
  "mustache": "4.2.0"
@@ -0,0 +1 @@
1
+ {{&content}}
@@ -0,0 +1 @@
1
+ {{&content}}
@@ -0,0 +1,3 @@
1
+ User-agent: *
2
+ Allow: /
3
+ Sitemap: {{&currentRequest.publicUrl}}/sitemap.xml
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
+ {{#sitemapPages}}
4
+ <url>
5
+ <loc>{{&currentRequest.publicUrl}}{{&path}}</loc>
6
+ <lastmod>{{&updated_at}}</lastmod>
7
+ </url>
8
+ {{/sitemapPages}}
9
+ </urlset>