@power-seo/sitemap 1.0.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.
package/README.md ADDED
@@ -0,0 +1,362 @@
1
+ # @power-seo/sitemap — XML Sitemap Generator for TypeScript — Streaming, Index Splitting & Image/Video/News Extensions
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@power-seo/sitemap)](https://www.npmjs.com/package/@power-seo/sitemap)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@power-seo/sitemap)](https://www.npmjs.com/package/@power-seo/sitemap)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/)
7
+ [![tree-shakeable](https://img.shields.io/badge/tree--shakeable-yes-brightgreen)](https://bundlephobia.com/package/@power-seo/sitemap)
8
+
9
+ ---
10
+
11
+ ## Overview
12
+
13
+ **@power-seo/sitemap** is a zero-dependency XML sitemap generator for TypeScript that helps you generate standards-compliant sitemaps with image, video, and news extensions — including streaming generation and automatic index splitting for large sites.
14
+
15
+ **What it does**
16
+ - ✅ **Generate XML sitemaps** — `generateSitemap()` produces spec-compliant `<urlset>` XML strings
17
+ - ✅ **Stream large sitemaps** — `streamSitemap()` yields XML chunks with constant memory usage
18
+ - ✅ **Split into multiple files** — `splitSitemap()` auto-chunks at the 50,000-URL limit
19
+ - ✅ **Generate sitemap indexes** — `generateSitemapIndex()` creates `<sitemapindex>` files pointing to child sitemaps
20
+ - ✅ **Validate URL entries** — `validateSitemapUrl()` checks against Google's sitemap spec requirements
21
+
22
+ **What it is not**
23
+ - ❌ **Not a sitemap crawler** — does not discover URLs by crawling your site
24
+ - ❌ **Not a submission client** — use `@power-seo/search-console` to submit sitemaps to GSC
25
+
26
+ **Recommended for**
27
+ - **Next.js App Router sites**, **Remix apps**, **Express servers**, **static site generators**, and any Node.js/edge environment that generates sitemaps programmatically
28
+
29
+ ---
30
+
31
+ ## Why @power-seo/sitemap Matters
32
+
33
+ **The problem**
34
+ - **Sites with 50,000+ URLs** cannot fit in a single sitemap file — the spec mandates splitting
35
+ - **Image and video sitemaps** require `<image:image>` and `<video:video>` namespace extensions that most generators don't support
36
+ - **Memory spikes** during XML string concatenation cause crashes or timeouts on large e-commerce catalogs
37
+
38
+ **Why developers care**
39
+ - **SEO:** Well-structured sitemaps improve crawl coverage and ensure all pages are discovered
40
+ - **Performance:** Streaming generation keeps memory usage constant for million-URL datasets
41
+ - **Indexing:** Image sitemaps help Google discover and index product images for Google Images
42
+
43
+ ---
44
+
45
+ ## Key Features
46
+
47
+ - **Full sitemap spec support** — `<loc>`, `<lastmod>`, `<changefreq>`, `<priority>`, and all optional elements
48
+ - **Image sitemap extension** — `<image:image>` tags with `loc`, `caption`, `title`, `license`
49
+ - **Video sitemap extension** — `<video:video>` tags with title, description, thumbnail, duration
50
+ - **News sitemap extension** — `<news:news>` tags with publication name, language, date
51
+ - **Streaming generation** — `streamSitemap()` returns `AsyncIterable<string>` — no memory spike on large lists
52
+ - **Automatic index splitting** — `splitSitemap()` chunks at `MAX_URLS_PER_SITEMAP` (50,000)
53
+ - **Sitemap index generation** — `generateSitemapIndex()` creates a `<sitemapindex>` pointing to child sitemaps
54
+ - **URL validation** — `validateSitemapUrl()` returns `{ valid, errors, warnings }` without throwing
55
+ - **Constants exported** — `MAX_URLS_PER_SITEMAP` (50,000) and `MAX_SITEMAP_SIZE_BYTES` (50MB)
56
+ - **Framework-agnostic** — works in Next.js API routes, Remix loaders, Express, Fastify, and edge runtimes
57
+ - **Full TypeScript support** — typed `SitemapURL`, `SitemapImage`, `SitemapVideo`, `SitemapNews`, `SitemapConfig`
58
+ - **Zero runtime dependencies** — pure TypeScript, no external XML libraries
59
+
60
+ ---
61
+
62
+ ## Benefits of Using @power-seo/sitemap
63
+
64
+ - **Improved crawl coverage**: Well-structured sitemaps with `lastmod` help Googlebot prioritize fresh pages
65
+ - **Better image indexing**: `<image:image>` extensions surface product images in Google Images
66
+ - **Safer implementation**: `validateSitemapUrl()` catches out-of-range `priority` values and invalid dates before serving
67
+ - **Faster delivery**: Zero-dependency streaming generation works in any runtime without configuration
68
+
69
+ ---
70
+
71
+ ## Quick Start
72
+
73
+ ```ts
74
+ import { generateSitemap } from '@power-seo/sitemap';
75
+
76
+ const xml = generateSitemap({
77
+ urls: [
78
+ { loc: 'https://example.com/', lastmod: '2026-01-01', changefreq: 'daily', priority: 1.0 },
79
+ { loc: 'https://example.com/about', changefreq: 'monthly', priority: 0.8 },
80
+ { loc: 'https://example.com/blog/post-1', lastmod: '2026-01-15', priority: 0.6 },
81
+ ],
82
+ });
83
+
84
+ // Returns valid XML string:
85
+ // <?xml version="1.0" encoding="UTF-8"?>
86
+ // <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">...
87
+ ```
88
+
89
+ **What you should see**
90
+ - A standards-compliant XML sitemap string ready to serve as `Content-Type: application/xml`
91
+ - `<urlset>` containing `<url>` entries with the fields you provided
92
+
93
+ ---
94
+
95
+ ## Installation
96
+
97
+ ```bash
98
+ npm i @power-seo/sitemap
99
+ # or
100
+ yarn add @power-seo/sitemap
101
+ # or
102
+ pnpm add @power-seo/sitemap
103
+ # or
104
+ bun add @power-seo/sitemap
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Framework Compatibility
110
+
111
+ **Supported**
112
+ - ✅ Next.js App Router — use in `app/sitemap.xml/route.ts` API route
113
+ - ✅ Next.js Pages Router — use in `pages/api/sitemap.xml.ts`
114
+ - ✅ Remix — use in resource routes
115
+ - ✅ Node.js 18+ — pure TypeScript, no native bindings
116
+ - ✅ Edge runtimes — no `fs`, no `path`, no Node.js-specific APIs (except `streamSitemap` which uses `AsyncIterable`)
117
+ - ✅ Fastify / Express — serve the XML string as response body
118
+
119
+ **Environment notes**
120
+ - **SSR/SSG:** Fully supported — generate at request time or at build time
121
+ - **Edge runtime:** `generateSitemap()` and `generateSitemapIndex()` are edge-compatible; `streamSitemap()` requires Node.js streams if writing to disk
122
+ - **Browser-only usage:** Not applicable — sitemap generation is a server-side concern
123
+
124
+ ---
125
+
126
+ ## Use Cases
127
+
128
+ - **Next.js SSG sites** — generate a full sitemap from your CMS at build time
129
+ - **E-commerce catalogs** — product image sitemaps with `<image:image>` for every listing
130
+ - **News publishers** — `<news:news>` extension for Google News sitemap submission
131
+ - **Multi-locale sites** — separate sitemaps per locale with a unified sitemap index
132
+ - **Programmatic SEO** — generate sitemaps for thousands of auto-generated pages
133
+ - **Large sites** — automatic splitting at 50,000 URLs per file with index generation
134
+ - **Video platforms** — `<video:video>` extension for YouTube-style video SEO
135
+
136
+ ---
137
+
138
+ ## Example (Before / After)
139
+
140
+ ```text
141
+ Before:
142
+ - Hand-built XML strings: missing namespace declarations, wrong date formats
143
+ - Single 80,000-URL sitemap: invalid per spec (max 50,000), Google truncates
144
+ - No image sitemap: product images not discovered by Googlebot Images
145
+
146
+ After (@power-seo/sitemap):
147
+ - generateSitemap({ urls }) → spec-compliant XML with correct namespace
148
+ - splitSitemap(allUrls) + generateSitemapIndex() → auto-split + index file
149
+ - urls[i].images = [{ loc, caption }] → <image:image> tags for each product
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Implementation Best Practices
155
+
156
+ - **Always include `lastmod`** — Googlebot uses it to prioritize re-crawling updated pages
157
+ - **Keep `priority` values realistic** — setting everything to 1.0 signals ignored priority; reserve 1.0 for the homepage
158
+ - **Use `changefreq: 'never'` for permanent content** — signals Googlebot to skip re-crawling
159
+ - **Set `sitemap.xml` URL in `robots.txt`** — `Sitemap: https://example.com/sitemap.xml` is required for discovery
160
+ - **Submit to Google Search Console** after generating — use `@power-seo/search-console` `submitSitemap()`
161
+
162
+ ---
163
+
164
+ ## Architecture Overview
165
+
166
+ **Where it runs**
167
+ - **Build-time**: Generate static `sitemap.xml` files during `next build` or Remix production builds
168
+ - **Runtime**: Serve dynamically from an API route; regenerate on demand or on ISR revalidation
169
+ - **CI/CD**: Validate sitemap URL entries as part of pull request checks
170
+
171
+ **Data flow**
172
+ 1. **Input**: Array of `SitemapURL` objects with `loc`, `lastmod`, optional images/videos/news
173
+ 2. **Analysis**: Spec validation, namespace detection, XML serialization
174
+ 3. **Output**: Valid XML string or `AsyncIterable<string>` stream
175
+ 4. **Action**: Serve as `application/xml`, write to disk, or submit to GSC via `@power-seo/search-console`
176
+
177
+ ---
178
+
179
+ ## Features Comparison with Popular Packages
180
+
181
+ | Capability | next-sitemap | sitemap (npm) | xmlbuilder2 | @power-seo/sitemap |
182
+ |---|---:|---:|---:|---:|
183
+ | Image sitemap extension | ✅ | ✅ | ❌ | ✅ |
184
+ | Video sitemap extension | ❌ | ✅ | ❌ | ✅ |
185
+ | News sitemap extension | ❌ | ✅ | ❌ | ✅ |
186
+ | Streaming generation | ❌ | ❌ | ❌ | ✅ |
187
+ | Auto index splitting | ✅ | ❌ | ❌ | ✅ |
188
+ | URL validation | ❌ | ❌ | ❌ | ✅ |
189
+ | Zero runtime dependencies | ❌ | ❌ | ❌ | ✅ |
190
+ | Edge runtime compatible | ❌ | ❌ | ❌ | ✅ |
191
+
192
+ ---
193
+
194
+ ## @power-seo Ecosystem
195
+
196
+ All 17 packages are independently installable — use only what you need.
197
+
198
+ | Package | Install | Description |
199
+ |---------|---------|-------------|
200
+ | [`@power-seo/core`](https://www.npmjs.com/package/@power-seo/core) | `npm i @power-seo/core` | Framework-agnostic utilities, types, validators, and constants |
201
+ | [`@power-seo/react`](https://www.npmjs.com/package/@power-seo/react) | `npm i @power-seo/react` | React SEO components — meta, Open Graph, Twitter Card, breadcrumbs |
202
+ | [`@power-seo/meta`](https://www.npmjs.com/package/@power-seo/meta) | `npm i @power-seo/meta` | SSR meta helpers for Next.js App Router, Remix v2, and generic SSR |
203
+ | [`@power-seo/schema`](https://www.npmjs.com/package/@power-seo/schema) | `npm i @power-seo/schema` | Type-safe JSON-LD structured data — 20 builders + 18 React components |
204
+ | [`@power-seo/content-analysis`](https://www.npmjs.com/package/@power-seo/content-analysis) | `npm i @power-seo/content-analysis` | Yoast-style SEO content scoring engine with React components |
205
+ | [`@power-seo/readability`](https://www.npmjs.com/package/@power-seo/readability) | `npm i @power-seo/readability` | Readability scoring — Flesch-Kincaid, Gunning Fog, Coleman-Liau, ARI |
206
+ | [`@power-seo/preview`](https://www.npmjs.com/package/@power-seo/preview) | `npm i @power-seo/preview` | SERP, Open Graph, and Twitter/X Card preview generators |
207
+ | [`@power-seo/sitemap`](https://www.npmjs.com/package/@power-seo/sitemap) | `npm i @power-seo/sitemap` | XML sitemap generation, streaming, index splitting, and validation |
208
+ | [`@power-seo/redirects`](https://www.npmjs.com/package/@power-seo/redirects) | `npm i @power-seo/redirects` | Redirect engine with Next.js, Remix, and Express adapters |
209
+ | [`@power-seo/links`](https://www.npmjs.com/package/@power-seo/links) | `npm i @power-seo/links` | Link graph analysis — orphan detection, suggestions, equity scoring |
210
+ | [`@power-seo/audit`](https://www.npmjs.com/package/@power-seo/audit) | `npm i @power-seo/audit` | Full SEO audit engine — meta, content, structure, performance rules |
211
+ | [`@power-seo/images`](https://www.npmjs.com/package/@power-seo/images) | `npm i @power-seo/images` | Image SEO — alt text, lazy loading, format analysis, image sitemaps |
212
+ | [`@power-seo/ai`](https://www.npmjs.com/package/@power-seo/ai) | `npm i @power-seo/ai` | LLM-agnostic AI prompt templates and parsers for SEO tasks |
213
+ | [`@power-seo/analytics`](https://www.npmjs.com/package/@power-seo/analytics) | `npm i @power-seo/analytics` | Merge GSC + audit data, trend analysis, ranking insights, dashboard |
214
+ | [`@power-seo/search-console`](https://www.npmjs.com/package/@power-seo/search-console) | `npm i @power-seo/search-console` | Google Search Console API — OAuth2, service account, URL inspection |
215
+ | [`@power-seo/integrations`](https://www.npmjs.com/package/@power-seo/integrations) | `npm i @power-seo/integrations` | Semrush and Ahrefs API clients with rate limiting and pagination |
216
+ | [`@power-seo/tracking`](https://www.npmjs.com/package/@power-seo/tracking) | `npm i @power-seo/tracking` | GA4, Clarity, PostHog, Plausible, Fathom — scripts + consent management |
217
+
218
+ ### Ecosystem vs alternatives
219
+
220
+ | Need | Common approach | @power-seo approach |
221
+ |---|---|---|
222
+ | Sitemap generation | `next-sitemap` | `@power-seo/sitemap` — streaming, image, video, news |
223
+ | Sitemap submission | Manual GSC UI | `@power-seo/search-console` — `submitSitemap()` |
224
+ | Image SEO | Manual `<image:image>` | `@power-seo/sitemap` + `@power-seo/images` |
225
+ | Structured data | Manual JSON-LD | `@power-seo/schema` — typed builders |
226
+
227
+ ---
228
+
229
+ ## Enterprise Integration
230
+
231
+ **Multi-tenant SaaS**
232
+ - **Per-tenant sitemaps**: Generate separate sitemaps per client domain; serve from tenant-aware API routes
233
+ - **Sitemap index**: Use `generateSitemapIndex()` to aggregate tenant sitemaps into a root index
234
+ - **Scheduled regeneration**: Rebuild sitemaps nightly as new content is published
235
+
236
+ **ERP / internal portals**
237
+ - Generate sitemaps only for public-facing modules (knowledge base, product catalog)
238
+ - Use `validateSitemapUrl()` to enforce URL format standards across generated entries
239
+ - Pipe large catalogs through `streamSitemap()` to avoid memory limits in serverless environments
240
+
241
+ **Recommended integration pattern**
242
+ - Generate sitemaps in **CI** at build time for static content
243
+ - Serve dynamically from **API routes** for frequently updated content
244
+ - Submit new sitemaps to GSC via `@power-seo/search-console` after deployment
245
+ - Monitor crawl coverage in Google Search Console
246
+
247
+ ---
248
+
249
+ ## Scope and Limitations
250
+
251
+ **This package does**
252
+ - ✅ Generate spec-compliant XML sitemaps from URL arrays
253
+ - ✅ Support image, video, and news sitemap extensions
254
+ - ✅ Stream large sitemaps to avoid memory spikes
255
+ - ✅ Split sitemaps and generate sitemap index files
256
+
257
+ **This package does not**
258
+ - ❌ Crawl your site to discover URLs — you provide the URL list
259
+ - ❌ Submit sitemaps to Google — use `@power-seo/search-console` for submission
260
+ - ❌ Monitor sitemap status — use Google Search Console for crawl coverage reports
261
+
262
+ ---
263
+
264
+ ## API Reference
265
+
266
+ ### `generateSitemap(config)`
267
+
268
+ ```ts
269
+ function generateSitemap(config: SitemapConfig): string
270
+ ```
271
+
272
+ | Prop | Type | Description |
273
+ |------|------|-------------|
274
+ | `urls` | `SitemapURL[]` | Array of URL entries |
275
+
276
+ #### `SitemapURL`
277
+
278
+ | Prop | Type | Default | Description |
279
+ |------|------|---------|-------------|
280
+ | `loc` | `string` | — | **Required.** Absolute URL |
281
+ | `lastmod` | `string` | — | Last modified (ISO 8601 or `YYYY-MM-DD`) |
282
+ | `changefreq` | `'always' \| 'hourly' \| 'daily' \| 'weekly' \| 'monthly' \| 'yearly' \| 'never'` | — | Change frequency |
283
+ | `priority` | `number` | `0.5` | Priority 0.0–1.0 |
284
+ | `images` | `SitemapImage[]` | — | Image extension entries |
285
+ | `videos` | `SitemapVideo[]` | — | Video extension entries |
286
+ | `news` | `SitemapNews` | — | News extension entry |
287
+
288
+ ### `streamSitemap(config)`
289
+
290
+ ```ts
291
+ function streamSitemap(config: SitemapConfig): AsyncIterable<string>
292
+ ```
293
+
294
+ Returns an async iterable that yields XML chunks. Suitable for piping to a Node.js `Writable`.
295
+
296
+ ### `splitSitemap(urls, maxPerFile?)`
297
+
298
+ ```ts
299
+ function splitSitemap(urls: SitemapURL[], maxPerFile?: number): SitemapURL[][]
300
+ ```
301
+
302
+ Splits `urls` into chunks of at most `maxPerFile` (default: `MAX_URLS_PER_SITEMAP` = 50,000).
303
+
304
+ ### `generateSitemapIndex(config)`
305
+
306
+ ```ts
307
+ function generateSitemapIndex(config: SitemapIndexConfig): string
308
+ ```
309
+
310
+ | Prop | Type | Description |
311
+ |------|------|-------------|
312
+ | `sitemaps` | `SitemapIndexEntry[]` | Array of `{ loc: string; lastmod?: string }` |
313
+
314
+ ### `validateSitemapUrl(url)`
315
+
316
+ ```ts
317
+ function validateSitemapUrl(url: SitemapURL): SitemapValidationResult
318
+ ```
319
+
320
+ Returns `{ valid: boolean; errors: string[]; warnings: string[] }`.
321
+
322
+ ---
323
+
324
+ ## Contributing
325
+
326
+ - Issues: [github.com/cybercraftbd/power-seo/issues](https://github.com/cybercraftbd/power-seo/issues)
327
+ - PRs: [github.com/cybercraftbd/power-seo/pulls](https://github.com/cybercraftbd/power-seo/pulls)
328
+ - Development setup:
329
+ 1. `pnpm i`
330
+ 2. `pnpm build`
331
+ 3. `pnpm test`
332
+
333
+ **Release workflow**
334
+ - `npm version patch|minor|major`
335
+ - `npm publish --access public`
336
+
337
+ ---
338
+
339
+ ## About CyberCraft Bangladesh
340
+
341
+ **CyberCraft Bangladesh** is a Bangladesh-based enterprise-grade software engineering company specializing in ERP system development, AI-powered SaaS and business applications, full-stack SEO services, custom website development, and scalable eCommerce platforms. We design and develop intelligent, automation-driven SaaS and enterprise solutions that help startups, SMEs, NGOs, educational institutes, and large organizations streamline operations, enhance digital visibility, and accelerate growth through modern cloud-native technologies.
342
+
343
+ | | |
344
+ |---|---|
345
+ | **Website** | [ccbd.dev](https://ccbd.dev) |
346
+ | **GitHub** | [github.com/cybercraftbd](https://github.com/cybercraftbd) |
347
+ | **npm Organization** | [npmjs.com/org/power-seo](https://www.npmjs.com/org/power-seo) |
348
+ | **Email** | [info@ccbd.dev](mailto:info@ccbd.dev) |
349
+
350
+ ---
351
+
352
+ ## License
353
+
354
+ **MIT**
355
+
356
+ ---
357
+
358
+ ## Keywords
359
+
360
+ ```text
361
+ seo, sitemap, xml-sitemap, sitemap-generator, image-sitemap, video-sitemap, news-sitemap, sitemap-index, typescript, nextjs, remix, streaming, crawl-budget, google-indexing
362
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ var core = require('@power-seo/core');
4
+
5
+ // src/generator.ts
6
+ function escapeXml(str) {
7
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
8
+ }
9
+ function detectNamespaces(urls) {
10
+ let image = false;
11
+ let video = false;
12
+ let news = false;
13
+ for (const url of urls) {
14
+ if (url.images && url.images.length > 0) image = true;
15
+ if (url.videos && url.videos.length > 0) video = true;
16
+ if (url.news) news = true;
17
+ if (image && video && news) break;
18
+ }
19
+ return { image, video, news };
20
+ }
21
+ function buildImageXml(images) {
22
+ return images.map((img) => {
23
+ let xml = " <image:image>\n";
24
+ xml += ` <image:loc>${escapeXml(img.loc)}</image:loc>
25
+ `;
26
+ if (img.caption) xml += ` <image:caption>${escapeXml(img.caption)}</image:caption>
27
+ `;
28
+ if (img.geoLocation) xml += ` <image:geo_location>${escapeXml(img.geoLocation)}</image:geo_location>
29
+ `;
30
+ if (img.title) xml += ` <image:title>${escapeXml(img.title)}</image:title>
31
+ `;
32
+ if (img.license) xml += ` <image:license>${escapeXml(img.license)}</image:license>
33
+ `;
34
+ xml += " </image:image>\n";
35
+ return xml;
36
+ }).join("");
37
+ }
38
+ function buildVideoXml(videos) {
39
+ return videos.map((vid) => {
40
+ let xml = " <video:video>\n";
41
+ xml += ` <video:thumbnail_loc>${escapeXml(vid.thumbnailLoc)}</video:thumbnail_loc>
42
+ `;
43
+ xml += ` <video:title>${escapeXml(vid.title)}</video:title>
44
+ `;
45
+ xml += ` <video:description>${escapeXml(vid.description)}</video:description>
46
+ `;
47
+ if (vid.contentLoc) xml += ` <video:content_loc>${escapeXml(vid.contentLoc)}</video:content_loc>
48
+ `;
49
+ if (vid.playerLoc) xml += ` <video:player_loc>${escapeXml(vid.playerLoc)}</video:player_loc>
50
+ `;
51
+ if (vid.duration !== void 0) xml += ` <video:duration>${vid.duration}</video:duration>
52
+ `;
53
+ if (vid.expirationDate) xml += ` <video:expiration_date>${escapeXml(vid.expirationDate)}</video:expiration_date>
54
+ `;
55
+ if (vid.rating !== void 0) xml += ` <video:rating>${vid.rating}</video:rating>
56
+ `;
57
+ if (vid.viewCount !== void 0) xml += ` <video:view_count>${vid.viewCount}</video:view_count>
58
+ `;
59
+ if (vid.publicationDate) xml += ` <video:publication_date>${escapeXml(vid.publicationDate)}</video:publication_date>
60
+ `;
61
+ if (vid.familyFriendly !== void 0) xml += ` <video:family_friendly>${vid.familyFriendly ? "yes" : "no"}</video:family_friendly>
62
+ `;
63
+ if (vid.live !== void 0) xml += ` <video:live>${vid.live ? "yes" : "no"}</video:live>
64
+ `;
65
+ xml += " </video:video>\n";
66
+ return xml;
67
+ }).join("");
68
+ }
69
+ function buildNewsXml(news) {
70
+ let xml = " <news:news>\n";
71
+ xml += " <news:publication>\n";
72
+ xml += ` <news:name>${escapeXml(news.publication.name)}</news:name>
73
+ `;
74
+ xml += ` <news:language>${escapeXml(news.publication.language)}</news:language>
75
+ `;
76
+ xml += " </news:publication>\n";
77
+ xml += ` <news:publication_date>${escapeXml(news.publicationDate)}</news:publication_date>
78
+ `;
79
+ xml += ` <news:title>${escapeXml(news.title)}</news:title>
80
+ `;
81
+ xml += " </news:news>\n";
82
+ return xml;
83
+ }
84
+ function buildUrlXml(url, hostname) {
85
+ const loc = url.loc.startsWith("http") ? url.loc : core.normalizeUrl(`${hostname}${url.loc.startsWith("/") ? "" : "/"}${url.loc}`);
86
+ let xml = " <url>\n";
87
+ xml += ` <loc>${escapeXml(loc)}</loc>
88
+ `;
89
+ if (url.lastmod) xml += ` <lastmod>${escapeXml(url.lastmod)}</lastmod>
90
+ `;
91
+ if (url.changefreq) xml += ` <changefreq>${url.changefreq}</changefreq>
92
+ `;
93
+ if (url.priority !== void 0) xml += ` <priority>${url.priority.toFixed(1)}</priority>
94
+ `;
95
+ if (url.images && url.images.length > 0) xml += buildImageXml(url.images);
96
+ if (url.videos && url.videos.length > 0) xml += buildVideoXml(url.videos);
97
+ if (url.news) xml += buildNewsXml(url.news);
98
+ xml += " </url>\n";
99
+ return xml;
100
+ }
101
+ function generateSitemap(config) {
102
+ const { hostname, urls } = config;
103
+ const ns = detectNamespaces(urls);
104
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
105
+ xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
106
+ if (ns.image) xml += '\n xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"';
107
+ if (ns.video) xml += '\n xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"';
108
+ if (ns.news) xml += '\n xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"';
109
+ xml += ">\n";
110
+ for (const url of urls) {
111
+ xml += buildUrlXml(url, hostname);
112
+ }
113
+ xml += "</urlset>\n";
114
+ return xml;
115
+ }
116
+
117
+ // src/types.ts
118
+ var MAX_URLS_PER_SITEMAP = 5e4;
119
+ var MAX_SITEMAP_SIZE_BYTES = 50 * 1024 * 1024;
120
+
121
+ // src/sitemap-index.ts
122
+ function escapeXml2(str) {
123
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
124
+ }
125
+ function generateSitemapIndex(config) {
126
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
127
+ xml += '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
128
+ for (const sitemap of config.sitemaps) {
129
+ xml += " <sitemap>\n";
130
+ xml += ` <loc>${escapeXml2(sitemap.loc)}</loc>
131
+ `;
132
+ if (sitemap.lastmod) {
133
+ xml += ` <lastmod>${escapeXml2(sitemap.lastmod)}</lastmod>
134
+ `;
135
+ }
136
+ xml += " </sitemap>\n";
137
+ }
138
+ xml += "</sitemapindex>\n";
139
+ return xml;
140
+ }
141
+ function splitSitemap(config, sitemapUrlPattern = "/sitemap-{index}.xml") {
142
+ const maxPerSitemap = config.maxUrlsPerSitemap ?? MAX_URLS_PER_SITEMAP;
143
+ const { hostname, urls } = config;
144
+ if (urls.length <= maxPerSitemap) {
145
+ const xml = generateSitemap(config);
146
+ const filename = sitemapUrlPattern.replace("{index}", "0");
147
+ const indexEntries2 = [{ loc: `${hostname}${filename}` }];
148
+ return {
149
+ index: generateSitemapIndex({ sitemaps: indexEntries2 }),
150
+ sitemaps: [{ filename, xml }]
151
+ };
152
+ }
153
+ const sitemaps = [];
154
+ const indexEntries = [];
155
+ for (let i = 0; i < urls.length; i += maxPerSitemap) {
156
+ const chunk = urls.slice(i, i + maxPerSitemap);
157
+ const chunkIndex = Math.floor(i / maxPerSitemap);
158
+ const filename = sitemapUrlPattern.replace("{index}", String(chunkIndex));
159
+ const xml = generateSitemap({ hostname, urls: chunk });
160
+ sitemaps.push({ filename, xml });
161
+ indexEntries.push({ loc: `${hostname}${filename}` });
162
+ }
163
+ return {
164
+ index: generateSitemapIndex({ sitemaps: indexEntries }),
165
+ sitemaps
166
+ };
167
+ }
168
+ function escapeXml3(str) {
169
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
170
+ }
171
+ function* streamSitemap(hostname, urls) {
172
+ yield '<?xml version="1.0" encoding="UTF-8"?>\n';
173
+ yield '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n';
174
+ yield ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"\n';
175
+ yield ' xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"\n';
176
+ yield ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">\n';
177
+ for (const url of urls) {
178
+ const loc = url.loc.startsWith("http") ? url.loc : core.normalizeUrl(`${hostname}${url.loc.startsWith("/") ? "" : "/"}${url.loc}`);
179
+ yield " <url>\n";
180
+ yield ` <loc>${escapeXml3(loc)}</loc>
181
+ `;
182
+ if (url.lastmod) yield ` <lastmod>${escapeXml3(url.lastmod)}</lastmod>
183
+ `;
184
+ if (url.changefreq) yield ` <changefreq>${url.changefreq}</changefreq>
185
+ `;
186
+ if (url.priority !== void 0) yield ` <priority>${url.priority.toFixed(1)}</priority>
187
+ `;
188
+ if (url.images) {
189
+ for (const img of url.images) {
190
+ yield " <image:image>\n";
191
+ yield ` <image:loc>${escapeXml3(img.loc)}</image:loc>
192
+ `;
193
+ if (img.caption) yield ` <image:caption>${escapeXml3(img.caption)}</image:caption>
194
+ `;
195
+ if (img.title) yield ` <image:title>${escapeXml3(img.title)}</image:title>
196
+ `;
197
+ yield " </image:image>\n";
198
+ }
199
+ }
200
+ if (url.videos) {
201
+ for (const vid of url.videos) {
202
+ yield " <video:video>\n";
203
+ yield ` <video:thumbnail_loc>${escapeXml3(vid.thumbnailLoc)}</video:thumbnail_loc>
204
+ `;
205
+ yield ` <video:title>${escapeXml3(vid.title)}</video:title>
206
+ `;
207
+ yield ` <video:description>${escapeXml3(vid.description)}</video:description>
208
+ `;
209
+ if (vid.contentLoc) yield ` <video:content_loc>${escapeXml3(vid.contentLoc)}</video:content_loc>
210
+ `;
211
+ if (vid.playerLoc) yield ` <video:player_loc>${escapeXml3(vid.playerLoc)}</video:player_loc>
212
+ `;
213
+ yield " </video:video>\n";
214
+ }
215
+ }
216
+ if (url.news) {
217
+ yield " <news:news>\n";
218
+ yield " <news:publication>\n";
219
+ yield ` <news:name>${escapeXml3(url.news.publication.name)}</news:name>
220
+ `;
221
+ yield ` <news:language>${escapeXml3(url.news.publication.language)}</news:language>
222
+ `;
223
+ yield " </news:publication>\n";
224
+ yield ` <news:publication_date>${escapeXml3(url.news.publicationDate)}</news:publication_date>
225
+ `;
226
+ yield ` <news:title>${escapeXml3(url.news.title)}</news:title>
227
+ `;
228
+ yield " </news:news>\n";
229
+ }
230
+ yield " </url>\n";
231
+ }
232
+ yield "</urlset>\n";
233
+ }
234
+ var VALID_CHANGEFREQ = ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"];
235
+ function validateSitemapUrl(url) {
236
+ const errors = [];
237
+ const warnings = [];
238
+ if (!url.loc || url.loc.trim().length === 0) {
239
+ errors.push('URL "loc" is required and cannot be empty.');
240
+ } else {
241
+ if (!core.isAbsoluteUrl(url.loc)) {
242
+ errors.push(`URL "${url.loc}" must be an absolute URL (starting with http:// or https://).`);
243
+ }
244
+ if (url.loc.length > 2048) {
245
+ errors.push(`URL "${url.loc}" exceeds the maximum length of 2048 characters.`);
246
+ }
247
+ if (url.loc.length > core.MAX_URL_LENGTH) {
248
+ warnings.push(
249
+ `URL "${url.loc}" is ${url.loc.length} characters. URLs under ${core.MAX_URL_LENGTH} characters are recommended for SEO.`
250
+ );
251
+ }
252
+ }
253
+ if (url.lastmod) {
254
+ const dateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?([+-]\d{2}:\d{2}|Z)?)?$/;
255
+ if (!dateRegex.test(url.lastmod)) {
256
+ errors.push(
257
+ `"lastmod" value "${url.lastmod}" is not a valid W3C datetime format (YYYY-MM-DD or ISO 8601).`
258
+ );
259
+ }
260
+ }
261
+ if (url.changefreq && !VALID_CHANGEFREQ.includes(url.changefreq)) {
262
+ errors.push(
263
+ `"changefreq" value "${url.changefreq}" is invalid. Must be one of: ${VALID_CHANGEFREQ.join(", ")}.`
264
+ );
265
+ }
266
+ if (url.priority !== void 0) {
267
+ if (url.priority < 0 || url.priority > 1) {
268
+ errors.push(`"priority" value ${url.priority} is out of range. Must be between 0.0 and 1.0.`);
269
+ }
270
+ }
271
+ if (url.images) {
272
+ for (let i = 0; i < url.images.length; i++) {
273
+ const img = url.images[i];
274
+ if (!img.loc || img.loc.trim().length === 0) {
275
+ errors.push(`Image ${i + 1}: "loc" is required.`);
276
+ } else if (!core.isAbsoluteUrl(img.loc)) {
277
+ errors.push(`Image ${i + 1}: "${img.loc}" must be an absolute URL.`);
278
+ }
279
+ }
280
+ if (url.images.length > 1e3) {
281
+ warnings.push(`URL has ${url.images.length} images. Google supports up to 1,000 images per page.`);
282
+ }
283
+ }
284
+ if (url.videos) {
285
+ for (let i = 0; i < url.videos.length; i++) {
286
+ const vid = url.videos[i];
287
+ if (!vid.title) errors.push(`Video ${i + 1}: "title" is required.`);
288
+ if (!vid.description) errors.push(`Video ${i + 1}: "description" is required.`);
289
+ if (!vid.thumbnailLoc) errors.push(`Video ${i + 1}: "thumbnailLoc" is required.`);
290
+ if (!vid.contentLoc && !vid.playerLoc) {
291
+ errors.push(`Video ${i + 1}: either "contentLoc" or "playerLoc" must be provided.`);
292
+ }
293
+ if (vid.rating !== void 0 && (vid.rating < 0 || vid.rating > 5)) {
294
+ errors.push(`Video ${i + 1}: "rating" must be between 0.0 and 5.0.`);
295
+ }
296
+ }
297
+ }
298
+ if (url.news) {
299
+ if (!url.news.publication?.name) errors.push('News: "publication.name" is required.');
300
+ if (!url.news.publication?.language) errors.push('News: "publication.language" is required.');
301
+ if (!url.news.publicationDate) errors.push('News: "publicationDate" is required.');
302
+ if (!url.news.title) errors.push('News: "title" is required.');
303
+ }
304
+ return {
305
+ valid: errors.length === 0,
306
+ errors,
307
+ warnings
308
+ };
309
+ }
310
+
311
+ exports.MAX_SITEMAP_SIZE_BYTES = MAX_SITEMAP_SIZE_BYTES;
312
+ exports.MAX_URLS_PER_SITEMAP = MAX_URLS_PER_SITEMAP;
313
+ exports.generateSitemap = generateSitemap;
314
+ exports.generateSitemapIndex = generateSitemapIndex;
315
+ exports.splitSitemap = splitSitemap;
316
+ exports.streamSitemap = streamSitemap;
317
+ exports.validateSitemapUrl = validateSitemapUrl;
318
+ //# sourceMappingURL=index.cjs.map
319
+ //# sourceMappingURL=index.cjs.map