@xexr/blog-platform 1.0.10 → 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.
- package/README.md +1 -1
- package/dist/app/api/blog/publish/route.ts +8 -4
- package/dist/app/api/blog/slugs/[slug]/route.ts +8 -5
- package/dist/app/api/blog/unpublish/route.ts +8 -4
- package/dist/app/api/blog/version/route.ts +8 -4
- package/dist/app/blog/[slug]/page.d.ts +7 -2
- package/dist/app/blog/[slug]/page.tsx +28 -2
- package/dist/app/blog/authors/[slug]/page.test.ts +3 -0
- package/dist/app/blog/authors/[slug]/page.tsx +11 -2
- package/dist/app/blog/page.d.ts +59 -0
- package/dist/app/blog/page.test.ts +128 -0
- package/dist/app/blog/page.tsx +86 -12
- package/dist/components/AuthorPages.test.ts +3 -0
- package/dist/components/AuthorPages.tsx +10 -2
- package/dist/components/NewsletterSignup.tsx +279 -0
- package/dist/components/TopicArchivePages.test.ts +79 -0
- package/dist/components/TopicArchivePages.tsx +16 -3
- package/dist/index.d.mts +62 -53
- package/dist/index.d.ts +62 -53
- package/dist/index.js +10 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -10
- package/dist/index.mjs.map +1 -1
- package/dist/lib/article-ui.ts +3 -0
- package/dist/lib/auth.ts +6 -2
- package/dist/types/contracts.ts +26 -0
- package/dist/types/index.d.mts +20 -1
- package/dist/types/index.d.ts +20 -1
- package/dist/types/index.ts +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useSyncExternalStore, type FormEvent } from "react";
|
|
4
|
+
import { ArrowUpRight, Check } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import type { PublishedNewsletterSettings } from "../types/contracts";
|
|
8
|
+
|
|
9
|
+
export const NEWSLETTER_SUCCESS_MESSAGE =
|
|
10
|
+
"Thanks, you're on the list. We'll confirm before sending updates.";
|
|
11
|
+
|
|
12
|
+
export interface NewsletterSignupProps {
|
|
13
|
+
siteId: string;
|
|
14
|
+
settings: PublishedNewsletterSettings;
|
|
15
|
+
sourceSurface: "blog_home" | "article";
|
|
16
|
+
sourceArticleSlug?: string;
|
|
17
|
+
sourceUrl?: string;
|
|
18
|
+
previewMode?: boolean;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function NewsletterSignup({
|
|
23
|
+
siteId,
|
|
24
|
+
settings,
|
|
25
|
+
sourceSurface,
|
|
26
|
+
sourceArticleSlug,
|
|
27
|
+
sourceUrl,
|
|
28
|
+
previewMode = false,
|
|
29
|
+
className,
|
|
30
|
+
}: NewsletterSignupProps) {
|
|
31
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
32
|
+
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
const [hasSubmitted, setHasSubmitted] = useState(false);
|
|
34
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
35
|
+
const subscriptionKey = getSubscriptionStorageKey(siteId);
|
|
36
|
+
const hasStoredSubscribed = useSyncExternalStore(
|
|
37
|
+
subscribeToStoredSubscription,
|
|
38
|
+
() => hasStoredSubscription(subscriptionKey),
|
|
39
|
+
() => false,
|
|
40
|
+
);
|
|
41
|
+
const hasSubscribed = hasSubmitted || (!previewMode && hasStoredSubscribed);
|
|
42
|
+
|
|
43
|
+
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
setError(null);
|
|
46
|
+
setMessage(null);
|
|
47
|
+
|
|
48
|
+
const formData = new FormData(event.currentTarget);
|
|
49
|
+
const honeypot = getFormString(formData, "company");
|
|
50
|
+
|
|
51
|
+
if (previewMode) {
|
|
52
|
+
setMessage(NEWSLETTER_SUCCESS_MESSAGE);
|
|
53
|
+
setHasSubmitted(true);
|
|
54
|
+
event.currentTarget.reset();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setIsSubmitting(true);
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(
|
|
61
|
+
`${settings.captureBaseUrl.replace(/\/$/, "")}/api/newsletter/subscribe`,
|
|
62
|
+
{
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
siteId: Number(siteId),
|
|
69
|
+
publicSubscribeToken: settings.publicSubscribeToken,
|
|
70
|
+
email: getFormString(formData, "email"),
|
|
71
|
+
name: getOptionalFormString(formData, "name"),
|
|
72
|
+
sourceSurface,
|
|
73
|
+
sourceUrl: resolveSourceUrl(sourceUrl),
|
|
74
|
+
sourceArticleSlug: sourceArticleSlug ?? null,
|
|
75
|
+
honeypot,
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error("Newsletter signup failed.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result: unknown = await response.json().catch(() => null);
|
|
85
|
+
const successMessage =
|
|
86
|
+
getNewsletterResponseMessage(result) ?? NEWSLETTER_SUCCESS_MESSAGE;
|
|
87
|
+
|
|
88
|
+
storeSubscription(subscriptionKey);
|
|
89
|
+
setMessage(successMessage);
|
|
90
|
+
setHasSubmitted(true);
|
|
91
|
+
event.currentTarget.reset();
|
|
92
|
+
} catch {
|
|
93
|
+
setMessage(null);
|
|
94
|
+
setError("Newsletter signup failed. Please try again.");
|
|
95
|
+
} finally {
|
|
96
|
+
setIsSubmitting(false);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<section
|
|
102
|
+
className={cn(
|
|
103
|
+
"border-y border-[var(--blog-accent-border)] py-5 sm:py-6",
|
|
104
|
+
className,
|
|
105
|
+
)}
|
|
106
|
+
aria-labelledby={`newsletter-${sourceSurface}-title`}
|
|
107
|
+
>
|
|
108
|
+
<div className="grid gap-4 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.4fr)] lg:items-end">
|
|
109
|
+
<div className="max-w-xl space-y-1">
|
|
110
|
+
<h2
|
|
111
|
+
id={`newsletter-${sourceSurface}-title`}
|
|
112
|
+
className="text-base leading-6 font-semibold"
|
|
113
|
+
>
|
|
114
|
+
{settings.title}
|
|
115
|
+
</h2>
|
|
116
|
+
<p className="text-muted-foreground text-sm leading-6">
|
|
117
|
+
{settings.description}
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{hasSubscribed ? (
|
|
122
|
+
<div
|
|
123
|
+
className="flex items-start gap-3 text-sm leading-6 text-emerald-500"
|
|
124
|
+
role="status"
|
|
125
|
+
aria-live="polite"
|
|
126
|
+
>
|
|
127
|
+
<span className="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-emerald-500/15">
|
|
128
|
+
<Check className="size-3.5" aria-hidden="true" />
|
|
129
|
+
</span>
|
|
130
|
+
<p>{message ?? NEWSLETTER_SUCCESS_MESSAGE}</p>
|
|
131
|
+
</div>
|
|
132
|
+
) : (
|
|
133
|
+
<form onSubmit={handleSubmit} className="space-y-3">
|
|
134
|
+
<div className="grid gap-2 sm:grid-cols-[minmax(0,0.8fr)_minmax(0,1fr)_auto]">
|
|
135
|
+
<label
|
|
136
|
+
className="sr-only"
|
|
137
|
+
htmlFor={`newsletter-name-${sourceSurface}`}
|
|
138
|
+
>
|
|
139
|
+
Name
|
|
140
|
+
</label>
|
|
141
|
+
<input
|
|
142
|
+
id={`newsletter-name-${sourceSurface}`}
|
|
143
|
+
name="name"
|
|
144
|
+
type="text"
|
|
145
|
+
autoComplete="name"
|
|
146
|
+
placeholder="Name"
|
|
147
|
+
className="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring min-h-10 rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
<label
|
|
151
|
+
className="sr-only"
|
|
152
|
+
htmlFor={`newsletter-email-${sourceSurface}`}
|
|
153
|
+
>
|
|
154
|
+
Email
|
|
155
|
+
</label>
|
|
156
|
+
<input
|
|
157
|
+
id={`newsletter-email-${sourceSurface}`}
|
|
158
|
+
name="email"
|
|
159
|
+
type="email"
|
|
160
|
+
autoComplete="email"
|
|
161
|
+
placeholder="Email address"
|
|
162
|
+
required
|
|
163
|
+
className="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring min-h-10 rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
164
|
+
/>
|
|
165
|
+
|
|
166
|
+
<input
|
|
167
|
+
type="text"
|
|
168
|
+
name="company"
|
|
169
|
+
tabIndex={-1}
|
|
170
|
+
autoComplete="off"
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
className="hidden"
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
<button
|
|
176
|
+
type="submit"
|
|
177
|
+
disabled={isSubmitting}
|
|
178
|
+
className="inline-flex min-h-10 w-full shrink-0 items-center justify-center gap-2 rounded-full bg-[var(--blog-accent)] px-5 py-2 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
|
|
179
|
+
>
|
|
180
|
+
{isSubmitting ? "Submitting..." : settings.buttonLabel}
|
|
181
|
+
<ArrowUpRight className="size-4" aria-hidden="true" />
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
186
|
+
</form>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
</section>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveSourceUrl(sourceUrl: string | undefined): string | null {
|
|
194
|
+
if (sourceUrl?.startsWith("http://") || sourceUrl?.startsWith("https://")) {
|
|
195
|
+
return sourceUrl;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (typeof window !== "undefined") {
|
|
199
|
+
return window.location.href;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getFormString(formData: FormData, key: string): string {
|
|
206
|
+
const value = formData.get(key);
|
|
207
|
+
return typeof value === "string" ? value : "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getOptionalFormString(formData: FormData, key: string): string | null {
|
|
211
|
+
const value = getFormString(formData, key).trim();
|
|
212
|
+
if (value.length === 0) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getSubscriptionStorageKey(siteId: string): string {
|
|
220
|
+
return `bloggy_newsletter_subscribed_${siteId}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function hasStoredSubscription(key: string): boolean {
|
|
224
|
+
if (typeof window === "undefined") {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
if (window.localStorage.getItem(key) === "true") {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// Ignore storage failures; the subscription API remains the source of truth.
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (typeof document === "undefined") {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return document.cookie
|
|
241
|
+
.split(";")
|
|
242
|
+
.some((cookie) => cookie.trim().startsWith(`${key}=true`));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function storeSubscription(key: string): void {
|
|
246
|
+
if (typeof window !== "undefined") {
|
|
247
|
+
try {
|
|
248
|
+
window.localStorage.setItem(key, "true");
|
|
249
|
+
} catch {
|
|
250
|
+
// Ignore storage failures; users can still submit normally.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (typeof document !== "undefined") {
|
|
255
|
+
document.cookie = `${key}=true; Max-Age=31536000; Path=/; SameSite=Lax`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function subscribeToStoredSubscription(callback: () => void): () => void {
|
|
260
|
+
if (typeof window === "undefined") {
|
|
261
|
+
return () => undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
window.addEventListener("storage", callback);
|
|
265
|
+
return () => window.removeEventListener("storage", callback);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getNewsletterResponseMessage(value: unknown): string | null {
|
|
269
|
+
if (
|
|
270
|
+
value &&
|
|
271
|
+
typeof value === "object" &&
|
|
272
|
+
"message" in value &&
|
|
273
|
+
typeof value.message === "string"
|
|
274
|
+
) {
|
|
275
|
+
return value.message;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { PublishedArticle, PublishedSite, PublishedAuthor, PublishResult, PublishedTopic } from './types/index.mjs';
|
|
1
|
+
import { PublishedArticle, PublishedSite, PublishedNewsletterSettings, PublishedAuthor, PublishResult, PublishedTopic } from './types/index.mjs';
|
|
2
2
|
export { PublishPayload, PublishedAuthorSocialLinks } from './types/index.mjs';
|
|
3
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
3
|
import * as React from 'react';
|
|
5
4
|
import { CSSProperties, ReactElement } from 'react';
|
|
5
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
6
6
|
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
7
7
|
import { VariantProps } from 'class-variance-authority';
|
|
8
8
|
import { ClassValue } from 'clsx';
|
|
@@ -42,61 +42,55 @@ declare function formatArticleDateMeta(publishedAt: string, updatedAt: string):
|
|
|
42
42
|
declare function extractTableOfContents(markdown: string): TocItem[];
|
|
43
43
|
|
|
44
44
|
interface BlogHomePageProps {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
};
|
|
61
|
-
/** Optional theme values for host-site brand integration. */
|
|
62
|
-
theme?: BlogTheme;
|
|
45
|
+
articles: PublishedArticle[];
|
|
46
|
+
siteName?: string;
|
|
47
|
+
siteTagline?: string;
|
|
48
|
+
className?: string;
|
|
49
|
+
site?: PublishedSite;
|
|
50
|
+
includeStructuredData?: boolean;
|
|
51
|
+
basePath?: string;
|
|
52
|
+
pagination?: {
|
|
53
|
+
currentPage: number;
|
|
54
|
+
totalPages: number;
|
|
55
|
+
baseUrl?: string;
|
|
56
|
+
};
|
|
57
|
+
theme?: BlogTheme;
|
|
58
|
+
newsletter?: PublishedNewsletterSettings;
|
|
59
|
+
newsletterPreviewMode?: boolean;
|
|
63
60
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Generate metadata for the blog home page for SEO optimization.
|
|
74
|
-
* This function should be used in the Next.js metadata API.
|
|
75
|
-
*/
|
|
76
|
-
declare function generateBlogHomeMetadata(siteName?: string, siteDescription?: string, siteUrl?: string, pagination?: {
|
|
61
|
+
|
|
62
|
+
declare function BlogHomePage(props: BlogHomePageProps): ReactElement;
|
|
63
|
+
|
|
64
|
+
declare function generateBlogHomeMetadata(
|
|
65
|
+
siteName?: string,
|
|
66
|
+
siteTagline?: string,
|
|
67
|
+
siteUrl?: string,
|
|
68
|
+
pagination?: {
|
|
77
69
|
currentPage: number;
|
|
78
70
|
totalPages: number;
|
|
79
|
-
},
|
|
71
|
+
},
|
|
72
|
+
basePath?: string,
|
|
73
|
+
): {
|
|
74
|
+
title: string;
|
|
75
|
+
description: string;
|
|
76
|
+
openGraph: {
|
|
80
77
|
title: string;
|
|
81
78
|
description: string;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
next?: string | undefined;
|
|
98
|
-
prev?: string | undefined;
|
|
99
|
-
};
|
|
79
|
+
type: string;
|
|
80
|
+
url?: string;
|
|
81
|
+
};
|
|
82
|
+
twitter: {
|
|
83
|
+
card: string;
|
|
84
|
+
title: string;
|
|
85
|
+
description: string;
|
|
86
|
+
};
|
|
87
|
+
alternates: {
|
|
88
|
+
canonical?: string;
|
|
89
|
+
};
|
|
90
|
+
other: {
|
|
91
|
+
prev?: string;
|
|
92
|
+
next?: string;
|
|
93
|
+
};
|
|
100
94
|
};
|
|
101
95
|
|
|
102
96
|
interface ArticlePageProps {
|
|
@@ -128,6 +122,8 @@ interface ArticlePageProps {
|
|
|
128
122
|
href: string;
|
|
129
123
|
label: string;
|
|
130
124
|
};
|
|
125
|
+
newsletter?: PublishedNewsletterSettings;
|
|
126
|
+
newsletterPreviewMode?: boolean;
|
|
131
127
|
relatedArticles?: PublishedArticle[];
|
|
132
128
|
showShareLinks?: boolean;
|
|
133
129
|
}
|
|
@@ -419,6 +415,18 @@ interface BlogAuthorsIndexPageProps {
|
|
|
419
415
|
}
|
|
420
416
|
declare function BlogAuthorsIndexPage({ articles, basePath, title, description, className, theme, }: BlogAuthorsIndexPageProps): react_jsx_runtime.JSX.Element;
|
|
421
417
|
|
|
418
|
+
declare const NEWSLETTER_SUCCESS_MESSAGE = "Thanks, you're on the list. We'll confirm before sending updates.";
|
|
419
|
+
interface NewsletterSignupProps {
|
|
420
|
+
siteId: string;
|
|
421
|
+
settings: PublishedNewsletterSettings;
|
|
422
|
+
sourceSurface: "blog_home" | "article";
|
|
423
|
+
sourceArticleSlug?: string;
|
|
424
|
+
sourceUrl?: string;
|
|
425
|
+
previewMode?: boolean;
|
|
426
|
+
className?: string;
|
|
427
|
+
}
|
|
428
|
+
declare function NewsletterSignup({ siteId, settings, sourceSurface, sourceArticleSlug, sourceUrl, previewMode, className, }: NewsletterSignupProps): react_jsx_runtime.JSX.Element;
|
|
429
|
+
|
|
422
430
|
/**
|
|
423
431
|
* Authentication configuration interface
|
|
424
432
|
* Customers can implement this to customize authentication behavior
|
|
@@ -456,6 +464,7 @@ interface AuthResult {
|
|
|
456
464
|
/** Additional user/context information */
|
|
457
465
|
context?: Record<string, unknown>;
|
|
458
466
|
}
|
|
467
|
+
declare function createTokenFingerprint(token: string): string;
|
|
459
468
|
/**
|
|
460
469
|
* Default token extraction from Authorization header
|
|
461
470
|
* Supports both "Bearer <token>" and "Token <token>" formats
|
|
@@ -2004,4 +2013,4 @@ declare function OPTIONS(request: NextRequest): Promise<NextResponse>;
|
|
|
2004
2013
|
|
|
2005
2014
|
declare const PACKAGE_VERSION: string;
|
|
2006
2015
|
|
|
2007
|
-
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, type NextjsSitemapEntry, NoAuth, type OrganizationSchema, PACKAGE_VERSION, Pagination$1 as Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, type PersonSchema, PublishResult, PublishedArticle, PublishedAuthor, 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 };
|