@reldens/cms 0.16.0 → 0.19.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.
Files changed (38) hide show
  1. package/README.md +92 -9
  2. package/admin/reldens-admin-client.css +55 -0
  3. package/admin/reldens-admin-client.js +24 -0
  4. package/admin/templates/cache-clean-button.html +4 -0
  5. package/admin/templates/clear-all-cache-button.html +18 -0
  6. package/admin/templates/fields/view/textarea.html +1 -1
  7. package/bin/reldens-cms-generate-entities.js +85 -18
  8. package/bin/reldens-cms.js +6 -6
  9. package/lib/admin-manager/contents-builder.js +257 -0
  10. package/lib/admin-manager/router-contents.js +618 -0
  11. package/lib/admin-manager/router.js +208 -0
  12. package/lib/admin-manager-validator.js +2 -1
  13. package/lib/admin-manager.js +116 -990
  14. package/lib/admin-translations.js +9 -1
  15. package/lib/cache/add-cache-button-subscriber.js +149 -0
  16. package/lib/cache/cache-manager.js +168 -0
  17. package/lib/cache/cache-routes-handler.js +99 -0
  18. package/lib/cms-pages-route-manager.js +45 -21
  19. package/lib/frontend.js +288 -71
  20. package/lib/installer.js +5 -2
  21. package/lib/json-fields-parser.js +74 -0
  22. package/lib/manager.js +49 -4
  23. package/lib/pagination-handler.js +243 -0
  24. package/lib/search-renderer.js +116 -0
  25. package/lib/search.js +344 -0
  26. package/lib/template-engine/collections-single-transformer.js +53 -0
  27. package/lib/template-engine/collections-transformer-base.js +84 -0
  28. package/lib/template-engine/collections-transformer.js +353 -0
  29. package/lib/template-engine/entities-transformer.js +65 -0
  30. package/lib/template-engine/partials-transformer.js +171 -0
  31. package/lib/template-engine.js +53 -387
  32. package/lib/templates-list.js +2 -0
  33. package/migrations/default-homepage.sql +6 -6
  34. package/migrations/install.sql +21 -20
  35. package/package.json +4 -4
  36. package/templates/page.html +19 -2
  37. package/templates/partials/entriesListView.html +14 -0
  38. package/templates/partials/pagedCollection.html +33 -0
@@ -4,6 +4,11 @@
4
4
  *
5
5
  */
6
6
 
7
+ const { JsonFieldsParser } = require('./json-fields-parser');
8
+ const { EntitiesTransformer } = require('./template-engine/entities-transformer');
9
+ const { CollectionsTransformer } = require('./template-engine/collections-transformer');
10
+ const { CollectionsSingleTransformer } = require('./template-engine/collections-single-transformer');
11
+ const { PartialsTransformer } = require('./template-engine/partials-transformer');
7
12
  const { Logger, sc } = require('@reldens/utils');
8
13
 
9
14
  class TemplateEngine
@@ -14,26 +19,52 @@ class TemplateEngine
14
19
  this.renderEngine = sc.get(props, 'renderEngine', false);
15
20
  this.dataServer = sc.get(props, 'dataServer', false);
16
21
  this.getPartials = sc.get(props, 'getPartials', false);
