@xexr/blog-platform 1.0.12 → 1.0.14

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.
@@ -30,12 +30,14 @@ import type {
30
30
  PublishedSite,
31
31
  PublishedAuthor,
32
32
  } from "../types/contracts";
33
+ import { getAuthorsFromArticles } from "./authors";
33
34
  import {
34
35
  generateCanonicalUrl,
35
36
  normalizeDomain,
36
37
  type CanonicalUrlOptions,
37
38
  } from "./seo";
38
- import { buildAuthorPath } from "./urls";
39
+ import { getAllTopics } from "./topics";
40
+ import { buildAuthorPath, buildBlogPath } from "./urls";
39
41
 
40
42
  /**
41
43
  * Configuration options for sitemap generation.
@@ -47,6 +49,9 @@ export interface SitemapOptions {
47
49
  /** Whether to include author pages in sitemap */
48
50
  includeAuthorPages?: boolean;
49
51
 
52
+ /** Whether to include topic pages in sitemap */
53
+ includeTopicPages?: boolean;
54
+
50
55
  /** Whether to include pagination pages in sitemap */
51
56
  includePaginationPages?: boolean;
52
57
 
@@ -64,6 +69,8 @@ export interface SitemapOptions {
64
69
  articles?: SitemapChangeFreq;
65
70
  /** Change frequency for author pages */
66
71
  authorPages?: SitemapChangeFreq;
72
+ /** Change frequency for topic index and archive pages */
73
+ topicPages?: SitemapChangeFreq;
67
74
  /** Change frequency for pagination pages */
68
75
  paginationPages?: SitemapChangeFreq;
69
76
  };
@@ -76,6 +83,8 @@ export interface SitemapOptions {
76
83
  articles?: number;
77
84
  /** Priority for author pages (0.0-1.0) */
78
85
  authorPages?: number;
86
+ /** Priority for topic index and archive pages (0.0-1.0) */
87
+ topicPages?: number;
79
88
  /** Priority for pagination pages (0.0-1.0) */
80
89
  paginationPages?: number;
81
90
  };
