metaowl 0.4.1 → 0.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +267 -2
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +144 -0
  10. package/build/runtime/modules/app-mounter.js +73 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/constants.js +38 -0
  15. package/build/runtime/modules/error-boundary.js +116 -0
  16. package/build/runtime/modules/fetch.js +31 -0
  17. package/build/runtime/modules/file-router.js +207 -0
  18. package/build/runtime/modules/fonts.js +172 -0
  19. package/build/runtime/modules/forms.js +193 -0
  20. package/build/runtime/modules/i18n.js +180 -0
  21. package/build/runtime/modules/image.js +175 -0
  22. package/build/runtime/modules/layouts.js +214 -0
  23. package/build/runtime/modules/link.js +141 -0
  24. package/build/runtime/modules/meta.js +117 -0
  25. package/build/runtime/modules/odoo-rpc.js +265 -0
  26. package/build/runtime/modules/pwa.js +272 -0
  27. package/build/runtime/modules/router.js +384 -0
  28. package/build/runtime/modules/seo.js +186 -0
  29. package/build/runtime/modules/store.js +198 -0
  30. package/build/runtime/modules/templates-manager.js +52 -0
  31. package/build/runtime/modules/test-utils.js +238 -0
  32. package/build/runtime/vite/plugin.js +197 -0
  33. package/eslint.js +29 -0
  34. package/package.json +45 -27
  35. package/CONTRIBUTING.md +0 -49
  36. package/bin/metaowl-build.js +0 -12
  37. package/bin/metaowl-dev.js +0 -12
  38. package/bin/metaowl-generate.js +0 -339
  39. package/bin/metaowl-lint.js +0 -71
  40. package/bin/utils.js +0 -82
  41. package/eslint.config.js +0 -3
  42. package/index.js +0 -328
  43. package/modules/app-mounter.js +0 -104
  44. package/modules/auto-import.js +0 -225
  45. package/modules/cache.js +0 -59
  46. package/modules/composables.js +0 -600
  47. package/modules/error-boundary.js +0 -228
  48. package/modules/fetch.js +0 -51
  49. package/modules/file-router.js +0 -478
  50. package/modules/forms.js +0 -353
  51. package/modules/i18n.js +0 -333
  52. package/modules/layouts.js +0 -431
  53. package/modules/link.js +0 -255
  54. package/modules/meta.js +0 -119
  55. package/modules/odoo-rpc.js +0 -511
  56. package/modules/pwa.js +0 -515
  57. package/modules/router.js +0 -769
  58. package/modules/seo.js +0 -501
  59. package/modules/store.js +0 -409
  60. package/modules/templates-manager.js +0 -89
  61. package/modules/test-utils.js +0 -532
  62. package/test/auto-import.test.js +0 -110
  63. package/test/cache.test.js +0 -55
  64. package/test/composables.test.js +0 -103
  65. package/test/dynamic-routes.test.js +0 -469
  66. package/test/error-boundary.test.js +0 -126
  67. package/test/fetch.test.js +0 -100
  68. package/test/file-router.test.js +0 -55
  69. package/test/forms.test.js +0 -203
  70. package/test/i18n.test.js +0 -188
  71. package/test/layouts.test.js +0 -395
  72. package/test/link.test.js +0 -189
  73. package/test/meta.test.js +0 -146
  74. package/test/odoo-rpc.test.js +0 -547
  75. package/test/pwa.test.js +0 -154
  76. package/test/router-guards.test.js +0 -229
  77. package/test/router.test.js +0 -77
  78. package/test/seo.test.js +0 -353
  79. package/test/store.test.js +0 -476
  80. package/test/templates-manager.test.js +0 -83
  81. package/test/test-utils.test.js +0 -314
  82. package/vite/plugin.js +0 -290
  83. package/vitest.config.js +0 -8