17
- this.currentDomain = '';
18
- }
19
-
20
- setCurrentDomain(domain)
21
- {
22
- this.currentDomain = domain;
23
- }
24
-
25
- async processAllTemplateFunctions(template)
22
+ this.jsonFieldsParser = new JsonFieldsParser({entitiesConfig: sc.get(props, 'entitiesConfig', {})});
23
+ this.entitiesTransformer = new EntitiesTransformer({
24
+ dataServer: this.dataServer,
25
+ jsonFieldsParser: this.jsonFieldsParser,
26
+ processAllTemplateFunctions: this.processAllTemplateFunctions.bind(this)
27
+ });
28
+ this.collectionsSingleTransformer = new CollectionsSingleTransformer({
29
+ dataServer: this.dataServer,
30
+ jsonFieldsParser: this.jsonFieldsParser,
31
+ renderEngine: this.renderEngine,
32
+ getPartials: this.getPartials,
33
+ findAllPartialTags: this.findAllPartialTags.bind(this),
34
+ loadPartialTemplate: this.loadPartialTemplate.bind(this)
35
+ });
36
+ this.collectionsTransformer = new CollectionsTransformer({
37
+ dataServer: this.dataServer,
38
+ jsonFieldsParser: this.jsonFieldsParser,
39
+ renderEngine: this.renderEngine,
40
+ getPartials: this.getPartials,
41
+ findAllPartialTags: this.findAllPartialTags.bind(this),
42
+ loadPartialTemplate: this.loadPartialTemplate.bind(this)
43
+ });
44
+ this.partialsTransformer = new PartialsTransformer({
45
+ renderEngine: this.renderEngine,
46
+ getPartials: this.getPartials
47
+ });
48
+ this.transformers = [
49
+ this.entitiesTransformer,
50
+ this.collectionsSingleTransformer,
51
+ this.collectionsTransformer,
52
+ this.partialsTransformer
53
+ ];
54
+ }
55
+
56
+ async processAllTemplateFunctions(template, domain, req)
26
57
  {
27
- return await this.processCustomPartials(
28
- await this.processLoopCollections(
29
- await this.processSingleFieldCollections(
30
- await this.processEntityFunctions(template)
31
- )
32
- )
33
- );
58
+ let processedTemplate = template;
59
+ for(let transformer of this.transformers){
60
+ if(sc.isFunction(transformer.transform)){
61
+ processedTemplate = await transformer.transform(processedTemplate, domain, req);
62
+ }
63
+ }
64
+ return processedTemplate;
34
65
  }
35
66
 
36
- async render(template, data, partials)
67
+ async render(template, data, partials, domain, req)
37
68
  {
38
69
  if(!this.renderEngine){
39
70
  Logger.error('Render engine not provided');
@@ -44,7 +75,7 @@ class TemplateEngine
44
75
  return '';
45
76
  }
46
77
  return this.renderEngine.render(
47
- this.unescapeHtml(await this.processAllTemplateFunctions(template)),
78
+ this.unescapeHtml(await this.processAllTemplateFunctions(template, domain, req)),
48
79
  data,
49
80
  partials
50
81
  );
@@ -63,381 +94,16 @@ class TemplateEngine
63
94
  .replace(/&/g, '&');
64
95
  }
65
96
 
