@cocreate/sitemap 1.1.0 → 1.2.1

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 (3) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +5 -2
  3. package/src/index.js +191 -44
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## [1.2.1](https://github.com/CoCreate-app/CoCreate-sitemap/compare/v1.2.0...v1.2.1) (2024-09-01)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * return if no title, parse sub object in createEntry ([97d8751](https://github.com/CoCreate-app/CoCreate-sitemap/commit/97d8751808e9f7e43c61de864820905949f89dba))
7
+
8
+ # [1.2.0](https://github.com/CoCreate-app/CoCreate-sitemap/compare/v1.1.0...v1.2.0) (2024-08-30)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * add sitemap to sitemapindex if not already in index ([0ff6911](https://github.com/CoCreate-app/CoCreate-sitemap/commit/0ff6911fe81eeb05295639fb984401d3e76b90ad))
14
+ * noindex metatag return and handling sitemap generation ([058116a](https://github.com/CoCreate-app/CoCreate-sitemap/commit/058116a34c44100bfa4ea1b20afe13fd489bc642))
15
+
16
+
17
+ ### Features
18
+
19
+ * added node-html-parser ([397c590](https://github.com/CoCreate-app/CoCreate-sitemap/commit/397c5908af1149d2d60d7c431a8a0f1bd60789f4))
20
+ * parseHtml function to read and generate nested sitemaps using sitemap-* attributes and metatags ([64edbdb](https://github.com/CoCreate-app/CoCreate-sitemap/commit/64edbdbdcee6f64bc5080997557f9691f072381c))
21
+
1
22
  # [1.1.0](https://github.com/CoCreate-app/CoCreate-sitemap/compare/v1.0.0...v1.1.0) (2024-08-24)
2
23
 
3
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/sitemap",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "A simple sitemap component in vanilla javascript. Easily configured using HTML5 data-attributes and/or JavaScript API.",
5
5
  "keywords": [
6
6
  "sitemap",
@@ -43,5 +43,8 @@
43
43
  "type": "GitHub Sponsors ❤",
44
44
  "url": "https://github.com/sponsors/CoCreate-app"
45
45
  },
46
- "main": "./src/index.js"
46
+ "main": "./src/index.js",
47
+ "dependencies": {
48
+ "node-html-parser": "^6.1.13"
49
+ }
47
50
  }
package/src/index.js CHANGED
@@ -20,6 +20,8 @@
20
20
  // you must obtain a commercial license from CoCreate LLC.
21
21
  // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
22
22
 
23
+ const { parse } = require("node-html-parser");
24
+
23
25
  class CoCreateSitemap {
24
26
  constructor(crud) {
25
27
  this.crud = crud;
@@ -34,14 +36,18 @@ class CoCreateSitemap {
34
36
  if (!file.public || file.public === "false")
35
37
  return;
36
38
 
37
- // Check if the file is HTML and contains a noindex meta tag
38
- if (file['content-type'] === 'text/html' && file.src.includes('<meta name="robots" content="noindex">'))
39
- return;
39
+ // Check if the file is HTML and contains a noindex meta or title tag
40
+ if (file['content-type'] === 'text/html') {
41
+ if (/<meta\s+name=["']robots["']\s+content=["'][^"']*noindex[^"']*["']/i.test(file.src))
42
+ return;
43
+ if (!(/<title[^>]*>[\s\S]*?<\/title>/i.test(file.src)))
44
+ return;
45
+ }
40
46
 
41
47
  // Compare the lastmod date in the sitemap with the modified.on date
42
48
  if (file.sitemap && file.sitemap.lastmod && file.modified.on) {
43
- if (new Date(file.sitemap.lastmod) >= new Date(file.modified.on))
44
- return;
49
+ // if (new Date(file.sitemap.lastmod) >= new Date(file.modified.on))
50
+ // return;
45
51
  }
46
52
 
47
53
  // Logic to update the sitemap
@@ -50,14 +56,17 @@ class CoCreateSitemap {
50
56
 
51
57
  async updateSitemap(file, host) {
52
58
  try {
59
+ if (!file.sitemap)
60
+ file.sitemap = {}
61
+
53
62
  // TODO: need to get info such as host
54
63
  const entry = this.createEntry(file);
55
64
 
56
65
  let { mainSitemap, sitemap } = await this.getSitemap(file, host);
57
66
 
58
- if (file.sitemap.pathname) {
67
+ if (file.pathname) {
59
68
  // Perform regex search starting at the pathname
60
- const regexPattern = `<url>\\s*<loc>.*?${file.sitemap.pathname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*?</loc>[\\s\\S]*?</url>`;
69
+ const regexPattern = `<url>\\s*<loc>.*?${file.pathname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*?</loc>[\\s\\S]*?</url>`;
61
70
  const match = sitemap.src.match(new RegExp(regexPattern));
62
71
 
63
72
  if (match) {
@@ -85,35 +94,54 @@ class CoCreateSitemap {
85
94
  }
86
95
 
87
96
  createEntry(file) {
88
- const depth = (file.pathname.match(/\//g) || []).length;
89
- const priority = Math.max(0.1, 1.0 - (depth - 1) * 0.1).toFixed(1);
90
-
91
- const defaultKeys = {
92
- loc: file.pathname,
93
- lastmod: file.modified.on,
94
- changefreq: 'monthly', // Example default value
95
- priority: priority,
96
- };
97
- // Merge default keys with file.sitemap, prioritizing file.sitemap values
98
- file.sitemap = { ...defaultKeys, ...file.sitemap };
97
+ file.sitemap.loc = file.pathname;
99
98
  file.sitemap.lastmod = file.modified.on;
100
99
 
100
+ if (file['content-type'] === 'text/html') {
101
+ this.parseHtml(file)
102
+
103
+ if (file.sitemap.type !== 'news') {
104
+ if (file.sitemap.changefreq)
105
+ file.sitemap.changefreq = 'monthly';
106
+
107
+ if (!file.sitemap.priority) {
108
+ const depth = (file.pathname.match(/\//g) || []).length;
109
+ file.sitemap.priority = Math.max(0.1, 1.0 - (depth - 1) * 0.1).toFixed(1);
110
+ }
111
+ }
112
+ }
113
+
101
114
  let entry = `\t<url>\n`;
102
115
 
103
116
  for (const key of Object.keys(file.sitemap)) {
104
- if (key === 'pathname')
117
+ if (key === 'pathname' || key === 'type')
105
118
  continue
106
- const value = file.sitemap[key];
107
-
108
- if (typeof value === 'object' && value !== null) {
109
- entry += `\t\t<${key}:${key}>\n`;
110
-
111
- for (const nestedKey of Object.keys(value)) {
112
- const nestedValue = value[nestedKey];
113
- entry += `\t\t\t<${key}:${nestedKey}>${nestedValue}</${key}:${nestedKey}>\n`;
119
+ let value = file.sitemap[key];
120
+
121
+ if (typeof value === 'object' && value !== null && !(value instanceof Date)) {
122
+ if (!Array.isArray(value))
123
+ value = [value]
124
+ for (let i = 0; i < value.length; i++) {
125
+ entry += `\t\t<${key}:${key}>\n`;
126
+
127
+ for (const nestedKey of Object.keys(value[i])) {
128
+ const nestedValue = value[i][nestedKey];
129
+
130
+ // Handle nested objects
131
+ if (typeof nestedValue === 'object' && nestedValue !== null && !(nestedValue instanceof Date)) {
132
+ entry += `\t\t\t<${key}:${nestedKey}>\n`;
133
+ for (const subKey of Object.keys(nestedValue)) {
134
+ const subValue = nestedValue[subKey];
135
+ entry += `\t\t\t\t<${key}:${subKey}>${subValue}</${key}:${subKey}>\n`;
136
+ }
137
+ entry += `\t\t\t</${key}:${nestedKey}>\n`;
138
+ } else {
139
+ entry += `\t\t\t<${key}:${nestedKey}>${nestedValue}</${key}:${nestedKey}>\n`;
140
+ }
141
+ }
142
+
143
+ entry += `\t\t</${key}:${key}>\n`;
114
144
  }
115
-
116
- entry += `\t\t</${key}:${key}>\n`;
117
145
  } else {
118
146
  entry += `\t\t<${key}>${value}</${key}>\n`;
119
147
  }
@@ -153,7 +181,6 @@ class CoCreateSitemap {
153
181
  // Update loc using pathname
154
182
  file.sitemap.loc = `${file.pathname}`
155
183
 
156
-
157
184
  // Query the database for the correct sitemap based on the loc and type
158
185
  if (file.sitemap.pathname) {
159
186
  sitemap = await this.readSitemap(sitemap, host);
@@ -167,8 +194,8 @@ class CoCreateSitemap {
167
194
  type = 'image';
168
195
  } else if (file['content-type'].startsWith('video/')) {
169
196
  type = 'video';
170
- } else if (file.sitemap.news) {
171
- // type = 'news';
197
+ } else if (file.sitemap.type === 'news') {
198
+ type = 'news';
172
199
  }
173
200
 
174
201
  let name = `sitemap`
@@ -180,24 +207,37 @@ class CoCreateSitemap {
180
207
  if (index) {
181
208
  sitemap.pathname = `/${name}${index}.xml`
182
209
  sitemap = await this.readSitemap(sitemap, host);
210
+ } else {
211
+ index = 1
183
212
  }
184
213
 
185
214
  // Check if there's room in the last index sitemap
186
215
  if (!this.checkSitemap(sitemap.src)) {
187
216
  if (sitemap.src)
188
217
  index += 1
218
+ else
219
+ sitemap.src = this.createSitemap(type);
220
+
189
221
  sitemap.name = `${name}${index}.xml`
190
222
  sitemap.pathname = `/${name}${index}.xml`
191
- sitemap.src = this.createSitemap(type);
192
-
193
- // Add the new sitemap entry
194
- const indexEntry = `\n<sitemap>\n\t<loc>{{$host}}/${name}${index}.xml</loc>\n</sitemap>`;
195
- mainSitemap.src = mainSitemap.src.replace('</sitemapindex>', `${indexEntry}\n</sitemapindex>`);
196
223
 
197
224
  }
198
225
 
199
226
  }
200
227
 
228
+ // Create the regex pattern to match the <sitemap> block containing the specific <loc> for the pathname
229
+ const regexPattern = `<sitemap>\\s*<loc>[^<]*${sitemap.pathname}[^<]*</loc>[\\s\\S]*?</sitemap>`;
230
+
231
+ // Execute the regex match against the sitemap index source
232
+ const match = mainSitemap.src.match(new RegExp(regexPattern));
233
+
234
+ // Check if a match is found
235
+ if (!match) {
236
+ //TODO: if sitemap found but not in index should we add to sitemap pathname to index or should we check the sitmap for the next index available see if room add or create new index.
237
+ const indexEntry = `\t<sitemap>\n\t\t<loc>{{$host}}${sitemap.pathname}</loc>\n</sitemap>`;
238
+ mainSitemap.src = mainSitemap.src.replace('</sitemapindex>', `${indexEntry}\n</sitemapindex>`);
239
+ }
240
+
201
241
  return { mainSitemap, sitemap }
202
242
  }
203
243
 
@@ -223,12 +263,24 @@ class CoCreateSitemap {
223
263
  }
224
264
 
225
265
  createSitemap(type) {
226
- if (type === 'main')
227
- return `<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n</sitemapindex>`;
228
- else if (type === 'image' || type === 'video' || type === 'news')
229
- return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:${type}="http://www.google.com/schemas/sitemap-${type}/1.1">\n</urlset>`;
230
- else
231
- return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n</urlset>`;
266
+ const xmlDeclaration = `<?xml version="1.0" encoding="UTF-8"?>\n`;
267
+ const sitemapNamespace = 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
268
+
269
+ if (type === 'main') {
270
+ return `${xmlDeclaration}<sitemapindex ${sitemapNamespace}>\n</sitemapindex>`;
271
+ } else {
272
+ const imageNamespace = 'xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"';
273
+ const videoNamespace = 'xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"';
274
+ const newsNamespace = 'xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"';
275
+
276
+ if (type === 'image') {
277
+ return `${xmlDeclaration}<urlset ${sitemapNamespace} ${imageNamespace}>\n</urlset>`;
278
+ } else if (type === 'video') {
279
+ return `${xmlDeclaration}<urlset ${sitemapNamespace} ${videoNamespace}>\n</urlset>`;
280
+ } else { // For 'news' type or any other types
281
+ return `${xmlDeclaration}<urlset ${sitemapNamespace} ${newsNamespace} ${imageNamespace} ${videoNamespace}>\n</urlset>`;
282
+ }
283
+ }
232
284
  }
233
285
 
234
286
  async saveSitemap(file, host) {
@@ -259,7 +311,7 @@ class CoCreateSitemap {
259
311
  const regex = new RegExp(`\\/${filename}(\\d*)\\.xml<\\/loc>`, 'g');
260
312
  const matches = mainSitemap.src.match(regex);
261
313
 
262
- return matches ? matches.length : 0;
314
+ return matches ? matches.length : null;
263
315
  } catch (err) {
264
316
  console.error(`Error determining next sitemap index for ${filename}:`, err);
265
317
  return null; // Or some default value or throw an error
@@ -293,6 +345,101 @@ class CoCreateSitemap {
293
345
  }
294
346
  }
295
347
 
348
+ parseHtml(file) {
349
+ const dom = parse(file.src);
350
+ const entries = dom.querySelectorAll('[sitemap="true"]');
351
+
352
+ let types = ['image', 'video', 'news']
353
+
354
+ const previousEntries = {}
355
+ for (let i = 0; i < types.length; i++) {
356
+ if (!file.sitemap[types[i]])
357
+ continue
358
+ if (Array.isArray(file.sitemap[types[i]]))
359
+ previousEntries[types[i]] = file.sitemap[types[i]]
360
+ else
361
+ previousEntries[types[i]] = [file.sitemap[types[i]]]
362
+
363
+ delete file.sitemap[types[i]]
364
+ }
365
+
366
+ for (let i = 0; i < entries.length; i++) {
367
+ let type = '', query = '';
368
+ let existingObject
369
+ let entryObject = {};
370
+
371
+ if (entries[i].tagName === 'IMG') { // Corrected to 'IMG' for images
372
+ type = 'image';
373
+ query = 'loc'
374
+ entryObject.loc = entries[i].getAttribute('src');
375
+ entryObject.title = entries[i].getAttribute('sitemap-title') || entries[i].getAttribute('title') || entries[i].getAttribute('alt');
376
+ entryObject.caption = entries[i].getAttribute('sitemap-caption') || entries[i].getAttribute('alt') || entryObject.title;
377
+ entryObject.geo_location = entries[i].getAttribute('sitemap-geo-location');
378
+ } else if (entries[i].tagName === 'VIDEO') {
379
+ type = 'video';
380
+ query = 'content_loc'
381
+ entryObject.content_loc = entries[i].src;
382
+ entryObject.title = entries[i].getAttribute('sitemap-title') || entries[i].getAttribute('title');
383
+ entryObject.description = entries[i].getAttribute('description'); // 'description' if available
384
+ entryObject.thumbnail_loc = entries[i].getAttribute('sitemap-thumbnail') || entries[i].getAttribute('poster');
385
+ entryObject.duration = entries[i].getAttribute('sitemap-duration');
386
+ } else {
387
+ type = 'news';
388
+ file.sitemap.type = 'news';
389
+ query = 'title'
390
+ entryObject.title = entries[i].getAttribute('sitemap-title');
391
+ if (!entryObject.title) {
392
+ const title = dom.querySelector('title');
393
+ entryObject.title = title ? title.text : '';
394
+ }
395
+
396
+ entryObject.publication = {
397
+ name: entries[i].getAttribute('sitemap-publication-name'), // Use proper attribute
398
+ language: entries[i].getAttribute('sitemap-publication-language') // Use proper attribute
399
+ };
400
+
401
+ if (!entryObject.publication.language) {
402
+ // Fallback to HTML lang attribute
403
+ const htmlElement = dom.querySelector('html');
404
+ entryObject.publication.language = htmlElement ? htmlElement.getAttribute('lang') : null;
405
+ }
406
+
407
+ entryObject.publication_date = entries[i].getAttribute('sitemap-publication-date') || file.modified.on;
408
+
409
+ entryObject.keywords = entries[i].getAttribute('sitemap-keywords');
410
+ if (!entryObject.keywords) {
411
+ const keywords = dom.querySelector('meta[name="keywords"]');
412
+ entryObject.keywords = keywords ? keywords.getAttribute('content') : '';
413
+ }
414
+
415
+ entryObject.genres = entries[i].getAttribute('sitemap-genres');
416
+ }
417
+
418
+ if (previousEntries[type]) {
419
+ existingObject = previousEntries[type].find(item => item[query] === entryObject[query]);
420
+ entryObject = { ...existingObject, ...entryObject }
421
+ }
422
+
423
+ Object.keys(entryObject).forEach(key => {
424
+ if (!entryObject[key])
425
+ delete entryObject[key]
426
+ });
427
+
428
+ if (!file.sitemap[type])
429
+ file.sitemap[type] = []
430
+
431
+ file.sitemap[type].push(entryObject)
432
+
433
+ }
434
+
435
+ if (file.sitemap.type !== 'news') {
436
+ const priorityMeta = dom.querySelector('meta[name="sitemap-priority"]');
437
+ const changefreqMeta = dom.querySelector('meta[name="sitemap-changefreq"]');
438
+ file.sitemap.priority = priorityMeta ? priorityMeta.getAttribute('content') : file.sitemap.priority; // Default priority if not specified
439
+ file.sitemap.changefreq = changefreqMeta ? changefreqMeta.getAttribute('content') : file.sitemap.changefreq; // Default changefreq if not specified
440
+ }
441
+
442
+ }
296
443
  }
297
444
 
298
445
  module.exports = CoCreateSitemap;