@cocreate/sitemap 1.1.0 → 1.2.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 (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +5 -2
  3. package/src/index.js +168 -37
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [1.2.0](https://github.com/CoCreate-app/CoCreate-sitemap/compare/v1.1.0...v1.2.0) (2024-08-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add sitemap to sitemapindex if not already in index ([0ff6911](https://github.com/CoCreate-app/CoCreate-sitemap/commit/0ff6911fe81eeb05295639fb984401d3e76b90ad))
7
+ * noindex metatag return and handling sitemap generation ([058116a](https://github.com/CoCreate-app/CoCreate-sitemap/commit/058116a34c44100bfa4ea1b20afe13fd489bc642))
8
+
9
+
10
+ ### Features
11
+
12
+ * added node-html-parser ([397c590](https://github.com/CoCreate-app/CoCreate-sitemap/commit/397c5908af1149d2d60d7c431a8a0f1bd60789f4))
13
+ * parseHtml function to read and generate nested sitemaps using sitemap-* attributes and metatags ([64edbdb](https://github.com/CoCreate-app/CoCreate-sitemap/commit/64edbdbdcee6f64bc5080997557f9691f072381c))
14
+
1
15
  # [1.1.0](https://github.com/CoCreate-app/CoCreate-sitemap/compare/v1.0.0...v1.1.0) (2024-08-24)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/sitemap",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
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;
@@ -35,7 +37,8 @@ class CoCreateSitemap {
35
37
  return;
36
38
 
37
39
  // 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">'))
40
+ if (file['content-type'] === 'text/html'
41
+ && /<meta\s+name=["']robots["']\s+content=["'][^"']*noindex[^"']*["']/i.test(file.src))
39
42
  return;
40
43
 
41
44
  // Compare the lastmod date in the sitemap with the modified.on date
@@ -55,9 +58,9 @@ class CoCreateSitemap {
55
58
 
56
59
  let { mainSitemap, sitemap } = await this.getSitemap(file, host);
57
60
 
58
- if (file.sitemap.pathname) {
61
+ if (file.pathname) {
59
62
  // Perform regex search starting at the pathname
60
- const regexPattern = `<url>\\s*<loc>.*?${file.sitemap.pathname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*?</loc>[\\s\\S]*?</url>`;
63
+ const regexPattern = `<url>\\s*<loc>.*?${file.pathname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*?</loc>[\\s\\S]*?</url>`;
61
64
  const match = sitemap.src.match(new RegExp(regexPattern));
62
65
 
63
66
  if (match) {
@@ -85,35 +88,43 @@ class CoCreateSitemap {
85
88
  }
86
89
 
87
90
  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 };
91
+ file.sitemap.loc = file.pathname;
99
92
  file.sitemap.lastmod = file.modified.on;
100
93
 
94
+ if (file['content-type'] === 'text/html') {
95
+ parseHtml(file)
96
+
97
+ if (file.sitemap.type !== 'news') {
98
+ if (file.sitemap.changefreq)
99
+ file.sitemap.changefreq = 'monthly';
100
+
101
+ if (!file.sitemap.priority) {
102
+ const depth = (file.pathname.match(/\//g) || []).length;
103
+ file.sitemap.priority = Math.max(0.1, 1.0 - (depth - 1) * 0.1).toFixed(1);
104
+ }
105
+ }
106
+ }
107
+
101
108
  let entry = `\t<url>\n`;
102
109
 
103
110
  for (const key of Object.keys(file.sitemap)) {
104
- if (key === 'pathname')
111
+ if (key === 'pathname' || key === 'type')
105
112
  continue
106
- const value = file.sitemap[key];
113
+ let value = file.sitemap[key];
107
114
 
108
- if (typeof value === 'object' && value !== null) {
109
- entry += `\t\t<${key}:${key}>\n`;
115
+ if (typeof value === 'object' && value !== null && !(value instanceof Date)) {
116
+ if (!Array.isArray(value))
117
+ value = [value]
118
+ for (let i = 0; i < value.length; i++) {
119
+ entry += `\t\t<${key}:${key}>\n`;
110
120
 
111
- for (const nestedKey of Object.keys(value)) {
112
- const nestedValue = value[nestedKey];
113
- entry += `\t\t\t<${key}:${nestedKey}>${nestedValue}</${key}:${nestedKey}>\n`;
114
- }
121
+ for (const nestedKey of Object.keys(value[i])) {
122
+ const nestedValue = value[nestedKey];
123
+ entry += `\t\t\t<${key}:${nestedKey}>${nestedValue}</${key}:${nestedKey}>\n`;
124
+ }
115
125
 
116
- entry += `\t\t</${key}:${key}>\n`;
126
+ entry += `\t\t</${key}:${key}>\n`;
127
+ }
117
128
  } else {
118
129
  entry += `\t\t<${key}>${value}</${key}>\n`;
119
130
  }
@@ -167,8 +178,8 @@ class CoCreateSitemap {
167
178
  type = 'image';
168
179
  } else if (file['content-type'].startsWith('video/')) {
169
180
  type = 'video';
170
- } else if (file.sitemap.news) {
171
- // type = 'news';
181
+ } else if (file.sitemap.type === 'news') {
182
+ type = 'news';
172
183
  }
173
184
 
174
185
  let name = `sitemap`
@@ -180,24 +191,37 @@ class CoCreateSitemap {
180
191
  if (index) {
181
192
  sitemap.pathname = `/${name}${index}.xml`
182
193
  sitemap = await this.readSitemap(sitemap, host);
194
+ } else {
195
+ index = 1
183
196
  }
184
197
 
185
198
  // Check if there's room in the last index sitemap
186
199
  if (!this.checkSitemap(sitemap.src)) {
187
200
  if (sitemap.src)
188
201
  index += 1
202
+ else
203
+ sitemap.src = this.createSitemap(type);
204
+
189
205
  sitemap.name = `${name}${index}.xml`
190
206
  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
207
 
197
208
  }
198
209
 
199
210
  }
200
211
 
212
+ // Create the regex pattern to match the <sitemap> block containing the specific <loc> for the pathname
213
+ const regexPattern = `<sitemap>\\s*<loc>[^<]*${sitemap.pathname}[^<]*</loc>[\\s\\S]*?</sitemap>`;
214
+
215
+ // Execute the regex match against the sitemap index source
216
+ const match = mainSitemap.src.match(new RegExp(regexPattern));
217
+
218
+ // Check if a match is found
219
+ if (!match) {
220
+ //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.
221
+ const indexEntry = `\t<sitemap>\n\t\t<loc>{{$host}}${sitemap.pathname}</loc>\n</sitemap>`;
222
+ mainSitemap.src = mainSitemap.src.replace('</sitemapindex>', `${indexEntry}\n</sitemapindex>`);
223
+ }
224
+
201
225
  return { mainSitemap, sitemap }
202
226
  }
203
227
 
@@ -223,12 +247,24 @@ class CoCreateSitemap {
223
247
  }
224
248
 
225
249
  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>`;
250
+ const xmlDeclaration = `<?xml version="1.0" encoding="UTF-8"?>\n`;
251
+ const sitemapNamespace = 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
252
+
253
+ if (type === 'main') {
254
+ return `${xmlDeclaration}<sitemapindex ${sitemapNamespace}>\n</sitemapindex>`;
255
+ } else {
256
+ const imageNamespace = 'xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"';
257
+ const videoNamespace = 'xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"';
258
+ const newsNamespace = 'xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"';
259
+
260
+ if (type === 'image') {
261
+ return `${xmlDeclaration}<urlset ${sitemapNamespace} ${imageNamespace}>\n</urlset>`;
262
+ } else if (type === 'video') {
263
+ return `${xmlDeclaration}<urlset ${sitemapNamespace} ${videoNamespace}>\n</urlset>`;
264
+ } else { // For 'news' type or any other types
265
+ return `${xmlDeclaration}<urlset ${sitemapNamespace} ${newsNamespace} ${imageNamespace} ${videoNamespace}>\n</urlset>`;
266
+ }
267
+ }
232
268
  }
233
269
 
234
270
  async saveSitemap(file, host) {
@@ -259,7 +295,7 @@ class CoCreateSitemap {
259
295
  const regex = new RegExp(`\\/${filename}(\\d*)\\.xml<\\/loc>`, 'g');
260
296
  const matches = mainSitemap.src.match(regex);
261
297
 
262
- return matches ? matches.length : 0;
298
+ return matches ? matches.length : null;
263
299
  } catch (err) {
264
300
  console.error(`Error determining next sitemap index for ${filename}:`, err);
265
301
  return null; // Or some default value or throw an error
@@ -293,6 +329,101 @@ class CoCreateSitemap {
293
329
  }
294
330
  }
295
331
 
332
+ parseHtml(file) {
333
+ const dom = parse(html);
334
+ const entries = dom.querySelectorAll('[sitemap="true"]');
335
+
336
+ let types = ['image', 'video', 'news']
337
+
338
+ const previousEntries = {}
339
+ for (let i = 0; i < types.length; i++) {
340
+ if (!file.sitemap[types[i]])
341
+ continue
342
+ if (Array.isArray(file.sitemap[types[i]]))
343
+ previousEntries[types[i]] = file.sitemap[types[i]]
344
+ else
345
+ previousEntries[types[i]] = [file.sitemap[types[i]]]
346
+
347
+ delete file.sitemap[types[i]]
348
+ }
349
+
350
+ for (let i = 0; i < entries.length; i++) {
351
+ let type = '', query = '';
352
+ let existingObject
353
+ let entryObject = {};
354
+
355
+ if (entries[i].tagName === 'IMG') { // Corrected to 'IMG' for images
356
+ type = 'image';
357
+ query = 'loc'
358
+ entryObject.loc = entries[i].src;
359
+ entryObject.title = entries[i].getAttribute('sitemap-title') || entries[i].getAttribute('title') || entries[i].getAttribute('alt');
360
+ entryObject.caption = entries[i].getAttribute('sitemap-caption') || entries[i].getAttribute('alt') || entryObject.title;
361
+ entryObject.geo_location = entries[i].getAttribute('sitemap-geo-location');
362
+ } else if (entries[i].tagName === 'VIDEO') {
363
+ type = 'video';
364
+ query = 'content_loc'
365
+ entryObject.content_loc = entries[i].src;
366
+ entryObject.title = entries[i].getAttribute('sitemap-title') || entries[i].getAttribute('title');
367
+ entryObject.description = entries[i].getAttribute('description'); // 'description' if available
368
+ entryObject.thumbnail_loc = entries[i].getAttribute('sitemap-thumbnail') || entries[i].getAttribute('poster');
369
+ entryObject.duration = entries[i].getAttribute('sitemap-duration');
370
+ } else {
371
+ type = 'news';
372
+ file.sitemap.type = 'news';
373
+ query = 'title'
374
+ entryObject.title = entries[i].getAttribute('sitemap-title');
375
+ if (!entryObject.title) {
376
+ const title = dom.querySelector('title');
377
+ entryObject.title = title ? title.text : '';
378
+ }
379
+
380
+ entryObject.publication = {
381
+ name: entries[i].getAttribute('sitemap-publication-name'), // Use proper attribute
382
+ language: entries[i].getAttribute('sitemap-publication-language') // Use proper attribute
383
+ };
384
+
385
+ if (!entryObject.publication.language) {
386
+ // Fallback to HTML lang attribute
387
+ const htmlElement = dom.querySelector('html');
388
+ entryObject.publication.language = htmlElement ? htmlElement.getAttribute('lang') : null;
389
+ }
390
+
391
+ entryObject.publication_date = entries[i].getAttribute('sitemap-publication-date') || file.modified.on;
392
+
393
+ entryObject.keywords = entries[i].getAttribute('sitemap-keywords');
394
+ if (!entryObject.keywords) {
395
+ const keywords = dom.querySelector('meta[name="keywords"]');
396
+ entryObject.keywords = keywords ? keywords.getAttribute('content') : '';
397
+ }
398
+
399
+ entryObject.genres = entries[i].getAttribute('sitemap-genres');
400
+ }
401
+
402
+ if (previousEntries[type]) {
403
+ existingObject = previousEntries[type].find(item => item[query] === entryObject[query]);
404
+ entryObject = { ...existingObject, ...entryObject }
405
+ }
406
+
407
+ Object.keys(entryObject).forEach(key => {
408
+ if (!entryObject[key])
409
+ delete entryObject[key]
410
+ });
411
+
412
+ if (!file.sitemap[type])
413
+ file.sitemap[type] = []
414
+
415
+ file.sitemap[type].push(entryObject)
416
+
417
+ }
418
+
419
+ if (file.sitemap.type !== 'news') {
420
+ const priorityMeta = dom.querySelector('meta[name="sitemap-priority"]');
421
+ const changefreqMeta = dom.querySelector('meta[name="sitemap-changefreq"]');
422
+ file.sitemap.priority = priorityMeta ? priorityMeta.getAttribute('content') : file.sitemap.priority; // Default priority if not specified
423
+ file.sitemap.changefreq = changefreqMeta ? changefreqMeta.getAttribute('content') : file.sitemap.changefreq; // Default changefreq if not specified
424
+ }
425
+
426
+ }
296
427
  }
297
428
 
298
429
  module.exports = CoCreateSitemap;