@xexr/blog-platform 1.0.11 → 1.0.12

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.
@@ -1,7 +1,11 @@
1
1
  import type { NextRequest, NextResponse } from "next/server";
2
2
  import { getStorage } from "@/lib/database";
3
3
  import { getBlogPlatformVersion, parseVersionHeader } from "@/lib/version";
4
- import { requireAuth, authenticateRequest } from "@/lib/auth";
4
+ import {
5
+ requireAuth,
6
+ authenticateRequest,
7
+ createTokenFingerprint,
8
+ } from "@/lib/auth";
5
9
  import { enforceRateLimit, getRateLimitHeaders } from "@/lib/ratelimit";
6
10
  import { createCorsResponse, handleCorsOptions } from "@/lib/cors";
7
11
  import type {
@@ -32,9 +36,9 @@ export async function POST(
32
36
  return createCorsResponse(request, authCheck.error!, { status: 401 });
33
37
  }
34
38
 
35
- // Apply rate limiting using the authentication token
36
- const token = auth.token ?? "anonymous";
37
- const rateLimitCheck = await enforceRateLimit(token);
39
+ // Apply rate limiting without exposing raw authentication tokens to Redis.
40
+ const tokenFingerprint = createTokenFingerprint(auth.token ?? "anonymous");
41
+ const rateLimitCheck = await enforceRateLimit(tokenFingerprint);
38
42
  if (!rateLimitCheck.allowed) {
39
43
  // Extract rate limit info from error response, with safe fallback
40
44
  const rateLimitData =
@@ -1,7 +1,11 @@
1
1
  import { type NextRequest, NextResponse } from "next/server";
2
2
  import { getStorage } from "@/lib/database";
3
3
  import { getBlogPlatformVersion, parseVersionHeader } from "@/lib/version";
4
- import { requireAuth, authenticateRequest } from "@/lib/auth";
4
+ import {
5
+ requireAuth,
6
+ authenticateRequest,
7
+ createTokenFingerprint,
8
+ } from "@/lib/auth";
5
9
  import { enforceRateLimit, getRateLimitHeaders } from "@/lib/ratelimit";
6
10
  import { createCorsResponse, handleCorsOptions } from "@/lib/cors";
7
11
  import { isValidSlug } from "@/app/api/blog/publish/route"; // Reuse slug validation from publish
@@ -29,10 +33,9 @@ export async function GET(
29
33
  return NextResponse.json(authCheck.error!, { status: 401 });
30
34
  }
31
35
 
32
- const token = auth.token ?? "anonymous";
33
-
34
- // Rate limiting (same as publish: 5/min per token)
35
- const rateLimitCheck = await enforceRateLimit(token);
36
+ // Rate limiting (same as publish: 5/min per token fingerprint)
37
+ const tokenFingerprint = createTokenFingerprint(auth.token ?? "anonymous");
38
+ const rateLimitCheck = await enforceRateLimit(tokenFingerprint);
36
39
  if (!rateLimitCheck.allowed) {
37
40
  const rateLimitData = (rateLimitCheck.errorResponse.error?.details
38
41
  ?.rateLimit as {
@@ -1,7 +1,11 @@
1
1
  import { type NextRequest, NextResponse } from "next/server";
2
2
  import { getStorage } from "@/lib/database";
3
3
  import { getBlogPlatformVersion, parseVersionHeader } from "@/lib/version";
4
- import { requireAuth, authenticateRequest } from "@/lib/auth";
4
+ import {
5
+ requireAuth,
6
+ authenticateRequest,
7
+ createTokenFingerprint,
8
+ } from "@/lib/auth";
5
9
  import { enforceRateLimit, getRateLimitHeaders } from "@/lib/ratelimit";
6
10
  import { createCorsResponse, handleCorsOptions } from "@/lib/cors";
7
11
  import type { UnpublishPayload, PublishResult } from "@/types/contracts";
@@ -28,9 +32,9 @@ export async function POST(
28
32
  return NextResponse.json(authCheck.error!, { status: 401 });
29
33
  }
30
34
 
31
- // Apply rate limiting using the authentication token
32
- const token = auth.token ?? "anonymous";
33
- const rateLimitCheck = await enforceRateLimit(token);
35
+ // Apply rate limiting without exposing raw authentication tokens to Redis.
36
+ const tokenFingerprint = createTokenFingerprint(auth.token ?? "anonymous");
37
+ const rateLimitCheck = await enforceRateLimit(tokenFingerprint);
34
38
  if (!rateLimitCheck.allowed) {
35
39
  // Extract rate limit info from error response, with safe fallback
36
40
  const rateLimitData =
@@ -6,7 +6,11 @@ import {
6
6
  type VersionCompatibilityResult,
7
7
  type VersionInfo,
8
8
  } from "@/lib/version";
9
- import { requireAuth, authenticateRequest } from "@/lib/auth";
9
+ import {
10
+ requireAuth,
11
+ authenticateRequest,
12
+ createTokenFingerprint,
13
+ } from "@/lib/auth";
10
14
  import { enforceRateLimit, getRateLimitHeaders } from "@/lib/ratelimit";
11
15
  import { createCorsResponse, handleCorsOptions } from "@/lib/cors";
12
16
 
@@ -67,9 +71,9 @@ export async function GET(
67
71
  return createCorsResponse(request, errorResponse, { status: 401 });
68
72
  }
69
73
 
70
- // Apply rate limiting using the authentication token
71
- const token = auth.token ?? "anonymous";
72
- const rateLimitCheck = await enforceRateLimit(token);
74
+ // Apply rate limiting without exposing raw authentication tokens to Redis.
75
+ const tokenFingerprint = createTokenFingerprint(auth.token ?? "anonymous");
76
+ const rateLimitCheck = await enforceRateLimit(tokenFingerprint);
73
77
  if (!rateLimitCheck.allowed) {
74
78
  // Extract rate limit info from error response, with safe fallback
75
79
  const rateLimitData =
@@ -28,6 +28,7 @@ import {
28
28
  } from "../../../lib/urls";
29
29
  import { NewsletterSignup } from "../../../components/NewsletterSignup";
30
30
  import {
31
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
31
32
  type BlogTheme,
32
33
  extractTableOfContents,
33
34
  formatArticleDateMeta,
@@ -236,7 +237,12 @@ export default function ArticlePage({
236
237
  {/* Article Header */}
237
238
  <header className="article-header mx-auto mb-6 max-w-4xl space-y-4 text-center sm:mb-8 sm:space-y-6 lg:mb-12 lg:space-y-8">
238
239
  {/* Title */}
239
- <h1 className="article-title bg-linear-to-br from-[var(--blog-accent-hover)] via-[var(--blog-text-accent)] to-fuchsia-200 bg-clip-text px-4 text-2xl leading-tight font-light tracking-tight text-balance text-transparent sm:px-0 sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl">
240
+ <h1
241
+ className={cn(
242
+ "article-title px-4 text-2xl leading-tight font-light tracking-tight text-balance sm:px-0 sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl",
243
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
244
+ )}
245
+ >
240
246
  {article.title}
241
247
  </h1>
242
248
 
@@ -70,6 +70,9 @@ describe("AuthorPage", () => {
70
70
  );
71
71
 
72
72
  expect(html).toContain("Jane Doe");
73
+ expect(html).toContain("bg-linear-to-br");
74
+ expect(html).toContain("via-[var(--blog-text-accent)]");
75
+ expect(html).toContain("to-fuchsia-200");
73
76
  expect(html).toContain("Writes about product systems.");
74
77
  expect(html).toContain("Guest Author");
75
78
  expect(html).not.toContain("she/her");
@@ -17,7 +17,11 @@ import {
17
17
  buildAuthorPath,
18
18
  normalizeBlogBasePath,
19
19
  } from "../../../../lib/urls";
20
- import { type BlogTheme, getBlogThemeStyle } from "../../../../lib/article-ui";
20
+ import {
21
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
22
+ type BlogTheme,
23
+ getBlogThemeStyle,
24
+ } from "../../../../lib/article-ui";
21
25
  import type {
22
26
  PublishedArticle,
23
27
  PublishedAuthor,
@@ -96,7 +100,12 @@ export default function AuthorPage({
96
100
  </Badge>
97
101
  )}
98
102
  </div>
99
- <h1 className="text-foreground text-4xl leading-tight font-semibold text-balance sm:text-5xl">
103
+ <h1
104
+ className={cn(
105
+ "text-4xl leading-tight font-light text-balance sm:text-5xl",
106
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
107
+ )}
108
+ >
100
109
  {author.name}
101
110
  </h1>
102
111
  </div>
@@ -34,6 +34,51 @@ const article: PublishedArticle = {
34
34
  };
35
35
 
36
36
  describe("BlogHomePage", () => {
37
+ it("uses the article title gradient for the blog home title and renders browse links", async () => {
38
+ const html = await renderToHtml(
39
+ React.createElement(BlogHomePage, {
40
+ articles: [article],
41
+ siteName: "Example Blog",
42
+ siteTagline: "Useful writing",
43
+ basePath: "/new-blog",
44
+ }),
45
+ );
46
+
47
+ expect(html).toContain("Example Blog");
48
+ expect(html).toContain("bg-linear-to-br");
49
+ expect(html).toContain("via-[var(--blog-text-accent)]");
50
+ expect(html).toContain("to-fuchsia-200");
51
+ expect(html).toContain('aria-label="Browse blog"');
52
+ expect(html).toContain('href="/new-blog/topics"');
53
+ expect(html).toContain('href="/new-blog/authors"');
54
+ });
55
+
56
+ it("links article topic chips to their topic archive pages", async () => {
57
+ const html = await renderToHtml(
58
+ React.createElement(BlogHomePage, {
59
+ articles: [
60
+ {
61
+ ...article,
62
+ topics: [
63
+ {
64
+ id: "topic-1",
65
+ name: "Recurring work and routines",
66
+ slug: "recurring-work-and-routines",
67
+ isPrimary: true,
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ basePath: "/new-blog",
73
+ }),
74
+ );
75
+
76
+ expect(html).toContain(
77
+ 'href="/new-blog/topics/recurring-work-and-routines"',
78
+ );
79
+ expect(html).toContain("Recurring work and routines");
80
+ });
81
+
37
82
  it("renders the newsletter form when configured", async () => {
38
83
  const html = await renderToHtml(
39
84
  React.createElement(BlogHomePage, {
@@ -6,6 +6,7 @@ import {
6
6
  type PublishedAuthor,
7
7
  type PublishedNewsletterSettings,
8
8
  type PublishedSite,
9
+ type PublishedTopic,
9
10
  } from "../../types/contracts";
10
11
  import { cn } from "../../lib/utils";
11
12
  import Pagination from "../../components/Pagination";
@@ -19,7 +20,11 @@ import {
19
20
  normalizeBlogBasePath,
20
21
  } from "../../lib/urls";
21
22
  import { formatDistanceToNow } from "date-fns";
22
- import { type BlogTheme, getBlogThemeStyle } from "../../lib/article-ui";
23
+ import {
24
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
25
+ type BlogTheme,
26
+ getBlogThemeStyle,
27
+ } from "../../lib/article-ui";
23
28
  import { NewsletterSignup } from "../../components/NewsletterSignup";
24
29
 
25
30
  interface BlogHomePageProps {
@@ -82,7 +87,12 @@ export default function BlogHomePage({
82
87
  {/* Site Header */}
83
88
  {siteName && (
84
89
  <header className="blog-header mb-8 text-center sm:mb-12 lg:mb-16">
85
- <h1 className="blog-title mb-2 bg-linear-to-r from-[var(--blog-accent-hover)] to-[var(--blog-accent)] bg-clip-text text-3xl font-semibold tracking-tight text-balance text-transparent sm:text-4xl lg:text-5xl">
90
+ <h1
91
+ className={cn(
92
+ "blog-title mb-2 text-3xl font-light tracking-tight text-balance sm:text-4xl lg:text-5xl",
93
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
94
+ )}
95
+ >
86
96
  {siteName}
87
97
  </h1>
88
98
  {siteTagline && (
@@ -90,6 +100,30 @@ export default function BlogHomePage({
90
100
  {siteTagline}
91
101
  </p>
92
102
  )}
103
+ <nav
104
+ aria-label="Browse blog"
105
+ className="mt-5 flex flex-wrap items-center justify-center gap-2 text-sm"
106
+ >
107
+ <Link
108
+ href={normalizedBasePath}
109
+ aria-current="page"
110
+ className="rounded-full border border-[var(--blog-accent-border)] bg-[var(--blog-accent-bg)] px-3 py-1.5 font-medium text-[var(--blog-text-accent)] transition-colors hover:border-[var(--blog-text-accent)] hover:text-[var(--blog-text-accent-hover)] hover:no-underline"
111
+ >
112
+ All articles
113
+ </Link>
114
+ <Link
115
+ href={buildBlogPath(normalizedBasePath, "topics")}
116
+ className="text-muted-foreground rounded-full border border-transparent px-3 py-1.5 font-medium transition-colors hover:border-[var(--blog-accent-border)] hover:text-[var(--blog-accent)] hover:no-underline"
117
+ >
118
+ Topics
119
+ </Link>
120
+ <Link
121
+ href={buildBlogPath(normalizedBasePath, "authors")}
122
+ className="text-muted-foreground rounded-full border border-transparent px-3 py-1.5 font-medium transition-colors hover:border-[var(--blog-accent-border)] hover:text-[var(--blog-accent)] hover:no-underline"
123
+ >
124
+ Authors
125
+ </Link>
126
+ </nav>
93
127
  </header>
94
128
  )}
95
129
 
@@ -248,12 +282,11 @@ function ArticlePreview({
248
282
  {(article.topics ?? []).length > 0 && (
249
283
  <div className="flex flex-wrap gap-2">
250
284
  {(article.topics ?? []).map((topic) => (
251
- <span
285
+ <TopicChip
252
286
  key={topic.id}
253
- className="rounded-full border border-[var(--blog-accent-border)] px-2.5 py-1 text-xs font-medium text-[var(--blog-text-accent)]"
254
- >
255
- {topic.name}
256
- </span>
287
+ topic={topic}
288
+ basePath={basePath}
289
+ />
257
290
  ))}
258
291
  </div>
259
292
  )}
@@ -301,12 +334,11 @@ function ArticlePreview({
301
334
  {(article.topics ?? []).length > 0 && (
302
335
  <div className="flex flex-wrap gap-2">
303
336
  {(article.topics ?? []).map((topic) => (
304
- <span
337
+ <TopicChip
305
338
  key={topic.id}
306
- className="rounded-full border border-[var(--blog-accent-border)] px-2.5 py-1 text-xs font-medium text-[var(--blog-text-accent)]"
307
- >
308
- {topic.name}
309
- </span>
339
+ topic={topic}
340
+ basePath={basePath}
341
+ />
310
342
  ))}
311
343
  </div>
312
344
  )}
@@ -335,6 +367,28 @@ function ArticlePreview({
335
367
  );
336
368
  }
337
369
 
370
+ function TopicChip({
371
+ topic,
372
+ basePath,
373
+ }: {
374
+ topic: PublishedTopic;
375
+ basePath: string;
376
+ }) {
377
+ const className =
378
+ "rounded-full border border-[var(--blog-accent-border)] px-2.5 py-1 text-xs font-medium text-[var(--blog-text-accent)] transition-colors hover:border-[var(--blog-text-accent)] hover:text-[var(--blog-text-accent-hover)]";
379
+ const slug = topic.slug.trim();
380
+
381
+ if (!slug) {
382
+ return <span className={className}>{topic.name}</span>;
383
+ }
384
+
385
+ return (
386
+ <Link href={buildBlogPath(basePath, `topics/${slug}`)} className={className}>
387
+ {topic.name}
388
+ </Link>
389
+ );
390
+ }
391
+
338
392
  function AuthorLink({
339
393
  author,
340
394
  basePath,
@@ -77,6 +77,9 @@ describe("BlogAuthorsIndexPage", () => {
77
77
  );
78
78
 
79
79
  expect(html).toContain("dypt authors");
80
+ expect(html).toContain("bg-linear-to-br");
81
+ expect(html).toContain("via-[var(--blog-text-accent)]");
82
+ expect(html).toContain("to-fuchsia-200");
80
83
  expect(html).toContain("Jane Doe");
81
84
  expect(html).toContain("Alex Stone");
82
85
  expect(html).toContain("/new-blog/authors/jane-doe");
@@ -4,7 +4,10 @@ import Link from "next/link";
4
4
 
5
5
  import type { PublishedArticle, PublishedAuthor } from "../types/contracts";
6
6
  import type { BlogTheme } from "../lib/article-ui";
7
- import { getBlogThemeStyle } from "../lib/article-ui";
7
+ import {
8
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
9
+ getBlogThemeStyle,
10
+ } from "../lib/article-ui";
8
11
  import { getAuthorsFromArticles } from "../lib/authors";
9
12
  import { buildAuthorPath, normalizeBlogBasePath } from "../lib/urls";
10
13
  import { cn } from "../lib/utils";
@@ -44,7 +47,12 @@ export function BlogAuthorsIndexPage({
44
47
  >
45
48
  Back to all articles
46
49
  </Link>
47
- <h1 className="text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
50
+ <h1
51
+ className={cn(
52
+ "text-4xl font-light tracking-tight text-balance sm:text-5xl",
53
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
54
+ )}
55
+ >
48
56
  {title}
49
57
  </h1>
50
58
  <p className="text-muted-foreground mx-auto max-w-2xl text-lg text-pretty">
@@ -0,0 +1,79 @@
1
+ import React from "react";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import {
6
+ BlogTopicArchivePage,
7
+ BlogTopicsIndexPage,
8
+ } from "./TopicArchivePages";
9
+ import type { PublishedArticle, PublishedSite } from "../types/contracts";
10
+
11
+ const site: PublishedSite = {
12
+ id: "site-1",
13
+ name: "Test Blog",
14
+ tagline: "Testing topic pages",
15
+ description: "A test blog",
16
+ domain: "example.com",
17
+ toneAdjectives: [],
18
+ locale: "en",
19
+ };
20
+
21
+ const article: PublishedArticle = {
22
+ id: "article-1",
23
+ slug: "useful-routines",
24
+ title: "Useful routines",
25
+ excerpt: "A practical guide to keeping routines useful.",
26
+ content: "# Article",
27
+ readingSeconds: 180,
28
+ wordCount: 500,
29
+ publishedAt: "2026-05-01T10:00:00.000Z",
30
+ updatedAt: "2026-05-01T10:00:00.000Z",
31
+ author: null,
32
+ site,
33
+ topics: [
34
+ {
35
+ id: "topic-1",
36
+ name: "Recurring work and routines",
37
+ slug: "recurring-work-and-routines",
38
+ description: "Systems for routines that repeat.",
39
+ isPrimary: true,
40
+ },
41
+ ],
42
+ meta: {
43
+ keywords: ["test"],
44
+ canonicalUrl: "https://example.com/blog/useful-routines",
45
+ },
46
+ version: 1,
47
+ };
48
+
49
+ describe("Blog topic archive pages", () => {
50
+ it("uses the article title gradient on the all topics heading", () => {
51
+ const html = renderToStaticMarkup(
52
+ React.createElement(BlogTopicsIndexPage, {
53
+ articles: [article],
54
+ basePath: "/new-blog",
55
+ title: "dypt topics",
56
+ }),
57
+ );
58
+
59
+ expect(html).toContain("dypt topics");
60
+ expect(html).toContain("bg-linear-to-br");
61
+ expect(html).toContain("via-[var(--blog-text-accent)]");
62
+ expect(html).toContain("to-fuchsia-200");
63
+ });
64
+
65
+ it("uses the article title gradient on a topic archive heading", () => {
66
+ const html = renderToStaticMarkup(
67
+ React.createElement(BlogTopicArchivePage, {
68
+ articles: [article],
69
+ topicSlug: "recurring-work-and-routines",
70
+ basePath: "/new-blog",
71
+ }),
72
+ );
73
+
74
+ expect(html).toContain("Recurring work and routines");
75
+ expect(html).toContain("bg-linear-to-br");
76
+ expect(html).toContain("via-[var(--blog-text-accent)]");
77
+ expect(html).toContain("to-fuchsia-200");
78
+ });
79
+ });
@@ -5,7 +5,10 @@ import { formatDistanceToNow } from "date-fns";
5
5
 
6
6
  import type { PublishedArticle, PublishedTopic } from "../types/contracts";
7
7
  import type { BlogTheme } from "../lib/article-ui";
8
- import { getBlogThemeStyle } from "../lib/article-ui";
8
+ import {
9
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
10
+ getBlogThemeStyle,
11
+ } from "../lib/article-ui";
9
12
  import { buildBlogPath, normalizeBlogBasePath } from "../lib/urls";
10
13
  import { cn } from "../lib/utils";
11
14
  import { getAllTopics, getTopicArchive } from "../lib/topics";
@@ -53,7 +56,12 @@ export function BlogTopicsIndexPage({
53
56
  >
54
57
  Back to all articles
55
58
  </Link>
56
- <h1 className="text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
59
+ <h1
60
+ className={cn(
61
+ "text-4xl font-light tracking-tight text-balance sm:text-5xl",
62
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
63
+ )}
64
+ >
57
65
  {title}
58
66
  </h1>
59
67
  <p className="text-muted-foreground mx-auto max-w-2xl text-lg text-pretty">
@@ -125,7 +133,12 @@ export function BlogTopicArchivePage({
125
133
  >
126
134
  Back to all topics
127
135
  </Link>
128
- <h1 className="text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
136
+ <h1
137
+ className={cn(
138
+ "text-4xl font-light tracking-tight text-balance sm:text-5xl",
139
+ BLOG_PAGE_TITLE_GRADIENT_CLASS,
140
+ )}
141
+ >
129
142
  {archive?.topic.name ?? "Topic not found"}
130
143
  </h1>
131
144
  {archive?.topic.description && (
package/dist/index.d.mts CHANGED
@@ -464,6 +464,7 @@ interface AuthResult {
464
464
  /** Additional user/context information */
465
465
  context?: Record<string, unknown>;
466
466
  }
467
+ declare function createTokenFingerprint(token: string): string;
467
468
  /**
468
469
  * Default token extraction from Authorization header
469
470
  * Supports both "Bearer <token>" and "Token <token>" formats
@@ -2012,4 +2013,4 @@ declare function OPTIONS(request: NextRequest): Promise<NextResponse>;
2012
2013
 
2013
2014
  declare const PACKAGE_VERSION: string;
2014
2015
 
2015
- export { ApiKeyAuth, type AuthConfig, type AuthResult, type AuthorArchive, type AuthorArchiveOptions, ArticlePage as BlogArticlePage, AuthorPage as BlogAuthorPage, BlogAuthorsIndexPage, BlogHomePage, type BlogPlatformConfig, type BlogPlatformStorage, type BlogPostingSchema, type BlogSitemapConfig, type BlogTheme, BlogTopicArchivePage, BlogTopicsIndexPage, type BreadcrumbListSchema, Button, type CanonicalUrlOptions, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DrizzleStorage, EnvAuth, type GeneratedMetadata, InMemoryStorage, type LinkTag, type MetaTag, type MetadataOptions, NEWSLETTER_SUCCESS_MESSAGE, NewsletterSignup, type NewsletterSignupProps, type NextjsSitemapEntry, NoAuth, type OrganizationSchema, PACKAGE_VERSION, Pagination$1 as Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, type PersonSchema, PublishResult, PublishedArticle, PublishedAuthor, PublishedNewsletterSettings, PublishedSite, PublishedTopic, type RateLimitConfig, type RateLimitResult, Pagination as ShadcnPagination, type SitemapChangeFreq, type SitemapData, type SitemapEntry, type SitemapOptions, StructuredDataError, type StructuredDataOptions, type TopicArchive, UninitializedAuth, type WebSiteSchema, autoConfigureRateLimit, bpArticles, bpOperations, buildAuthorPath, buildBlogPath, buttonVariants, checkRateLimit, cn, configureRateLimit, createBlogSitemapRoute, createDynamicBlogSitemap, createRateLimitErrorResponse, createStructuredDataScript, enforceRateLimit, extractKeywordsFromArticle, extractTableOfContents, extractTokenFromHeader, formatArticleDateMeta, generateArticleMetadata, generateArticleStructuredData, generateAuthorPageMetadata, generateAuthorStructuredData, generateBlogHomeMetadata, generateBlogHomeStructuredData, generateBlogSitemapEntries, generateBreadcrumbStructuredData, generateCanonicalUrl, generateCanonicalUrlFromSlug, generateCompleteArticleStructuredData, generateOrganizationStructuredData, generatePaginationMetadata, generateSiteMetadata, generateSitemap, generateSitemapIndex, generateWebSiteStructuredData, getAllTopics, getArticlePrimaryTopic, getAuthConfig, getAuthorArchive, getAuthorsFromArticles, getBaseUrl, getBlogThemeStyle, getDrizzleAdapterSql, getPathPrefixFromBlogUrl, getRateLimitHeaders, getStorage, getTopicArchive, initBlogPlatform, initBlogPlatformDev, initBlogPlatformOnce, initBlogPlatformProd, initEnvAuth, isValidCanonicalUrl, loadAuthorArchive, metadataToHtml, normalizeBlogBasePath, 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 };
2016
+ export { ApiKeyAuth, type AuthConfig, type AuthResult, type AuthorArchive, type AuthorArchiveOptions, ArticlePage as BlogArticlePage, AuthorPage as BlogAuthorPage, BlogAuthorsIndexPage, BlogHomePage, type BlogPlatformConfig, type BlogPlatformStorage, type BlogPostingSchema, type BlogSitemapConfig, type BlogTheme, BlogTopicArchivePage, BlogTopicsIndexPage, type BreadcrumbListSchema, Button, type CanonicalUrlOptions, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DrizzleStorage, EnvAuth, type GeneratedMetadata, InMemoryStorage, type LinkTag, type MetaTag, type MetadataOptions, NEWSLETTER_SUCCESS_MESSAGE, NewsletterSignup, type NewsletterSignupProps, type NextjsSitemapEntry, NoAuth, type OrganizationSchema, PACKAGE_VERSION, Pagination$1 as Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, type PersonSchema, PublishResult, PublishedArticle, PublishedAuthor, PublishedNewsletterSettings, PublishedSite, PublishedTopic, type RateLimitConfig, type RateLimitResult, Pagination as ShadcnPagination, type SitemapChangeFreq, type SitemapData, type SitemapEntry, type SitemapOptions, StructuredDataError, type StructuredDataOptions, type TopicArchive, UninitializedAuth, type WebSiteSchema, autoConfigureRateLimit, bpArticles, bpOperations, buildAuthorPath, buildBlogPath, buttonVariants, checkRateLimit, cn, configureRateLimit, createBlogSitemapRoute, createDynamicBlogSitemap, createRateLimitErrorResponse, createStructuredDataScript, createTokenFingerprint, enforceRateLimit, extractKeywordsFromArticle, extractTableOfContents, extractTokenFromHeader, formatArticleDateMeta, generateArticleMetadata, generateArticleStructuredData, generateAuthorPageMetadata, generateAuthorStructuredData, generateBlogHomeMetadata, generateBlogHomeStructuredData, generateBlogSitemapEntries, generateBreadcrumbStructuredData, generateCanonicalUrl, generateCanonicalUrlFromSlug, generateCompleteArticleStructuredData, generateOrganizationStructuredData, generatePaginationMetadata, generateSiteMetadata, generateSitemap, generateSitemapIndex, generateWebSiteStructuredData, getAllTopics, getArticlePrimaryTopic, getAuthConfig, getAuthorArchive, getAuthorsFromArticles, getBaseUrl, getBlogThemeStyle, getDrizzleAdapterSql, getPathPrefixFromBlogUrl, getRateLimitHeaders, getStorage, getTopicArchive, initBlogPlatform, initBlogPlatformDev, initBlogPlatformOnce, initBlogPlatformProd, initEnvAuth, isValidCanonicalUrl, loadAuthorArchive, metadataToHtml, normalizeBlogBasePath, 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 };
package/dist/index.d.ts CHANGED
@@ -464,6 +464,7 @@ interface AuthResult {
464
464
  /** Additional user/context information */
465
465
  context?: Record<string, unknown>;
466
466
  }
467
+ declare function createTokenFingerprint(token: string): string;
467
468
  /**
468
469
  * Default token extraction from Authorization header
469
470
  * Supports both "Bearer <token>" and "Token <token>" formats
@@ -2012,4 +2013,4 @@ declare function OPTIONS(request: NextRequest): Promise<NextResponse>;
2012
2013
 
2013
2014
  declare const PACKAGE_VERSION: string;
2014
2015
 
2015
- export { ApiKeyAuth, type AuthConfig, type AuthResult, type AuthorArchive, type AuthorArchiveOptions, ArticlePage as BlogArticlePage, AuthorPage as BlogAuthorPage, BlogAuthorsIndexPage, BlogHomePage, type BlogPlatformConfig, type BlogPlatformStorage, type BlogPostingSchema, type BlogSitemapConfig, type BlogTheme, BlogTopicArchivePage, BlogTopicsIndexPage, type BreadcrumbListSchema, Button, type CanonicalUrlOptions, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DrizzleStorage, EnvAuth, type GeneratedMetadata, InMemoryStorage, type LinkTag, type MetaTag, type MetadataOptions, NEWSLETTER_SUCCESS_MESSAGE, NewsletterSignup, type NewsletterSignupProps, type NextjsSitemapEntry, NoAuth, type OrganizationSchema, PACKAGE_VERSION, Pagination$1 as Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, type PersonSchema, PublishResult, PublishedArticle, PublishedAuthor, PublishedNewsletterSettings, PublishedSite, PublishedTopic, type RateLimitConfig, type RateLimitResult, Pagination as ShadcnPagination, type SitemapChangeFreq, type SitemapData, type SitemapEntry, type SitemapOptions, StructuredDataError, type StructuredDataOptions, type TopicArchive, UninitializedAuth, type WebSiteSchema, autoConfigureRateLimit, bpArticles, bpOperations, buildAuthorPath, buildBlogPath, buttonVariants, checkRateLimit, cn, configureRateLimit, createBlogSitemapRoute, createDynamicBlogSitemap, createRateLimitErrorResponse, createStructuredDataScript, enforceRateLimit, extractKeywordsFromArticle, extractTableOfContents, extractTokenFromHeader, formatArticleDateMeta, generateArticleMetadata, generateArticleStructuredData, generateAuthorPageMetadata, generateAuthorStructuredData, generateBlogHomeMetadata, generateBlogHomeStructuredData, generateBlogSitemapEntries, generateBreadcrumbStructuredData, generateCanonicalUrl, generateCanonicalUrlFromSlug, generateCompleteArticleStructuredData, generateOrganizationStructuredData, generatePaginationMetadata, generateSiteMetadata, generateSitemap, generateSitemapIndex, generateWebSiteStructuredData, getAllTopics, getArticlePrimaryTopic, getAuthConfig, getAuthorArchive, getAuthorsFromArticles, getBaseUrl, getBlogThemeStyle, getDrizzleAdapterSql, getPathPrefixFromBlogUrl, getRateLimitHeaders, getStorage, getTopicArchive, initBlogPlatform, initBlogPlatformDev, initBlogPlatformOnce, initBlogPlatformProd, initEnvAuth, isValidCanonicalUrl, loadAuthorArchive, metadataToHtml, normalizeBlogBasePath, 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 };
2016
+ export { ApiKeyAuth, type AuthConfig, type AuthResult, type AuthorArchive, type AuthorArchiveOptions, ArticlePage as BlogArticlePage, AuthorPage as BlogAuthorPage, BlogAuthorsIndexPage, BlogHomePage, type BlogPlatformConfig, type BlogPlatformStorage, type BlogPostingSchema, type BlogSitemapConfig, type BlogTheme, BlogTopicArchivePage, BlogTopicsIndexPage, type BreadcrumbListSchema, Button, type CanonicalUrlOptions, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DrizzleStorage, EnvAuth, type GeneratedMetadata, InMemoryStorage, type LinkTag, type MetaTag, type MetadataOptions, NEWSLETTER_SUCCESS_MESSAGE, NewsletterSignup, type NewsletterSignupProps, type NextjsSitemapEntry, NoAuth, type OrganizationSchema, PACKAGE_VERSION, Pagination$1 as Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, type PersonSchema, PublishResult, PublishedArticle, PublishedAuthor, PublishedNewsletterSettings, PublishedSite, PublishedTopic, type RateLimitConfig, type RateLimitResult, Pagination as ShadcnPagination, type SitemapChangeFreq, type SitemapData, type SitemapEntry, type SitemapOptions, StructuredDataError, type StructuredDataOptions, type TopicArchive, UninitializedAuth, type WebSiteSchema, autoConfigureRateLimit, bpArticles, bpOperations, buildAuthorPath, buildBlogPath, buttonVariants, checkRateLimit, cn, configureRateLimit, createBlogSitemapRoute, createDynamicBlogSitemap, createRateLimitErrorResponse, createStructuredDataScript, createTokenFingerprint, enforceRateLimit, extractKeywordsFromArticle, extractTableOfContents, extractTokenFromHeader, formatArticleDateMeta, generateArticleMetadata, generateArticleStructuredData, generateAuthorPageMetadata, generateAuthorStructuredData, generateBlogHomeMetadata, generateBlogHomeStructuredData, generateBlogSitemapEntries, generateBreadcrumbStructuredData, generateCanonicalUrl, generateCanonicalUrlFromSlug, generateCompleteArticleStructuredData, generateOrganizationStructuredData, generatePaginationMetadata, generateSiteMetadata, generateSitemap, generateSitemapIndex, generateWebSiteStructuredData, getAllTopics, getArticlePrimaryTopic, getAuthConfig, getAuthorArchive, getAuthorsFromArticles, getBaseUrl, getBlogThemeStyle, getDrizzleAdapterSql, getPathPrefixFromBlogUrl, getRateLimitHeaders, getStorage, getTopicArchive, initBlogPlatform, initBlogPlatformDev, initBlogPlatformOnce, initBlogPlatformProd, initEnvAuth, isValidCanonicalUrl, loadAuthorArchive, metadataToHtml, normalizeBlogBasePath, 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 };