@@ -0,0 +1,384 @@
1
+ /**
2
+ * @module Router
3
+ *
4
+ * Enhanced router with navigation guards support.
5
+ */
6
+ import { buildSimpleRoutePattern } from './constants.js';
7
+ let currentRoute = null;
8
+ let previousRoute = null;
9
+ const beforeEachGuards = [];
10
+ const afterEachHooks = [];
11
+ let navigating = false;
12
+ let cancelCurrentNavigation = null;
13
+ let spaNavigationCallback = null;
14
+ let spaEnabled = false;
15
+ class Router {
16
+ routes;
17
+ routeMap;
18
+ constructor(routes) {
19
+ this.routes = routes;
20
+ this.routeMap = new Map();
21
+ for (const route of routes) {
22
+ for (const path of route.path) {
23
+ this.routeMap.set(path, route);
24
+ }
25
+ }
26
+ }
27
+ resolve(path) {
28
+ const currentPath = path || document.location.pathname;
29
+ if (this.routeMap.has(currentPath)) {
30
+ return this.routeMap.get(currentPath) || null;
31
+ }
32
+ for (const route of this.routes) {
33
+ if (this.matchRoute(route, currentPath)) {
34
+ return route;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ matchRoute(route, path) {
40
+ for (const routePath of route.path) {
41
+ if (this.pathMatches(routePath, path)) {
42
+ return true;
43
+ }
44
+ }
45
+ return false;
46
+ }
47
+ pathMatches(routePath, currentPath) {
48
+ if (!routePath.includes(':') && !routePath.includes('*')) {
49
+ const normalizedRoute = (routePath.replace(/\/$/, '') || '/');
50
+ const normalizedCurrent = (currentPath.replace(/\/$/, '') || '/');
51
+ return normalizedRoute === normalizedCurrent;
52
+ }
53
+ const pattern = buildSimpleRoutePattern(routePath);
54
+ return new RegExp(pattern).test(currentPath);
55
+ }
56
+ extractParams(route, path) {
57
+ const params = {};
58
+ for (const routePath of route.path) {
59
+ const match = this.matchAndExtract(routePath, path);
60
+ if (match) {
61
+ Object.assign(params, match);
62
+ }
63
+ }
64
+ return params;
65
+ }
66
+ matchAndExtract(routePath, currentPath) {
67
+ if (!routePath.includes(':')) {
68
+ return null;
69
+ }
70
+ const paramNames = [];
71
+ const pattern = routePath
72
+ .replace(/:([^/(]+)\(\.\*\)/g, (_match, name) => {
73
+ paramNames.push(name);
74
+ return '(.*)';
75
+ })
76
+ .replace(/\/:([^/(]+)\?/g, (_match, name) => {
77
+ paramNames.push(name);
78
+ return '(?:/([^/]+))?';
79
+ })
80
+ .replace(/:([^/(?\s]+)/g, (_match, name) => {
81
+ paramNames.push(name);
82
+ return '([^/]+)';
83
+ });
84
+ const matches = currentPath.match(new RegExp('^' + pattern + '$'));
85
+ if (!matches)
86
+ return null;
87
+ const params = {};
88
+ for (let index = 0; index < paramNames.length; index++) {
89
+ const value = matches[index + 1];
90
+ if (value !== undefined) {
91
+ params[paramNames[index]] = value;
92
+ }
93
+ }
94
+ return params;
95
+ }
96
+ }
97
+ const injectedRouteSets = new WeakSet();
98
+ export async function processRoutes(routes, customPath) {
99
+ const targetPath = customPath || document.location.pathname;
100
+ if (!injectedRouteSets.has(routes)) {
101
+ injectedRouteSets.add(routes);
102
+ for (const route of routes) {
103
+ const originalPaths = [...route.path];
104
+ for (const path of originalPaths) {
105
+ if (typeof path === 'string') {
106
+ injectSystemRoutes(route, path);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ const routerInstance = new Router(routes);
112
+ const toRoute = routerInstance.resolve(targetPath);
113
+ if (!toRoute) {
114
+ throw new Error(`No route found for "${targetPath}".`);
115
+ }
116
+ const to = buildRouteObject(toRoute, routerInstance);
117
+ const from = currentRoute;
118
+ try {
119
+ await runGuards(to, from);
120
+ previousRoute = currentRoute;
121
+ currentRoute = to;
122
+ for (const hook of afterEachHooks) {
123
+ hook(to, from);
124
+ }
125
+ return [toRoute];
126
+ }
127
+ catch (error) {
128
+ if (isNavigationRedirect(error) && error.path) {
129
+ window.location.href = error.path;
130
+ return null;
131
+ }
132
+ throw error;
133
+ }
134
+ }
135
+ function buildRouteObject(routeDef, routerInstance) {
136
+ const currentPath = document.location.pathname;
137
+ const params = routerInstance.extractParams(routeDef, currentPath);
138
+ return {
139
+ name: routeDef.name,
140
+ path: routeDef.path,
141
+ fullPath: currentPath,
142
+ component: routeDef.component,
143
+ meta: routeDef.meta || {},
144
+ beforeEnter: routeDef.beforeEnter,
145
+ params,
146
+ query: parseQuery(document.location.search)
147
+ };
148
+ }
149
+ function parseQuery(search) {
150
+ const query = {};
151
+ if (!search || search === '?')
152
+ return query;
153
+ const params = new URLSearchParams(search.substring(1));
154
+ for (const [key, value] of params) {
155
+ const existing = query[key];
156
+ if (existing) {
157
+ if (Array.isArray(existing)) {
158
+ existing.push(value);
159
+ }
160
+ else {
161
+ query[key] = [existing, value];
162
+ }
163
+ }
164
+ else {
165
+ query[key] = value;
166
+ }
167
+ }
168
+ return query;
169
+ }
170
+ async function runGuards(to, from) {
171
+ navigating = true;
172
+ let cancelled = false;
173
+ cancelCurrentNavigation = () => {
174
+ cancelled = true;
175
+ };
176
+ try {
177
+ for (const guard of beforeEachGuards) {
178
+ if (cancelled)
179
+ break;
180
+ const result = await runGuard(guard, to, from);
181
+ if (result === false) {
182
+ throw new NavigationCancelled();
183
+ }
184
+ if (typeof result === 'string') {
185
+ throw new NavigationRedirect(result);
186
+ }
187
+ if (result && typeof result === 'object' && 'path' in result && typeof result.path === 'string') {
188
+ throw new NavigationRedirect(result.path);
189
+ }
190
+ }
191
+ if (to.beforeEnter && !cancelled) {
192
+ const result = await runGuard(to.beforeEnter, to, from);
193
+ if (result === false) {
194
+ throw new NavigationCancelled();
195
+ }
196
+ if (typeof result === 'string') {
197
+ throw new NavigationRedirect(result);
198
+ }
199
+ }
200
+ }
201
+ finally {
202
+ navigating = false;
203
+ cancelCurrentNavigation = null;
204
+ }
205
+ }
206
+ async function runGuard(guard, to, from) {
207
+ return await new Promise((resolve, reject) => {
208
+ const next = (result) => {
209
+ if (result instanceof Error) {
210
+ reject(result);
211
+ }
212
+ else {
213
+ resolve(result);
214
+ }
215
+ };
216
+ try {
217
+ const guardResult = guard(to, from, next);
218
+ if (guardResult && typeof guardResult.then === 'function') {
219
+ ;
220
+ guardResult.then(resolve).catch(reject);
221
+ }
222
+ else if (guardResult !== undefined) {
223
+ resolve(guardResult);
224
+ }
225
+ }
226
+ catch (error) {
227
+ reject(error);
228
+ }
229
+ });
230
+ }
231
+ export function resetRouter() {
232
+ beforeEachGuards.length = 0;
233
+ afterEachHooks.length = 0;
234
+ navigating = false;
235
+ cancelCurrentNavigation = null;
236
+ currentRoute = null;
237
+ previousRoute = null;
238
+ spaNavigationCallback = null;
239
+ spaEnabled = false;
240
+ }
241
+ class NavigationCancelled extends Error {
242
+ constructor() {
243
+ super('Navigation cancelled');
244
+ this.name = 'NavigationCancelled';
245
+ }
246
+ }
247
+ class NavigationRedirect extends Error {
248
+ path;
249
+ constructor(path) {
250
+ super('Navigation redirect');
251
+ this.name = 'NavigationRedirect';
252
+ this.path = path;
253
+ }
254
+ }
255
+ function isNavigationRedirect(error) {
256
+ return error instanceof Error && error.name === 'NavigationRedirect' && 'path' in error;
257
+ }
258
+ export function beforeEach(guard) {
259
+ beforeEachGuards.push(guard);
260
+ return () => {
261
+ const index = beforeEachGuards.indexOf(guard);
262
+ if (index > -1)
263
+ beforeEachGuards.splice(index, 1);
264
+ };
265
+ }
266
+ export function afterEach(hook) {
267
+ afterEachHooks.push(hook);
268
+ return () => {
269
+ const index = afterEachHooks.indexOf(hook);
270
+ if (index > -1)
271
+ afterEachHooks.splice(index, 1);
272
+ };
273
+ }
274
+ export function getCurrentRoute() {
275
+ return currentRoute;
276
+ }
277
+ export function getPreviousRoute() {
278
+ return previousRoute;
279
+ }
280
+ export function isNavigating() {
281
+ return navigating;
282
+ }
283
+ export function cancelNavigation() {
284
+ if (cancelCurrentNavigation) {
285
+ cancelCurrentNavigation();
286
+ }
287
+ }
288
+ export function _setSpaNavigationCallback(callback) {
289
+ spaNavigationCallback = callback;
290
+ }
291
+ export function setSpaMode(enabled) {
292
+ spaEnabled = enabled;
293
+ }
294
+ export function isSpaMode() {
295
+ return spaEnabled;
296
+ }
297
+ export async function navigateTo(path, options = {}) {
298
+ const { replace = false } = options;
299
+ if (!spaEnabled || !spaNavigationCallback) {
300
+ if (replace) {
301
+ window.location.replace(path);
302
+ }
303
+ else {
304
+ window.location.href = path;
305
+ }
306
+ return false;
307
+ }
308
+ try {
309
+ if (replace) {
310
+ window.history.replaceState({ path }, '', path);
311
+ }
312
+ else {
313
+ window.history.pushState({ path }, '', path);
314
+ }
315
+ await spaNavigationCallback(path);
316
+ return true;
317
+ }
318
+ catch (error) {
319
+ console.error('[metaowl] SPA navigation failed:', error);
320
+ window.location.href = path;
321
+ return false;
322
+ }
323
+ }
324
+ export function navigate(path, options = {}) {
325
+ const { replace = false, reload = true } = options;
326
+ if (reload || !spaEnabled) {
327
+ if (replace) {
328
+ window.location.replace(path);
329
+ }
330
+ else {
331
+ window.location.href = path;
332
+ }
333
+ }
334
+ else {
335
+ void navigateTo(path, { replace });
336
+ }
337
+ }
338
+ export function push(path) {
339
+ void navigateTo(path, { replace: false });
340
+ }
341
+ export function replace(path) {
342
+ void navigateTo(path, { replace: true });
343
+ }
344
+ export function back() {
345
+ window.history.back();
346
+ }
347
+ export function forward() {
348
+ window.history.forward();
349
+ }
350
+ export function go(n) {
351
+ window.history.go(n);
352
+ }
353
+ export const router = {
354
+ beforeEach,
355
+ afterEach,
356
+ get currentRoute() { return getCurrentRoute(); },
357
+ get previousRoute() { return getPreviousRoute(); },
358
+ get isNavigating() { return isNavigating(); },
359
+ cancel: cancelNavigation,
360
+ push,
361
+ replace,
362
+ back,
363
+ forward,
364
+ go,
365
+ navigate,
366
+ navigateTo,
367
+ setSpaMode,
368
+ isSpaMode
369
+ };
370
+ function injectSystemRoutes(route, path) {
371
+ if (path === '/') {
372
+ if (!route.path.includes('/index.html'))
373
+ route.path.push('/index.html');
374
+ }
375
+ else {
376
+ if (!route.path.includes(`${path}.html`))
377
+ route.path.push(`${path}.html`);
378
+ if (!route.path.includes(`${path}/`))
379
+ route.path.push(`${path}/`);
380
+ if (!route.path.includes(`${path}/index.html`))
381
+ route.path.push(`${path}/index.html`);
382
+ }
383
+ return route;
384
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @module SEO
3
+ *
4
+ * SEO utilities for MetaOwl applications.
5
+ */
6
+ const VALID_CHANGE_FREQUENCIES = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'];
7
+ export function generateSitemap(entries, options = {}) {
8
+ const { baseUrl } = options;
9
+ if (!baseUrl) {
10
+ throw new Error('[SEO] baseUrl is required for sitemap generation');
11
+ }
12
+ const normalizedBase = baseUrl.replace(/\/$/, '');
13
+ const urls = entries.map((entry) => {
14
+ const routeUrl = entry.url ?? '';
15
+ const location = routeUrl.startsWith('http')
16
+ ? routeUrl
17
+ : `${normalizedBase}${routeUrl.startsWith('/') ? routeUrl : '/' + routeUrl}`;
18
+ let urlXml = ` <url>\n <loc>${escapeXml(location)}</loc>\n`;
19
+ if (entry.lastmod) {
20
+ urlXml += ` <lastmod>${entry.lastmod}</lastmod>\n`;
21
+ }
22
+ if (entry.changefreq && VALID_CHANGE_FREQUENCIES.includes(entry.changefreq)) {
23
+ urlXml += ` <changefreq>${entry.changefreq}</changefreq>\n`;
24
+ }
25
+ if (entry.priority !== undefined) {
26
+ const priority = Math.max(0, Math.min(1, entry.priority)).toFixed(1);
27
+ urlXml += ` <priority>${priority}</priority>\n`;
28
+ }
29
+ if (entry.image) {
30
+ urlXml += ' <image:image xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">\n';
31
+ urlXml += ` <image:loc>${escapeXml(entry.image)}</image:loc>\n`;
32
+ urlXml += ' </image:image>\n';
33
+ }
34
+ urlXml += ' </url>';
35
+ return urlXml;
36
+ });
37
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.join('\n')}\n</urlset>`;
38
+ }
39
+ export function generateRobotsTxt(config = {}) {
40
+ const configs = Array.isArray(config) ? config : [config];
41
+ const sections = configs.map((cfg) => {
42
+ const { userAgent = '*', allow = [], disallow = [], crawlDelay } = cfg;
43
+ let section = `User-agent: ${userAgent}\n`;
44
+ for (const path of allow) {
45
+ section += `Allow: ${path}\n`;
46
+ }
47
+ for (const path of disallow) {
48
+ section += `Disallow: ${path}\n`;
49
+ }
50
+ if (crawlDelay !== undefined && crawlDelay > 0) {
51
+ section += `Crawl-delay: ${crawlDelay}\n`;
52
+ }
53
+ return section.trim();
54
+ });
55
+ const globalConfig = configs.find((cfg) => cfg.sitemap || cfg.host);
56
+ if (globalConfig?.sitemap) {
57
+ sections.push(`Sitemap: ${globalConfig.sitemap}`);
58
+ }
59
+ if (globalConfig?.host) {
60
+ sections.push(`Host: ${globalConfig.host}`);
61
+ }
62
+ return sections.join('\n\n');
63
+ }
64
+ export function jsonLd(schema) {
65
+ const fullSchema = {
66
+ '@context': 'https://schema.org',
67
+ ...schema
68
+ };
69
+ return JSON.stringify(fullSchema, null, 2);
70
+ }
71
+ export function createCanonicalUrl(baseUrl, path, options = {}) {
72
+ const { removeQueryParams = false, allowedParams = [] } = options;
73
+ const normalizedBase = baseUrl.replace(/\/$/, '');
74
+ const [pathname, queryString] = path.split('?');
75
+ const normalizedPath = pathname.startsWith('/') ? pathname : '/' + pathname;
76
+ if (!queryString || removeQueryParams) {
77
+ return `${normalizedBase}${normalizedPath}`;
78
+ }
79
+ if (allowedParams.length > 0) {
80
+ const params = new URLSearchParams(queryString);
81
+ const filtered = new URLSearchParams();
82
+ for (const key of allowedParams) {
83
+ const value = params.get(key);
84
+ if (value !== null) {
85
+ filtered.set(key, value);
86
+ }
87
+ }
88
+ const filteredQuery = filtered.toString();
89
+ return filteredQuery
90
+ ? `${normalizedBase}${normalizedPath}?${filteredQuery}`
91
+ : `${normalizedBase}${normalizedPath}`;
92
+ }
93
+ return `${normalizedBase}${normalizedPath}?${queryString}`;
94
+ }
95
+ export function generateOpenGraph(data) {
96
+ const { title, description, type = 'website', url, image, siteName } = data;
97
+ const tags = {
98
+ 'og:title': title,
99
+ 'og:type': type
100
+ };
101
+ if (description)
102
+ tags['og:description'] = description;
103
+ if (url)
104
+ tags['og:url'] = url;
105
+ if (image)
106
+ tags['og:image'] = image;
107
+ if (siteName)
108
+ tags['og:site_name'] = siteName;
109
+ return tags;
110
+ }
111
+ export function generateTwitterCard(data) {
112
+ const { title, description, card = 'summary_large_image', image, site } = data;
113
+ const tags = {
114
+ 'twitter:card': card,
115
+ 'twitter:title': title
116
+ };
117
+ if (description)
118
+ tags['twitter:description'] = description;
119
+ if (image)
120
+ tags['twitter:image'] = image;
121
+ if (site)
122
+ tags['twitter:site'] = site;
123
+ return tags;
124
+ }
125
+ export function validateSitemap(entries) {
126
+ const errors = [];
127
+ for (let index = 0; index < entries.length; index++) {
128
+ const entry = entries[index];
129
+ if (!entry.url) {
130
+ errors.push(`Entry ${index}: Missing required 'url'`);
131
+ }
132
+ if (entry.priority !== undefined && (entry.priority < 0 || entry.priority > 1)) {
133
+ errors.push(`Entry ${index}: Priority must be between 0 and 1`);
134
+ }
135
+ if (entry.changefreq && !VALID_CHANGE_FREQUENCIES.includes(entry.changefreq)) {
136
+ errors.push(`Entry ${index}: Invalid changefreq '${entry.changefreq}'`);
137
+ }
138
+ if (entry.lastmod) {
139
+ const date = new Date(entry.lastmod);
140
+ if (Number.isNaN(date.getTime())) {
141
+ errors.push(`Entry ${index}: Invalid lastmod date '${entry.lastmod}'`);
142
+ }
143
+ }
144
+ }
145
+ return {
146
+ valid: errors.length === 0,
147
+ errors
148
+ };
149
+ }
150
+ export function getPriorityByDepth(url, options = {}) {
151
+ const { maxDepth = 3 } = options;
152
+ const depth = url.split('/').filter(Boolean).length;
153
+ const priority = Math.max(0.1, 1 - (depth / maxDepth) * 0.3);
154
+ return Math.round(priority * 10) / 10;
155
+ }
156
+ export function generateSitemapIndex(sitemaps) {
157
+ const entries = sitemaps.map((sitemap) => {
158
+ let entry = ` <sitemap>\n <loc>${escapeXml(sitemap.loc)}</loc>\n`;
159
+ if (sitemap.lastmod) {
160
+ entry += ` <lastmod>${sitemap.lastmod}</lastmod>\n`;
161
+ }
162
+ entry += ' </sitemap>';
163
+ return entry;
164
+ });
165
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.join('\n')}\n</sitemapindex>`;
166
+ }
167
+ function escapeXml(str) {
168
+ return str
169
+ .replace(/&/g, '&amp;')
170
+ .replace(/</g, '&lt;')
171
+ .replace(/>/g, '&gt;')
172
+ .replace(/"/g, '&quot;')
173
+ .replace(/'/g, '&#x27;');
174
+ }
175
+ export const SEO = {
176
+ generateSitemap,
177
+ generateRobotsTxt,
178
+ jsonLd,
179
+ createCanonicalUrl,
180
+ generateOpenGraph,
181
+ generateTwitterCard,
182
+ validateSitemap,
183
+ getPriorityByDepth,
184
+ generateSitemapIndex
185
+ };
186
+ export default SEO;