@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.
- package/CHANGELOG.md +61 -0
- package/LICENSE +21 -0
- package/README.md +1175 -0
- package/dist/app/api/blog/publish/route.ts +360 -0
- package/dist/app/api/blog/slugs/[slug]/route.ts +166 -0
- package/dist/app/api/blog/unpublish/route.ts +301 -0
- package/dist/app/api/blog/version/route.ts +259 -0
- package/dist/app/blog/[slug]/page.tsx +304 -0
- package/dist/app/blog/page.tsx +316 -0
- package/dist/index.d.mts +1799 -0
- package/dist/index.d.ts +1799 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +31 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types/index.d.mts +142 -0
- package/dist/types/index.d.ts +142 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +2 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +60 -0
package/dist/index.d.mts
ADDED
|
@@ -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 };
|