@cocreate/sitemap 1.0.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 +28 -0
  2. package/package.json +5 -2
  3. package/src/index.js +386 -29
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
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
+
15
+ # [1.1.0](https://github.com/CoCreate-app/CoCreate-sitemap/compare/v1.0.0...v1.1.0) (2024-08-24)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * apply host for crud ([9a9fbf0](https://github.com/CoCreate-app/CoCreate-sitemap/commit/9a9fbf0c636d6aa50a607e9fdd2cf96c29f0e1bc))
21
+ * check logic to auto add html files to sitemap ([6ca692b](https://github.com/CoCreate-app/CoCreate-sitemap/commit/6ca692b486ad2f26c7140349756ebc7454ce2e00))
22
+ * handle mainSitmap and sitmap as a file object ([99b97c3](https://github.com/CoCreate-app/CoCreate-sitemap/commit/99b97c3aa3198ea090a2efe8d9d2af5875af73ed))
23
+
24
+
25
+ ### Features
26
+
27
+ * create multiple sitemaps for the various supported types ([98af2dc](https://github.com/CoCreate-app/CoCreate-sitemap/commit/98af2dc972ae15a34caaf709358d543f67828e95))
28
+
1
29
  # 1.0.0 (2024-06-27)
2
30
 
3
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/sitemap",
3
- "version": "1.0.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,53 +20,410 @@
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 fs = require('fs');
23
+ const { parse } = require("node-html-parser");
24
24
 
25
25
  class CoCreateSitemap {
26
- constructor(render) {
27
- this.render = render;
26
+ constructor(crud) {
27
+ this.crud = crud;
28
28
  }
29
29
 
30
- async updateUrlInSitemap(urlToFind) {
31
- try {
32
- // Path to your sitemap file
33
- const sitemapPath = '/path/to/your/sitemap.xml';
30
+ async check(file, host) {
31
+ // Ensure the file is HTML and does not have a sitemap object yet
32
+ if (!file.sitemap && file['content-type'] !== 'text/html')
33
+ return;
34
34
 
35
- // Read and parse the sitemap XML
36
- let sitemapXml = fs.readFileSync(sitemapPath, 'utf8');
35
+ // Ensure the file is public
36
+ if (!file.public || file.public === "false")
37
+ return;
37
38
 
38
- // Regex pattern to find the entire <url>...</url> block containing the URL
39
- const regexPattern = `<url>\\s*<loc>${urlToFind}</loc>[\\s\\S]*?</url>`;
39
+ // Check if the file is HTML and contains a noindex meta tag
40
+ if (file['content-type'] === 'text/html'
41
+ && /<meta\s+name=["']robots["']\s+content=["'][^"']*noindex[^"']*["']/i.test(file.src))
42
+ return;
40
43
 
41
- // Perform regex search
42
- const match = sitemapXml.match(regexPattern);
44
+ // Compare the lastmod date in the sitemap with the modified.on date
45
+ if (file.sitemap && file.sitemap.lastmod && file.modified.on) {
46
+ if (new Date(file.sitemap.lastmod) >= new Date(file.modified.on))
47
+ return;
48
+ }
43
49
 
44
- if (match) {
45
- const position = match.index; // Start position of the <url> block
46
- const endPosition = match.index + match[0].length; // End position of the <url> block
47
- console.log(`URL ${urlToFind} found in sitemap at position ${position}-${endPosition}.`);
50
+ // Logic to update the sitemap
51
+ this.updateSitemap(file, host);
52
+ }
48
53
 
49
- // Replace the matched <url> block with a modified version (example)
50
- const modifiedUrlBlock = `<url>
51
- <loc>${urlToFind}</loc>
52
- <lastmod>${new Date().toISOString()}</lastmod>
53
- <priority>1.0</priority>
54
- </url>`;
54
+ async updateSitemap(file, host) {
55
+ try {
56
+ // TODO: need to get info such as host
57
+ const entry = this.createEntry(file);
55
58
 
56
- // Replace the original <url> block with the modified one
57
- sitemapXml = sitemapXml.slice(0, position) + modifiedUrlBlock + sitemapXml.slice(endPosition);
59
+ let { mainSitemap, sitemap } = await this.getSitemap(file, host);
58
60
 
59
- // Write back the modified sitemap XML to the file (optional)
60
- fs.writeFileSync(sitemapPath, sitemapXml);
61
+ if (file.pathname) {
62
+ // Perform regex search starting at the pathname
63
+ const regexPattern = `<url>\\s*<loc>.*?${file.pathname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*?</loc>[\\s\\S]*?</url>`;
64
+ const match = sitemap.src.match(new RegExp(regexPattern));
61
65
 
62
- console.log('Sitemap updated successfully.');
66
+ if (match) {
67
+ const position = match.index; // Start position of the <url> block
68
+ const endPosition = match.index + match[0].length; // End position of the <url> block
69
+
70
+ // Replace the original <url> block with the modified one
71
+ sitemap.src = sitemap.src.slice(0, position) + entry + sitemap.src.slice(endPosition);
72
+ } else {
73
+ sitemap.src = sitemap.src.replace('</urlset>', `${entry}</urlset>`);
74
+ }
63
75
  } else {
64
- console.log(`URL ${urlToFind} not found in sitemap.`);
76
+ file.sitemap.pathname = sitemap.pathname
77
+ sitemap.src = sitemap.src.replace('</urlset>', `${entry}</urlset>`);
65
78
  }
79
+
80
+ this.saveSitemap(mainSitemap, host);
81
+ this.saveSitemap(sitemap, host);
82
+ this.saveSitemap(file, host);
83
+
84
+ // console.log('Sitemap updated successfully.');
66
85
  } catch (err) {
67
86
  console.error('Error updating sitemap:', err);
68
87
  }
69
88
  }
89
+
90
+ createEntry(file) {
91
+ file.sitemap.loc = file.pathname;
92
+ file.sitemap.lastmod = file.modified.on;
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
+
108
+ let entry = `\t<url>\n`;
109
+
110
+ for (const key of Object.keys(file.sitemap)) {
111
+ if (key === 'pathname' || key === 'type')
112
+ continue
113
+ let value = file.sitemap[key];
114
+
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`;
120
+
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
+ }
125
+
126
+ entry += `\t\t</${key}:${key}>\n`;
127
+ }
128
+ } else {
129
+ entry += `\t\t<${key}>${value}</${key}>\n`;
130
+ }
131
+ }
132
+
133
+ entry += `\t</url>\n`;
134
+
135
+ return entry;
136
+ }
137
+
138
+ async getSitemap(file, host) {
139
+ let mainSitemap = {
140
+ host: file.host,
141
+ name: 'sitemap.xml',
142
+ path: '/',
143
+ pathname: '/sitemap.xml',
144
+ directory: '/',
145
+ 'content-type': 'application/xml',
146
+ public: true,
147
+ organization_id: file.organization_id
148
+ }
149
+
150
+ mainSitemap = await this.readSitemap(mainSitemap, host);
151
+ if (!mainSitemap.src)
152
+ mainSitemap.src = this.createSitemap('main')
153
+
154
+ let sitemap = {
155
+ host: file.host,
156
+ path: '/',
157
+ pathname: file.sitemap.pathname,
158
+ directory: '/',
159
+ 'content-type': 'application/xml',
160
+ public: true,
161
+ organization_id: file.organization_id
162
+ }
163
+
164
+ // Update loc using pathname
165
+ file.sitemap.loc = `${file.pathname}`
166
+
167
+
168
+ // Query the database for the correct sitemap based on the loc and type
169
+ if (file.sitemap.pathname) {
170
+ sitemap = await this.readSitemap(sitemap, host);
171
+ }
172
+
173
+ if (!sitemap.src) {
174
+ let type = 'sitemap';
175
+
176
+ // Identify content type to determine sitemap type
177
+ if (file['content-type'].startsWith('image/')) {
178
+ type = 'image';
179
+ } else if (file['content-type'].startsWith('video/')) {
180
+ type = 'video';
181
+ } else if (file.sitemap.type === 'news') {
182
+ type = 'news';
183
+ }
184
+
185
+ let name = `sitemap`
186
+ if (type === 'image' || type === 'video' || type === 'news')
187
+ name = `sitemap-${type}`
188
+
189
+ // If no existing sitemap found check last index sitemap
190
+ let index = await this.getLastSitemapIndex(mainSitemap, name);
191
+ if (index) {
192
+ sitemap.pathname = `/${name}${index}.xml`
193
+ sitemap = await this.readSitemap(sitemap, host);
194
+ } else {
195
+ index = 1
196
+ }
197
+
198
+ // Check if there's room in the last index sitemap
199
+ if (!this.checkSitemap(sitemap.src)) {
200
+ if (sitemap.src)
201
+ index += 1
202
+ else
203
+ sitemap.src = this.createSitemap(type);
204
+
205
+ sitemap.name = `${name}${index}.xml`
206
+ sitemap.pathname = `/${name}${index}.xml`
207
+
208
+ }
209
+
210
+ }
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
+
225
+ return { mainSitemap, sitemap }
226
+ }
227
+
228
+ async readSitemap(file, host) {
229
+ let data = {
230
+ method: 'object.read',
231
+ host: host,
232
+ array: 'files',
233
+ $filter: {
234
+ query: {
235
+ host: { $in: [host, '*'] },
236
+ pathname: file.pathname
237
+ },
238
+ limit: 1
239
+ },
240
+ organization_id: file.organization_id
241
+ }
242
+ data = await this.crud.send(data)
243
+ if (data.object && data.object.length)
244
+ return data.object[0]
245
+ else
246
+ return file
247
+ }
248
+
249
+ createSitemap(type) {
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
+ }
268
+ }
269
+
270
+ async saveSitemap(file, host) {
271
+ let data = {
272
+ method: 'object.update',
273
+ host: host,
274
+ array: 'files',
275
+ object: file,
276
+ upsert: true,
277
+ organization_id: file.organization_id
278
+ }
279
+ if (!file._id)
280
+ data.$filter = {
281
+ query: {
282
+ host: { $in: [host, '*'] },
283
+ pathname: file.pathname
284
+ },
285
+ limit: 1
286
+ }
287
+
288
+ data = await this.crud.send(data)
289
+
290
+ }
291
+
292
+ async getLastSitemapIndex(mainSitemap, filename) {
293
+ try {
294
+ // Use regex to match all sitemap entries for the given type
295
+ const regex = new RegExp(`\\/${filename}(\\d*)\\.xml<\\/loc>`, 'g');
296
+ const matches = mainSitemap.src.match(regex);
297
+
298
+ return matches ? matches.length : null;
299
+ } catch (err) {
300
+ console.error(`Error determining next sitemap index for ${filename}:`, err);
301
+ return null; // Or some default value or throw an error
302
+ }
303
+ }
304
+
305
+ checkSitemap(sitemap) {
306
+ try {
307
+ if (!sitemap)
308
+ return false
309
+
310
+ // Count the number of <url> entries
311
+ const urlCount = (sitemap.match(/<url>/g) || []).length;
312
+ if (urlCount >= 50000)
313
+ return false;
314
+
315
+ // Get the size of the sitemap string in bytes
316
+ const fileSizeInBytes = Buffer.byteLength(sitemap, 'utf8');
317
+ const fileSizeInMB = fileSizeInBytes / (1024 * 1024);
318
+
319
+ // console.log(`Sitemap has ${urlCount} entries and is ${fileSizeInMB.toFixed(2)} MB.`);
320
+
321
+ // Check if the file size exceeds either the 50MB limit or the MongoDB 16MB limit
322
+ if (fileSizeInMB >= 50 || fileSizeInMB >= 15)
323
+ return false;
324
+
325
+ return true;
326
+ } catch (err) {
327
+ console.error('Error checking sitemap file:', err);
328
+ return false;
329
+ }
330
+ }
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
+ }
70
427
  }
71
428
 
72
429
  module.exports = CoCreateSitemap;