@@ -194,6 +203,7 @@ export async function generateBlogSitemapEntries(
194
203
  ): Promise<NextjsSitemapEntry[]> {
195
204
  const {
196
205
  includeAuthorPages = true,
206
+ includeTopicPages = true,
197
207
  includePaginationPages = true,
198
208
  articlesPerPage = 10,
199
209
  maxEntries = 50000,
@@ -208,6 +218,9 @@ export async function generateBlogSitemapEntries(
208
218
  );
209
219
 
210
220
  const pathPrefix = options.canonical?.pathPrefix ?? "/blog";
221
+ const sitemapAuthors =
222
+ authors.length > 0 ? authors : getAuthorsFromArticles(articles);
223
+ const sitemapTopics = getAllTopics([...articles]);
211
224
 
212
225
  // Add blog home page
213
226
  entries.push({
@@ -230,9 +243,44 @@ export async function generateBlogSitemapEntries(
230
243
  });
231
244
  });
232
245
 
246
+ if (includeTopicPages && sitemapTopics.length > 0) {
247
+ if (entries.length < maxEntries) {
248
+ entries.push({
249
+ url: `${baseUrl}${buildBlogPath(pathPrefix, "topics")}`,
250
+ lastModified: getLatestModificationDate(articles),
251
+ changeFrequency: changeFrequency.topicPages ?? "weekly",
252
+ priority: priority.topicPages ?? 0.6,
253
+ });
254
+ }
255
+
256
+ sitemapTopics.forEach((topic) => {
257
+ if (entries.length >= maxEntries) return;
258
+
259
+ const topicArticles = articles.filter((article) =>
260
+ (article.topics ?? []).some((candidate) => candidate.slug === topic.slug),
261
+ );
262
+
263
+ entries.push({
264
+ url: `${baseUrl}${buildBlogPath(pathPrefix, `topics/${topic.slug}`)}`,
265
+ lastModified: getLatestModificationDate(topicArticles),
266
+ changeFrequency: changeFrequency.topicPages ?? "weekly",
267
+ priority: priority.topicPages ?? 0.6,
268
+ });
269
+ });
270
+ }
271
+
233
272
  // Add author pages if enabled
234
- if (includeAuthorPages) {
235
- authors.forEach((author) => {
273
+ if (includeAuthorPages && sitemapAuthors.length > 0) {
274
+ if (entries.length < maxEntries) {
275
+ entries.push({
276
+ url: `${baseUrl}${buildBlogPath(pathPrefix, "authors")}`,
277
+ lastModified: getLatestModificationDate(articles),
278
+ changeFrequency: changeFrequency.authorPages ?? "weekly",
279
+ priority: priority.authorPages ?? 0.6,
280
+ });
281
+ }
282
+
283
+ sitemapAuthors.forEach((author) => {
236
284
  if (entries.length >= maxEntries) return;
237
285
 
238
286
  const authorUrl = `${baseUrl}${buildAuthorPath(pathPrefix, author.slug)}`;
@@ -289,6 +337,7 @@ export function generateSitemap(
289
337
  ): SitemapData {
290
338
  const {
291
339
  includeAuthorPages = true,
340
+ includeTopicPages = true,
292
341
  includePaginationPages = true,
293
342
  articlesPerPage = 10,
294
343
  maxEntries = 50000,
@@ -304,12 +353,18 @@ export function generateSitemap(
304
353
 
305
354
  const pathPrefix = options.canonical?.pathPrefix ?? "/blog";
306
355
  const generatedAt = new Date().toISOString();
356
+ const sitemapAuthors =
357
+ authors.length > 0 ? authors : getAuthorsFromArticles(articles);
358
+ const sitemapTopics = getAllTopics([...articles]);
307
359
 
308
360
  // Calculate total potential entries to determine if truncation will occur
309
361
  let potentialEntries = 1; // Home page
310
362
  potentialEntries += articles.length; // Article pages
363
+ if (includeTopicPages && sitemapTopics.length > 0) {
364
+ potentialEntries += sitemapTopics.length + 1; // Topic index + topic pages
365
+ }
311
366
  if (includeAuthorPages) {
312
- potentialEntries += authors.length; // Author pages
367
+ potentialEntries += sitemapAuthors.length > 0 ? sitemapAuthors.length + 1 : 0; // Authors index + author pages
313
368
  }
314
369
  if (includePaginationPages && articles.length > articlesPerPage) {
315
370
  const totalPages = Math.ceil(articles.length / articlesPerPage);
@@ -339,9 +394,44 @@ export function generateSitemap(
339
394
  });
340
395
  });
341
396
 
397
+ if (includeTopicPages && sitemapTopics.length > 0) {
398
+ if (entries.length < maxEntries) {
399
+ entries.push({
400
+ loc: `${baseUrl}${buildBlogPath(pathPrefix, "topics")}`,
401
+ lastmod: getLatestModificationDate(articles),
402
+ changefreq: changeFrequency.topicPages ?? "weekly",
403
+ priority: priority.topicPages ?? 0.6,
404
+ });
405
+ }
406
+
407
+ sitemapTopics.forEach((topic) => {
408
+ if (entries.length >= maxEntries) return;
409
+
410
+ const topicArticles = articles.filter((article) =>
411
+ (article.topics ?? []).some((candidate) => candidate.slug === topic.slug),
412
+ );
413
+
414
+ entries.push({
415
+ loc: `${baseUrl}${buildBlogPath(pathPrefix, `topics/${topic.slug}`)}`,
416
+ lastmod: getLatestModificationDate(topicArticles),
417
+ changefreq: changeFrequency.topicPages ?? "weekly",
418
+ priority: priority.topicPages ?? 0.6,
419
+ });
420
+ });
421
+ }
422
+
342
423
  // Add author pages if enabled
343
- if (includeAuthorPages) {
344
- authors.forEach((author) => {
424
+ if (includeAuthorPages && sitemapAuthors.length > 0) {
425
+ if (entries.length < maxEntries) {
426
+ entries.push({
427
+ loc: `${baseUrl}${buildBlogPath(pathPrefix, "authors")}`,
428
+ lastmod: getLatestModificationDate(articles),
429
+ changefreq: changeFrequency.authorPages ?? "weekly",
430
+ priority: priority.authorPages ?? 0.6,
431
+ });
432
+ }
433
+
434
+ sitemapAuthors.forEach((author) => {
345
435
  if (entries.length >= maxEntries) return;
346
436
 
347
437
  const authorUrl = `${baseUrl}${buildAuthorPath(pathPrefix, author.slug)}`;
@@ -654,6 +744,12 @@ export function createBlogSitemapRoute(
654
744
  * Used to fetch blog data for sitemap generation.
655
745
  */
656
746
  export interface BlogSitemapConfig {
747
+ /** Static host app routes to include before Bloggy routes */
748
+ staticRoutes?: readonly NextjsSitemapEntry[];
749
+
750
+ /** Function to fetch static host app routes */
751
+ getStaticRoutes?: () => Promise<readonly NextjsSitemapEntry[]>;
752
+
657
753
  /** Function to fetch site data */
658
754
  getSite: () => Promise<PublishedSite>;
659
755
 
@@ -693,11 +789,23 @@ export interface BlogSitemapConfig {
693
789
  */
694
790
  export function createDynamicBlogSitemap(config: BlogSitemapConfig) {
695
791
  return async function sitemap(): Promise<NextjsSitemapEntry[]> {
792
+ const staticRoutes = [
793
+ ...(config.staticRoutes ?? []),
794
+ ...(config.getStaticRoutes ? await config.getStaticRoutes() : []),
795
+ ];
696
796
  const site = await config.getSite();
697
797
  const articles = await config.getArticles();
698
798
  const authors = config.getAuthors ? await config.getAuthors() : [];
699
799
 
700
- return generateBlogSitemapEntries(site, articles, authors, config.options);
800
+ return [
801
+ ...staticRoutes,
802
+ ...(await generateBlogSitemapEntries(
803
+ site,
804
+ articles,
805
+ authors,
806
+ config.options,
807
+ )),
808
+ ];
701
809
  };
702
810
  }
703
811
 
@@ -0,0 +1,422 @@
1
+ import { PublishedArticle, PublishedSite, PublishedAuthor } from './types/index.js';
2
+
3
+ /**
4
+ * SEO utilities for canonical URL generation and validation.
5
+ *
6
+ * These utilities ensure proper canonical URL structure for SEO optimization,
7
+ * preventing duplicate content issues and improving search engine indexing.
8
+ */
9
+
10
+ /**
11
+ * Configuration options for canonical URL generation.
12
+ */
13
+ interface CanonicalUrlOptions {
14
+ /** Whether to force HTTPS protocol (default: true) */
15
+ forceHttps?: boolean;
16
+ /** Whether to remove trailing slashes (default: true) */
17
+ removeTrailingSlash?: boolean;
18
+ /** Custom domain override (uses site.domain if not provided) */
19
+ domain?: string;
20
+ /** Custom path prefix (default: '/blog') */
21
+ pathPrefix?: string;
22
+ }
23
+ /**
24
+ * Normalizes a domain by ensuring proper protocol and removing trailing slashes.
25
+ *
26
+ * @param domain - The domain to normalize
27
+ * @param forceHttps - Whether to force HTTPS protocol
28
+ * @returns The normalized domain
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * normalizeDomain('example.com') // 'https://example.com'
33
+ * normalizeDomain('http://example.com/') // 'https://example.com'
34
+ * normalizeDomain('https://example.com', false) // 'https://example.com'
35
+ * ```
36
+ */
37
+ declare function normalizeDomain(domain: string, forceHttps?: boolean): string;
38
+ /**
39
+ * Validates and normalizes a slug for URL usage.
40
+ *
41
+ * @param slug - The slug to validate and normalize
42
+ * @returns The normalized slug
43
+ * @throws Error if slug is invalid
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * normalizeSlug('hello-world') // 'hello-world'
48
+ * normalizeSlug('Hello World!') // throws Error
49
+ * normalizeSlug('hello--world') // 'hello-world'
50
+ * ```
51
+ */
52
+ declare function normalizeSlug(slug: string): string;
53
+ /**
54
+ * Generates a canonical URL for an article.
55
+ *
56
+ * @param article - The published article
57
+ * @param options - Configuration options for URL generation
58
+ * @returns The canonical URL
59
+ * @throws Error if article or site data is invalid
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * const article = {
64
+ * slug: 'hello-world',
65
+ * site: { domain: 'example.com' },
66
+ * // ... other fields
67
+ * };
68
+ *
69
+ * generateCanonicalUrl(article)
70
+ * // 'https://example.com/blog/hello-world'
71
+ *
72
+ * generateCanonicalUrl(article, { pathPrefix: '/posts' })
73
+ * // 'https://example.com/posts/hello-world'
74
+ * ```
75
+ */
76
+ declare function generateCanonicalUrl(article: PublishedArticle, options?: CanonicalUrlOptions): string;
77
+ /**
78
+ * Generates a canonical URL for an article from site and slug data.
79
+ * Useful when you don't have a full PublishedArticle object.
80
+ *
81
+ * @param site - The published site
82
+ * @param slug - The article slug
83
+ * @param options - Configuration options for URL generation
84
+ * @returns The canonical URL
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const site = { domain: 'example.com', ... };
89
+ * generateCanonicalUrlFromSlug(site, 'hello-world')
90
+ * // 'https://example.com/blog/hello-world'
91
+ * ```
92
+ */
93
+ declare function generateCanonicalUrlFromSlug(site: PublishedSite, slug: string, options?: CanonicalUrlOptions): string;
94
+ /**
95
+ * Validates if a URL is a valid canonical URL format.
96
+ *
97
+ * @param url - The URL to validate
98
+ * @returns True if the URL is valid for canonical use
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * isValidCanonicalUrl('https://example.com/blog/hello-world') // true
103
+ * isValidCanonicalUrl('http://example.com/blog/hello-world') // true
104
+ * isValidCanonicalUrl('example.com/blog/hello-world') // false (missing protocol)
105
+ * isValidCanonicalUrl('https://example.com/blog/hello world') // false (spaces)
106
+ * ```
107
+ */
108
+ declare function isValidCanonicalUrl(url: string): boolean;
109
+ /**
110
+ * Extracts the base URL (protocol + hostname + port) from a canonical URL.
111
+ *
112
+ * @param canonicalUrl - The canonical URL
113
+ * @returns The base URL
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * getBaseUrl('https://example.com/blog/hello-world') // 'https://example.com'
118
+ * getBaseUrl('https://blog.example.com:8080/posts/test') // 'https://blog.example.com:8080'
119
+ * ```
120
+ */
121
+ declare function getBaseUrl(canonicalUrl: string): string;
122
+ /**
123
+ * Updates an article's canonical URL in its meta field.
124
+ *
125
+ * @param article - The article to update
126
+ * @param options - Configuration options for URL generation
127
+ * @returns A new article object with updated canonical URL
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * const updatedArticle = updateArticleCanonicalUrl(article);
132
+ * console.log(updatedArticle.meta.canonicalUrl); // Generated canonical URL
133
+ * ```
134
+ */
135
+ declare function updateArticleCanonicalUrl(article: PublishedArticle, options?: CanonicalUrlOptions): PublishedArticle;
136
+
137
+ /**
138
+ * Next.js-compatible sitemap generation utilities for blog platform integration.
139
+ *
140
+ * These utilities generate sitemap entries that follow Next.js MetadataRoute.Sitemap
141
+ * conventions, allowing seamless integration with existing Next.js applications.
142
+ *
143
+ * @example Integration with existing Next.js sitemap:
144
+ * ```typescript
145
+ * // app/sitemap.ts
146
+ * import { generateBlogSitemapEntries } from '@xexr/blog-platform';
147
+ * import type { MetadataRoute } from 'next';
148
+ *
149
+ * export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
150
+ * // Your existing routes
151
+ * const staticRoutes = [
152
+ * { url: 'https://yoursite.com', lastModified: new Date(), priority: 1 },
153
+ * { url: 'https://yoursite.com/about', lastModified: new Date(), priority: 0.8 }
154
+ * ];
155
+ *
156
+ * // Blog platform entries
157
+ * const blogEntries = await generateBlogSitemapEntries(blogConfig);
158
+ *
159
+ * return [...staticRoutes, ...blogEntries];
160
+ * }
161
+ * ```
162
+ */
163
+
164
+ /**
165
+ * Configuration options for sitemap generation.
166
+ */
167
+ interface SitemapOptions {
168
+ /** Canonical URL configuration */
169
+ canonical?: CanonicalUrlOptions;
170
+ /** Whether to include author pages in sitemap */
171
+ includeAuthorPages?: boolean;
172
+ /** Whether to include topic pages in sitemap */
173
+ includeTopicPages?: boolean;
174
+ /** Whether to include pagination pages in sitemap */
175
+ includePaginationPages?: boolean;
176
+ /** Number of articles per page for pagination calculation */
177
+ articlesPerPage?: number;
178
+ /** Maximum number of sitemap entries (defaults to 50,000 per Google guidelines) */
179
+ maxEntries?: number;
180
+ /** Change frequency for different page types */
181
+ changeFrequency?: {
182
+ /** Change frequency for blog home page */
183
+ homePage?: SitemapChangeFreq;
184
+ /** Change frequency for article pages */
185
+ articles?: SitemapChangeFreq;
186
+ /** Change frequency for author pages */
187
+ authorPages?: SitemapChangeFreq;
188
+ /** Change frequency for topic index and archive pages */
189
+ topicPages?: SitemapChangeFreq;
190
+ /** Change frequency for pagination pages */
191
+ paginationPages?: SitemapChangeFreq;
192
+ };
193
+ /** Priority values for different page types */
194
+ priority?: {
195
+ /** Priority for blog home page (0.0-1.0) */
196
+ homePage?: number;
197
+ /** Priority for article pages (0.0-1.0) */
198
+ articles?: number;
199
+ /** Priority for author pages (0.0-1.0) */
200
+ authorPages?: number;
201
+ /** Priority for topic index and archive pages (0.0-1.0) */
202
+ topicPages?: number;
203
+ /** Priority for pagination pages (0.0-1.0) */
204
+ paginationPages?: number;
205
+ };
206
+ }
207
+ /**
208
+ * Valid sitemap change frequency values as per XML sitemap standard.
209
+ */
210
+ type SitemapChangeFreq = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
211
+ /**
212
+ * Next.js MetadataRoute.Sitemap compatible entry.
213
+ * This matches the expected format for Next.js sitemap generation.
214
+ */
215
+ interface NextjsSitemapEntry {
216
+ /** The URL of the page */
217
+ url: string;
218
+ /** Last modification date */
219
+ lastModified?: string | Date;
220
+ /** Change frequency hint for search engines */
221
+ changeFrequency?: SitemapChangeFreq;
222
+ /** Priority relative to other URLs on the site (0.0-1.0) */
223
+ priority?: number;
224
+ /** Alternate language versions (for i18n) */
225
+ alternates?: {
226
+ languages?: Record<string, string>;
227
+ };
228
+ }
229
+ /**
230
+ * Legacy XML sitemap entry format (for backward compatibility).
231
+ * @deprecated Use NextjsSitemapEntry for new implementations
232
+ */
233
+ interface SitemapEntry {
234
+ /** The URL of the page */
235
+ loc: string;
236
+ /** Last modification date as ISO 8601 string */
237
+ lastmod?: string;
238
+ /** Change frequency hint for search engines */
239
+ changefreq?: SitemapChangeFreq;
240
+ /** Priority relative to other URLs on the site (0.0-1.0) */
241
+ priority?: number;
242
+ }
243
+ /**
244
+ * Complete sitemap data structure.
245
+ */
246
+ interface SitemapData {
247
+ /** All URL entries in the sitemap */
248
+ entries: SitemapEntry[];
249
+ /** Total number of entries */
250
+ totalEntries: number;
251
+ /** Whether the sitemap was truncated due to size limits */
252
+ truncated: boolean;
253
+ /** Generation timestamp */
254
+ generatedAt: string;
255
+ /** Base URL used for canonical URL generation */
256
+ baseUrl: string;
257
+ }
258
+ /**
259
+ * Generates Next.js-compatible sitemap entries for blog platform integration.
260
+ *
261
+ * @param site - The published site information
262
+ * @param articles - Array of published articles
263
+ * @param authors - Array of published authors (optional)
264
+ * @param options - Sitemap generation options
265
+ * @returns Array of Next.js MetadataRoute.Sitemap compatible entries
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * // In your Next.js app/sitemap.ts file:
270
+ * import { generateBlogSitemapEntries } from '@xexr/blog-platform';
271
+ * import type { MetadataRoute } from 'next';
272
+ *
273
+ * export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
274
+ * const blogEntries = await generateBlogSitemapEntries(site, articles, authors, {
275
+ * includeAuthorPages: true,
276
+ * includePaginationPages: true,
277
+ * changeFrequency: { articles: 'monthly' }
278
+ * });
279
+ *
280
+ * return [
281
+ * // Your existing site routes
282
+ * { url: 'https://yoursite.com', lastModified: new Date(), priority: 1 },
283
+ * { url: 'https://yoursite.com/about', lastModified: new Date() },
284
+ * // Blog entries
285
+ * ...blogEntries
286
+ * ];
287
+ * }
288
+ * ```
289
+ */
290
+ declare function generateBlogSitemapEntries(site: PublishedSite, articles: readonly PublishedArticle[], authors?: readonly PublishedAuthor[], options?: SitemapOptions): Promise<NextjsSitemapEntry[]>;
291
+ /**
292
+ * Legacy sitemap generation for backward compatibility.
293
+ * @deprecated Use generateBlogSitemapEntries for Next.js integration
294
+ *
295
+ * @param site - The published site information
296
+ * @param articles - Array of published articles
297
+ * @param authors - Array of published authors (optional)
298
+ * @param options - Sitemap generation options
299
+ * @returns Complete sitemap data
300
+ */
301
+ declare function generateSitemap(site: PublishedSite, articles: readonly PublishedArticle[], authors?: readonly PublishedAuthor[], options?: SitemapOptions): SitemapData;
302
+ /**
303
+ * Converts sitemap data to XML format.
304
+ *
305
+ * @param sitemapData - The sitemap data to convert
306
+ * @returns XML string in sitemap format
307
+ *
308
+ * @example
309
+ * ```typescript
310
+ * const sitemap = generateSitemap(site, articles);
311
+ * const xmlString = sitemapToXml(sitemap);
312
+ * // Use in API route response
313
+ * ```
314
+ */
315
+ declare function sitemapToXml(sitemapData: SitemapData): string;
316
+ /**
317
+ * Generates a sitemap index for large blogs that need multiple sitemaps.
318
+ *
319
+ * @param sitemaps - Array of sitemap information
320
+ * @param baseUrl - Base URL for the site
321
+ * @returns XML string for sitemap index
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * const sitemapIndex = generateSitemapIndex([
326
+ * { url: 'https://example.com/sitemap-articles.xml', lastmod: '2023-01-01T00:00:00Z' },
327
+ * { url: 'https://example.com/sitemap-authors.xml', lastmod: '2023-01-01T00:00:00Z' }
328
+ * ], 'https://example.com');
329
+ * ```
330
+ */
331
+ declare function generateSitemapIndex(sitemaps: readonly {
332
+ url: string;
333
+ lastmod?: string;
334
+ }[], _baseUrl: string): string;
335
+ /**
336
+ * Validates sitemap data for compliance with search engine guidelines.
337
+ *
338
+ * @param sitemapData - The sitemap data to validate
339
+ * @returns Validation results with errors and warnings
340
+ *
341
+ * @example
342
+ * ```typescript
343
+ * const sitemap = generateSitemap(site, articles);
344
+ * const validation = validateSitemap(sitemap);
345
+ *
346
+ * if (validation.errors.length > 0) {
347
+ * console.error('Sitemap errors:', validation.errors);
348
+ * }
349
+ * ```
350
+ */
351
+ declare function validateSitemap(sitemapData: SitemapData): {
352
+ valid: boolean;
353
+ errors: string[];
354
+ warnings: string[];
355
+ };
356
+ /**
357
+ * Creates a separate sitemap specifically for blog content.
358
+ * Useful for large sites that need to split sitemaps by content type.
359
+ *
360
+ * @param site - The published site information
361
+ * @param articles - Array of published articles
362
+ * @param authors - Array of published authors (optional)
363
+ * @param options - Sitemap generation options
364
+ * @returns Next.js sitemap function
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * // app/blog-sitemap/route.ts - separate blog sitemap endpoint
369
+ * import { createBlogSitemapRoute } from '@xexr/blog-platform';
370
+ *
371
+ * export const GET = createBlogSitemapRoute(site, articles, authors, {
372
+ * includeAuthorPages: true,
373
+ * articlesPerPage: 20
374
+ * });
375
+ * ```
376
+ */
377
+ declare function createBlogSitemapRoute(site: PublishedSite, articles: readonly PublishedArticle[], authors?: readonly PublishedAuthor[], options?: SitemapOptions): () => Promise<Response>;
378
+ /**
379
+ * Configuration interface for blog data fetching.
380
+ * Used to fetch blog data for sitemap generation.
381
+ */
382
+ interface BlogSitemapConfig {
383
+ /** Static host app routes to include before Bloggy routes */
384
+ staticRoutes?: readonly NextjsSitemapEntry[];
385
+ /** Function to fetch static host app routes */
386
+ getStaticRoutes?: () => Promise<readonly NextjsSitemapEntry[]>;
387
+ /** Function to fetch site data */
388
+ getSite: () => Promise<PublishedSite>;
389
+ /** Function to fetch published articles */
390
+ getArticles: () => Promise<readonly PublishedArticle[]>;
391
+ /** Function to fetch authors (optional) */
392
+ getAuthors?: () => Promise<readonly PublishedAuthor[]>;
393
+ /** Sitemap generation options */
394
+ options?: SitemapOptions;
395
+ }
396
+ /**
397
+ * Higher-order function that creates a Next.js sitemap function with data fetching.
398
+ * Ideal for dynamic blog content that needs to be fetched at sitemap generation time.
399
+ *
400
+ * @param config - Configuration for fetching blog data
401
+ * @returns Next.js sitemap function
402
+ *
403
+ * @example
404
+ * ```typescript
405
+ * // app/sitemap.ts
406
+ * import { createDynamicBlogSitemap } from '@xexr/blog-platform';
407
+ * import { getBlogData } from './lib/blog';
408
+ *
409
+ * export default createDynamicBlogSitemap({
410
+ * getSite: () => getBlogData().site,
411
+ * getArticles: () => getBlogData().articles,
412
+ * getAuthors: () => getBlogData().authors,
413
+ * options: {
414
+ * includeAuthorPages: true,
415
+ * changeFrequency: { articles: 'weekly' }
416
+ * }
417
+ * });
418
+ * ```
419
+ */
420
+ declare function createDynamicBlogSitemap(config: BlogSitemapConfig): () => Promise<NextjsSitemapEntry[]>;
421
+
422
+ export { type BlogSitemapConfig as B, type CanonicalUrlOptions as C, type NextjsSitemapEntry as N, type SitemapChangeFreq as S, type SitemapData as a, type SitemapEntry as b, type SitemapOptions as c, createBlogSitemapRoute as d, createDynamicBlogSitemap as e, generateCanonicalUrl as f, generateBlogSitemapEntries as g, generateCanonicalUrlFromSlug as h, generateSitemap as i, generateSitemapIndex as j, getBaseUrl as k, isValidCanonicalUrl as l, normalizeSlug as m, normalizeDomain as n, sitemapToXml as s, updateArticleCanonicalUrl as u, validateSitemap as v };