66
- getEntityRegex()
67
- {
68
- return /<entity\s+name="([^"]+)"(?:\s+field="([^"]+)"\s+value="([^"]+)"|\s+id="([^"]+)")?\s*\/?>/g;
69
- }
70
-
71
- getSingleFieldCollectionRegex()
72
- {
73
- return /<collection\s+name=(['"])([^'"]+)\1(?:\s+filters=(['"])([^'"]*)\3)?\s+field=(['"])([^'"]+)\5(?:\s+data=(['"])([^'"]*)\7)?\s*\/>/g;
74
- }
75
-
76
- getLoopCollectionStartRegex()
77
- {
78
- return /<collection\s+name=(['"])([^'"]+)\1(?:\s+filters=(['"])([^'"]*)\3)?(?:\s+data=(['"])([^'"]*)\5)?\s*>/g;
79
- }
80
-
81
- getLoopCollectionEndRegex()
82
- {
83
- return new RegExp('<\\/collection>');
84
- }
85
-
86
- async processEntityFunctions(template)
87
- {
88
- let processedTemplate = template;
89
- for(let match of template.matchAll(this.getEntityRegex())){
90
- let tableName = match[1];
91
- let field = sc.get(match, '2', 'id');
92
- let value = sc.get(match, '3', sc.get(match, '4', ''));
93
- if(!value){
94
- Logger.warning('Entity tag missing value: '+match[0]);
95
- continue;
96
- }
97
- processedTemplate = processedTemplate.replace(
98
- match[0],
99
- await this.processAllTemplateFunctions(
100
- sc.get(await this.fetchEntityForTemplate(tableName, value, field), 'content', '')
101
- )
102
- );
103
- }
104
- return processedTemplate;
105
- }
106
-
107
- async processSingleFieldCollections(template)
108
- {
109
- let processedTemplate = template;
110
- for(let match of template.matchAll(this.getSingleFieldCollectionRegex())){
111
- let tableName = match[2];
112
- let filtersJson = sc.get(match, '4', '{}');
113
- let fieldName = match[6];
114
- let queryOptionsJson = sc.get(match, '8', '{}');
115
- processedTemplate = processedTemplate.replace(
116
- match[0],
117
- this.extractFieldValues(
118
- await this.fetchCollectionForTemplate(tableName, filtersJson, queryOptionsJson),
119
- fieldName
120
- )
121
- );
122
- }
123
- return processedTemplate;
124
- }
125
-
126
- async processLoopCollections(template)
127
- {
128
- let processedTemplate = template;
129
- let matches = [...template.matchAll(this.getLoopCollectionStartRegex())];
130
- for(let i = matches.length - 1; i >= 0; i--){
131
- let startMatch = matches[i];
132
- let loopResult = await this.processLoopCollection(
133
- processedTemplate,
134
- startMatch,
135
- startMatch[2],
136
- sc.get(startMatch, '4', '{}'),
137
- sc.get(startMatch, '6', '{}')
138
- );
139
- if(loopResult){
140
- processedTemplate = loopResult;
141
- }
142
- }
143
- return processedTemplate;
144
- }
145
-
146
- async processCustomPartials(template)
147
- {
148
- let processedTemplate = template;
149
- let partialTags = this.findAllPartialTags(template);
150
- for(let i = partialTags.length - 1; i >= 0; i--){
151
- let tag = partialTags[i];
152
- let partialContent = this.loadPartialTemplate(tag.name);
153
- if(!partialContent){
154
- Logger.warning('Partial template not found: ' + tag.name);
155
- processedTemplate = processedTemplate.substring(0, tag.start) + '' + processedTemplate.substring(tag.end);
156
- continue;
157
- }
158
- let wrapperTemplate = '{{#vars}}{{> ' + tag.name + '}}{{/vars}}';
159
- let renderData = { vars: tag.attributes };
160
- let partials = {[tag.name]: partialContent};
161
- processedTemplate = processedTemplate.substring(0, tag.start) +
162
- this.renderEngine.render(wrapperTemplate, renderData, partials) +
163
- processedTemplate.substring(tag.end);
164
- }
165
- return processedTemplate;
166
- }
167
-
168
97
  findAllPartialTags(template)
169
98
  {
170
- let partialTags = [];
171
- let searchPos = 0;
172
- let partialTagName = '<partial';
173
- for(let tagStart = template.indexOf(partialTagName, searchPos); -1 !== tagStart; tagStart = template.indexOf(partialTagName, searchPos)){
174
- let tagEnd = this.findPartialTagEnd(template, tagStart);
175
- if(-1 === tagEnd){
176
- searchPos = tagStart + partialTagName.length;
177
- continue;
178
- }
179
- let fullTag = template.substring(tagStart, tagEnd);
180
- let nameMatch = fullTag.match(/name=["']([^"']+)["']/);
181
- if(!nameMatch){
182
- searchPos = tagStart + partialTagName.length;
183
- continue;
184
- }
185
- let partialName = nameMatch[1];
186
- let attributes = this.parsePartialAttributes(fullTag, partialName);
187
- partialTags.push({
188
- start: tagStart,
189
- end: tagEnd,
190
- name: partialName,
191
- attributes: attributes,
192
- fullTag: fullTag
193
- });
194
- searchPos = tagEnd;
195
- }
196
- return partialTags;
197
- }
198
-
199
- findPartialTagEnd(template, tagStart)
200
- {
201
- let inQuotes = false;
202
- let quoteChar = '';
203
- let selfCloseTag = '/>';
204
- let openCloseTag = '</partial>';
205
- for(let i = tagStart; i < template.length; i++){
206
- let char = template[i];
207
- if(!inQuotes && ('"' === char || "'" === char)){
208
- inQuotes = true;
209
- quoteChar = char;
210
- continue;
211
- }
212
- if(inQuotes && char === quoteChar && '\\' !== template[i - 1]){
213
- inQuotes = false;
214
- quoteChar = '';
215
- continue;
216
- }
217
- if(!inQuotes){
218
- if(template.substring(i, i + selfCloseTag.length) === selfCloseTag){
219
- return i + selfCloseTag.length;
220
- }
221
- if('>' === char){
222
- let closeIndex = template.indexOf(openCloseTag, i);
223
- if(-1 !== closeIndex){
224
- return closeIndex + openCloseTag.length;
225
- }
226
- return i + 1;
227
- }
228
- }
229
- }
230
- return -1;
231
- }
232
-
233
- parsePartialAttributes(fullTag, partialName)
234
- {
235
- let namePattern = 'name=' + this.getQuotePattern(fullTag, partialName);
236
- let nameIndex = fullTag.indexOf(namePattern);
237
- if(-1 === nameIndex){
238
- return {};
239
- }
240
- let attributesStart = nameIndex + namePattern.length;
241
- let attributesEnd = fullTag.lastIndexOf('/>');
242
- if(-1 === attributesEnd){
243
- attributesEnd = fullTag.lastIndexOf('</partial>');
244
- }
245
- if(-1 === attributesEnd){
246
- attributesEnd = fullTag.lastIndexOf('>');
247
- }
248
- if(-1 === attributesEnd || attributesEnd <= attributesStart){
249
- return {};
250
- }
251
- let attributesString = fullTag.substring(attributesStart, attributesEnd).trim();
252
- return this.extractAttributesObject(attributesString);
253
- }
254
-
255
- getQuotePattern(fullTag, partialName)
256
- {
257
- if(fullTag.includes('name="' + partialName + '"')){
258
- return '"' + partialName + '"';
259
- }
260
- if(fullTag.includes("name='" + partialName + "'")){
261
- return "'" + partialName + "'";
262
- }
263
- return '"' + partialName + '"';
264
- }
265
-
266
- extractAttributesObject(attributesString)
267
- {
268
- if(!attributesString){
269
- return {};
270
- }
271
- let attributes = {};
272
- let valueRegex = /(\w+)=(['"])((?:(?!\2)[^\\]|\\.)*)(\2)/g;
273
- for(let match of attributesString.matchAll(valueRegex)){
274
- attributes[match[1]] = match[3];
275
- }
276
- let booleanRegex = /\b(\w+)(?!\s*=)/g;
277
- for(let match of attributesString.matchAll(booleanRegex)){
278
- if(!sc.hasOwn(attributes, match[1])){
279
- attributes[match[1]] = true;
280
- }
281
- }
282
- return attributes;
283
- }
284
-
285
- loadPartialTemplate(partialName)
286
- {
287
- if(!this.getPartials){
288
- return false;
289
- }
290
- let partials = this.getPartials(this.currentDomain);
291
- if(sc.hasOwn(partials, partialName)){
292
- return partials[partialName];
293
- }
294
- return false;
295
- }
296
-
297
- async processLoopCollection(template, startMatch, tableName, filtersJson, queryOptionsJson)
298
- {
299
- let startPos = startMatch.index;
300
- let startEnd = startPos + startMatch[0].length;
301
- let endMatch = this.getLoopCollectionEndRegex(tableName).exec(template.substring(startEnd));
302
- if(!endMatch){
303
- Logger.warning('No matching end tag found for collection: '+tableName);
304
- return false;
305
- }
306
- let endPos = startEnd + endMatch.index;
307
- let loopContent = template.substring(startEnd, endPos);
308
- let collectionData = await this.fetchCollectionForTemplate(tableName, filtersJson, queryOptionsJson);
309
- let renderedContent = await this.renderCollectionLoop(loopContent, collectionData);
310
- return template.substring(0, startPos) + renderedContent + template.substring(endPos + endMatch[0].length);
311
- }
312
-
313
- async renderCollectionLoop(loopContent, collectionData)
314
- {
315
- let renderedContent = '';
316
- for(let row of collectionData){
317
- renderedContent += await this.processPartialsInLoop(loopContent, row);
318
- }
319
- return renderedContent;
320
- }
321
-
322
- async processPartialsInLoop(content, rowData)
323
- {
324
- let processedContent = content;
325
- let partialTags = this.findAllPartialTags(content);
326
- for(let i = partialTags.length - 1; i >= 0; i--){
327
- let tag = partialTags[i];
328
- let partialContent = this.loadPartialTemplate(tag.name);
329
- if(!partialContent){
330
- Logger.warning('Partial template not found: ' + tag.name);
331
- processedContent = processedContent.substring(0, tag.start) + '' + processedContent.substring(tag.end);
332
- continue;
333
- }
334
- if(sc.hasOwn(tag.attributes, 'row')){
335
- let renderedPartial = this.renderEngine.render(partialContent, {row: rowData}, this.getPartials(this.currentDomain));
336
- processedContent = processedContent.substring(0, tag.start) + renderedPartial + processedContent.substring(tag.end);
337
- continue;
338
- }
339
- let wrapperTemplate = '{{#vars}}{{> ' + tag.name + '}}{{/vars}}';
340
- let renderData = { vars: tag.attributes };
341
- let partials = {[tag.name]: partialContent};
342
- processedContent = processedContent.substring(0, tag.start) +
343
- this.renderEngine.render(wrapperTemplate, renderData, partials) +
344
- processedContent.substring(tag.end);
345
- }
346
- return this.renderEngine.render(processedContent, {row: rowData}, this.getPartials(this.currentDomain));
347
- }
348
-
349
- extractFieldValues(collectionData, fieldName)
350
- {
351
- let fieldValues = '';
352
- for(let row of collectionData){
353
- fieldValues += sc.get(row, fieldName, '');
354
- }
355
- return fieldValues;
356
- }
357
-
358
- async fetchEntityForTemplate(tableName, identifier, identifierField)
359
- {
360
- let entity = this.dataServer.getEntity(tableName);
361
- if(!entity){
362
- Logger.warning('Entity not found in dataServer: '+tableName);
363
- return false;
364
- }
365
- return await entity.loadOneBy(identifierField, identifier);
366
- }
367
-
368
- convertJsObjectToJson(jsObjectString)
369
- {
370
- if(!jsObjectString || '' === jsObjectString.trim()){
371
- return '';
372
- }
373
- return jsObjectString.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":');
374
- }
375
-
376
- async fetchCollectionForTemplate(tableName, filtersJson, queryOptionsJson)
377
- {
378
- let entity = this.dataServer.getEntity(tableName);
379
- if(!entity){
380
- Logger.warning('Entity not found in dataServer: '+tableName);
381
- return [];
382
- }
383
- let filters = false;
384
- if(filtersJson && '' !== filtersJson.trim()){
385
- let convertedFiltersJson = this.convertJsObjectToJson(filtersJson);
386
- filters = sc.parseJson(convertedFiltersJson, false);
387
- if(!filters){
388
- Logger.warning('Invalid filters JSON: '+filtersJson);
389
- }
390
- }
391
- let originalState = this.preserveEntityState(entity);
392
- let queryOptions = {};
393
- if(queryOptionsJson && '' !== queryOptionsJson.trim()){
394
- let convertedOptionsJson = this.convertJsObjectToJson(queryOptionsJson);
395
- queryOptions = sc.parseJson(convertedOptionsJson, {});
396
- if(!queryOptions){
397
- Logger.warning('Invalid query options JSON: '+queryOptionsJson);
398
- queryOptions = {};
399
- }
400
- }
401
- this.applyQueryOptions(entity, queryOptions);
402
- let result = filters ? await entity.load(filters) : await entity.loadAll();
403
- this.restoreEntityState(entity, originalState);
404
- return result;
405
- }
406
-
407
- preserveEntityState(entity)
408
- {
409
- return {
410
- limit: entity.limit,
411
- offset: entity.offset,
412
- sortBy: entity.sortBy,
413
- sortDirection: entity.sortDirection
414
- };
415
- }
416
-
417
- restoreEntityState(entity, originalState)
418
- {
419
- entity.limit = originalState.limit;
420
- entity.offset = originalState.offset;
421
- entity.sortBy = originalState.sortBy;
422
- entity.sortDirection = originalState.sortDirection;
99
+ return this.partialsTransformer.findAllPartialTags(template);
423
100
  }
424
101
 
425
- applyQueryOptions(entity, queryOptions)
102
+ loadPartialTemplate(partialName, domain)
426
103
  {
427
- if(sc.hasOwn(queryOptions, 'limit')){
428
- entity.limit = queryOptions.limit;
429
- }
430
- if(sc.hasOwn(queryOptions, 'offset')){
431
- entity.offset = queryOptions.offset;
432
- }
433
- if(sc.hasOwn(queryOptions, 'sortBy')){
434
- entity.sortBy = queryOptions.sortBy;
435
- }
436
- if(sc.hasOwn(queryOptions, 'sortDirection')){
437
- entity.sortDirection = queryOptions.sortDirection;
438
- }
104
+ return this.partialsTransformer.loadPartialTemplate(partialName, domain);
439
105
  }
440
106
 
441
107
  }
442
108
 
443
- module.exports.TemplateEngine = TemplateEngine;
109
+ module.exports.TemplateEngine = TemplateEngine;
@@ -18,6 +18,8 @@ module.exports.TemplatesList = {
18
18
  sideBarItem: 'sidebar-item.html',
19
19
  paginationLink: 'pagination-link.html',
20
20
  defaultCopyRight: 'default-copyright.html',
21
+ cacheCleanButton: 'cache-clean-button.html',
22
+ clearAllCacheButton: 'clear-all-cache-button.html',
21
23
  fields: {
22
24
  view: {
23
25
  text: 'text.html',
@@ -1,9 +1,11 @@
1
-
2
1
  -- Default homepage:
3
2
 
4
- -- Create a default homepage route if not exists
3
+ -- Create a default route first
4
+ REPLACE INTO `routes` (`id`, `path`, `router`, `cache_ttl_seconds`, `enabled`, `created_at`) VALUES (1, '/home', 'cmsPages', 3600, 1, NOW());
5
+
6
+ -- Create a default homepage with route_id reference
5
7
  REPLACE INTO `cms_pages` (
6
- `id`, `title`, `content`, `template`, `meta_title`, `meta_description`,
8
+ `id`, `title`, `content`, `template`, `route_id`, `meta_title`, `meta_description`,
7
9
  `canonical_url`, `meta_robots`, `meta_og_title`, `meta_og_description`,
8
10
  `meta_og_image`, `meta_twitter_card_type`, `status`, `locale`, `publish_date`, `expire_date`, `created_at`
9
11
  ) VALUES (
@@ -11,6 +13,7 @@ REPLACE INTO `cms_pages` (
11
13
  'Home',
12
14
  '<h1>Welcome to Reldens CMS</h1><p>This is your homepage. Edit this content in the admin panel.</p>',
13
15
  NULL,
16
+ 1,
14
17
  'Home - Reldens CMS',
15
18
  'Welcome to Reldens CMS',
16
19
  NULL,
@@ -25,6 +28,3 @@ REPLACE INTO `cms_pages` (
25
28
  NULL,
26
29
  NOW()
27
30
  );
28
-
29
- -- Create a default route to the homepage
30
- REPLACE INTO `routes` (`id`, `path`, `router`, `cms_page_id`, `cache_ttl_seconds`, `enabled`, `created_at`) VALUES (1, '/home', 'cmsPages', 1, 3600, 1, NOW());
@@ -5,7 +5,6 @@ CREATE TABLE IF NOT EXISTS `routes` (
5
5
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
6
6
  `path` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
7
7
  `router` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
8
- `cms_page_id` INT UNSIGNED NOT NULL,
9
8
  `cache_ttl_seconds` INT UNSIGNED NULL DEFAULT 3600,
10
9
  `enabled` TINYINT UNSIGNED NOT NULL DEFAULT '1',
11
10
  `domain` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
@@ -30,32 +29,34 @@ CREATE TABLE IF NOT EXISTS `cms_categories` (
30
29
 
31
30
  CREATE TABLE IF NOT EXISTS `cms_pages` (
32
31
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
33
- `title` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
34
- `content` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
35
- `markdown` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
36
- `json_data` JSON NULL,
37
- `template` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
38
- `layout` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'default',
32
+ `title` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_unicode_ci',
33
+ `content` LONGTEXT NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
34
+ `json_data` JSON NULL DEFAULT NULL,
35
+ `template` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
36
+ `layout` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
39
37
  `category_id` INT UNSIGNED NULL DEFAULT NULL,
38
+ `route_id` INT UNSIGNED NULL DEFAULT NULL,
40
39
  `enabled` TINYINT UNSIGNED NOT NULL DEFAULT '1',
41
- `meta_title` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
42
- `meta_description` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
43
- `meta_robots` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'index,follow',
40
+ `meta_title` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_unicode_ci',
41
+ `meta_description` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
42
+ `meta_robots` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
44
43
  `meta_theme_color` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
45
- `meta_og_title` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
46
- `meta_og_description` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
47
- `meta_og_image` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
48
- `meta_twitter_card_type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'summary',
49
- `canonical_url` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
50
- `status` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
51
- `locale` VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'en',
52
- `publish_date` TIMESTAMP NULL,
53
- `expire_date` TIMESTAMP NULL,
44
+ `meta_og_title` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
45
+ `meta_og_description` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
46
+ `meta_og_image` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
47
+ `meta_twitter_card_type` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
48
+ `canonical_url` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
49
+ `status` VARCHAR(20) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
50
+ `locale` VARCHAR(10) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
51
+ `publish_date` TIMESTAMP NULL DEFAULT (NOW()),
52
+ `expire_date` TIMESTAMP NULL DEFAULT NULL,
54
53
  `created_at` TIMESTAMP NOT NULL DEFAULT (NOW()),
55
54
  `updated_at` TIMESTAMP NOT NULL DEFAULT (NOW()) ON UPDATE CURRENT_TIMESTAMP,
56
55
  PRIMARY KEY (`id`) USING BTREE,
57
56
  INDEX `FK_cms_pages_cms_categories` (`category_id`) USING BTREE,
58
- CONSTRAINT `FK_cms_pages_cms_categories` FOREIGN KEY (`category_id`) REFERENCES `cms_categories` (`id`) ON UPDATE CASCADE ON DELETE NO ACTION
57
+ INDEX `FK_cms_pages_routes` (`route_id`) USING BTREE,
58
+ CONSTRAINT `FK_cms_pages_cms_categories` FOREIGN KEY (`category_id`) REFERENCES `cms_categories` (`id`) ON UPDATE CASCADE ON DELETE NO ACTION,
59
+ CONSTRAINT `FK_cms_pages_routes` FOREIGN KEY (`route_id`) REFERENCES `routes` (`id`) ON UPDATE CASCADE ON DELETE NO ACTION
59
60
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
60
61
 
61
62
  CREATE TABLE IF NOT EXISTS `cms_blocks` (
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/cms",
3
3
  "scope": "@reldens",
4
- "version": "0.16.0",
4
+ "version": "0.19.0",
5
5
  "description": "Reldens - CMS",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -33,10 +33,10 @@
33
33
  "url": "https://github.com/damian-pastorini/reldens-cms/issues"
34
34
  },
35
35
  "dependencies": {
36
- "@reldens/server-utils": "^0.19.0",
37
- "@reldens/storage": "^0.56.0",
36
+ "@reldens/server-utils": "^0.20.0",
37
+ "@reldens/storage": "^0.60.0",
38
38
  "@reldens/utils": "^0.50.0",
39
- "dotenv": "^16.5.0",
39
+ "dotenv": "^17.0.0",
40
40
  "mustache": "^4.2.0"
41
41
  }
42
42
  }
@@ -2,23 +2,40 @@
2
2
  <html lang="{{locale}}">
3
3
  <head>
4
4
  <title>{{meta_title}}</title>
5
+ <meta name="language" content="{{locale}}"/>
5
6
  <meta charset="utf-8"/>
6
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover"/>
7
- <meta name="description" content="{{meta_description}}">
8
8
  <meta name="robots" content="{{meta_robots}}">
9
9
  <meta name="theme-color" content="{{meta_theme_color}}"/>
10
+ {{#meta_description}}
11
+ <meta name="description" content="{{meta_description}}"/>
12
+ {{/meta_description}}
10
13
  <meta property="og:title" content="{{meta_og_title}}"/>
14
+ {{#meta_og_description}}
11
15
  <meta property="og:description" content="{{meta_og_description}}"/>
16
+ {{/meta_og_description}}
17
+ {{#meta_og_image}}
12
18
  <meta property="og:image" content="{{meta_og_image}}"/>
19
+ {{/meta_og_image}}
20
+ {{#meta_twitter_card_type}}
13
21
  <meta name="twitter:card" content="{{meta_twitter_card_type}}"/>
14
- <meta name="language" content="{{locale}}"/>
22
+ {{/meta_twitter_card_type}}
23
+ {{#publish_date}}
15
24
  <meta name="publish-date" content="{{publish_date}}"/>
25
+ {{/publish_date}}
26
+ {{#expire_date}}
16
27
  <meta name="expire-date" content="{{expire_date}}"/>
28
+ {{/expire_date}}
29
+ {{#canonical_url}}
17
30
  <link rel="canonical" href="{{canonical_url}}"/>
31
+ {{/canonical_url}}
32
+ <!-- CSS and JS -->
18
33
  <link href="/css/styles.css" rel="stylesheet"/>
34
+ <script src="/js/cookie-consent.js"></script>
19
35
  </head>
20
36
  <body class="{{siteHandle}}">
21
37
  {{&content}}
38
+ {{>cookie-consent}}
22
39
  <script type="text/javascript" defer src="/js/scripts.js"></script>
23
40
  </body>
24
41
  </html>
@@ -0,0 +1,14 @@
1
+ <div class="col-lg-6 mt-2 mb-2">
2
+ <div class="entry-item">
3
+ <div class="pic use-circle">
4
+ <img src="{{&row.json_data.image}}" alt="{{row.title}}"/>
5
+ </div>
6
+ <div class="entry-info">
7
+ <a href="{{&row.routes.path}}"{{targetBlank}}>
8
+ <h4>{{ row.title }}</h4>
9
+ {{&row.json_data.shortDescription}}
10
+ <p>{{&row.json_data.date}}</p>
11
+ </a>
12
+ </div>
13
+ </div>
14
+ </div>