@cocreate/sitemap 1.4.1 → 1.6.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.
@@ -1,8 +1,10 @@
1
1
  name: Automated Workflow
2
+
2
3
  on:
3
4
  push:
4
5
  branches:
5
6
  - master
7
+
6
8
  jobs:
7
9
  about:
8
10
  runs-on: ubuntu-latest
@@ -18,52 +20,37 @@ jobs:
18
20
  with:
19
21
  direction: overwrite-github
20
22
  githubToken: "${{ secrets.GITHUB }}"
23
+
21
24
  release:
22
25
  runs-on: ubuntu-latest
23
26
  steps:
24
27
  - name: Checkout
25
- uses: actions/checkout@v3
26
- - name: Setup Node.js
27
- uses: actions/setup-node@v3
28
+ uses: actions/checkout@v4
28
29
  with:
29
- node-version: 14
30
- - name: Semantic Release
31
- uses: cycjimmy/semantic-release-action@v3
32
- id: semantic
30
+ fetch-depth: 0 # Required so semantic-release can trace git tags/history
31
+
32
+ - name: Setup Node.js
33
+ uses: actions/setup-node@v4
33
34
  with:
34
- extra_plugins: |
35
- @semantic-release/changelog
35
+ node-version: 22
36
+
37
+ - name: Install Semantic Release & Plugins
38
+ # Installs semantic-release and its plugins on the runner
39
+ run: |
40
+ npm install -g semantic-release \
41
+ @semantic-release/changelog \
42
+ @semantic-release/npm \
43
+ @semantic-release/github \
36
44
  @semantic-release/git
37
- @semantic-release/github
45
+
46
+ - name: Run Semantic Release (Native)
47
+ id: semantic
48
+ # This will automatically pick up your export default config file in the repository root
49
+ run: npx semantic-release
38
50
  env:
39
- GITHUB_TOKEN: "${{ secrets.GITHUB }}"
51
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
40
52
  NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
53
+
41
54
  outputs:
42
55
  new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
