@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.
- package/README.md +22 -28
- package/dist/app/blog/[slug]/page.tsx +1 -0
- package/dist/components/ArticleReadingProgress.tsx +118 -87
- package/dist/index.d.mts +3 -410
- package/dist/index.d.ts +3 -410
- package/dist/index.js +9 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -84
- package/dist/index.mjs.map +1 -1
- package/dist/lib/sitemap.ts +115 -7
- package/dist/sitemap-BPTdXb9C.d.ts +422 -0
- package/dist/sitemap-Dz9DB7py.d.mts +422 -0
- package/dist/sitemap.d.mts +2 -0
- package/dist/sitemap.d.ts +2 -0
- package/dist/sitemap.js +6 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/sitemap.mjs +6 -0
- package/dist/sitemap.mjs.map +1 -0
- package/package.json +17 -11
package/dist/lib/sitemap.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 +=
|
|
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
|
-
|
|
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
|
|
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 };
|