@xexr/blog-platform 1.0.8 → 1.0.9

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.
@@ -0,0 +1,100 @@
1
+ import type { BlogPlatformStorage } from "./database";
2
+ import { getStorage } from "./database";
3
+ import type { PublishedArticle, PublishedAuthor } from "../types/contracts";
4
+
5
+ export interface AuthorArchive {
6
+ author: PublishedAuthor;
7
+ articles: PublishedArticle[];
8
+ }
9
+
10
+ export interface AuthorArchiveOptions {
11
+ limit?: number;
12
+ offset?: number;
13
+ }
14
+
15
+ export function getAuthorsFromArticles(
16
+ articles: readonly PublishedArticle[],
17
+ ): PublishedAuthor[] {
18
+ const authorsBySlug = new Map<string, PublishedAuthor>();
19
+
20
+ for (const article of articles) {
21
+ if (article.author?.slug) {
22
+ authorsBySlug.set(
23
+ article.author.slug,
24
+ mergeAuthorProfile(
25
+ authorsBySlug.get(article.author.slug),
26
+ article.author,
27
+ ),
28
+ );
29
+ }
30
+ }
31
+
32
+ return Array.from(authorsBySlug.values()).sort((a, b) =>
33
+ a.name.localeCompare(b.name),
34
+ );
35
+ }
36
+
37
+ export function getAuthorArchive(
38
+ slug: string,
39
+ articles: readonly PublishedArticle[],
40
+ ): AuthorArchive | null {
41
+ const authorArticles = articles
42
+ .filter((article) => article.author?.slug === slug)
43
+ .sort(
44
+ (a, b) =>
45
+ new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime(),
46
+ );
47
+
48
+ const author = authorArticles.reduce<PublishedAuthor | null>(
49
+ (preferredAuthor, article) =>
50
+ article.author
51
+ ? mergeAuthorProfile(preferredAuthor, article.author)
52
+ : preferredAuthor,
53
+ null,
54
+ );
55
+
56
+ if (!author) {
57
+ return null;
58
+ }
59
+
60
+ return {
61
+ author,
62
+ articles: authorArticles,
63
+ };
64
+ }
65
+
66
+ export async function loadAuthorArchive(
67
+ slug: string,
68
+ options: AuthorArchiveOptions = {},
69
+ storage: BlogPlatformStorage = getStorage(),
70
+ ): Promise<AuthorArchive | null> {
71
+ const { articles } = await storage.listArticles({
72
+ limit: options.limit ?? 1000,
73
+ offset: options.offset ?? 0,
74
+ sortBy: "publishedAt",
75
+ order: "desc",
76
+ });
77
+
78
+ return getAuthorArchive(slug, articles);
79
+ }
80
+
81
+ function mergeAuthorProfile(
82
+ current: PublishedAuthor | null | undefined,
83
+ next: PublishedAuthor,
84
+ ): PublishedAuthor {
85
+ if (!current) {
86
+ return next;
87
+ }
88
+
89
+ return {
90
+ ...current,
91
+ bio: current.bio ?? next.bio,
92
+ avatarUrl: current.avatarUrl ?? next.avatarUrl,
93
+ pronouns: current.pronouns ?? next.pronouns,
94
+ socialLinks: current.socialLinks ?? next.socialLinks,
95
+ toneAdjectives:
96
+ current.toneAdjectives.length > 0
97
+ ? current.toneAdjectives
98
+ : next.toneAdjectives,
99
+ };
100
+ }
@@ -25,8 +25,17 @@
25
25
  * ```
26
26
  */
27
27
 
28
- import type { PublishedArticle, PublishedSite, PublishedAuthor } from '../types/contracts';
29
- import { generateCanonicalUrl, normalizeDomain, type CanonicalUrlOptions } from './seo';
28
+ import type {
29
+ PublishedArticle,
30
+ PublishedSite,
31
+ PublishedAuthor,
32
+ } from "../types/contracts";
33
+ import {
34
+ generateCanonicalUrl,
35
+ normalizeDomain,
36
+ type CanonicalUrlOptions,
37
+ } from "./seo";
38
+ import { buildAuthorPath } from "./urls";
30
39
 
31
40
  /**
32
41
  * Configuration options for sitemap generation.
@@ -76,13 +85,13 @@ export interface SitemapOptions {
76
85
  * Valid sitemap change frequency values as per XML sitemap standard.
77
86
  */
78
87
  export type SitemapChangeFreq =
79
- | 'always'
80
- | 'hourly'
81
- | 'daily'
82
- | 'weekly'
83
- | 'monthly'
84
- | 'yearly'
85
- | 'never';
88
+ | "always"
89
+ | "hourly"
90
+ | "daily"
91
+ | "weekly"
92
+ | "monthly"
93
+ | "yearly"
94
+ | "never";
86
95
 
87
96
  /**
88
97
  * Next.js MetadataRoute.Sitemap compatible entry.
@@ -181,7 +190,7 @@ export async function generateBlogSitemapEntries(
181
190
  site: PublishedSite,
182
191
  articles: readonly PublishedArticle[],
183
192
  authors: readonly PublishedAuthor[] = [],
184
- options: SitemapOptions = {}
193
+ options: SitemapOptions = {},
185
194
  ): Promise<NextjsSitemapEntry[]> {
186
195
  const {
187
196
  includeAuthorPages = true,
@@ -195,49 +204,49 @@ export async function generateBlogSitemapEntries(
195
204
  const entries: NextjsSitemapEntry[] = [];
196
205
  const baseUrl = normalizeDomain(
197
206
  options.canonical?.domain ?? site.domain,
198
- options.canonical?.forceHttps ?? true
207
+ options.canonical?.forceHttps ?? true,
199
208
  );
200
209
 
201
- const pathPrefix = options.canonical?.pathPrefix ?? '/blog';
210
+ const pathPrefix = options.canonical?.pathPrefix ?? "/blog";
202
211
 
203
212
  // Add blog home page
204
213
  entries.push({
205
214
  url: `${baseUrl}${pathPrefix}`,
206
215
  lastModified: getLatestModificationDate(articles),
207
- changeFrequency: changeFrequency.homePage ?? 'daily',
216
+ changeFrequency: changeFrequency.homePage ?? "daily",
208
217
  priority: priority.homePage ?? 1.0,
209
218
  });
210
219
 
211
220
  // Add individual article pages
212
- articles.forEach(article => {
221
+ articles.forEach((article) => {
213
222
  if (entries.length >= maxEntries) return;
214
223
 
215
224
  const articleUrl = generateCanonicalUrl(article, options.canonical);
216
225
  entries.push({
217
226
  url: articleUrl,
218
227
  lastModified: article.updatedAt,
219
- changeFrequency: changeFrequency.articles ?? 'monthly',
228
+ changeFrequency: changeFrequency.articles ?? "monthly",
220
229
  priority: priority.articles ?? 0.8,
221
230
  });
222
231
  });
223
232
 
224
233
  // Add author pages if enabled
225
234
  if (includeAuthorPages) {
226
- authors.forEach(author => {
235
+ authors.forEach((author) => {
227
236
  if (entries.length >= maxEntries) return;
228
237
 
229
- const authorUrl = `${baseUrl}${pathPrefix}/authors/${author.slug}`;
238
+ const authorUrl = `${baseUrl}${buildAuthorPath(pathPrefix, author.slug)}`;
230
239
 
231
240
  // Find the latest article by this author for lastModified
232
- const authorArticles = articles.filter(article =>
233
- article.author?.id === author.id
241
+ const authorArticles = articles.filter(
242
+ (article) => article.author?.id === author.id,
234
243
  );
235
244
  const latestAuthorMod = getLatestModificationDate(authorArticles);
236
245
 
237
246
  entries.push({
238
247
  url: authorUrl,
239
248
  lastModified: latestAuthorMod,
240
- changeFrequency: changeFrequency.authorPages ?? 'weekly',
249
+ changeFrequency: changeFrequency.authorPages ?? "weekly",
241
250
  priority: priority.authorPages ?? 0.6,
242
251
  });
243
252
  });
@@ -253,7 +262,7 @@ export async function generateBlogSitemapEntries(
253
262
  entries.push({
254
263
  url: `${baseUrl}${pathPrefix}?page=${page}`,
255
264
  lastModified: getLatestModificationDate(articles),
256
- changeFrequency: changeFrequency.paginationPages ?? 'daily',
265
+ changeFrequency: changeFrequency.paginationPages ?? "daily",
257
266
  priority: priority.paginationPages ?? 0.5,
258
267
  });
259
268
  }
@@ -276,7 +285,7 @@ export function generateSitemap(
276
285
  site: PublishedSite,
277
286
  articles: readonly PublishedArticle[],
278
287
  authors: readonly PublishedAuthor[] = [],
279
- options: SitemapOptions = {}
288
+ options: SitemapOptions = {},
280
289
  ): SitemapData {
281
290
  const {
282
291
  includeAuthorPages = true,
@@ -290,10 +299,10 @@ export function generateSitemap(
290
299
  const entries: SitemapEntry[] = [];
291
300
  const baseUrl = normalizeDomain(
292
301
  options.canonical?.domain ?? site.domain,
293
- options.canonical?.forceHttps ?? true
302
+ options.canonical?.forceHttps ?? true,
294
303
  );
295
304
 
296
- const pathPrefix = options.canonical?.pathPrefix ?? '/blog';
305
+ const pathPrefix = options.canonical?.pathPrefix ?? "/blog";
297
306
  const generatedAt = new Date().toISOString();
298
307
 
299
308
  // Calculate total potential entries to determine if truncation will occur
@@ -313,40 +322,40 @@ export function generateSitemap(
313
322
  entries.push({
314
323
  loc: `${baseUrl}${pathPrefix}`,
315
324
  lastmod: getLatestModificationDate(articles),
316
- changefreq: changeFrequency.homePage ?? 'daily',
325
+ changefreq: changeFrequency.homePage ?? "daily",
317
326
  priority: priority.homePage ?? 1.0,
318
327
  });
319
328
 
320
329
  // Add individual article pages
321
- articles.forEach(article => {
330
+ articles.forEach((article) => {
322
331
  if (entries.length >= maxEntries) return;
323
332
 
324
333
  const articleUrl = generateCanonicalUrl(article, options.canonical);
325
334
  entries.push({
326
335
  loc: articleUrl,
327
336
  lastmod: article.updatedAt,
328
- changefreq: changeFrequency.articles ?? 'monthly',
337
+ changefreq: changeFrequency.articles ?? "monthly",
329
338
  priority: priority.articles ?? 0.8,
330
339
  });
331
340
  });
332
341
 
333
342
  // Add author pages if enabled
334
343
  if (includeAuthorPages) {
335
- authors.forEach(author => {
344
+ authors.forEach((author) => {
336
345
  if (entries.length >= maxEntries) return;
337
346
 
338
- const authorUrl = `${baseUrl}${pathPrefix}/authors/${author.slug}`;
347
+ const authorUrl = `${baseUrl}${buildAuthorPath(pathPrefix, author.slug)}`;
339
348
 
340
349
  // Find the latest article by this author for lastmod
341
- const authorArticles = articles.filter(article =>
342
- article.author?.id === author.id
350
+ const authorArticles = articles.filter(
351
+ (article) => article.author?.id === author.id,
343
352
  );
344
353
  const latestAuthorMod = getLatestModificationDate(authorArticles);
345
354
 
346
355
  entries.push({
347
356
  loc: authorUrl,
348
357
  lastmod: latestAuthorMod,
349
- changefreq: changeFrequency.authorPages ?? 'weekly',
358
+ changefreq: changeFrequency.authorPages ?? "weekly",
350
359
  priority: priority.authorPages ?? 0.6,
351
360
  });
352
361
  });
@@ -362,7 +371,7 @@ export function generateSitemap(
362
371
  entries.push({
363
372
  loc: `${baseUrl}${pathPrefix}?page=${page}`,
364
373
  lastmod: getLatestModificationDate(articles),
365
- changefreq: changeFrequency.paginationPages ?? 'daily',
374
+ changefreq: changeFrequency.paginationPages ?? "daily",
366
375
  priority: priority.paginationPages ?? 0.5,
367
376
  });
368
377
  }
@@ -392,10 +401,11 @@ export function generateSitemap(
392
401
  */
393
402
  export function sitemapToXml(sitemapData: SitemapData): string {
394
403
  const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>';
395
- const urlsetOpen = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
396
- const urlsetClose = '</urlset>';
404
+ const urlsetOpen =
405
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
406
+ const urlsetClose = "</urlset>";
397
407
 
398
- const urlEntries = sitemapData.entries.map(entry => {
408
+ const urlEntries = sitemapData.entries.map((entry) => {
399
409
  const elements: string[] = [];
400
410
 
401
411
  elements.push(` <url>`);
@@ -406,7 +416,9 @@ export function sitemapToXml(sitemapData: SitemapData): string {
406
416
  }
407
417
 
408
418
  if (entry.changefreq) {
409
- elements.push(` <changefreq>${escapeXml(entry.changefreq)}</changefreq>`);
419
+ elements.push(
420
+ ` <changefreq>${escapeXml(entry.changefreq)}</changefreq>`,
421
+ );
410
422
  }
411
423
 
412
424
  if (entry.priority !== undefined) {
@@ -415,15 +427,10 @@ export function sitemapToXml(sitemapData: SitemapData): string {
415
427
 
416
428
  elements.push(` </url>`);
417
429
 
418
- return elements.join('\n');
430
+ return elements.join("\n");
419
431
  });
420
432
 
421
- return [
422
- xmlHeader,
423
- urlsetOpen,
424
- ...urlEntries,
425
- urlsetClose,
426
- ].join('\n');
433
+ return [xmlHeader, urlsetOpen, ...urlEntries, urlsetClose].join("\n");
427
434
  }
428
435
 
429
436
  /**
@@ -443,13 +450,14 @@ export function sitemapToXml(sitemapData: SitemapData): string {
443
450
  */
444
451
  export function generateSitemapIndex(
445
452
  sitemaps: readonly { url: string; lastmod?: string }[],
446
- _baseUrl: string
453
+ _baseUrl: string,
447
454
  ): string {
448
455
  const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>';
449
- const indexOpen = '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
450
- const indexClose = '</sitemapindex>';
456
+ const indexOpen =
457
+ '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
458
+ const indexClose = "</sitemapindex>";
451
459
 
452
- const sitemapEntries = sitemaps.map(sitemap => {
460
+ const sitemapEntries = sitemaps.map((sitemap) => {
453
461
  const elements: string[] = [];
454
462
 
455
463
  elements.push(` <sitemap>`);
@@ -461,15 +469,10 @@ export function generateSitemapIndex(
461
469
 
462
470
  elements.push(` </sitemap>`);
463
471
 
464
- return elements.join('\n');
472
+ return elements.join("\n");
465
473
  });
466
474
 
467
- return [
468
- xmlHeader,
469
- indexOpen,
470
- ...sitemapEntries,
471
- indexClose,
472
- ].join('\n');
475
+ return [xmlHeader, indexOpen, ...sitemapEntries, indexClose].join("\n");
473
476
  }
474
477
 
475
478
  /**
@@ -498,14 +501,14 @@ export function validateSitemap(sitemapData: SitemapData): {
498
501
 
499
502
  // Check entry count
500
503
  if (sitemapData.entries.length === 0) {
501
- errors.push('Sitemap must contain at least one URL');
504
+ errors.push("Sitemap must contain at least one URL");
502
505
  } else if (sitemapData.entries.length > 50000) {
503
- errors.push('Sitemap contains more than 50,000 URLs (Google limit)');
506
+ errors.push("Sitemap contains more than 50,000 URLs (Google limit)");
504
507
  }
505
508
 
506
509
  // Check for truncation
507
510
  if (sitemapData.truncated) {
508
- warnings.push('Sitemap was truncated due to size limits');
511
+ warnings.push("Sitemap was truncated due to size limits");
509
512
  }
510
513
 
511
514
  // Validate individual entries
@@ -518,13 +521,20 @@ export function validateSitemap(sitemapData: SitemapData): {
518
521
  }
519
522
 
520
523
  // Validate priority range
521
- if (entry.priority !== undefined && (entry.priority < 0 || entry.priority > 1)) {
522
- errors.push(`Invalid priority at entry ${index + 1}: must be between 0.0 and 1.0`);
524
+ if (
525
+ entry.priority !== undefined &&
526
+ (entry.priority < 0 || entry.priority > 1)
527
+ ) {
528
+ errors.push(
529
+ `Invalid priority at entry ${index + 1}: must be between 0.0 and 1.0`,
530
+ );
523
531
  }
524
532
 
525
533
  // Validate lastmod format
526
534
  if (entry.lastmod && !isValidISODate(entry.lastmod)) {
527
- errors.push(`Invalid lastmod date at entry ${index + 1}: ${entry.lastmod}`);
535
+ errors.push(
536
+ `Invalid lastmod date at entry ${index + 1}: ${entry.lastmod}`,
537
+ );
528
538
  }
529
539
 
530
540
  // Check URL length (Google limit is 2048 characters)
@@ -535,7 +545,7 @@ export function validateSitemap(sitemapData: SitemapData): {
535
545
 
536
546
  // Check for duplicate URLs
537
547
  const urlCounts = new Map<string, number>();
538
- sitemapData.entries.forEach(entry => {
548
+ sitemapData.entries.forEach((entry) => {
539
549
  const count = urlCounts.get(entry.loc) ?? 0;
540
550
  urlCounts.set(entry.loc, count + 1);
541
551
  });
@@ -556,7 +566,9 @@ export function validateSitemap(sitemapData: SitemapData): {
556
566
  /**
557
567
  * Gets the latest modification date from an array of articles.
558
568
  */
559
- function getLatestModificationDate(articles: readonly PublishedArticle[]): string {
569
+ function getLatestModificationDate(
570
+ articles: readonly PublishedArticle[],
571
+ ): string {
560
572
  if (articles.length === 0) {
561
573
  return new Date().toISOString();
562
574
  }
@@ -594,17 +606,23 @@ export function createBlogSitemapRoute(
594
606
  site: PublishedSite,
595
607
  articles: readonly PublishedArticle[],
596
608
  authors: readonly PublishedAuthor[] = [],
597
- options: SitemapOptions = {}
609
+ options: SitemapOptions = {},
598
610
  ) {
599
611
  return async function GET() {
600
- const entries = await generateBlogSitemapEntries(site, articles, authors, options);
612
+ const entries = await generateBlogSitemapEntries(
613
+ site,
614
+ articles,
615
+ authors,
616
+ options,
617
+ );
601
618
 
602
619
  // Convert to Next.js sitemap format and return as XML
603
- const xmlEntries = entries.map(entry => ({
620
+ const xmlEntries = entries.map((entry) => ({
604
621
  loc: entry.url,
605
- lastmod: typeof entry.lastModified === 'string'
606
- ? entry.lastModified
607
- : entry.lastModified?.toISOString(),
622
+ lastmod:
623
+ typeof entry.lastModified === "string"
624
+ ? entry.lastModified
625
+ : entry.lastModified?.toISOString(),
608
626
  changefreq: entry.changeFrequency,
609
627
  priority: entry.priority,
610
628
  }));
@@ -616,7 +634,7 @@ export function createBlogSitemapRoute(
616
634
  generatedAt: new Date().toISOString(),
617
635
  baseUrl: normalizeDomain(
618
636
  options.canonical?.domain ?? site.domain,
619
- options.canonical?.forceHttps ?? true
637
+ options.canonical?.forceHttps ?? true,
620
638
  ),
621
639
  };
622
640
 
@@ -624,8 +642,8 @@ export function createBlogSitemapRoute(
624
642
 
625
643
  return new Response(xml, {
626
644
  headers: {
627
- 'Content-Type': 'application/xml',
628
- 'Cache-Control': 'public, max-age=3600, s-maxage=3600',
645
+ "Content-Type": "application/xml",
646
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
629
647
  },
630
648
  });
631
649
  };
@@ -688,14 +706,14 @@ export function createDynamicBlogSitemap(config: BlogSitemapConfig) {
688
706
  */
689
707
  function escapeXml(text: string): string {
690
708
  const xmlEscapes: Record<string, string> = {
691
- '&': '&amp;',
692
- '<': '&lt;',
693
- '>': '&gt;',
694
- '"': '&quot;',
695
- "'": '&#x27;',
709
+ "&": "&amp;",
710
+ "<": "&lt;",
711
+ ">": "&gt;",
712
+ '"': "&quot;",
713
+ "'": "&#x27;",
696
714
  };
697
715
 
698
- return text.replace(/[&<>"']/g, char => xmlEscapes[char] ?? char);
716
+ return text.replace(/[&<>"']/g, (char) => xmlEscapes[char] ?? char);
699
717
  }
700
718
 
701
719
  /**
@@ -708,12 +726,13 @@ function isValidISODate(dateString: string): boolean {
708
726
 
709
727
  // Handle both formats: with and without milliseconds
710
728
  // 2023-01-01T00:00:00.000Z and 2023-01-01T00:00:00Z are both valid
711
- const normalizedInput = dateString.endsWith('Z') && !dateString.includes('.')
712
- ? dateString.replace('Z', '.000Z')
713
- : dateString;
729
+ const normalizedInput =
730
+ dateString.endsWith("Z") && !dateString.includes(".")
731
+ ? dateString.replace("Z", ".000Z")
732
+ : dateString;
714
733
 
715
734
  return isoString === normalizedInput && !isNaN(date.getTime());
716
735
  } catch {
717
736
  return false;
718
737
  }
719
- }
738
+ }
@@ -14,6 +14,7 @@ import type {
14
14
  PublishedAuthor,
15
15
  PublishedSite,
16
16
  } from "../types/contracts";
17
+ import { buildAuthorPath } from "./urls";
17
18
 
18
19
  /**
19
20
  * Base schema.org context and type
@@ -277,8 +278,8 @@ export function generateAuthorStructuredData(
277
278
 
278
279
  if (author.slug?.trim()) {
279
280
  const authorUrl = customDomain
280
- ? `https://${customDomain}${pathPrefix}/authors/${author.slug}`
281
- : `${pathPrefix}/authors/${author.slug}`;
281
+ ? `https://${customDomain}${buildAuthorPath(pathPrefix, author.slug)}`
282
+ : buildAuthorPath(pathPrefix, author.slug);
282
283
  schema.url = authorUrl;
