@xexr/blog-platform 0.2.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.
@@ -0,0 +1,1799 @@
1
+ import { PublishedArticle, PublishedSite, PublishResult, PublishedAuthor } from './types/index.mjs';
2
+ export { PublishPayload } from './types/index.mjs';
3
+ import * as React from 'react';
4
+ import React__default from 'react';
5
+ import * as class_variance_authority_types from 'class-variance-authority/types';
6
+ import { VariantProps } from 'class-variance-authority';
7
+ import { ClassValue } from 'clsx';
8
+ import { NextRequest, NextResponse } from 'next/server';
9
+ import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
10
+ import { LibSQLDatabase } from 'drizzle-orm/libsql';
11
+
12
+ interface BlogHomePageProps {
13
+ articles: PublishedArticle[];
14
+ siteName?: string;
15
+ siteTagline?: string;
16
+ className?: string;
17
+ /** Site data for structured data generation */
18
+ site?: PublishedSite;
19
+ /** Whether to include structured data */
20
+ includeStructuredData?: boolean;
21
+ /** Pagination configuration */
22
+ pagination?: {
23
+ currentPage: number;
24
+ totalPages: number;
25
+ baseUrl?: string;
26
+ };
27
+ }
28
+ /**
29
+ * Blog home page component that displays a list of published articles.
30
+ *
31
+ * This component is designed to be used by customers who install the
32
+ * @xexr/blog-platform package. It provides a clean, SEO-friendly
33
+ * article listing with proper metadata.
34
+ */
35
+ declare function BlogHomePage({ articles, siteName, siteTagline, className, site, includeStructuredData, pagination, }: BlogHomePageProps): React__default.JSX.Element;
36
+ /**
37
+ * Generate metadata for the blog home page for SEO optimization.
38
+ * This function should be used in the Next.js metadata API.
39
+ */
40
+ declare function generateBlogHomeMetadata(siteName?: string, siteDescription?: string, siteUrl?: string, pagination?: {
41
+ currentPage: number;
42
+ totalPages: number;
43
+ }): {
44
+ title: string;
45
+ description: string;
46
+ openGraph: {
47
+ url?: string | undefined;
48
+ title: string;
49
+ description: string;
50
+ type: string;
51
+ };
52
+ twitter: {
53
+ card: string;
54
+ title: string;
55
+ description: string;
56
+ };
57
+ alternates: {
58
+ canonical?: string | undefined;
59
+ };
60
+ other: {
61
+ next?: string | undefined;
62
+ prev?: string | undefined;
63
+ };
64
+ };
65
+
66
+ interface ArticlePageProps {
67
+ article: PublishedArticle;
68
+ className?: string;
69
+ /** Optional navigation links */
70
+ navigation?: {
71
+ backToHome?: {
72
+ url: string;
73
+ label?: string;
74
+ };
75
+ previousArticle?: {
76
+ url: string;
77
+ title: string;
78
+ };
79
+ nextArticle?: {
80
+ url: string;
81
+ title: string;
82
+ };
83
+ };
84
+ }
85
+ declare function ArticlePage({ article, className, navigation, }: ArticlePageProps): React__default.JSX.Element;
86
+ /**
87
+ * Generate metadata for the article page for SEO optimization.
88
+ * This function should be used in the Next.js metadata API.
89
+ */
90
+ declare function generateArticleMetadata(article: PublishedArticle): {
91
+ title: string;
92
+ description: string;
93
+ keywords: string;
94
+ authors: {
95
+ name: string;
96
+ }[] | undefined;
97
+ openGraph: {
98
+ title: string;
99
+ description: string;
100
+ type: string;
101
+ url: string;
102
+ images: {
103
+ url: string;
104
+ alt: string;
105
+ }[] | undefined;
106
+ publishedTime: string;
107
+ modifiedTime: string;
108
+ authors: string[] | undefined;
109
+ siteName: string;
110
+ };
111
+ twitter: {
112
+ card: string;
113
+ title: string;
114
+ description: string;
115
+ images: string[] | undefined;
116
+ };
117
+ alternates: {
118
+ canonical: string;
119
+ };
120
+ };
121
+
122
+ interface PaginationProps {
123
+ /** Current page number (1-indexed) */
124
+ currentPage: number;
125
+ /** Total number of pages */
126
+ totalPages: number;
127
+ /** Base URL for pagination links (e.g., "/blog") */
128
+ baseUrl: string;
129
+ /** Additional CSS classes */
130
+ className?: string;
131
+ /** Maximum number of page links to show around current page */
132
+ maxVisiblePages?: number;
133
+ /** Show/hide previous and next buttons */
134
+ showPrevNext?: boolean;
135
+ /** Show/hide first and last page links */
136
+ showFirstLast?: boolean;
137
+ /** Custom labels for navigation */
138
+ labels?: {
139
+ previous?: string;
140
+ next?: string;
141
+ first?: string;
142
+ last?: string;
143
+ };
144
+ }
145
+ /**
146
+ * Pagination component for blog articles listing.
147
+ *
148
+ * Provides SEO-friendly pagination with proper URL structure,
149
+ * keyboard navigation, and accessibility features.
150
+ */
151
+ declare function Pagination$1({ currentPage, totalPages, baseUrl, className, maxVisiblePages, showPrevNext, showFirstLast, }: PaginationProps): React__default.JSX.Element | null;
152
+ /**
153
+ * Hook to calculate pagination metadata from URL parameters.
154
+ * Useful for extracting current page from Next.js searchParams.
155
+ */
156
+ declare function usePagination(searchParams: Record<string, string | string[] | undefined>, itemsPerPage?: number): {
157
+ currentPage: number;
158
+ itemsPerPage: number;
159
+ offset: number;
160
+ calculateTotalPages: (totalItems: number) => number;
161
+ };
162
+ /**
163
+ * Generate pagination metadata for SEO.
164
+ * Provides canonical URLs, next/prev relationships for search engines.
165
+ */
166
+ declare function generatePaginationMetadata(currentPage: number, totalPages: number, baseUrl: string, siteUrl?: string): {
167
+ alternates: {
168
+ canonical?: string | undefined;
169
+ };
170
+ other: {
171
+ next?: string | undefined;
172
+ prev?: string | undefined;
173
+ };
174
+ };
175
+
176
+ declare const buttonVariants: (props?: ({
177
+ variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
178
+ size?: "default" | "sm" | "lg" | "icon" | null | undefined;
179
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
180
+ declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
181
+ asChild?: boolean;
182
+ }): React.JSX.Element;
183
+
184
+ declare function Card({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
185
+ declare function CardHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
186
+ declare function CardTitle({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
187
+ declare function CardDescription({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
188
+ declare function CardContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
189
+ declare function CardFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
190
+
191
+ declare function Pagination({ className, ...props }: React.ComponentProps<"nav">): React.JSX.Element;
192
+ declare function PaginationContent({ className, ...props }: React.ComponentProps<"ul">): React.JSX.Element;
193
+ declare function PaginationItem({ ...props }: React.ComponentProps<"li">): React.JSX.Element;
194
+ type PaginationLinkProps = {
195
+ isActive?: boolean;
196
+ size?: "default" | "sm" | "lg" | "icon";
197
+ } & React.ComponentProps<"a">;
198
+ declare function PaginationLink({ className, isActive, size, ...props }: PaginationLinkProps): React.JSX.Element;
199
+ declare function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>): React.JSX.Element;
200
+ declare function PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>): React.JSX.Element;
201
+ declare function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
202
+
203
+ declare function cn(...inputs: ClassValue[]): string;
204
+
205
+ /**
206
+ * Authentication configuration interface
207
+ * Customers can implement this to customize authentication behavior
208
+ */
209
+ interface AuthConfig {
210
+ /**
211
+ * Validate an authentication token
212
+ * @param token - The token to validate
213
+ * @returns Promise that resolves to validation result
214
+ */
215
+ validateToken(token: string): Promise<AuthResult>;
216
+ /**
217
+ * Extract token from request
218
+ * @param request - The incoming request
219
+ * @returns The extracted token or null if not found
220
+ */
221
+ extractToken?(request: NextRequest): string | null;
222
+ /**
223
+ * Custom error messages for authentication failures
224
+ */
225
+ errorMessages?: {
226
+ missing?: string;
227
+ invalid?: string;
228
+ expired?: string;
229
+ };
230
+ }
231
+ /**
232
+ * Authentication result from token validation
233
+ */
234
+ interface AuthResult {
235
+ /** Whether the token is valid */
236
+ valid: boolean;
237
+ /** Error code if invalid */
238
+ errorCode?: "missing" | "invalid" | "expired";
239
+ /** Additional user/context information */
240
+ context?: Record<string, unknown>;
241
+ }
242
+ /**
243
+ * Default token extraction from Authorization header
244
+ * Supports both "Bearer <token>" and "Token <token>" formats
245
+ */
246
+ declare function extractTokenFromHeader(request: NextRequest): string | null;
247
+ /**
248
+ * Simple API key based authentication implementation
249
+ * Customers can extend this or provide their own AuthConfig
250
+ */
251
+ declare class ApiKeyAuth implements AuthConfig {
252
+ private validApiKeys;
253
+ constructor(apiKeys: string[]);
254
+ validateToken(token: string): Promise<AuthResult>;
255
+ extractToken: (request: NextRequest) => string | null;
256
+ private hashToken;
257
+ }
258
+ /**
259
+ * Environment variable based authentication
260
+ * Reads API keys from environment variables
261
+ */
262
+ declare class EnvAuth extends ApiKeyAuth {
263
+ constructor(envVarName?: string);
264
+ }
265
+ /**
266
+ * No-op authentication for development/testing
267
+ * WARNING: Never use this in production!
268
+ */
269
+ declare class NoAuth implements AuthConfig {
270
+ validateToken(): Promise<AuthResult>;
271
+ extractToken(): string | null;
272
+ }
273
+ /**
274
+ * Uninitialized authentication - rejects all requests
275
+ * This is the secure default before initialization
276
+ */
277
+ declare class UninitializedAuth implements AuthConfig {
278
+ private hasWarned;
279
+ validateToken(_token: string): Promise<AuthResult>;
280
+ }
281
+ declare global {
282
+ var __BLOG_PLATFORM_AUTH_CONFIG__: AuthConfig | undefined;
283
+ }
284
+ /**
285
+ * Set the authentication configuration
286
+ * Customers should call this in their app initialization
287
+ */
288
+ declare function setAuthConfig(config: AuthConfig): void;
289
+ /**
290
+ * Get the current authentication configuration
291
+ */
292
+ declare function getAuthConfig(): AuthConfig;
293
+ /**
294
+ * Initialize authentication with environment-based API keys
295
+ * This is a convenience function for common setups
296
+ */
297
+ declare function initEnvAuth(envVarName?: string): void;
298
+ /**
299
+ * Utility to check if current environment should skip authentication
300
+ * Useful for development and testing
301
+ */
302
+ declare function shouldSkipAuth(): boolean;
303
+
304
+ /**
305
+ * Database interface for blog platform storage
306
+ *
307
+ * This provides a simple interface for storing published articles
308
+ * without coupling to any specific database implementation.
309
+ * Customers can implement this interface with their preferred database.
310
+ */
311
+
312
+ interface BlogPlatformStorage {
313
+ /**
314
+ * Store a published article
315
+ */
316
+ saveArticle(article: PublishedArticle, idempotencyKey: string): Promise<PublishResult>;
317
+ /**
318
+ * Get an article by slug
319
+ */
320
+ getArticle(slug: string): Promise<PublishedArticle | null>;
321
+ /**
322
+ * List all published articles
323
+ */
324
+ listArticles(options?: {
325
+ limit?: number;
326
+ offset?: number;
327
+ sortBy?: "publishedAt" | "updatedAt";
328
+ order?: "asc" | "desc";
329
+ }): Promise<{
330
+ articles: PublishedArticle[];
331
+ total: number;
332
+ }>;
333
+ /**
334
+ * Remove an article by slug
335
+ */
336
+ removeArticle(slug: string, idempotencyKey: string): Promise<PublishResult>;
337
+ /**
338
+ * Check if an operation was already performed (idempotency)
339
+ */
340
+ checkIdempotency(idempotencyKey: string): Promise<PublishResult | null>;
341
+ /**
342
+ * Store the result of an operation for idempotency
343
+ */
344
+ storeOperationResult(idempotencyKey: string, result: PublishResult): Promise<void>;
345
+ }
346
+ /**
347
+ * Default in-memory storage implementation for development/testing
348
+ * IMPORTANT: This is not suitable for production use!
349
+ */
350
+ declare class InMemoryStorage implements BlogPlatformStorage {
351
+ private articles;
352
+ private operations;
353
+ saveArticle(article: PublishedArticle, idempotencyKey: string): Promise<PublishResult>;
354
+ getArticle(slug: string): Promise<PublishedArticle | null>;
355
+ listArticles(options?: {
356
+ limit?: number;
357
+ offset?: number;
358
+ sortBy?: "publishedAt" | "updatedAt";
359
+ order?: "asc" | "desc";
360
+ }): Promise<{
361
+ articles: PublishedArticle[];
362
+ total: number;
363
+ }>;
364
+ removeArticle(slug: string, idempotencyKey: string): Promise<PublishResult>;
365
+ checkIdempotency(idempotencyKey: string): Promise<PublishResult | null>;
366
+ storeOperationResult(idempotencyKey: string, result: PublishResult): Promise<void>;
367
+ }
368
+ declare function getStorage(): BlogPlatformStorage;
369
+ declare function setStorage(storage: BlogPlatformStorage): void;
370
+
371
+ /**
372
+ * Rate limiting for blog platform API endpoints
373
+ *
374
+ * This implements rate limiting using Upstash Redis to prevent abuse
375
+ * and ensure fair usage across all API consumers.
376
+ */
377
+
378
+ /**
379
+ * Rate limit configuration interface
380
+ * Allows customers to customize rate limiting behavior
381
+ */
382
+ interface RateLimitConfig {
383
+ /** Upstash Redis REST URL */
384
+ redisUrl: string;
385
+ /** Upstash Redis REST token */
386
+ redisToken: string;
387
+ /** Requests per minute limit (default: 5) */
388
+ requestsPerMinute?: number;
389
+ /** Redis key prefix for rate limiting (default: "blog-platform-ratelimit") */
390
+ keyPrefix?: string;
391
+ /** Enable analytics (default: true) */
392
+ analytics?: boolean;
393
+ }
394
+ /**
395
+ * Rate limit result
396
+ */
397
+ interface RateLimitResult {
398
+ /** Whether the request is allowed */
399
+ success: boolean;
400
+ /** Number of requests remaining in current window */
401
+ remaining: number;
402
+ /** When the rate limit window resets (Unix timestamp) */
403
+ reset: number;
404
+ /** Total requests allowed per window */
405
+ limit: number;
406
+ /** Current request count in window */
407
+ count: number;
408
+ }
409
+ /**
410
+ * Configure the rate limiter
411
+ * This must be called during app initialization
412
+ */
413
+ declare function configureRateLimit(config: RateLimitConfig): void;
414
+ /**
415
+ * Auto-configure rate limiting from environment variables
416
+ * Uses UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN
417
+ */
418
+ declare function autoConfigureRateLimit(): void;
419
+ /**
420
+ * Check rate limit for a given identifier (typically an auth token)
421
+ * @param identifier - Unique identifier (usually the authentication token)
422
+ * @returns Rate limit result
423
+ */
424
+ declare function checkRateLimit(identifier: string): Promise<RateLimitResult>;
425
+ /**
426
+ * Create a rate limit error response
427
+ * @param rateLimitResult - The rate limit result that failed
428
+ * @param idempotencyKey - Optional idempotency key from request
429
+ * @returns PublishResult with rate limit error
430
+ */
431
+ declare function createRateLimitErrorResponse(rateLimitResult: RateLimitResult, idempotencyKey?: string): PublishResult;
432
+ /**
433
+ * Rate limiting middleware for API routes
434
+ * @param identifier - Unique identifier for rate limiting (typically auth token)
435
+ * @param idempotencyKey - Optional idempotency key from request
436
+ * @returns Rate limit result or error response
437
+ */
438
+ declare function enforceRateLimit(identifier: string, idempotencyKey?: string): Promise<{
439
+ allowed: true;
440
+ rateLimitResult: RateLimitResult;
441
+ } | {
442
+ allowed: false;
443
+ errorResponse: PublishResult;
444
+ }>;
445
+ /**
446
+ * Get rate limit headers for HTTP responses
447
+ * @param rateLimitResult - Rate limit result
448
+ * @returns Headers object for HTTP response
449
+ */
450
+ declare function getRateLimitHeaders(rateLimitResult: RateLimitResult): Record<string, string>;
451
+
452
+ /**
453
+ * Initialization utilities for the blog platform package
454
+ *
455
+ * This module provides convenient setup functions for customers
456
+ * to configure authentication, storage, and other platform features.
457
+ */
458
+
459
+ declare global {
460
+ var __BLOG_PLATFORM_INITIALISED__: boolean | undefined;
461
+ }
462
+ /**
463
+ * Configuration options for blog platform initialization
464
+ */
465
+ interface BlogPlatformConfig {
466
+ /** Authentication configuration (REQUIRED) */
467
+ auth: AuthConfig;
468
+ /** Custom storage implementation */
469
+ storage?: BlogPlatformStorage;
470
+ /** Custom rate limiting configuration */
471
+ rateLimit?: RateLimitConfig;
472
+ /** Whether to automatically configure rate limiting from environment */
473
+ autoRateLimit?: boolean;
474
+ }
475
+ /**
476
+ * Initialize the blog platform with custom configuration
477
+ *
478
+ * This should be called once during your app startup, typically in:
479
+ * - Next.js: middleware.ts or layout.tsx
480
+ * - Express: app startup
481
+ * - Other frameworks: main entry point
482
+ *
483
+ * @param config - Configuration options (auth is REQUIRED)
484
+ *
485
+ * @example
486
+ * ```typescript
487
+ * // Environment-based API key authentication
488
+ * initBlogPlatform({
489
+ * auth: new EnvAuth(), // Reads from BLOG_PLATFORM_API_KEYS
490
+ * });
491
+ *
492
+ * // Custom environment variable
493
+ * initBlogPlatform({
494
+ * auth: new EnvAuth("MY_BLOG_API_KEYS"),
495
+ * });
496
+ *
497
+ * // Custom authentication provider
498
+ * initBlogPlatform({
499
+ * auth: new CustomAuthProvider(),
500
+ * storage: new PostgresStorage(connectionString),
501
+ * rateLimit: {
502
+ * redisUrl: "https://...",
503
+ * redisToken: "...",
504
+ * requestsPerMinute: 10
505
+ * }
506
+ * });
507
+ *
508
+ * // Disable rate limiting for testing
509
+ * initBlogPlatform({
510
+ * auth: new NoAuth(), // Development only!
511
+ * autoRateLimit: false
512
+ * });
513
+ * ```
514
+ */
515
+ declare function initBlogPlatform(config: BlogPlatformConfig): void;
516
+ /**
517
+ * Initialize the blog platform only once per process.
518
+ * Safe to call from multiple modules (e.g., layout + API routes).
519
+ *
520
+ * @param config - Configuration options (auth is REQUIRED)
521
+ */
522
+ declare function initBlogPlatformOnce(config: BlogPlatformConfig): void;
523
+ /**
524
+ * Quick setup for development/testing
525
+ * WARNING: Disables authentication - never use in production!
526
+ * Uses NoAuth (accepts all requests), in-memory storage, and disables rate limiting
527
+ */
528
+ declare function initBlogPlatformDev(): void;
529
+ /**
530
+ * Production setup with environment-based API keys and rate limiting
531
+ * Requires BLOG_PLATFORM_API_KEYS (or custom env var), UPSTASH_REDIS_REST_URL,
532
+ * and UPSTASH_REDIS_REST_TOKEN environment variables
533
+ *
534
+ * @param apiKeysEnvVar - Optional custom environment variable name for API keys (default: BLOG_PLATFORM_API_KEYS)
535
+ */
536
+ declare function initBlogPlatformProd(apiKeysEnvVar?: string): void;
537
+
538
+ declare const bpArticles: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
539
+ name: "bp_articles";
540
+ schema: undefined;
541
+ columns: {
542
+ id: drizzle_orm_sqlite_core.SQLiteColumn<{
543
+ name: "id";
544
+ tableName: "bp_articles";
545
+ dataType: "string";
546
+ columnType: "SQLiteText";
547
+ data: string;
548
+ driverParam: string;
549
+ notNull: true;
550
+ hasDefault: false;
551
+ isPrimaryKey: true;
552
+ isAutoincrement: false;
553
+ hasRuntimeDefault: false;
554
+ enumValues: [string, ...string[]];
555
+ baseColumn: never;
556
+ identity: undefined;
557
+ generated: undefined;
558
+ }, {}, {
559
+ length: number | undefined;
560
+ }>;
561
+ slug: drizzle_orm_sqlite_core.SQLiteColumn<{
562
+ name: "slug";
563
+ tableName: "bp_articles";
564
+ dataType: "string";
565
+ columnType: "SQLiteText";
566
+ data: string;
567
+ driverParam: string;
568
+ notNull: true;
569
+ hasDefault: false;
570
+ isPrimaryKey: false;
571
+ isAutoincrement: false;
572
+ hasRuntimeDefault: false;
573
+ enumValues: [string, ...string[]];
574
+ baseColumn: never;
575
+ identity: undefined;
576
+ generated: undefined;
577
+ }, {}, {
578
+ length: number | undefined;
579
+ }>;
580
+ title: drizzle_orm_sqlite_core.SQLiteColumn<{
581
+ name: "title";
582
+ tableName: "bp_articles";
583
+ dataType: "string";
584
+ columnType: "SQLiteText";
585
+ data: string;
586
+ driverParam: string;
587
+ notNull: true;
588
+ hasDefault: false;
589
+ isPrimaryKey: false;
590
+ isAutoincrement: false;
591
+ hasRuntimeDefault: false;
592
+ enumValues: [string, ...string[]];
593
+ baseColumn: never;
594
+ identity: undefined;
595
+ generated: undefined;
596
+ }, {}, {
597
+ length: number | undefined;
598
+ }>;
599
+ excerpt: drizzle_orm_sqlite_core.SQLiteColumn<{
600
+ name: "excerpt";
601
+ tableName: "bp_articles";
602
+ dataType: "string";
603
+ columnType: "SQLiteText";
604
+ data: string;
605
+ driverParam: string;
606
+ notNull: true;
607
+ hasDefault: false;
608
+ isPrimaryKey: false;
609
+ isAutoincrement: false;
610
+ hasRuntimeDefault: false;
611
+ enumValues: [string, ...string[]];
612
+ baseColumn: never;
613
+ identity: undefined;
614
+ generated: undefined;
615
+ }, {}, {
616
+ length: number | undefined;
617
+ }>;
618
+ content: drizzle_orm_sqlite_core.SQLiteColumn<{
619
+ name: "content";
620
+ tableName: "bp_articles";
621
+ dataType: "string";
622
+ columnType: "SQLiteText";
623
+ data: string;
624
+ driverParam: string;
625
+ notNull: true;
626
+ hasDefault: false;
627
+ isPrimaryKey: false;
628
+ isAutoincrement: false;
629
+ hasRuntimeDefault: false;
630
+ enumValues: [string, ...string[]];
631
+ baseColumn: never;
632
+ identity: undefined;
633
+ generated: undefined;
634
+ }, {}, {
635
+ length: number | undefined;
636
+ }>;
637
+ readingSeconds: drizzle_orm_sqlite_core.SQLiteColumn<{
638
+ name: "reading_seconds";
639
+ tableName: "bp_articles";
640
+ dataType: "number";
641
+ columnType: "SQLiteInteger";
642
+ data: number;
643
+ driverParam: number;
644
+ notNull: true;
645
+ hasDefault: false;
646
+ isPrimaryKey: false;
647
+ isAutoincrement: false;
648
+ hasRuntimeDefault: false;
649
+ enumValues: undefined;
650
+ baseColumn: never;
651
+ identity: undefined;
652
+ generated: undefined;
653
+ }, {}, {}>;
654
+ wordCount: drizzle_orm_sqlite_core.SQLiteColumn<{
655
+ name: "word_count";
656
+ tableName: "bp_articles";
657
+ dataType: "number";
658
+ columnType: "SQLiteInteger";
659
+ data: number;
660
+ driverParam: number;
661
+ notNull: true;
662
+ hasDefault: false;
663
+ isPrimaryKey: false;
664
+ isAutoincrement: false;
665
+ hasRuntimeDefault: false;
666
+ enumValues: undefined;
667
+ baseColumn: never;
668
+ identity: undefined;
669
+ generated: undefined;
670
+ }, {}, {}>;
671
+ publishedAt: drizzle_orm_sqlite_core.SQLiteColumn<{
672
+ name: "published_at";
673
+ tableName: "bp_articles";
674
+ dataType: "string";
675
+ columnType: "SQLiteText";
676
+ data: string;
677
+ driverParam: string;
678
+ notNull: true;
679
+ hasDefault: false;
680
+ isPrimaryKey: false;
681
+ isAutoincrement: false;
682
+ hasRuntimeDefault: false;
683
+ enumValues: [string, ...string[]];
684
+ baseColumn: never;
685
+ identity: undefined;
686
+ generated: undefined;
687
+ }, {}, {
688
+ length: number | undefined;
689
+ }>;
690
+ updatedAt: drizzle_orm_sqlite_core.SQLiteColumn<{
691
+ name: "updated_at";
692
+ tableName: "bp_articles";
693
+ dataType: "string";
694
+ columnType: "SQLiteText";
695
+ data: string;
696
+ driverParam: string;
697
+ notNull: true;
698
+ hasDefault: false;
699
+ isPrimaryKey: false;
700
+ isAutoincrement: false;
701
+ hasRuntimeDefault: false;
702
+ enumValues: [string, ...string[]];
703
+ baseColumn: never;
704
+ identity: undefined;
705
+ generated: undefined;
706
+ }, {}, {
707
+ length: number | undefined;
708
+ }>;
709
+ featuredImage: drizzle_orm_sqlite_core.SQLiteColumn<{
710
+ name: "featured_image";
711
+ tableName: "bp_articles";
712
+ dataType: "string";
713
+ columnType: "SQLiteText";
714
+ data: string;
715
+ driverParam: string;
716
+ notNull: false;
717
+ hasDefault: false;
718
+ isPrimaryKey: false;
719
+ isAutoincrement: false;
720
+ hasRuntimeDefault: false;
721
+ enumValues: [string, ...string[]];
722
+ baseColumn: never;
723
+ identity: undefined;
724
+ generated: undefined;
725
+ }, {}, {
726
+ length: number | undefined;
727
+ }>;
728
+ featuredImageAlt: drizzle_orm_sqlite_core.SQLiteColumn<{
729
+ name: "featured_image_alt";
730
+ tableName: "bp_articles";
731
+ dataType: "string";
732
+ columnType: "SQLiteText";
733
+ data: string;
734
+ driverParam: string;
735
+ notNull: false;
736
+ hasDefault: false;
737
+ isPrimaryKey: false;
738
+ isAutoincrement: false;
739
+ hasRuntimeDefault: false;
740
+ enumValues: [string, ...string[]];
741
+ baseColumn: never;
742
+ identity: undefined;
743
+ generated: undefined;
744
+ }, {}, {
745
+ length: number | undefined;
746
+ }>;
747
+ authorJson: drizzle_orm_sqlite_core.SQLiteColumn<{
748
+ name: "author_json";
749
+ tableName: "bp_articles";
750
+ dataType: "string";
751
+ columnType: "SQLiteText";
752
+ data: string;
753
+ driverParam: string;
754
+ notNull: false;
755
+ hasDefault: false;
756
+ isPrimaryKey: false;
757
+ isAutoincrement: false;
758
+ hasRuntimeDefault: false;
759
+ enumValues: [string, ...string[]];
760
+ baseColumn: never;
761
+ identity: undefined;
762
+ generated: undefined;
763
+ }, {}, {
764
+ length: number | undefined;
765
+ }>;
766
+ siteJson: drizzle_orm_sqlite_core.SQLiteColumn<{
767
+ name: "site_json";
768
+ tableName: "bp_articles";
769
+ dataType: "string";
770
+ columnType: "SQLiteText";
771
+ data: string;
772
+ driverParam: string;
773
+ notNull: true;
774
+ hasDefault: false;
775
+ isPrimaryKey: false;
776
+ isAutoincrement: false;
777
+ hasRuntimeDefault: false;
778
+ enumValues: [string, ...string[]];
779
+ baseColumn: never;
780
+ identity: undefined;
781
+ generated: undefined;
782
+ }, {}, {
783
+ length: number | undefined;
784
+ }>;
785
+ metaJson: drizzle_orm_sqlite_core.SQLiteColumn<{
786
+ name: "meta_json";
787
+ tableName: "bp_articles";
788
+ dataType: "string";
789
+ columnType: "SQLiteText";
790
+ data: string;
791
+ driverParam: string;
792
+ notNull: true;
793
+ hasDefault: false;
794
+ isPrimaryKey: false;
795
+ isAutoincrement: false;
796
+ hasRuntimeDefault: false;
797
+ enumValues: [string, ...string[]];
798
+ baseColumn: never;
799
+ identity: undefined;
800
+ generated: undefined;
801
+ }, {}, {
802
+ length: number | undefined;
803
+ }>;
804
+ version: drizzle_orm_sqlite_core.SQLiteColumn<{
805
+ name: "version";
806
+ tableName: "bp_articles";
807
+ dataType: "number";
808
+ columnType: "SQLiteInteger";
809
+ data: number;
810
+ driverParam: number;
811
+ notNull: true;
812
+ hasDefault: false;
813
+ isPrimaryKey: false;
814
+ isAutoincrement: false;
815
+ hasRuntimeDefault: false;
816
+ enumValues: undefined;
817
+ baseColumn: never;
818
+ identity: undefined;
819
+ generated: undefined;
820
+ }, {}, {}>;
821
+ };
822
+ dialect: "sqlite";
823
+ }>;
824
+ declare const bpOperations: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
825
+ name: "bp_operations";
826
+ schema: undefined;
827
+ columns: {
828
+ key: drizzle_orm_sqlite_core.SQLiteColumn<{
829
+ name: "key";
830
+ tableName: "bp_operations";
831
+ dataType: "string";
832
+ columnType: "SQLiteText";
833
+ data: string;
834
+ driverParam: string;
835
+ notNull: true;
836
+ hasDefault: false;
837
+ isPrimaryKey: true;
838
+ isAutoincrement: false;
839
+ hasRuntimeDefault: false;
840
+ enumValues: [string, ...string[]];
841
+ baseColumn: never;
842
+ identity: undefined;
843
+ generated: undefined;
844
+ }, {}, {
845
+ length: number | undefined;
846
+ }>;
847
+ resultJson: drizzle_orm_sqlite_core.SQLiteColumn<{
848
+ name: "result_json";
849
+ tableName: "bp_operations";
850
+ dataType: "string";
851
+ columnType: "SQLiteText";
852
+ data: string;
853
+ driverParam: string;
854
+ notNull: true;
855
+ hasDefault: false;
856
+ isPrimaryKey: false;
857
+ isAutoincrement: false;
858
+ hasRuntimeDefault: false;
859
+ enumValues: [string, ...string[]];
860
+ baseColumn: never;
861
+ identity: undefined;
862
+ generated: undefined;
863
+ }, {}, {
864
+ length: number | undefined;
865
+ }>;
866
+ createdAt: drizzle_orm_sqlite_core.SQLiteColumn<{
867
+ name: "created_at";
868
+ tableName: "bp_operations";
869
+ dataType: "date";
870
+ columnType: "SQLiteTimestamp";
871
+ data: Date;
872
+ driverParam: number;
873
+ notNull: true;
874
+ hasDefault: false;
875
+ isPrimaryKey: false;
876
+ isAutoincrement: false;
877
+ hasRuntimeDefault: false;
878
+ enumValues: undefined;
879
+ baseColumn: never;
880
+ identity: undefined;
881
+ generated: undefined;
882
+ }, {}, {}>;
883
+ expiresAt: drizzle_orm_sqlite_core.SQLiteColumn<{
884
+ name: "expires_at";
885
+ tableName: "bp_operations";
886
+ dataType: "date";
887
+ columnType: "SQLiteTimestamp";
888
+ data: Date;
889
+ driverParam: number;
890
+ notNull: false;
891
+ hasDefault: false;
892
+ isPrimaryKey: false;
893
+ isAutoincrement: false;
894
+ hasRuntimeDefault: false;
895
+ enumValues: undefined;
896
+ baseColumn: never;
897
+ identity: undefined;
898
+ generated: undefined;
899
+ }, {}, {}>;
900
+ };
901
+ dialect: "sqlite";
902
+ }>;
903
+ interface DrizzleStorageOptions {
904
+ operationTtlMs?: number;
905
+ }
906
+ /**
907
+ * Drizzle-backed durable storage adapter.
908
+ * Pass a Drizzle Database instance created with drizzle-orm/libsql.
909
+ */
910
+ declare class DrizzleStorage implements BlogPlatformStorage {
911
+ private readonly db;
912
+ private readonly options;
913
+ constructor(db: LibSQLDatabase, options?: DrizzleStorageOptions);
914
+ saveArticle(article: PublishedArticle, idempotencyKey: string): Promise<PublishResult>;
915
+ getArticle(slug: string): Promise<PublishedArticle | null>;
916
+ listArticles(options?: {
917
+ limit?: number;
918
+ offset?: number;
919
+ sortBy?: "publishedAt" | "updatedAt";
920
+ order?: "asc" | "desc";
921
+ }): Promise<{
922
+ articles: PublishedArticle[];
923
+ total: number;
924
+ }>;
925
+ removeArticle(slug: string, idempotencyKey: string): Promise<PublishResult>;
926
+ checkIdempotency(idempotencyKey: string): Promise<PublishResult | null>;
927
+ storeOperationResult(idempotencyKey: string, result: PublishResult): Promise<void>;
928
+ }
929
+ /**
930
+ * Helper: Return SQL strings for creating required tables.
931
+ * Consumers can embed these in their migrations.
932
+ */
933
+ declare function getDrizzleAdapterSql(): {
934
+ createArticles: string;
935
+ createOperations: string;
936
+ };
937
+
938
+ /**
939
+ * SEO utilities for canonical URL generation and validation.
940
+ *
941
+ * These utilities ensure proper canonical URL structure for SEO optimization,
942
+ * preventing duplicate content issues and improving search engine indexing.
943
+ */
944
+
945
+ /**
946
+ * Configuration options for canonical URL generation.
947
+ */
948
+ interface CanonicalUrlOptions {
949
+ /** Whether to force HTTPS protocol (default: true) */
950
+ forceHttps?: boolean;
951
+ /** Whether to remove trailing slashes (default: true) */
952
+ removeTrailingSlash?: boolean;
953
+ /** Custom domain override (uses site.domain if not provided) */
954
+ domain?: string;
955
+ /** Custom path prefix (default: '/blog') */
956
+ pathPrefix?: string;
957
+ }
958
+ /**
959
+ * Normalizes a domain by ensuring proper protocol and removing trailing slashes.
960
+ *
961
+ * @param domain - The domain to normalize
962
+ * @param forceHttps - Whether to force HTTPS protocol
963
+ * @returns The normalized domain
964
+ *
965
+ * @example
966
+ * ```typescript
967
+ * normalizeDomain('example.com') // 'https://example.com'
968
+ * normalizeDomain('http://example.com/') // 'https://example.com'
969
+ * normalizeDomain('https://example.com', false) // 'https://example.com'
970
+ * ```
971
+ */
972
+ declare function normalizeDomain(domain: string, forceHttps?: boolean): string;
973
+ /**
974
+ * Validates and normalizes a slug for URL usage.
975
+ *
976
+ * @param slug - The slug to validate and normalize
977
+ * @returns The normalized slug
978
+ * @throws Error if slug is invalid
979
+ *
980
+ * @example
981
+ * ```typescript
982
+ * normalizeSlug('hello-world') // 'hello-world'
983
+ * normalizeSlug('Hello World!') // throws Error
984
+ * normalizeSlug('hello--world') // 'hello-world'
985
+ * ```
986
+ */
987
+ declare function normalizeSlug(slug: string): string;
988
+ /**
989
+ * Generates a canonical URL for an article.
990
+ *
991
+ * @param article - The published article
992
+ * @param options - Configuration options for URL generation
993
+ * @returns The canonical URL
994
+ * @throws Error if article or site data is invalid
995
+ *
996
+ * @example
997
+ * ```typescript
998
+ * const article = {
999
+ * slug: 'hello-world',
1000
+ * site: { domain: 'example.com' },
1001
+ * // ... other fields
1002
+ * };
1003
+ *
1004
+ * generateCanonicalUrl(article)
1005
+ * // 'https://example.com/blog/hello-world'
1006
+ *
1007
+ * generateCanonicalUrl(article, { pathPrefix: '/posts' })
1008
+ * // 'https://example.com/posts/hello-world'
1009
+ * ```
1010
+ */
1011
+ declare function generateCanonicalUrl(article: PublishedArticle, options?: CanonicalUrlOptions): string;
1012
+ /**
1013
+ * Generates a canonical URL for an article from site and slug data.
1014
+ * Useful when you don't have a full PublishedArticle object.
1015
+ *
1016
+ * @param site - The published site
1017
+ * @param slug - The article slug
1018
+ * @param options - Configuration options for URL generation
1019
+ * @returns The canonical URL
1020
+ *
1021
+ * @example
1022
+ * ```typescript
1023
+ * const site = { domain: 'example.com', ... };
1024
+ * generateCanonicalUrlFromSlug(site, 'hello-world')
1025
+ * // 'https://example.com/blog/hello-world'
1026
+ * ```
1027
+ */
1028
+ declare function generateCanonicalUrlFromSlug(site: PublishedSite, slug: string, options?: CanonicalUrlOptions): string;
1029
+ /**
1030
+ * Validates if a URL is a valid canonical URL format.
1031
+ *
1032
+ * @param url - The URL to validate
1033
+ * @returns True if the URL is valid for canonical use
1034
+ *
1035
+ * @example
1036
+ * ```typescript
1037
+ * isValidCanonicalUrl('https://example.com/blog/hello-world') // true
1038
+ * isValidCanonicalUrl('http://example.com/blog/hello-world') // true
1039
+ * isValidCanonicalUrl('example.com/blog/hello-world') // false (missing protocol)
1040
+ * isValidCanonicalUrl('https://example.com/blog/hello world') // false (spaces)
1041
+ * ```
1042
+ */
1043
+ declare function isValidCanonicalUrl(url: string): boolean;
1044
+ /**
1045
+ * Extracts the base URL (protocol + hostname + port) from a canonical URL.
1046
+ *
1047
+ * @param canonicalUrl - The canonical URL
1048
+ * @returns The base URL
1049
+ *
1050
+ * @example
1051
+ * ```typescript
1052
+ * getBaseUrl('https://example.com/blog/hello-world') // 'https://example.com'
1053
+ * getBaseUrl('https://blog.example.com:8080/posts/test') // 'https://blog.example.com:8080'
1054
+ * ```
1055
+ */
1056
+ declare function getBaseUrl(canonicalUrl: string): string;
1057
+ /**
1058
+ * Updates an article's canonical URL in its meta field.
1059
+ *
1060
+ * @param article - The article to update
1061
+ * @param options - Configuration options for URL generation
1062
+ * @returns A new article object with updated canonical URL
1063
+ *
1064
+ * @example
1065
+ * ```typescript
1066
+ * const updatedArticle = updateArticleCanonicalUrl(article);
1067
+ * console.log(updatedArticle.meta.canonicalUrl); // Generated canonical URL
1068
+ * ```
1069
+ */
1070
+ declare function updateArticleCanonicalUrl(article: PublishedArticle, options?: CanonicalUrlOptions): PublishedArticle;
1071
+
1072
+ /**
1073
+ * Metadata generation utilities for SEO meta tags, OpenGraph, and Twitter Cards.
1074
+ *
1075
+ * These utilities create comprehensive metadata for blog articles and pages,
1076
+ * optimizing for search engines and social media sharing platforms.
1077
+ */
1078
+
1079
+ /**
1080
+ * Configuration options for metadata generation.
1081
+ */
1082
+ interface MetadataOptions {
1083
+ /** Canonical URL generation options */
1084
+ canonical?: CanonicalUrlOptions;
1085
+ /** OpenGraph configuration */
1086
+ openGraph?: {
1087
+ /** Site name for OpenGraph */
1088
+ siteName?: string;
1089
+ /** Default OpenGraph image URL */
1090
+ defaultImage?: string;
1091
+ /** OpenGraph locale (defaults to site locale) */
1092
+ locale?: string;
1093
+ /** OpenGraph type (defaults to 'article') */
1094
+ type?: 'article' | 'website';
1095
+ };
1096
+ /** Twitter Card configuration */
1097
+ twitter?: {
1098
+ /** Twitter site handle (e.g., '@blogname') */
1099
+ site?: string;
1100
+ /** Twitter creator handle (e.g., '@authorname') */
1101
+ creator?: string;
1102
+ /** Default Twitter card type */
1103
+ cardType?: 'summary' | 'summary_large_image' | 'app' | 'player';
1104
+ };
1105
+ /** Additional meta tag configuration */
1106
+ meta?: {
1107
+ /** Robots meta tag directive */
1108
+ robots?: string;
1109
+ /** Viewport meta tag content */
1110
+ viewport?: string;
1111
+ /** Theme color for mobile browsers */
1112
+ themeColor?: string;
1113
+ /** Application name */
1114
+ applicationName?: string;
1115
+ };
1116
+ }
1117
+ /**
1118
+ * Generated metadata for HTML head section.
1119
+ */
1120
+ interface GeneratedMetadata {
1121
+ /** Basic HTML meta tags */
1122
+ meta: MetaTag[];
1123
+ /** OpenGraph meta tags */
1124
+ openGraph: MetaTag[];
1125
+ /** Twitter Card meta tags */
1126
+ twitter: MetaTag[];
1127
+ /** Link tags (canonical, etc.) */
1128
+ links: LinkTag[];
1129
+ /** Title tag content */
1130
+ title: string;
1131
+ /** Meta description content */
1132
+ description: string;
1133
+ /** Canonical URL */
1134
+ canonicalUrl: string;
1135
+ }
1136
+ /**
1137
+ * HTML meta tag representation.
1138
+ */
1139
+ interface MetaTag {
1140
+ name?: string;
1141
+ property?: string;
1142
+ content: string;
1143
+ httpEquiv?: string;
1144
+ }
1145
+ /**
1146
+ * HTML link tag representation.
1147
+ */
1148
+ interface LinkTag {
1149
+ rel: string;
1150
+ href: string;
1151
+ type?: string;
1152
+ title?: string;
1153
+ }
1154
+ /**
1155
+ * Generates metadata for a blog home page or listing page.
1156
+ *
1157
+ * @param site - The published site information
1158
+ * @param options - Metadata generation options
1159
+ * @param pageInfo - Optional page-specific information
1160
+ * @returns Complete metadata for HTML head section
1161
+ *
1162
+ * @example
1163
+ * ```typescript
1164
+ * const metadata = generateSiteMetadata(site, {
1165
+ * openGraph: {
1166
+ * defaultImage: 'https://example.com/og-home.jpg'
1167
+ * }
1168
+ * }, {
1169
+ * title: 'Latest Articles',
1170
+ * description: 'Browse our latest blog posts',
1171
+ * path: '/blog'
1172
+ * });
1173
+ * ```
1174
+ */
1175
+ declare function generateSiteMetadata(site: PublishedSite, options?: MetadataOptions, pageInfo?: {
1176
+ title?: string;
1177
+ description?: string;
1178
+ path?: string;
1179
+ pageNumber?: number;
1180
+ }): GeneratedMetadata;
1181
+ /**
1182
+ * Converts metadata to HTML string for server-side rendering.
1183
+ *
1184
+ * @param metadata - Generated metadata object
1185
+ * @returns HTML string for head section
1186
+ *
1187
+ * @example
1188
+ * ```typescript
1189
+ * const metadata = generateArticleMetadata(article);
1190
+ * const htmlHead = metadataToHtml(metadata);
1191
+ * // Use in server-side rendering
1192
+ * ```
1193
+ */
1194
+ declare function metadataToHtml(metadata: GeneratedMetadata): string;
1195
+ /**
1196
+ * Validates that metadata meets SEO best practices.
1197
+ *
1198
+ * @param metadata - Generated metadata to validate
1199
+ * @returns Validation results with warnings and errors
1200
+ *
1201
+ * @example
1202
+ * ```typescript
1203
+ * const metadata = generateArticleMetadata(article);
1204
+ * const validation = validateMetadata(metadata);
1205
+ *
1206
+ * if (validation.errors.length > 0) {
1207
+ * console.error('Metadata errors:', validation.errors);
1208
+ * }
1209
+ * ```
1210
+ */
1211
+ declare function validateMetadata(metadata: GeneratedMetadata): {
1212
+ valid: boolean;
1213
+ errors: string[];
1214
+ warnings: string[];
1215
+ };
1216
+
1217
+ /**
1218
+ * Next.js-compatible sitemap generation utilities for blog platform integration.
1219
+ *
1220
+ * These utilities generate sitemap entries that follow Next.js MetadataRoute.Sitemap
1221
+ * conventions, allowing seamless integration with existing Next.js applications.
1222
+ *
1223
+ * @example Integration with existing Next.js sitemap:
1224
+ * ```typescript
1225
+ * // app/sitemap.ts
1226
+ * import { generateBlogSitemapEntries } from '@xexr/blog-platform';
1227
+ * import type { MetadataRoute } from 'next';
1228
+ *
1229
+ * export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
1230
+ * // Your existing routes
1231
+ * const staticRoutes = [
1232
+ * { url: 'https://yoursite.com', lastModified: new Date(), priority: 1 },
1233
+ * { url: 'https://yoursite.com/about', lastModified: new Date(), priority: 0.8 }
1234
+ * ];
1235
+ *
1236
+ * // Blog platform entries
1237
+ * const blogEntries = await generateBlogSitemapEntries(blogConfig);
1238
+ *
1239
+ * return [...staticRoutes, ...blogEntries];
1240
+ * }
1241
+ * ```
1242
+ */
1243
+
1244
+ /**
1245
+ * Configuration options for sitemap generation.
1246
+ */
1247
+ interface SitemapOptions {
1248
+ /** Canonical URL configuration */
1249
+ canonical?: CanonicalUrlOptions;
1250
+ /** Whether to include author pages in sitemap */
1251
+ includeAuthorPages?: boolean;
1252
+ /** Whether to include pagination pages in sitemap */
1253
+ includePaginationPages?: boolean;
1254
+ /** Number of articles per page for pagination calculation */
1255
+ articlesPerPage?: number;
1256
+ /** Maximum number of sitemap entries (defaults to 50,000 per Google guidelines) */
1257
+ maxEntries?: number;
1258
+ /** Change frequency for different page types */
1259
+ changeFrequency?: {
1260
+ /** Change frequency for blog home page */
1261
+ homePage?: SitemapChangeFreq;
1262
+ /** Change frequency for article pages */
1263
+ articles?: SitemapChangeFreq;
1264
+ /** Change frequency for author pages */
1265
+ authorPages?: SitemapChangeFreq;
1266
+ /** Change frequency for pagination pages */
1267
+ paginationPages?: SitemapChangeFreq;
1268
+ };
1269
+ /** Priority values for different page types */
1270
+ priority?: {
1271
+ /** Priority for blog home page (0.0-1.0) */
1272
+ homePage?: number;
1273
+ /** Priority for article pages (0.0-1.0) */
1274
+ articles?: number;
1275
+ /** Priority for author pages (0.0-1.0) */
1276
+ authorPages?: number;
1277
+ /** Priority for pagination pages (0.0-1.0) */
1278
+ paginationPages?: number;
1279
+ };
1280
+ }
1281
+ /**
1282
+ * Valid sitemap change frequency values as per XML sitemap standard.
1283
+ */
1284
+ type SitemapChangeFreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
1285
+ /**
1286
+ * Next.js MetadataRoute.Sitemap compatible entry.
1287
+ * This matches the expected format for Next.js sitemap generation.
1288
+ */
1289
+ interface NextjsSitemapEntry {
1290
+ /** The URL of the page */
1291
+ url: string;
1292
+ /** Last modification date */
1293
+ lastModified?: string | Date;
1294
+ /** Change frequency hint for search engines */
1295
+ changeFrequency?: SitemapChangeFreq;
1296
+ /** Priority relative to other URLs on the site (0.0-1.0) */
1297
+ priority?: number;
1298
+ /** Alternate language versions (for i18n) */
1299
+ alternates?: {
1300
+ languages?: Record<string, string>;
1301
+ };
1302
+ }
1303
+ /**
1304
+ * Legacy XML sitemap entry format (for backward compatibility).
1305
+ * @deprecated Use NextjsSitemapEntry for new implementations
1306
+ */
1307
+ interface SitemapEntry {
1308
+ /** The URL of the page */
1309
+ loc: string;
1310
+ /** Last modification date as ISO 8601 string */
1311
+ lastmod?: string;
1312
+ /** Change frequency hint for search engines */
1313
+ changefreq?: SitemapChangeFreq;
1314
+ /** Priority relative to other URLs on the site (0.0-1.0) */
1315
+ priority?: number;
1316
+ }
1317
+ /**
1318
+ * Complete sitemap data structure.
1319
+ */
1320
+ interface SitemapData {
1321
+ /** All URL entries in the sitemap */
1322
+ entries: SitemapEntry[];
1323
+ /** Total number of entries */
1324
+ totalEntries: number;
1325
+ /** Whether the sitemap was truncated due to size limits */
1326
+ truncated: boolean;
1327
+ /** Generation timestamp */
1328
+ generatedAt: string;
1329
+ /** Base URL used for canonical URL generation */
1330
+ baseUrl: string;
1331
+ }
1332
+ /**
1333
+ * Generates Next.js-compatible sitemap entries for blog platform integration.
1334
+ *
1335
+ * @param site - The published site information
1336
+ * @param articles - Array of published articles
1337
+ * @param authors - Array of published authors (optional)
1338
+ * @param options - Sitemap generation options
1339
+ * @returns Array of Next.js MetadataRoute.Sitemap compatible entries
1340
+ *
1341
+ * @example
1342
+ * ```typescript
1343
+ * // In your Next.js app/sitemap.ts file:
1344
+ * import { generateBlogSitemapEntries } from '@xexr/blog-platform';
1345
+ * import type { MetadataRoute } from 'next';
1346
+ *
1347
+ * export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
1348
+ * const blogEntries = await generateBlogSitemapEntries(site, articles, authors, {
1349
+ * includeAuthorPages: true,
1350
+ * includePaginationPages: true,
1351
+ * changeFrequency: { articles: 'monthly' }
1352
+ * });
1353
+ *
1354
+ * return [
1355
+ * // Your existing site routes
1356
+ * { url: 'https://yoursite.com', lastModified: new Date(), priority: 1 },
1357
+ * { url: 'https://yoursite.com/about', lastModified: new Date() },
1358
+ * // Blog entries
1359
+ * ...blogEntries
1360
+ * ];
1361
+ * }
1362
+ * ```
1363
+ */
1364
+ declare function generateBlogSitemapEntries(site: PublishedSite, articles: readonly PublishedArticle[], authors?: readonly PublishedAuthor[], options?: SitemapOptions): Promise<NextjsSitemapEntry[]>;
1365
+ /**
1366
+ * Legacy sitemap generation for backward compatibility.
1367
+ * @deprecated Use generateBlogSitemapEntries for Next.js integration
1368
+ *
1369
+ * @param site - The published site information
1370
+ * @param articles - Array of published articles
1371
+ * @param authors - Array of published authors (optional)
1372
+ * @param options - Sitemap generation options
1373
+ * @returns Complete sitemap data
1374
+ */
1375
+ declare function generateSitemap(site: PublishedSite, articles: readonly PublishedArticle[], authors?: readonly PublishedAuthor[], options?: SitemapOptions): SitemapData;
1376
+ /**
1377
+ * Converts sitemap data to XML format.
1378
+ *
1379
+ * @param sitemapData - The sitemap data to convert
1380
+ * @returns XML string in sitemap format
1381
+ *
1382
+ * @example
1383
+ * ```typescript
1384
+ * const sitemap = generateSitemap(site, articles);
1385
+ * const xmlString = sitemapToXml(sitemap);
1386
+ * // Use in API route response
1387
+ * ```
1388
+ */
1389
+ declare function sitemapToXml(sitemapData: SitemapData): string;
1390
+ /**
1391
+ * Generates a sitemap index for large blogs that need multiple sitemaps.
1392
+ *
1393
+ * @param sitemaps - Array of sitemap information
1394
+ * @param baseUrl - Base URL for the site
1395
+ * @returns XML string for sitemap index
1396
+ *
1397
+ * @example
1398
+ * ```typescript
1399
+ * const sitemapIndex = generateSitemapIndex([
1400
+ * { url: 'https://example.com/sitemap-articles.xml', lastmod: '2023-01-01T00:00:00Z' },
1401
+ * { url: 'https://example.com/sitemap-authors.xml', lastmod: '2023-01-01T00:00:00Z' }
1402
+ * ], 'https://example.com');
1403
+ * ```
1404
+ */
1405
+ declare function generateSitemapIndex(sitemaps: readonly {
1406
+ url: string;
1407
+ lastmod?: string;
1408
+ }[], _baseUrl: string): string;
1409
+ /**
1410
+ * Validates sitemap data for compliance with search engine guidelines.
1411
+ *
1412
+ * @param sitemapData - The sitemap data to validate
1413
+ * @returns Validation results with errors and warnings
1414
+ *
1415
+ * @example
1416
+ * ```typescript
1417
+ * const sitemap = generateSitemap(site, articles);
1418
+ * const validation = validateSitemap(sitemap);
1419
+ *
1420
+ * if (validation.errors.length > 0) {
1421
+ * console.error('Sitemap errors:', validation.errors);
1422
+ * }
1423
+ * ```
1424
+ */
1425
+ declare function validateSitemap(sitemapData: SitemapData): {
1426
+ valid: boolean;
1427
+ errors: string[];
1428
+ warnings: string[];
1429
+ };
1430
+ /**
1431
+ * Creates a separate sitemap specifically for blog content.
1432
+ * Useful for large sites that need to split sitemaps by content type.
1433
+ *
1434
+ * @param site - The published site information
1435
+ * @param articles - Array of published articles
1436
+ * @param authors - Array of published authors (optional)
1437
+ * @param options - Sitemap generation options
1438
+ * @returns Next.js sitemap function
1439
+ *
1440
+ * @example
1441
+ * ```typescript
1442
+ * // app/blog-sitemap/route.ts - separate blog sitemap endpoint
1443
+ * import { createBlogSitemapRoute } from '@xexr/blog-platform';
1444
+ *
1445
+ * export const GET = createBlogSitemapRoute(site, articles, authors, {
1446
+ * includeAuthorPages: true,
1447
+ * articlesPerPage: 20
1448
+ * });
1449
+ * ```
1450
+ */
1451
+ declare function createBlogSitemapRoute(site: PublishedSite, articles: readonly PublishedArticle[], authors?: readonly PublishedAuthor[], options?: SitemapOptions): () => Promise<Response>;
1452
+ /**
1453
+ * Configuration interface for blog data fetching.
1454
+ * Used to fetch blog data for sitemap generation.
1455
+ */
1456
+ interface BlogSitemapConfig {
1457
+ /** Function to fetch site data */
1458
+ getSite: () => Promise<PublishedSite>;
1459
+ /** Function to fetch published articles */
1460
+ getArticles: () => Promise<readonly PublishedArticle[]>;
1461
+ /** Function to fetch authors (optional) */
1462
+ getAuthors?: () => Promise<readonly PublishedAuthor[]>;
1463
+ /** Sitemap generation options */
1464
+ options?: SitemapOptions;
1465
+ }
1466
+ /**
1467
+ * Higher-order function that creates a Next.js sitemap function with data fetching.
1468
+ * Ideal for dynamic blog content that needs to be fetched at sitemap generation time.
1469
+ *
1470
+ * @param config - Configuration for fetching blog data
1471
+ * @returns Next.js sitemap function
1472
+ *
1473
+ * @example
1474
+ * ```typescript
1475
+ * // app/sitemap.ts
1476
+ * import { createDynamicBlogSitemap } from '@xexr/blog-platform';
1477
+ * import { getBlogData } from './lib/blog';
1478
+ *
1479
+ * export default createDynamicBlogSitemap({
1480
+ * getSite: () => getBlogData().site,
1481
+ * getArticles: () => getBlogData().articles,
1482
+ * getAuthors: () => getBlogData().authors,
1483
+ * options: {
1484
+ * includeAuthorPages: true,
1485
+ * changeFrequency: { articles: 'weekly' }
1486
+ * }
1487
+ * });
1488
+ * ```
1489
+ */
1490
+ declare function createDynamicBlogSitemap(config: BlogSitemapConfig): () => Promise<NextjsSitemapEntry[]>;
1491
+
1492
+ /**
1493
+ * Structured Data (JSON-LD) utilities for SEO optimization
1494
+ *
1495
+ * This module provides comprehensive schema.org structured data generation
1496
+ * for blog articles, authors, and organizations. It follows Google's
1497
+ * recommended guidelines for structured data markup.
1498
+ *
1499
+ * @see https://developers.google.com/search/docs/appearance/structured-data
1500
+ * @see https://schema.org/
1501
+ */
1502
+
1503
+ /**
1504
+ * Base schema.org context and type
1505
+ */
1506
+ interface BaseStructuredData {
1507
+ "@context": "https://schema.org";
1508
+ "@type": string;
1509
+ "@id"?: string;
1510
+ }
1511
+ /**
1512
+ * Person schema for authors
1513
+ * @see https://schema.org/Person
1514
+ */
1515
+ interface PersonSchema extends BaseStructuredData {
1516
+ "@type": "Person";
1517
+ name: string;
1518
+ description?: string;
1519
+ identifier?: string;
1520
+ url?: string;
1521
+ sameAs?: string[];
1522
+ jobTitle?: string;
1523
+ affiliation?: OrganizationSchema;
1524
+ knowsAbout?: string[];
1525
+ additionalType?: string;
1526
+ }
1527
+ /**
1528
+ * Organization schema for blog sites
1529
+ * @see https://schema.org/Organization
1530
+ */
1531
+ interface OrganizationSchema extends BaseStructuredData {
1532
+ "@type": "Organization";
1533
+ name: string;
1534
+ description?: string;
1535
+ url?: string;
1536
+ logo?: string;
1537
+ sameAs?: string[];
1538
+ foundingDate?: string;
1539
+ location?: string;
1540
+ contactPoint?: {
1541
+ "@type": "ContactPoint";
1542
+ contactType: string;
1543
+ url?: string;
1544
+ email?: string;
1545
+ };
1546
+ }
1547
+ /**
1548
+ * BlogPosting schema for articles
1549
+ * @see https://schema.org/BlogPosting
1550
+ */
1551
+ interface BlogPostingSchema extends BaseStructuredData {
1552
+ "@type": "BlogPosting";
1553
+ headline: string;
1554
+ description?: string;
1555
+ image?: string | string[];
1556
+ datePublished: string;
1557
+ dateModified?: string;
1558
+ author?: PersonSchema | PersonSchema[];
1559
+ publisher: OrganizationSchema;
1560
+ mainEntityOfPage: {
1561
+ "@type": "WebPage";
1562
+ "@id": string;
1563
+ };
1564
+ keywords?: string | string[];
1565
+ wordCount?: number;
1566
+ timeRequired?: string;
1567
+ inLanguage?: string;
1568
+ isAccessibleForFree?: boolean;
1569
+ genre?: string | string[];
1570
+ about?: {
1571
+ "@type": "Thing";
1572
+ name: string;
1573
+ }[];
1574
+ mentions?: {
1575
+ "@type": "Thing";
1576
+ name: string;
1577
+ }[];
1578
+ }
1579
+ /**
1580
+ * WebSite schema for the blog home page
1581
+ * @see https://schema.org/WebSite
1582
+ */
1583
+ interface WebSiteSchema extends BaseStructuredData {
1584
+ "@type": "WebSite";
1585
+ name: string;
1586
+ description?: string;
1587
+ url: string;
1588
+ publisher: OrganizationSchema;
1589
+ inLanguage?: string;
1590
+ potentialAction?: {
1591
+ "@type": "SearchAction";
1592
+ target: {
1593
+ "@type": "EntryPoint";
1594
+ urlTemplate: string;
1595
+ };
1596
+ "query-input": string;
1597
+ };
1598
+ }
1599
+ /**
1600
+ * BreadcrumbList schema for navigation
1601
+ * @see https://schema.org/BreadcrumbList
1602
+ */
1603
+ interface BreadcrumbListSchema extends BaseStructuredData {
1604
+ "@type": "BreadcrumbList";
1605
+ itemListElement: {
1606
+ "@type": "ListItem";
1607
+ position: number;
1608
+ name: string;
1609
+ item: string;
1610
+ }[];
1611
+ }
1612
+ /**
1613
+ * Options for customizing structured data generation
1614
+ */
1615
+ interface StructuredDataOptions {
1616
+ /** Include optional properties like keywords, mentions, etc. */
1617
+ includeOptionalProperties?: boolean;
1618
+ /** Include search action in WebSite schema */
1619
+ includeSearchAction?: boolean;
1620
+ /** Custom domain override for URLs */
1621
+ customDomain?: string;
1622
+ /** Path prefix for blog URLs (e.g., '/blog', '/articles') */
1623
+ pathPrefix?: string;
1624
+ /** Language code (e.g., 'en-US') */
1625
+ language?: string;
1626
+ /** Whether content is free to access */
1627
+ isAccessibleForFree?: boolean;
1628
+ }
1629
+ /**
1630
+ * Generate comprehensive structured data for a blog article
1631
+ */
1632
+ declare function generateArticleStructuredData(article: PublishedArticle, options?: StructuredDataOptions): BlogPostingSchema;
1633
+ /**
1634
+ * Generate structured data for an author
1635
+ */
1636
+ declare function generateAuthorStructuredData(author: PublishedAuthor, options?: StructuredDataOptions): PersonSchema;
1637
+ /**
1638
+ * Generate structured data for a blog site organization
1639
+ */
1640
+ declare function generateOrganizationStructuredData(site: PublishedSite, options?: StructuredDataOptions): OrganizationSchema;
1641
+ /**
1642
+ * Generate structured data for the blog website
1643
+ */
1644
+ declare function generateWebSiteStructuredData(site: PublishedSite, options?: StructuredDataOptions): WebSiteSchema;
1645
+ /**
1646
+ * Generate breadcrumb structured data for navigation
1647
+ */
1648
+ declare function generateBreadcrumbStructuredData(breadcrumbs: Array<{
1649
+ name: string;
1650
+ url: string;
1651
+ }>, options?: StructuredDataOptions): BreadcrumbListSchema;
1652
+ /**
1653
+ * Generate combined structured data for a complete article page
1654
+ */
1655
+ declare function generateCompleteArticleStructuredData(article: PublishedArticle, options?: StructuredDataOptions): Array<BlogPostingSchema | WebSiteSchema | BreadcrumbListSchema>;
1656
+ /**
1657
+ * Generate structured data for the blog home page
1658
+ */
1659
+ declare function generateBlogHomeStructuredData(site: PublishedSite, featuredArticles?: PublishedArticle[], options?: StructuredDataOptions): Array<WebSiteSchema | BlogPostingSchema>;
1660
+ /**
1661
+ * Validate structured data schema
1662
+ */
1663
+ declare function validateStructuredData(schema: BaseStructuredData): {
1664
+ isValid: boolean;
1665
+ errors: string[];
1666
+ };
1667
+ /**
1668
+ * Convert structured data to JSON-LD string for HTML embedding
1669
+ */
1670
+ declare function structuredDataToJsonLd(schema: BaseStructuredData | BaseStructuredData[]): string;
1671
+ /**
1672
+ * Create a React script element for JSON-LD structured data
1673
+ */
1674
+ declare function createStructuredDataScript(schema: BaseStructuredData | BaseStructuredData[]): {
1675
+ type: string;
1676
+ dangerouslySetInnerHTML: {
1677
+ __html: string;
1678
+ };
1679
+ };
1680
+ /**
1681
+ * Extract keywords from article content for structured data
1682
+ */
1683
+ declare function extractKeywordsFromArticle(article: PublishedArticle): string[];
1684
+ /**
1685
+ * Error types for structured data operations
1686
+ */
1687
+ declare class StructuredDataError extends Error {
1688
+ readonly code: string;
1689
+ readonly details?: unknown | undefined;
1690
+ constructor(message: string, code: string, details?: unknown | undefined);
1691
+ }
1692
+ /**
1693
+ * Generate structured data with error handling
1694
+ */
1695
+ declare function safeGenerateArticleStructuredData(article: PublishedArticle, options?: StructuredDataOptions): {
1696
+ schema?: BlogPostingSchema;
1697
+ error?: StructuredDataError;
1698
+ };
1699
+
1700
+ /**
1701
+ * POST /api/blog/publish
1702
+ *
1703
+ * Publishes an article to the blog platform with full idempotency support.
1704
+ * This endpoint receives content from Bloggy core and stores it for rendering.
1705
+ *
1706
+ * Request body should contain a PublishPayload with:
1707
+ * - article: The complete article data
1708
+ * - idempotencyKey: SHA-256 hash for duplicate prevention
1709
+ * - Optional overrides for slug, meta description, etc.
1710
+ */
1711
+ declare function POST$1(request: NextRequest): Promise<NextResponse<PublishResult>>;
1712
+
1713
+ /**
1714
+ * POST /api/blog/unpublish
1715
+ *
1716
+ * Unpublishes an article from the blog platform with full idempotency support.
1717
+ * This endpoint receives unpublish requests from Bloggy core and removes the content.
1718
+ *
1719
+ * Request body should contain an UnpublishPayload with:
1720
+ * - slug: The article slug to unpublish
1721
+ * - idempotencyKey: SHA-256 hash for duplicate prevention
1722
+ * - Optional dry run flag for validation
1723
+ */
1724
+ declare function POST(request: NextRequest): Promise<NextResponse<PublishResult>>;
1725
+
1726
+ /**
1727
+ * Version information
1728
+ */
1729
+ interface VersionInfo {
1730
+ name: string;
1731
+ version: string;
1732
+ major: number;
1733
+ minor: number;
1734
+ patch: number;
1735
+ }
1736
+ /**
1737
+ * Version compatibility result
1738
+ */
1739
+ interface VersionCompatibilityResult {
1740
+ compatible: boolean;
1741
+ errorCode?: string;
1742
+ errorMessage?: string;
1743
+ warning?: string;
1744
+ }
1745
+
1746
+ /**
1747
+ * Version compatibility response
1748
+ */
1749
+ interface VersionResponse {
1750
+ success: boolean;
1751
+ versions: {
1752
+ bloggy: VersionInfo;
1753
+ platform: VersionInfo;
1754
+ };
1755
+ compatibility: VersionCompatibilityResult;
1756
+ error?: string;
1757
+ }
1758
+ /**
1759
+ * GET /api/blog/version
1760
+ *
1761
+ * Returns version information and compatibility status for Bloggy core and blog platform.
1762
+ * Used by customers to verify compatibility before publishing.
1763
+ *
1764
+ * Optional X-Bloggy-Version header can be provided to check compatibility
1765
+ * with specific versions.
1766
+ */
1767
+ declare function GET$1(request: NextRequest): Promise<NextResponse<VersionResponse>>;
1768
+
1769
+ /**
1770
+ * GET /api/blog/slugs/{slug}
1771
+ *
1772
+ * Checks if a slug is already in use on the blog platform.
1773
+ * Used for dry-run validation in publishing workflow.
1774
+ *
1775
+ * Returns: { exists: boolean, message?: string }
1776
+ * - exists: false -> available
1777
+ * - exists: true -> taken, with suggestion message
1778
+ */
1779
+ declare function GET(request: NextRequest, { params }: {
1780
+ params: Promise<{
1781
+ slug: string;
1782
+ }>;
1783
+ }): Promise<NextResponse>;
1784
+ /**
1785
+ * OPTIONS /api/blog/slugs/{slug}
1786
+ * Handle CORS preflight
1787
+ */
1788
+ declare function OPTIONS(request: NextRequest): Promise<NextResponse>;
1789
+
1790
+ /**
1791
+ * @xexr/blog-platform
1792
+ *
1793
+ * SEO-friendly blog platform for Next.js with Bloggy integration.
1794
+ * Enables publishing AI-generated content to custom domains.
1795
+ */
1796
+
1797
+ declare const PACKAGE_VERSION: string;
1798
+
1799
+ export { ApiKeyAuth, type AuthConfig, type AuthResult, ArticlePage as BlogArticlePage, BlogHomePage, type BlogPlatformConfig, type BlogPlatformStorage, type BlogPostingSchema, type BlogSitemapConfig, type BreadcrumbListSchema, Button, type CanonicalUrlOptions, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DrizzleStorage, EnvAuth, type GeneratedMetadata, InMemoryStorage, type LinkTag, type MetaTag, type MetadataOptions, type NextjsSitemapEntry, NoAuth, type OrganizationSchema, PACKAGE_VERSION, Pagination$1 as Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, type PersonSchema, PublishResult, PublishedArticle, PublishedAuthor, PublishedSite, type RateLimitConfig, type RateLimitResult, Pagination as ShadcnPagination, type SitemapChangeFreq, type SitemapData, type SitemapEntry, type SitemapOptions, StructuredDataError, type StructuredDataOptions, UninitializedAuth, type WebSiteSchema, autoConfigureRateLimit, bpArticles, bpOperations, buttonVariants, checkRateLimit, cn, configureRateLimit, createBlogSitemapRoute, createDynamicBlogSitemap, createRateLimitErrorResponse, createStructuredDataScript, enforceRateLimit, extractKeywordsFromArticle, extractTokenFromHeader, generateArticleMetadata, generateArticleStructuredData, generateAuthorStructuredData, generateBlogHomeMetadata, generateBlogHomeStructuredData, generateBlogSitemapEntries, generateBreadcrumbStructuredData, generateCanonicalUrl, generateCanonicalUrlFromSlug, generateCompleteArticleStructuredData, generateOrganizationStructuredData, generatePaginationMetadata, generateSiteMetadata, generateSitemap, generateSitemapIndex, generateWebSiteStructuredData, getAuthConfig, getBaseUrl, getDrizzleAdapterSql, getRateLimitHeaders, getStorage, initBlogPlatform, initBlogPlatformDev, initBlogPlatformOnce, initBlogPlatformProd, initEnvAuth, isValidCanonicalUrl, metadataToHtml, normalizeDomain, normalizeSlug, POST$1 as publishHandler, safeGenerateArticleStructuredData, setAuthConfig, setStorage, shouldSkipAuth, sitemapToXml, GET as slugExistsHandler, OPTIONS as slugExistsOptionsHandler, structuredDataToJsonLd, POST as unpublishHandler, updateArticleCanonicalUrl, usePagination, validateMetadata, validateSitemap, validateStructuredData, GET$1 as versionHandler };