@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.
- package/CHANGELOG.md +21 -0
- package/package.json +5 -2
- 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
|
|
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'
|
|
39
|
-
|
|
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
|
-
|
|
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.
|
|
67
|
+
if (file.pathname) {
|
|
59
68
|
// Perform regex search starting at the pathname
|
|
60
|
-
const regexPattern = `<url>\\s*<loc>.*?${file.
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
if (typeof value === 'object' && value !== null) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
for (
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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 :
|
|
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;
|