283
284
  }
284
285
 
package/dist/lib/urls.ts CHANGED
@@ -23,6 +23,13 @@ export function buildBlogPath(
23
23
  return `${normalizedBasePath}/${cleanSlug}`.replace(/\/+/g, "/");
24
24
  }
25
25
 
26
+ export function buildAuthorPath(
27
+ basePath: string | undefined,
28
+ authorSlug: string,
29
+ ): string {
30
+ return buildBlogPath(basePath, `authors/${authorSlug}`);
31
+ }
32
+
26
33
  export function getPathPrefixFromBlogUrl(blogUrl: string): string {
27
34
  const parsed = new URL(blogUrl);
28
35
  return normalizeBlogBasePath(parsed.pathname);
@@ -18,6 +18,9 @@ export interface PublishedAuthorSocialLinks {
18
18
  /** X/Twitter profile URL */
19
19
  readonly x?: string;
20
20
 
21
+ /** Legacy Twitter profile URL */
22
+ readonly twitter?: string;
23
+
21
24
  /** LinkedIn profile URL */
22
25
  readonly linkedin?: string;
23
26
 
@@ -15,6 +15,8 @@ interface PublishedAuthorSocialLinks {
15
15
  readonly website?: string;
16
16
  /** X/Twitter profile URL */
17
17
  readonly x?: string;
18
+ /** Legacy Twitter profile URL */
19
+ readonly twitter?: string;
18
20
  /** LinkedIn profile URL */
19
21
  readonly linkedin?: string;
20
22
  /** GitHub profile URL */
@@ -15,6 +15,8 @@ interface PublishedAuthorSocialLinks {
15
15
  readonly website?: string;
16
16
  /** X/Twitter profile URL */
17
17
  readonly x?: string;
18
+ /** Legacy Twitter profile URL */
19
+ readonly twitter?: string;
18
20
  /** LinkedIn profile URL */
19
21
  readonly linkedin?: string;
20
22
  /** GitHub profile URL */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xexr/blog-platform",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "SEO-friendly blog platform for Next.js 15 with Bloggy integration",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",