43
- new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
44
- upload:
45
- runs-on: ubuntu-latest
46
- needs: release
47
- if: needs.release.outputs.new_release_published == 'true'
48
- env:
49
- VERSION: "${{ needs.release.outputs.new_release_version }}"
50
- steps:
51
- - name: Checkout
52
- uses: actions/checkout@v3
53
- - name: Setup Node.js
54
- uses: actions/setup-node@v3
55
- with:
56
- node-version: 16
57
- - name: Set npm registry auth
58
- run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
59
- - name: Install dependencies
60
- run: yarn install
61
-
62
- - name: Set Environment Variables
63
- run: |
64
- echo "organization_id=${{ secrets.COCREATE_ORGANIZATION_ID }}" >> $GITHUB_ENV
65
- echo "key=${{ secrets.COCREATE_KEY }}" >> $GITHUB_ENV
66
- echo "host=${{ secrets.COCREATE_HOST }}" >> $GITHUB_ENV
67
- - name: CoCreate Upload
68
- run: coc upload
69
-
56
+ new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ # [1.6.0](https://github.com/CoCreate-app/CoCreate-sitemap/compare/v1.5.0...v1.6.0) (2026-07-17)
2
+
3
+
4
+ ### Features
5
+
6
+ * update automated workflow and release configuration for improved semantic release handling ([fd3183f](https://github.com/CoCreate-app/CoCreate-sitemap/commit/fd3183f36440cdc6ef003611161bcec483c094f7))
7
+
8
+ # [1.5.0](https://github.com/CoCreate-app/CoCreate-sitemap/compare/v1.4.2...v1.5.0) (2026-07-14)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * removed post install ([65d5ea3](https://github.com/CoCreate-app/CoCreate-sitemap/commit/65d5ea331baf439992fab9c1dce5a95d7210e32e))
14
+ * semantic version handling. Reorganize .gitignore for improved clarity and structure ([63fa105](https://github.com/CoCreate-app/CoCreate-sitemap/commit/63fa105fe480103454c88f23d210585053190179))
15
+ * update .gitignore to include package-lock.json and pnpm-lock.yaml ([9bab5d7](https://github.com/CoCreate-app/CoCreate-sitemap/commit/9bab5d740c465515755a5daac62a55c697375b3d))
16
+ * update module export to ES6 syntax in release.config.js ([4096874](https://github.com/CoCreate-app/CoCreate-sitemap/commit/409687476de524c2bc1c88882031a8c43547c0e3))
17
+
18
+
19
+ ### Features
20
+
21
+ * update package.json and refactor index.js to use ES modules ([a951277](https://github.com/CoCreate-app/CoCreate-sitemap/commit/a9512776ddd5f74bf9f9aab0ce947b80b210f6b7))
22
+
23
+ ## [1.4.2](https://github.com/CoCreate-app/CoCreate-sitemap/compare/v1.4.1...v1.4.2) (2025-09-01)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * added ToDo comment for future implementation ([b146feb](https://github.com/CoCreate-app/CoCreate-sitemap/commit/b146feb30816b49455143ee6adc5a0395b4f37de))
29
+
1
30
  ## [1.4.1](https://github.com/CoCreate-app/CoCreate-sitemap/compare/v1.4.0...v1.4.1) (2025-05-01)
2
31
 
3
32
 
package/package.json CHANGED
@@ -1,18 +1,10 @@
1
1
  {
2
2
  "name": "@cocreate/sitemap",
3
- "version": "1.4.1",
3
+ "version": "1.6.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",
7
- "cocreate",
8
- "low-code-framework",
9
- "no-code-framework",
10
- "cocreatejs",
11
- "cocreatejs-component",
12
- "cocreate-framework",
13
- "no-code",
14
7
  "low-code",
15
- "collaborative-framework",
16
8
  "realtime",
17
9
  "realtime-framework",
18
10
  "collaboration",
@@ -26,8 +18,7 @@
26
18
  "scripts": {
27
19
  "start": "npx webpack --config webpack.config.js",
28
20
  "build": "npx webpack --mode=production --config webpack.config.js",
29
- "dev": "npx webpack --config webpack.config.js --watch",
30
- "postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('coc --version', { stdio: 'ignore' }); } catch (error) { try { execSync('npm install -g @cocreate/cli', { stdio: 'inherit' }); console.log('Installed \"@cocreate/cli\" globally.'); } catch (error) { console.error('Failed to install \"@cocreate/cli\" globally:', error); } }\""
21
+ "dev": "npx webpack --config webpack.config.js --watch"
31
22
  },
32
23
  "repository": {
33
24
  "type": "git",
@@ -43,6 +34,7 @@
43
34
  "type": "GitHub Sponsors ❤",
44
35
  "url": "https://github.com/sponsors/CoCreate-app"
45
36
  },
37
+ "type": "module",
46
38
  "main": "./src/index.js",
47
39
  "dependencies": {
48
40
  "node-html-parser": "^6.1.13"
package/release.config.js CHANGED
@@ -1,4 +1,4 @@
1
- module.exports = {
1
+ export default {
2
2
  dryRun: false,
3
3
  branches: ["master"],
4
4
  plugins: [
@@ -10,12 +10,20 @@ module.exports = {
10
10
  changelogFile: "CHANGELOG.md",
11
11
  },
12
12
  ],
13
- "@semantic-release/npm",
14
- "@semantic-release/github",
13
+ "@semantic-release/npm",
15
14
  [
16
- "@semantic-release/git",
15
+ "@semantic-release/github",
17
16
  {
17
+ successComment: false,
18
+ failTitle: false,
19
+ },
20
+ ],
21
+ [
22
+ "@semantic-release/git",
23
+ {
24
+ // Only stage and commit the changelog and package.json
18
25
  assets: ["CHANGELOG.md", "package.json"],
26
+ message: "chore(release): ${nextRelease.version} [skip ci]",
19
27
  },
20
28
  ],
21
29
  ],
package/src/index.js CHANGED
@@ -1,479 +1,530 @@
1
- /********************************************************************************
2
- * Copyright (C) 2023 CoCreate and Contributors.
3
- *
4
- * This program is free software: you can redistribute it and/or modify
5
- * it under the terms of the GNU Affero General Public License as published
6
- * by the Free Software Foundation, either version 3 of the License, or
7
- * (at your option) any later version.
8
- *
9
- * This program is distributed in the hope that it will be useful,
10
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- * GNU Affero General Public License for more details.
13
- *
14
- * You should have received a copy of the GNU Affero General Public License
15
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
- ********************************************************************************/
17
-
18
- // Commercial Licensing Information:
19
- // For commercial use of this software without the copyleft provisions of the AGPLv3,
20
- // you must obtain a commercial license from CoCreate LLC.
21
- // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
22
-
23
- const { parse } = require("node-html-parser");
24
-
25
- class CoCreateSitemap {
26
- constructor(crud) {
27
- this.crud = crud;
28
- }
29
-
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')
1
+ import { parse } from "node-html-parser";
2
+
3
+ /**
4
+ * Clean, stateless functional entry point to check and verify a file against the sitemap.
5
+ * All dependencies are passed explicitly via the parameters object.
6
+ * * @param {Object} options
7
+ * @param {Object} options.file - The file object being verified
8
+ * @param {string} options.host - The active website hostname
9
+ * @param {Object} options.crud - Crud database instance
10
+ */
11
+ export async function check({ file, host, crud }) {
12
+ // Ensure the file is HTML and does not have a sitemap object yet
13
+ if (!file.sitemap && file["content-type"] !== "text/html") return;
14
+
15
+ // Ensure the file is public and is not sitemap false
16
+ if (
17
+ file.sitemap === false ||
18
+ file.sitemap === "false" ||
19
+ !file.public ||
20
+ file.public === "false"
21
+ )
22
+ return;
23
+
24
+ // Check if the file is HTML and contains a noindex meta or title tag
25
+ if (file["content-type"] === "text/html") {
26
+ if (
27
+ /<meta\s+name=["']robots["']\s+content=["'][^"']*noindex[^"']*["']/i.test(
28
+ file.src
29
+ )
30
+ )
33
31
  return;
32
+ if (!/<title[^>]*>[\s\S]*?<\/title>/i.test(file.src)) return;
33
+ }
34
34
 
35
- // Ensure the file is public and is not sitemap false
36
- if (file.sitemap === false || file.sitemap === 'false' || !file.public || file.public === "false")
35
+ // Compare the lastmod date in the sitemap with the modified.on date
36
+ if (file.sitemap && file.sitemap.lastmod && file.modified?.on) {
37
+ if (new Date(file.sitemap.lastmod) >= new Date(file.modified.on))
37
38
  return;
39
+ }
38
40
 
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
- }
46
-
47
- // Compare the lastmod date in the sitemap with the modified.on date
48
- if (file.sitemap && file.sitemap.lastmod && file.modified.on) {
49
- if (new Date(file.sitemap.lastmod) >= new Date(file.modified.on))
50
- return;
51
- }
41
+ // Logic to update the sitemap asynchronously
42
+ await updateSitemap({ file, host, crud });
43
+ }
52
44
 
53
- // Logic to update the sitemap
54
- this.updateSitemap(file, host);
55
- }
45
+ // ==========================================
46
+ // Stateless Private Helper Functions
47
+ // ==========================================
56
48
 
57
- async updateSitemap(file, host) {
58
- try {
59
- if (!file.sitemap)
60
- file.sitemap = {}
49
+ async function updateSitemap({ file, host, crud }) {
50
+ try {
51
+ if (!file.sitemap) file.sitemap = {};
61
52
 
62
- const entry = this.createEntry(file, host);
53
+ const entry = createEntry(file);
63
54
 
64
- let { mainSitemap, sitemap } = await this.getSitemap(file, host);
55
+ let { mainSitemap, sitemap } = await getSitemap({ file, host, crud });
65
56
 
66
- if (file.pathname) {
67
- // Perform regex search starting at the pathname
68
- const regexPattern = `<url>\\s*<loc>.*?${file.pathname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*?</loc>[\\s\\S]*?</url>`;
69
- const match = sitemap.src.match(new RegExp(regexPattern));
57
+ if (file.pathname) {
58
+ // Perform regex search starting at the pathname
59
+ const regexPattern = `<url>\\s*<loc>.*?${file.pathname.replace(
60
+ /[.*+?^${}()|[\]\\]/g,
61
+ "\\$&"
62
+ )}.*?</loc>[\\s\\S]*?</url>`;
63
+ const match = sitemap.src.match(new RegExp(regexPattern));
70
64
 
71
- if (match) {
72
- const position = match.index; // Start position of the <url> block
73
- const endPosition = match.index + match[0].length; // End position of the <url> block
65
+ if (match) {
66
+ const position = match.index; // Start position of the <url> block
67
+ const endPosition = match.index + match[0].length; // End position of the <url> block
74
68
 
75
- // Replace the original <url> block with the modified one
76
- sitemap.src = sitemap.src.slice(0, position) + entry + sitemap.src.slice(endPosition);
77
- } else {
78
- sitemap.src = sitemap.src.replace('</urlset>', `${entry}</urlset>`);
79
- }
69
+ // Replace the original <url> block with the modified one
70
+ sitemap.src =
71
+ sitemap.src.slice(0, position) +
72
+ entry +
73
+ sitemap.src.slice(endPosition);
80
74
  } else {
81
- file.sitemap.pathname = sitemap.pathname
82
- sitemap.src = sitemap.src.replace('</urlset>', `${entry}</urlset>`);
75
+ sitemap.src = sitemap.src.replace(
76
+ "</urlset>",
77
+ `${entry}</urlset>`
78
+ );
83
79
  }
84
-
85
- this.saveSitemap(mainSitemap, host);
86
- this.saveSitemap(sitemap, host);
87
- this.saveSitemap(file, host);
88
-
89
- // console.log('Sitemap updated successfully.');
90
- } catch (err) {
91
- console.error('Error updating sitemap:', err);
80
+ } else {
81
+ file.sitemap.pathname = sitemap.pathname;
82
+ sitemap.src = sitemap.src.replace(
83
+ "</urlset>",
84
+ `${entry}</urlset>`
85
+ );
92
86
  }
93
- }
94
-
95
- createEntry(file) {
96
- file.sitemap.loc = file.pathname;
97
- file.sitemap.lastmod = file.modified.on;
98
87
 
99
- if (file['content-type'] === 'text/html') {
100
- this.parseHtml(file)
88
+ await saveSitemap({ file: mainSitemap, host, crud });
89
+ await saveSitemap({ file: sitemap, host, crud });
90
+ await saveSitemap({ file, host, crud });
101
91
 
102
- if (file.sitemap.type !== 'news' && file.sitemap.type !== 'image' && file.sitemap.type !== 'video') {
103
- if (!file.sitemap.changefreq)
104
- file.sitemap.changefreq = 'monthly';
92
+ } catch (err) {
93
+ console.error("Error updating sitemap:", err);
94
+ }
95
+ }
105
96
 
106
- if (!file.sitemap.priority) {
107
- const depth = (file.pathname.match(/\//g) || []).length;
108
- file.sitemap.priority = Math.max(0.1, 1.0 - (depth - 1) * 0.1).toFixed(1);
109
- }
110
- } else {
111
- delete file.sitemap.changefreq
112
- delete file.sitemap.priority
97
+ function createEntry(file) {
98
+ file.sitemap.loc = file.pathname;
99
+ file.sitemap.lastmod = file.modified?.on;
100
+
101
+ if (file["content-type"] === "text/html") {
102
+ parseHtml(file);
103
+
104
+ if (
105
+ file.sitemap.type !== "news" &&
106
+ file.sitemap.type !== "image" &&
107
+ file.sitemap.type !== "video"
108
+ ) {
109
+ if (!file.sitemap.changefreq)
110
+ file.sitemap.changefreq = "monthly";
111
+
112
+ if (!file.sitemap.priority) {
113
+ const depth = (file.pathname.match(/\//g) || []).length;
114
+ file.sitemap.priority = Math.max(
115
+ 0.1,
116
+ 1.0 - (depth - 1) * 0.1
117
+ ).toFixed(1);
113
118
  }
119
+ } else {
120
+ delete file.sitemap.changefreq;
121
+ delete file.sitemap.priority;
114
122
  }
123
+ }
115
124
 
116
- let entry = `\t<url>\n`;
117
-
118
- for (const key of Object.keys(file.sitemap)) {
119
- if (key === 'pathname' || key === 'type')
120
- continue
121
-
122
- let value = file.sitemap[key];
123
-
124
- if (typeof value === 'object' && value !== null && !(value instanceof Date)) {
125
- if (!Array.isArray(value))
126
- value = [value]
127
-
128
- for (let i = 0; i < value.length; i++) {
129
- entry += `\t\t<${key}:${key}>\n`;
130
-
131
- for (const nestedKey of Object.keys(value[i])) {
132
- let nestedValue = value[i][nestedKey];
133
- // Handle nested objects
134
- if (typeof nestedValue === 'object' && nestedValue !== null && !(nestedValue instanceof Date)) {
135
- entry += `\t\t\t<${key}:${nestedKey}>\n`;
136
- for (const subKey of Object.keys(nestedValue)) {
137
- const subValue = nestedValue[subKey];
138
- entry += `\t\t\t\t<${key}:${subKey}>${subValue}</${key}:${subKey}>\n`;
125
+ let entry = `\t<url>\n`;
126
+
127
+ for (const key of Object.keys(file.sitemap)) {
128
+ if (key === "pathname" || key === "type") continue;
129
+
130
+ let value = file.sitemap[key];
131
+
132
+ if (
133
+ typeof value === "object" &&
134
+ value !== null &&
135
+ !(value instanceof Date)
136
+ ) {
137
+ if (!Array.isArray(value)) value = [value];
138
+
139
+ for (let i = 0; i < value.length; i++) {
140
+ entry += `\t\t<${key}:${key}>\n`;
141
+
142
+ for (const nestedKey of Object.keys(value[i])) {
143
+ let nestedValue = value[i][nestedKey];
144
+ // Handle nested objects
145
+ if (
146
+ typeof nestedValue === "object" &&
147
+ nestedValue !== null &&
148
+ !(nestedValue instanceof Date)
149
+ ) {
150
+ entry += `\t\t\t<${key}:${nestedKey}>\n`;
151
+ for (const subKey of Object.keys(nestedValue)) {
152
+ const subValue = nestedValue[subKey];
153
+ entry += `\t\t\t\t<${key}:${subKey}>${subValue}</${key}:${subKey}>\n`;
154
+ }
155
+ entry += `\t\t\t</${key}:${nestedKey}>\n`;
156
+ } else {
157
+ if (nestedKey === "loc") {
158
+ if (
159
+ !nestedValue.startsWith("https://") &&
160
+ !nestedValue.startsWith("http://") &&
161
+ !nestedValue.startsWith("{{$host}}")
162
+ ) {
163
+ nestedValue = `{{$host}}${nestedValue}`;
139
164
  }
140
- entry += `\t\t\t</${key}:${nestedKey}>\n`;
165
+ } else if (nestedKey === "publication_date") {
166
+ nestedValue =
167
+ new Date(nestedValue)
168
+ .toISOString()
169
+ .split(".")[0] + "Z";
141
170
  } else {
142
- if (nestedKey === 'loc') {
143
- if (!nestedValue.startsWith('https://') && !nestedValue.startsWith('http://') && !nestedValue.startsWith('{{$host}}')) {
144
- nestedValue = `{{$host}}${nestedValue}`;
145
- }
146
- } else if (nestedKey === 'publication_date') {
147
- nestedValue = new Date(nestedValue).toISOString().split('.')[0] + "Z";
148
- } else {
149
- nestedValue = this.encodeXML(nestedValue)
150
- }
151
-
152
- entry += `\t\t\t<${key}:${nestedKey}>${nestedValue}</${key}:${nestedKey}>\n`;
171
+ nestedValue = encodeXML(nestedValue);
153
172
  }
154
- }
155
173
 
156
- entry += `\t\t</${key}:${key}>\n`;
157
- }
158
- } else {
159
- if (key === 'loc') {
160
- if (!value.startsWith('https://') && !value.startsWith('http://')) {
161
- value = `{{$host}}${value}`;
174
+ entry += `\t\t\t<${key}:${nestedKey}>${nestedValue}</${key}:${nestedKey}>\n`;
162
175
  }
163
- } else if (key === 'lastmod') {
164
- value = new Date(file.modified.on).toISOString().split('.')[0] + "Z";
165
- } else {
166
- value = this.encodeXML(value)
167
176
  }
168
177
 
169
- entry += `\t\t<${key}>${value}</${key}>\n`;
178
+ entry += `\t\t</${key}:${key}>\n`;
170
179
  }
171
- }
172
-
173
- entry += `\t</url>\n`;
174
-
175
- return entry;
176
- }
177
-
178
- async getSitemap(file, host) {
179
- let mainSitemap = {
180
- host: file.host,
181
- name: 'sitemap.xml',
182
- path: '/',
183
- pathname: '/sitemap.xml',
184
- directory: '/',
185
- 'content-type': 'application/xml',
186
- public: true,
187
- organization_id: file.organization_id
188
- }
189
-
190
- mainSitemap = await this.readSitemap(mainSitemap, host);
191
- if (!mainSitemap.src)
192
- mainSitemap.src = this.createSitemap('main')
193
-
194
- let sitemap = {
195
- host: file.host,
196
- path: '/',
197
- pathname: file.sitemap.pathname,
198
- directory: '/',
199
- 'content-type': 'application/xml',
200
- public: true,
201
- organization_id: file.organization_id
202
- }
203
-
204
- // Update loc using pathname
205
- file.sitemap.loc = `${file.pathname}`
206
-
207
- // Query the database for the correct sitemap based on the loc and type
208
- if (file.sitemap.pathname) {
209
- sitemap = await this.readSitemap(sitemap, host);
210
- }
211
-
212
- if (!sitemap.src) {
213
- let type = 'sitemap';
214
-
215
- // Identify content type to determine sitemap type
216
- if (file['content-type'].startsWith('image/')) {
217
- type = 'image';
218
- } else if (file['content-type'].startsWith('video/')) {
219
- type = 'video';
220
- } else if (file.sitemap.type === 'news') {
221
- type = 'news';
222
- }
223
-
224
- let name = `sitemap`
225
- if (type === 'image' || type === 'video' || type === 'news')
226
- name = `sitemap-${type}`
227
-
228
- // If no existing sitemap found check last index sitemap
229
- let index = await this.getLastSitemapIndex(mainSitemap, name);
230
- if (index) {
231
- sitemap.pathname = `/${name}${index}.xml`
232
- sitemap = await this.readSitemap(sitemap, host);
180
+ } else {
181
+ if (key === "loc") {
182
+ if (
183
+ !value.startsWith("https://") &&
184
+ !value.startsWith("http://")
185
+ ) {
186
+ value = `{{$host}}${value}`;
187
+ }
188
+ } else if (key === "lastmod") {
189
+ value =
190
+ new Date(file.modified?.on).toISOString().split(".")[0] +
191
+ "Z";
233
192
  } else {
234
- index = 1
193
+ value = encodeXML(value);
235
194
  }
236
195
 
237
- // Check if there's room in the last index sitemap
238
- if (!this.checkSitemap(sitemap.src)) {
239
- if (sitemap.src)
240
- index += 1
241
- else
242
- sitemap.src = this.createSitemap(type);
243
-
244
- sitemap.name = `${name}${index}.xml`
245
- sitemap.pathname = `/${name}${index}.xml`
196
+ entry += `\t\t<${key}>${value}</${key}>\n`;
197
+ }
198
+ }
246
199
 
247
- }
200
+ entry += `\t</url>\n`;
248
201
 
249
- }
202
+ return entry;
203
+ }
250
204
 
251
- // Create the regex pattern to match the <sitemap> block containing the specific <loc> for the pathname
252
- const regexPattern = `<sitemap>\\s*<loc>[^<]*${sitemap.pathname}[^<]*</loc>[\\s\\S]*?</sitemap>`;
205
+ async function getSitemap({ file, host, crud }) {
206
+ let mainSitemap = {
207
+ host: file.host,
208
+ name: "sitemap.xml",
209
+ path: "/",
210
+ pathname: "/sitemap.xml",
211
+ directory: "/",
212
+ "content-type": "application/xml",
213
+ public: true,
214
+ organization_id: file.organization_id
215
+ };
216
+
217
+ mainSitemap = await readSitemap({ file: mainSitemap, host, crud });
218
+ if (!mainSitemap.src) mainSitemap.src = generateSitemapXml("main");
219
+
220
+ let sitemap = {
221
+ host: file.host,
222
+ path: "/",
223
+ pathname: file.sitemap.pathname,
224
+ directory: "/",
225
+ "content-type": "application/xml",
226
+ public: true,
227
+ organization_id: file.organization_id
228
+ };
229
+
230
+ // Update loc using pathname
231
+ file.sitemap.loc = `${file.pathname}`;
232
+
233
+ // Query the database for the correct sitemap based on the loc and type
234
+ if (file.sitemap.pathname) {
235
+ sitemap = await readSitemap({ file: sitemap, host, crud });
236
+ }
253
237
 
254
- // Execute the regex match against the sitemap index source
255
- const match = mainSitemap.src.match(new RegExp(regexPattern));
238
+ if (!sitemap.src) {
239
+ let type = "sitemap";
256
240
 
257
- // Check if a match is found
258
- if (!match) {
259
- //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.
260
- const indexEntry = `\t<sitemap>\n\t\t<loc>{{$host}}${sitemap.pathname}</loc>\n</sitemap>`;
261
- mainSitemap.src = mainSitemap.src.replace('</sitemapindex>', `${indexEntry}\n</sitemapindex>`);
241
+ // Identify content type to determine sitemap type
242
+ if (file["content-type"].startsWith("image/")) {
243
+ type = "image";
244
+ } else if (file["content-type"].startsWith("video/")) {
245
+ type = "video";
246
+ } else if (file.sitemap.type === "news") {
247
+ type = "news";
262
248
  }
263
249
 
264
- return { mainSitemap, sitemap }
265
- }
250
+ let name = `sitemap`;
251
+ if (type === "image" || type === "video" || type === "news")
252
+ name = `sitemap-${type}`;
266
253
 
267
- async readSitemap(file, host) {
268
- let data = {
269
- method: 'object.read',
270
- host: host,
271
- array: 'files',
272
- $filter: {
273
- query: {
274
- host: { $in: [host, '*'] },
275
- pathname: file.pathname
276
- },
277
- limit: 1
278
- },
279
- organization_id: file.organization_id
254
+ // If no existing sitemap found check last index sitemap
255
+ let index = await getLastSitemapIndex(mainSitemap, name);
256
+ if (index) {
257
+ sitemap.pathname = `/${name}${index}.xml`;
258
+ sitemap = await readSitemap({ file: sitemap, host, crud });
259
+ } else {
260
+ index = 1;
280
261
  }
281
- data = await this.crud.send(data)
282
- if (data.object && data.object.length)
283
- return data.object[0]
284
- else
285
- return file
286
- }
287
262
 
288
- createSitemap(type) {
289
- const xmlDeclaration = `<?xml version="1.0" encoding="UTF-8"?>\n`;
290
- const sitemapNamespace = 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
263
+ // Check if there's room in the last index sitemap
264
+ if (!checkSitemapXml(sitemap.src)) {
265
+ if (sitemap.src) index += 1;
266
+ else sitemap.src = generateSitemapXml(type);
291
267
 
292
- if (type === 'main') {
293
- return `${xmlDeclaration}<sitemapindex ${sitemapNamespace}>\n</sitemapindex>`;
294
- } else {
295
- const imageNamespace = 'xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"';
296
- const videoNamespace = 'xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"';
297
- const newsNamespace = 'xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"';
298
-
299
- if (type === 'image') {
300
- return `${xmlDeclaration}<urlset ${sitemapNamespace} ${imageNamespace}>\n</urlset>`;
301
- } else if (type === 'video') {
302
- return `${xmlDeclaration}<urlset ${sitemapNamespace} ${videoNamespace}>\n</urlset>`;
303
- } else { // For 'news' type or any other types
304
- return `${xmlDeclaration}<urlset ${sitemapNamespace} ${newsNamespace} ${imageNamespace} ${videoNamespace}>\n</urlset>`;
305
- }
268
+ sitemap.name = `${name}${index}.xml`;
269
+ sitemap.pathname = `/${name}${index}.xml`;
306
270
  }
307
271
  }
308
272
 
309
- async saveSitemap(file, host) {
310
- let data = {
311
- method: 'object.update',
312
- host: host,
313
- array: 'files',
314
- object: file,
315
- upsert: true,
316
- organization_id: file.organization_id
317
- }
318
- if (!file._id)
319
- data.$filter = {
320
- query: {
321
- host: { $in: [host, '*'] },
322
- pathname: file.pathname
323
- },
324
- limit: 1
325
- }
273
+ // Create the regex pattern to match the <sitemap> block containing the specific <loc> for the pathname
274
+ const regexPattern = `<sitemap>\\s*<loc>[^<]*${sitemap.pathname}[^<]*</loc>[\\s\\S]*?</sitemap>`;
326
275
 
327
- data = await this.crud.send(data)
276
+ // Execute the regex match against the sitemap index source
277
+ const match = mainSitemap.src.match(new RegExp(regexPattern));
328
278
 
279
+ // Check if a match is found
280
+ if (!match) {
281
+ const indexEntry = `\t<sitemap>\n\t\t<loc>{{$host}}${sitemap.pathname}</loc>\n</sitemap>`;
282
+ mainSitemap.src = mainSitemap.src.replace(
283
+ "</sitemapindex>",
284
+ `${indexEntry}\n</sitemapindex>`
285
+ );
329
286
  }
330
287
 
331
- async getLastSitemapIndex(mainSitemap, filename) {
332
- try {
333
- // Use regex to match all sitemap entries for the given type
334
- const regex = new RegExp(`\\/${filename}(\\d*)\\.xml<\\/loc>`, 'g');
335
- const matches = mainSitemap.src.match(regex);
288
+ return { mainSitemap, sitemap };
289
+ }
336
290
 
337
- return matches ? matches.length : null;
338
- } catch (err) {
339
- console.error(`Error determining next sitemap index for ${filename}:`, err);
340
- return null; // Or some default value or throw an error
291
+ async function readSitemap({ file, host, crud }) {
292
+ let data = {
293
+ method: "object.read",
294
+ host: host,
295
+ array: "files",
296
+ $filter: {
297
+ query: {
298
+ host: { $in: [host, "*"] },
299
+ pathname: file.pathname
300
+ },
301
+ limit: 1
302
+ },
303
+ organization_id: file.organization_id
304
+ };
305
+ data = await crud.send(data);
306
+ if (data.object && data.object.length) return data.object[0];
307
+ else return file;
308
+ }
309
+
310
+ function generateSitemapXml(type) {
311
+ const xmlDeclaration = `<?xml version="1.0" encoding="UTF-8"?>\n`;
312
+ const sitemapNamespace =
313
+ 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
314
+
315
+ if (type === "main") {
316
+ return `${xmlDeclaration}<sitemapindex ${sitemapNamespace}>\n</sitemapindex>`;
317
+ } else {
318
+ const imageNamespace =
319
+ 'xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"';
320
+ const videoNamespace =
321
+ 'xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"';
322
+ const newsNamespace =
323
+ 'xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"';
324
+
325
+ if (type === "image") {
326
+ return `${xmlDeclaration}<urlset ${sitemapNamespace} ${imageNamespace}>\n</urlset>`;
327
+ } else if (type === "video") {
328
+ return `${xmlDeclaration}<urlset ${sitemapNamespace} ${videoNamespace}>\n</urlset>`;
329
+ } else {
330
+ // For 'news' type or any other types
331
+ return `${xmlDeclaration}<urlset ${sitemapNamespace} ${newsNamespace} ${imageNamespace} ${videoNamespace}>\n</urlset>`;
341
332
  }
342
333
  }
334
+ }
343
335
 
344
- checkSitemap(sitemap) {
345
- try {
346
- if (!sitemap)
347
- return false
348
-
349
- // Count the number of <url> entries
350
- const urlCount = (sitemap.match(/<url>/g) || []).length;
351
- if (urlCount >= 50000)
352
- return false;
353
-
354
- // Get the size of the sitemap string in bytes
355
- const fileSizeInBytes = Buffer.byteLength(sitemap, 'utf8');
356
- const fileSizeInMB = fileSizeInBytes / (1024 * 1024);
357
-
358
- // console.log(`Sitemap has ${urlCount} entries and is ${fileSizeInMB.toFixed(2)} MB.`);
336
+ async function saveSitemap({ file, host, crud }) {
337
+ let data = {
338
+ method: "object.update",
339
+ host: host,
340
+ array: "files",
341
+ object: file,
342
+ upsert: true,
343
+ organization_id: file.organization_id
344
+ };
345
+ if (!file._id)
346
+ data.$filter = {
347
+ query: {
348
+ host: { $in: [host, "*"] },
349
+ pathname: file.pathname
350
+ },
351
+ limit: 1
352
+ };
359
353
 
360
- // Check if the file size exceeds either the 50MB limit or the MongoDB 16MB limit
361
- if (fileSizeInMB >= 50 || fileSizeInMB >= 15)
362
- return false;
354
+ await crud.send(data);
355
+ }
363
356
 
364
- return true;
365
- } catch (err) {
366
- console.error('Error checking sitemap file:', err);
367
- return false;
368
- }
357
+ async function getLastSitemapIndex(mainSitemap, filename) {
358
+ try {
359
+ // Use regex to match all sitemap entries for the given type
360
+ const regex = new RegExp(`\\/${filename}(\\d*)\\.xml<\\/loc>`, "g");
361
+ const matches = mainSitemap.src.match(regex);
362
+
363
+ return matches ? matches.length : null;
364
+ } catch (err) {
365
+ console.error(
366
+ `Error determining next sitemap index for ${filename}:`,
367
+ err
368
+ );
369
+ return null;
369
370
  }
371
+ }
370
372
 
371
- parseHtml(file) {
372
- const dom = parse(file.src);
373
- const entries = dom.querySelectorAll('[sitemap="true"]');
373
+ function checkSitemapXml(sitemap) {
374
+ try {
375
+ if (!sitemap) return false;
374
376
 
375
- let types = ['image', 'video', 'news']
377
+ // Count the number of <url> entries
378
+ const urlCount = (sitemap.match(/<url>/g) || []).length;
379
+ if (urlCount >= 50000) return false;
376
380
 
377
- const previousEntries = {}
378
- for (let i = 0; i < types.length; i++) {
379
- if (!file.sitemap[types[i]])
380
- continue
381
- if (Array.isArray(file.sitemap[types[i]]))
382
- previousEntries[types[i]] = file.sitemap[types[i]]
383
- else
384
- previousEntries[types[i]] = [file.sitemap[types[i]]]
381
+ // Get the size of the sitemap string in bytes
382
+ const fileSizeInBytes = Buffer.byteLength(sitemap, "utf8");
383
+ const fileSizeInMB = fileSizeInBytes / (1024 * 1024);
385
384
 
386
- delete file.sitemap[types[i]]
387
- }
385
+ // Check if the file size exceeds either the 50MB limit or the MongoDB 16MB limit
386
+ if (fileSizeInMB >= 50 || fileSizeInMB >= 15) return false;
388
387
 
389
- for (let i = 0; i < entries.length; i++) {
390
- let type = '', query = '';
391
- let existingObject
392
- let entryObject = {};
393
-
394
- if (entries[i].tagName === 'IMG') { // Corrected to 'IMG' for images
395
- type = 'image';
396
- query = 'loc'
397
- entryObject.loc = entries[i].getAttribute('src');
398
- entryObject.title = entries[i].getAttribute('sitemap-title') || entries[i].getAttribute('title') || entries[i].getAttribute('alt');
399
- entryObject.caption = entries[i].getAttribute('sitemap-caption') || entries[i].getAttribute('alt') || entryObject.title;
400
- entryObject.geo_location = entries[i].getAttribute('sitemap-geo-location');
401
- } else if (entries[i].tagName === 'VIDEO') {
402
- type = 'video';
403
- query = 'content_loc'
404
- entryObject.content_loc = entries[i].src;
405
- entryObject.title = entries[i].getAttribute('sitemap-title') || entries[i].getAttribute('title');
406
- entryObject.description = entries[i].getAttribute('description'); // 'description' if available
407
- entryObject.thumbnail_loc = entries[i].getAttribute('sitemap-thumbnail') || entries[i].getAttribute('poster');
408
- entryObject.duration = entries[i].getAttribute('sitemap-duration');
409
- } else {
410
- type = 'news';
411
- file.sitemap.type = 'news';
412
- query = 'title'
413
- entryObject.title = entries[i].getAttribute('sitemap-title');
414
- if (!entryObject.title) {
415
- const title = dom.querySelector('title');
416
- entryObject.title = title ? title.text : '';
417
- }
388
+ return true;
389
+ } catch (err) {
390
+ console.error("Error checking sitemap file:", err);
391
+ return false;
392
+ }
393
+ }
418
394
 
419
- entryObject.publication = {
420
- name: entries[i].getAttribute('sitemap-publication-name'), // Use proper attribute
421
- language: entries[i].getAttribute('sitemap-publication-language') // Use proper attribute
422
- };
395
+ function parseHtml(file) {
396
+ const dom = parse(file.src);
397
+ const entries = dom.querySelectorAll('[sitemap="true"]');
423
398
 
424
- if (!entryObject.publication.language) {
425
- // Fallback to HTML lang attribute
426
- const htmlElement = dom.querySelector('html');
427
- entryObject.publication.language = htmlElement ? htmlElement.getAttribute('lang') : null;
428
- }
399
+ let types = ["image", "video", "news"];
429
400
 
430
- entryObject.publication_date = entries[i].getAttribute('sitemap-publication-date') || file.modified.on;
401
+ const previousEntries = {};
402
+ for (let i = 0; i < types.length; i++) {
403
+ if (!file.sitemap[types[i]]) continue;
404
+ if (Array.isArray(file.sitemap[types[i]]))
405
+ previousEntries[types[i]] = file.sitemap[types[i]];
406
+ else previousEntries[types[i]] = [file.sitemap[types[i]]];
431
407
 
432
- entryObject.keywords = entries[i].getAttribute('sitemap-keywords');
433
- if (!entryObject.keywords) {
434
- const keywords = dom.querySelector('meta[name="keywords"]');
435
- entryObject.keywords = keywords ? keywords.getAttribute('content') : '';
436
- }
408
+ delete file.sitemap[types[i]];
409
+ }
437
410
 
438
- entryObject.genres = entries[i].getAttribute('sitemap-genres');
411
+ for (let i = 0; i < entries.length; i++) {
412
+ let type = "",
413
+ query = "";
414
+ let existingObject;
415
+ let entryObject = {};
416
+
417
+ if (entries[i].tagName === "IMG") {
418
+ type = "image";
419
+ query = "loc";
420
+ entryObject.loc = entries[i].getAttribute("src");
421
+ entryObject.title =
422
+ entries[i].getAttribute("sitemap-title") ||
423
+ entries[i].getAttribute("title") ||
424
+ entries[i].getAttribute("alt");
425
+ entryObject.caption =
426
+ entries[i].getAttribute("sitemap-caption") ||
427
+ entries[i].getAttribute("alt") ||
428
+ entryObject.title;
429
+ entryObject.geo_location = entries[i].getAttribute(
430
+ "sitemap-geo-location"
431
+ );
432
+ } else if (entries[i].tagName === "VIDEO") {
433
+ type = "video";
434
+ query = "content_loc";
435
+ entryObject.content_loc = entries[i].src;
436
+ entryObject.title =
437
+ entries[i].getAttribute("sitemap-title") ||
438
+ entries[i].getAttribute("title");
439
+ entryObject.description =
440
+ entries[i].getAttribute("description");
441
+ entryObject.thumbnail_loc =
442
+ entries[i].getAttribute("sitemap-thumbnail") ||
443
+ entries[i].getAttribute("poster");
444
+ entryObject.duration =
445
+ entries[i].getAttribute("sitemap-duration");
446
+ } else {
447
+ type = "news";
448
+ file.sitemap.type = "news";
449
+ query = "title";
450
+ entryObject.title = entries[i].getAttribute("sitemap-title");
451
+ if (!entryObject.title) {
452
+ const title = dom.querySelector("title");
453
+ entryObject.title = title ? title.text : "";
439
454
  }
440
455
 
441
- if (previousEntries[type]) {
442
- existingObject = previousEntries[type].find(item => item[query] === entryObject[query]);
443
- entryObject = { ...existingObject, ...entryObject }
456
+ entryObject.publication = {
457
+ name: entries[i].getAttribute("sitemap-publication-name"),
458
+ language: entries[i].getAttribute(
459
+ "sitemap-publication-language"
460
+ )
461
+ };
462
+
463
+ if (!entryObject.publication.language) {
464
+ const htmlElement = dom.querySelector("html");
465
+ entryObject.publication.language = htmlElement
466
+ ? htmlElement.getAttribute("lang")
467
+ : null;
444
468
  }
445
469
 
446
- Object.keys(entryObject).forEach(key => {
447
- if (!entryObject[key])
448
- delete entryObject[key]
449
- });
450
-
451
- if (!file.sitemap[type])
452
- file.sitemap[type] = []
453
-
454
- file.sitemap[type].push(entryObject)
470
+ entryObject.publication_date =
471
+ entries[i].getAttribute("sitemap-publication-date") ||
472
+ file.modified?.on;
473
+
474
+ entryObject.keywords =
475
+ entries[i].getAttribute("sitemap-keywords");
476
+ if (!entryObject.keywords) {
477
+ const keywords = dom.querySelector('meta[name="keywords"]');
478
+ entryObject.keywords = keywords
479
+ ? keywords.getAttribute("content")
480
+ : "";
481
+ }
455
482
 
483
+ entryObject.genres = entries[i].getAttribute("sitemap-genres");
456
484
  }
457
485
 
458
- if (file.sitemap.type !== 'news' && file.sitemap.type !== 'image' && file.sitemap.type !== 'video') {
459
- const priorityMeta = dom.querySelector('meta[name="sitemap-priority"]');
460
- const changefreqMeta = dom.querySelector('meta[name="sitemap-changefreq"]');
461
- file.sitemap.priority = priorityMeta ? priorityMeta.getAttribute('content') : file.sitemap.priority; // Default priority if not specified
462
- file.sitemap.changefreq = changefreqMeta ? changefreqMeta.getAttribute('content') : file.sitemap.changefreq || 'monthly'; // Default changefreq if not specified
486
+ if (previousEntries[type]) {
487
+ existingObject = previousEntries[type].find(
488
+ (item) => item[query] === entryObject[query]
489
+ );
490
+ entryObject = { ...existingObject, ...entryObject };
463
491
  }
464
492
 
465
- }
493
+ Object.keys(entryObject).forEach((key) => {
494
+ if (!entryObject[key]) delete entryObject[key];
495
+ });
466
496
 
467
- encodeXML(str) {
468
- if (str)
469
- return str
470
- .replace(/&/g, '&amp;')
471
- .replace(/</g, '&lt;')
472
- .replace(/>/g, '&gt;')
473
- .replace(/"/g, '&quot;')
474
- .replace(/'/g, '&apos;');
497
+ if (!file.sitemap[type]) file.sitemap[type] = [];
498
+
499
+ file.sitemap[type].push(entryObject);
475
500
  }
476
501
 
502
+ if (
503
+ file.sitemap.type !== "news" &&
504
+ file.sitemap.type !== "image" &&
505
+ file.sitemap.type !== "video"
506
+ ) {
507
+ const priorityMeta = dom.querySelector(
508
+ 'meta[name="sitemap-priority"]'
509
+ );
510
+ const changefreqMeta = dom.querySelector(
511
+ 'meta[name="sitemap-changefreq"]'
512
+ );
513
+ file.sitemap.priority = priorityMeta
514
+ ? priorityMeta.getAttribute("content")
515
+ : file.sitemap.priority;
516
+ file.sitemap.changefreq = changefreqMeta
517
+ ? changefreqMeta.getAttribute("content")
518
+ : file.sitemap.changefreq || "monthly";
519
+ }
477
520
  }
478
521
 
479
- module.exports = CoCreateSitemap;
522
+ function encodeXML(str) {
523
+ if (str)
524
+ return str
525
+ .replace(/&/g, "&amp;")
526
+ .replace(/</g, "&lt;")
527
+ .replace(/>/g, "&gt;")
528
+ .replace(/"/g, "&quot;")
529
+ .replace(/'/g, "&apos;");
530
+ }