@xexr/blog-platform 1.0.9 → 1.0.11
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/blog/[slug]/page.d.ts +7 -2
- package/dist/app/blog/[slug]/page.tsx +138 -14
- package/dist/app/blog/page.d.ts +59 -0
- package/dist/app/blog/page.test.ts +83 -0
- package/dist/app/blog/page.tsx +20 -0
- package/dist/components/NewsletterSignup.tsx +279 -0
- package/dist/index.d.mts +118 -53
- package/dist/index.d.ts +118 -53
- package/dist/index.js +51 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +51 -11
- package/dist/index.mjs.map +1 -1
- package/dist/lib/storage/drizzle.ts +15 -0
- package/dist/types/contracts.ts +35 -0
- package/dist/types/index.d.mts +26 -1
- package/dist/types/index.d.ts +26 -1
- package/dist/types/index.ts +1 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -237,7 +237,7 @@ export default async function ArticlePage({
|
|
|
237
237
|
}
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
-
> **Note**: The `navigation` prop is optional. You can omit `previousArticle` and `nextArticle` if you don't want article navigation links.
|
|
240
|
+
> **Note**: The `navigation` prop is optional. You can omit `previousArticle` and `nextArticle` if you don't want article navigation links. Bloggy can also publish a site-scoped `newsletter` configuration; pass it through to `BlogHomePage` or `BlogArticlePage` if you are not using the article/site objects returned by Bloggy storage directly. The newsletter form posts back to Bloggy's `/api/newsletter/subscribe` endpoint so subscribers are captured consistently across host apps.
|
|
241
241
|
|
|
242
242
|
**`app/blog/authors/[slug]/page.tsx`** - Author profile pages:
|
|
243
243
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
PublishedArticle,
|
|
4
|
+
PublishedNewsletterSettings,
|
|
5
|
+
} from "../../../types/contracts";
|
|
3
6
|
import type { BlogTheme } from "../../../lib/article-ui";
|
|
4
7
|
|
|
5
|
-
interface ArticlePageProps {
|
|
8
|
+
export interface ArticlePageProps {
|
|
6
9
|
article: PublishedArticle;
|
|
7
10
|
className?: string;
|
|
8
11
|
navigation?: {
|
|
@@ -31,6 +34,8 @@ interface ArticlePageProps {
|
|
|
31
34
|
href: string;
|
|
32
35
|
label: string;
|
|
33
36
|
};
|
|
37
|
+
newsletter?: PublishedNewsletterSettings;
|
|
38
|
+
newsletterPreviewMode?: boolean;
|
|
34
39
|
relatedArticles?: PublishedArticle[];
|
|
35
40
|
showShareLinks?: boolean;
|
|
36
41
|
}
|
|
@@ -5,6 +5,7 @@ import { MDXRemote } from "next-mdx-remote-client/rsc";
|
|
|
5
5
|
import {
|
|
6
6
|
type PublishedArticle,
|
|
7
7
|
type PublishedAuthor,
|
|
8
|
+
type PublishedNewsletterSettings,
|
|
8
9
|
} from "../../../types/contracts";
|
|
9
10
|
import { cn } from "../../../lib/utils";
|
|
10
11
|
import { ArrowLeft, ArrowUpRight, Globe, Mail } from "lucide-react";
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
buildBlogPath,
|
|
26
27
|
normalizeBlogBasePath,
|
|
27
28
|
} from "../../../lib/urls";
|
|
29
|
+
import { NewsletterSignup } from "../../../components/NewsletterSignup";
|
|
28
30
|
import {
|
|
29
31
|
type BlogTheme,
|
|
30
32
|
extractTableOfContents,
|
|
@@ -32,7 +34,7 @@ import {
|
|
|
32
34
|
getBlogThemeStyle,
|
|
33
35
|
} from "../../../lib/article-ui";
|
|
34
36
|
|
|
35
|
-
interface ArticlePageProps {
|
|
37
|
+
export interface ArticlePageProps {
|
|
36
38
|
article: PublishedArticle;
|
|
37
39
|
className?: string;
|
|
38
40
|
/** Optional navigation links */
|
|
@@ -65,6 +67,10 @@ interface ArticlePageProps {
|
|
|
65
67
|
href: string;
|
|
66
68
|
label: string;
|
|
67
69
|
};
|
|
70
|
+
/** Optional Bloggy-owned newsletter capture configuration. */
|
|
71
|
+
newsletter?: PublishedNewsletterSettings;
|
|
72
|
+
/** Render interactive preview success without persisting subscribers. */
|
|
73
|
+
newsletterPreviewMode?: boolean;
|
|
68
74
|
/** Optional related articles supplied by the host site. */
|
|
69
75
|
relatedArticles?: PublishedArticle[];
|
|
70
76
|
/** Whether to render static article share links. */
|
|
@@ -86,6 +92,46 @@ function formatReadingTime(seconds: number): string {
|
|
|
86
92
|
return `${minutes} min read`;
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
function formatCreditUrl(url: string): string {
|
|
96
|
+
try {
|
|
97
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
98
|
+
} catch {
|
|
99
|
+
return url;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function FeaturedImageCredit({
|
|
104
|
+
creditText,
|
|
105
|
+
creditUrl,
|
|
106
|
+
}: {
|
|
107
|
+
creditText?: string;
|
|
108
|
+
creditUrl?: string;
|
|
109
|
+
}) {
|
|
110
|
+
if (!creditText && !creditUrl) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const label = creditText ?? formatCreditUrl(creditUrl ?? "");
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<span>
|
|
118
|
+
Credit:{" "}
|
|
119
|
+
{creditUrl ? (
|
|
120
|
+
<a
|
|
121
|
+
href={creditUrl}
|
|
122
|
+
target="_blank"
|
|
123
|
+
rel="noreferrer"
|
|
124
|
+
className="underline underline-offset-2 transition-colors hover:text-[var(--blog-text-accent)]"
|
|
125
|
+
>
|
|
126
|
+
{label}
|
|
127
|
+
</a>
|
|
128
|
+
) : (
|
|
129
|
+
label
|
|
130
|
+
)}
|
|
131
|
+
</span>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
89
135
|
export default function ArticlePage({
|
|
90
136
|
article,
|
|
91
137
|
className,
|
|
@@ -93,6 +139,8 @@ export default function ArticlePage({
|
|
|
93
139
|
basePath = "/blog",
|
|
94
140
|
theme,
|
|
95
141
|
postArticleCta,
|
|
142
|
+
newsletter,
|
|
143
|
+
newsletterPreviewMode = false,
|
|
96
144
|
relatedArticles = [],
|
|
97
145
|
showShareLinks = true,
|
|
98
146
|
}: ArticlePageProps) {
|
|
@@ -106,9 +154,15 @@ export default function ArticlePage({
|
|
|
106
154
|
const articleUrl =
|
|
107
155
|
article.meta.canonicalUrl ??
|
|
108
156
|
buildBlogPath(normalizedBasePath, article.slug);
|
|
157
|
+
const newsletterSettings = newsletter ?? article.site.newsletter;
|
|
109
158
|
|
|
110
159
|
// Use the featured image URL provided by Bloggy (hosted on Bloggy's domain)
|
|
111
160
|
const imageUrl = article.featuredImage;
|
|
161
|
+
const hasFeaturedImageMeta = Boolean(
|
|
162
|
+
article.featuredImageCaption ??
|
|
163
|
+
article.featuredImageCreditText ??
|
|
164
|
+
article.featuredImageCreditUrl,
|
|
165
|
+
);
|
|
112
166
|
|
|
113
167
|
return (
|
|
114
168
|
<article
|
|
@@ -140,6 +194,45 @@ export default function ArticlePage({
|
|
|
140
194
|
</header>
|
|
141
195
|
)}
|
|
142
196
|
|
|
197
|
+
<style>{`
|
|
198
|
+
.article-content figure > p {
|
|
199
|
+
margin-top: 0 !important;
|
|
200
|
+
margin-bottom: 0 !important;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.article-content figure figcaption {
|
|
204
|
+
margin-top: 0.5rem !important;
|
|
205
|
+
text-align: center;
|
|
206
|
+
font-size: 0.875rem !important;
|
|
207
|
+
line-height: 1.35 !important;
|
|
208
|
+
color: hsl(var(--muted-foreground));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.article-content figure figcaption .blog-image-credit {
|
|
212
|
+
display: block;
|
|
213
|
+
margin-top: 0.25rem;
|
|
214
|
+
font-size: 0.75rem;
|
|
215
|
+
line-height: 1.25;
|
|
216
|
+
letter-spacing: 0.025em;
|
|
217
|
+
opacity: 0.8;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.article-content figure figcaption > span:not(.blog-image-credit) {
|
|
221
|
+
margin-left: 0.5rem;
|
|
222
|
+
padding-left: 0.5rem;
|
|
223
|
+
border-left: 1px solid var(--blog-accent-border);
|
|
224
|
+
font-size: 0.75rem;
|
|
225
|
+
letter-spacing: 0.025em;
|
|
226
|
+
opacity: 0.8;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.article-content figure figcaption a {
|
|
230
|
+
color: inherit;
|
|
231
|
+
text-decoration-line: underline;
|
|
232
|
+
text-underline-offset: 0.18em;
|
|
233
|
+
}
|
|
234
|
+
`}</style>
|
|
235
|
+
|
|
143
236
|
{/* Article Header */}
|
|
144
237
|
<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">
|
|
145
238
|
{/* Title */}
|
|
@@ -237,18 +330,38 @@ export default function ArticlePage({
|
|
|
237
330
|
|
|
238
331
|
{/* Featured Image */}
|
|
239
332
|
{imageUrl && (
|
|
240
|
-
<
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
333
|
+
<figure className="article-featured-image mx-auto my-8 w-full max-w-4xl sm:my-12 lg:my-16">
|
|
334
|
+
<div className="aspect-[16/9] overflow-hidden rounded-3xl border border-[var(--blog-accent-border)] shadow-[0_24px_80px_rgb(168_85_247_/_0.14)]">
|
|
335
|
+
<Image
|
|
336
|
+
src={imageUrl}
|
|
337
|
+
alt={
|
|
338
|
+
article.featuredImageAlt ??
|
|
339
|
+
`Featured image for ${article.title}`
|
|
340
|
+
}
|
|
341
|
+
className="h-full w-full object-cover"
|
|
342
|
+
width={800}
|
|
343
|
+
height={450}
|
|
344
|
+
priority
|
|
345
|
+
/>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
{hasFeaturedImageMeta && (
|
|
349
|
+
<figcaption className="text-muted-foreground mt-3 space-y-1 px-2 text-center text-sm leading-relaxed sm:px-0">
|
|
350
|
+
{article.featuredImageCaption && (
|
|
351
|
+
<p>{article.featuredImageCaption}</p>
|
|
352
|
+
)}
|
|
353
|
+
{(article.featuredImageCreditText ??
|
|
354
|
+
article.featuredImageCreditUrl) && (
|
|
355
|
+
<p className="text-xs">
|
|
356
|
+
<FeaturedImageCredit
|
|
357
|
+
creditText={article.featuredImageCreditText}
|
|
358
|
+
creditUrl={article.featuredImageCreditUrl}
|
|
359
|
+
/>
|
|
360
|
+
</p>
|
|
361
|
+
)}
|
|
362
|
+
</figcaption>
|
|
363
|
+
)}
|
|
364
|
+
</figure>
|
|
252
365
|
)}
|
|
253
366
|
|
|
254
367
|
{/* Article Content */}
|
|
@@ -265,7 +378,7 @@ export default function ArticlePage({
|
|
|
265
378
|
|
|
266
379
|
<div
|
|
267
380
|
id="article-body"
|
|
268
|
-
className="prose prose-sm sm:prose-base lg:prose-lg xl:prose-xl prose-gray dark:prose-invert prose-headings:scroll-mt-20 prose-headings:font-light prose-headings:text-balance prose-headings:text-[var(--blog-text-accent)] prose-h1:text-2xl sm:prose-h1:text-3xl lg:prose-h1:text-4xl prose-h2:text-xl sm:prose-h2:text-2xl lg:prose-h2:text-3xl prose-h3:text-lg sm:prose-h3:text-xl lg:prose-h3:text-2xl prose-p:leading-relaxed prose-p:text-pretty prose-a:text-[var(--blog-text-accent)] prose-a:transition-colors hover:prose-a:text-[var(--blog-text-accent-hover)] max-w-none"
|
|
381
|
+
className="prose prose-sm sm:prose-base lg:prose-lg xl:prose-xl prose-gray dark:prose-invert prose-headings:scroll-mt-20 prose-headings:font-light prose-headings:text-balance prose-headings:text-[var(--blog-text-accent)] prose-h1:text-2xl sm:prose-h1:text-3xl lg:prose-h1:text-4xl prose-h2:text-xl sm:prose-h2:text-2xl lg:prose-h2:text-3xl prose-h3:text-lg sm:prose-h3:text-xl lg:prose-h3:text-2xl prose-p:leading-relaxed prose-p:text-pretty prose-a:text-[var(--blog-text-accent)] prose-a:transition-colors hover:prose-a:text-[var(--blog-text-accent-hover)] [&_figure>p]:!my-0 [&_figcaption]:!mt-2 [&_figcaption]:text-center [&_figcaption]:!text-sm [&_figcaption]:!leading-snug [&_figcaption]:text-muted-foreground [&_figcaption_.blog-image-credit]:mt-1 [&_figcaption_.blog-image-credit]:block [&_figcaption_.blog-image-credit]:text-xs [&_figcaption_.blog-image-credit]:tracking-wide [&_figcaption_.blog-image-credit]:text-muted-foreground/80 [&_figcaption>span:not(.blog-image-credit)]:ml-2 [&_figcaption>span:not(.blog-image-credit)]:border-l [&_figcaption>span:not(.blog-image-credit)]:border-[var(--blog-accent-border)] [&_figcaption>span:not(.blog-image-credit)]:pl-2 [&_figcaption>span:not(.blog-image-credit)]:text-xs [&_figcaption>span:not(.blog-image-credit)]:tracking-wide [&_figcaption>span:not(.blog-image-credit)]:text-muted-foreground/80 max-w-none"
|
|
269
382
|
>
|
|
270
383
|
<MDXRemote
|
|
271
384
|
source={article.content}
|
|
@@ -327,11 +440,22 @@ export default function ArticlePage({
|
|
|
327
440
|
)}
|
|
328
441
|
|
|
329
442
|
{((postArticleCta ?? false) ||
|
|
443
|
+
(newsletterSettings?.enabled ?? false) ||
|
|
330
444
|
showShareLinks ||
|
|
331
445
|
relatedArticles.length > 0) && (
|
|
332
446
|
<div className="mx-auto mt-12 max-w-4xl space-y-8">
|
|
333
447
|
<Separator />
|
|
334
448
|
{postArticleCta && <PostArticleCta cta={postArticleCta} />}
|
|
449
|
+
{newsletterSettings?.enabled && (
|
|
450
|
+
<NewsletterSignup
|
|
451
|
+
siteId={article.site.id}
|
|
452
|
+
settings={newsletterSettings}
|
|
453
|
+
sourceSurface="article"
|
|
454
|
+
sourceArticleSlug={article.slug}
|
|
455
|
+
sourceUrl={articleUrl}
|
|
456
|
+
previewMode={newsletterPreviewMode}
|
|
457
|
+
/>
|
|
458
|
+
)}
|
|
335
459
|
{showShareLinks && (
|
|
336
460
|
<ArticleShareLinks title={article.title} url={articleUrl} />
|
|
337
461
|
)}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
PublishedArticle,
|
|
4
|
+
PublishedNewsletterSettings,
|
|
5
|
+
PublishedSite,
|
|
6
|
+
} from "../../types/contracts";
|
|
7
|
+
import type { BlogTheme } from "../../lib/article-ui";
|
|
8
|
+
|
|
9
|
+
export interface BlogHomePageProps {
|
|
10
|
+
articles: PublishedArticle[];
|
|
11
|
+
siteName?: string;
|
|
12
|
+
siteTagline?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
site?: PublishedSite;
|
|
15
|
+
includeStructuredData?: boolean;
|
|
16
|
+
basePath?: string;
|
|
17
|
+
pagination?: {
|
|
18
|
+
currentPage: number;
|
|
19
|
+
totalPages: number;
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
};
|
|
22
|
+
theme?: BlogTheme;
|
|
23
|
+
newsletter?: PublishedNewsletterSettings;
|
|
24
|
+
newsletterPreviewMode?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function BlogHomePage(props: BlogHomePageProps): ReactElement;
|
|
28
|
+
|
|
29
|
+
export function generateBlogHomeMetadata(
|
|
30
|
+
siteName?: string,
|
|
31
|
+
siteTagline?: string,
|
|
32
|
+
siteUrl?: string,
|
|
33
|
+
pagination?: {
|
|
34
|
+
currentPage: number;
|
|
35
|
+
totalPages: number;
|
|
36
|
+
},
|
|
37
|
+
basePath?: string,
|
|
38
|
+
): {
|
|
39
|
+
title: string;
|
|
40
|
+
description: string;
|
|
41
|
+
openGraph: {
|
|
42
|
+
title: string;
|
|
43
|
+
description: string;
|
|
44
|
+
type: string;
|
|
45
|
+
url?: string;
|
|
46
|
+
};
|
|
47
|
+
twitter: {
|
|
48
|
+
card: string;
|
|
49
|
+
title: string;
|
|
50
|
+
description: string;
|
|
51
|
+
};
|
|
52
|
+
alternates: {
|
|
53
|
+
canonical?: string;
|
|
54
|
+
};
|
|
55
|
+
other: {
|
|
56
|
+
prev?: string;
|
|
57
|
+
next?: string;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { renderToPipeableStream } from "react-dom/server";
|
|
3
|
+
import { PassThrough } from "stream";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import BlogHomePage from "./page";
|
|
7
|
+
import type { PublishedArticle } from "../../types/contracts";
|
|
8
|
+
|
|
9
|
+
const article: PublishedArticle = {
|
|
10
|
+
id: "article-1",
|
|
11
|
+
slug: "useful-routines",
|
|
12
|
+
title: "Useful routines",
|
|
13
|
+
excerpt: "A practical guide to keeping routines useful.",
|
|
14
|
+
content: "Routines work best when they are easy to maintain.",
|
|
15
|
+
readingSeconds: 360,
|
|
16
|
+
wordCount: 900,
|
|
17
|
+
publishedAt: "2026-05-06T12:00:00.000Z",
|
|
18
|
+
updatedAt: "2026-05-07T12:00:00.000Z",
|
|
19
|
+
author: null,
|
|
20
|
+
site: {
|
|
21
|
+
id: "site-1",
|
|
22
|
+
name: "Example Blog",
|
|
23
|
+
tagline: "Useful writing",
|
|
24
|
+
description: "Articles about useful systems.",
|
|
25
|
+
domain: "example.com",
|
|
26
|
+
toneAdjectives: [],
|
|
27
|
+
locale: "en",
|
|
28
|
+
},
|
|
29
|
+
meta: {
|
|
30
|
+
canonicalUrl: "https://example.com/blog/useful-routines",
|
|
31
|
+
keywords: [],
|
|
32
|
+
},
|
|
33
|
+
version: 1,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe("BlogHomePage", () => {
|
|
37
|
+
it("renders the newsletter form when configured", async () => {
|
|
38
|
+
const html = await renderToHtml(
|
|
39
|
+
React.createElement(BlogHomePage, {
|
|
40
|
+
articles: [article],
|
|
41
|
+
siteName: "Example Blog",
|
|
42
|
+
newsletter: {
|
|
43
|
+
enabled: true,
|
|
44
|
+
title: "Get product updates",
|
|
45
|
+
description: "Follow new articles and product notes.",
|
|
46
|
+
buttonLabel: "Get updates",
|
|
47
|
+
captureBaseUrl: "https://bloggy.example.com",
|
|
48
|
+
publicSubscribeToken: "public_subscribe_token",
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(html).toContain("Get product updates");
|
|
54
|
+
expect(html).toContain("Follow new articles and product notes.");
|
|
55
|
+
expect(html).toContain('type="email"');
|
|
56
|
+
expect(html).toContain('name="name"');
|
|
57
|
+
expect(html.indexOf("Get product updates")).toBeGreaterThan(
|
|
58
|
+
html.indexOf("Useful routines"),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function renderToHtml(element: React.ReactElement): Promise<string> {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
let html = "";
|
|
66
|
+
const stream = new PassThrough();
|
|
67
|
+
stream.setEncoding("utf8");
|
|
68
|
+
stream.on("data", (chunk: string) => {
|
|
69
|
+
html += chunk;
|
|
70
|
+
});
|
|
71
|
+
stream.on("end", () => resolve(html));
|
|
72
|
+
stream.on("error", reject);
|
|
73
|
+
|
|
74
|
+
const { pipe } = renderToPipeableStream(element, {
|
|
75
|
+
onAllReady() {
|
|
76
|
+
pipe(stream);
|
|
77
|
+
},
|
|
78
|
+
onError(error) {
|
|
79
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
package/dist/app/blog/page.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import Image from "next/image";
|
|
|
4
4
|
import {
|
|
5
5
|
type PublishedArticle,
|
|
6
6
|
type PublishedAuthor,
|
|
7
|
+
type PublishedNewsletterSettings,
|
|
7
8
|
type PublishedSite,
|
|
8
9
|
} from "../../types/contracts";
|
|
9
10
|
import { cn } from "../../lib/utils";
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from "../../lib/urls";
|
|
20
21
|
import { formatDistanceToNow } from "date-fns";
|
|
21
22
|
import { type BlogTheme, getBlogThemeStyle } from "../../lib/article-ui";
|
|
23
|
+
import { NewsletterSignup } from "../../components/NewsletterSignup";
|
|
22
24
|
|
|
23
25
|
interface BlogHomePageProps {
|
|
24
26
|
articles: PublishedArticle[];
|
|
@@ -39,6 +41,10 @@ interface BlogHomePageProps {
|
|
|
39
41
|
};
|
|
40
42
|
/** Optional theme values for host-site brand integration. */
|
|
41
43
|
theme?: BlogTheme;
|
|
44
|
+
/** Optional Bloggy-owned newsletter capture configuration. */
|
|
45
|
+
newsletter?: PublishedNewsletterSettings;
|
|
46
|
+
/** Render interactive preview success without persisting subscribers. */
|
|
47
|
+
newsletterPreviewMode?: boolean;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
/**
|
|
@@ -58,8 +64,12 @@ export default function BlogHomePage({
|
|
|
58
64
|
basePath = "/blog",
|
|
59
65
|
pagination,
|
|
60
66
|
theme,
|
|
67
|
+
newsletter,
|
|
68
|
+
newsletterPreviewMode = false,
|
|
61
69
|
}: BlogHomePageProps) {
|
|
62
70
|
const normalizedBasePath = normalizeBlogBasePath(basePath) || "/";
|
|
71
|
+
const newsletterSettings = newsletter ?? site?.newsletter;
|
|
72
|
+
const newsletterSiteId = site?.id ?? articles[0]?.site.id;
|
|
63
73
|
|
|
64
74
|
return (
|
|
65
75
|
<div
|
|
@@ -119,6 +129,16 @@ export default function BlogHomePage({
|
|
|
119
129
|
)}
|
|
120
130
|
</main>
|
|
121
131
|
|
|
132
|
+
{newsletterSettings?.enabled && newsletterSiteId && (
|
|
133
|
+
<NewsletterSignup
|
|
134
|
+
siteId={newsletterSiteId}
|
|
135
|
+
settings={newsletterSettings}
|
|
136
|
+
sourceSurface="blog_home"
|
|
137
|
+
previewMode={newsletterPreviewMode}
|
|
138
|
+
className="mt-10 sm:mt-12"
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
|
|
122
142
|
{/* JSON-LD Structured Data */}
|
|
123
143
|
{includeStructuredData &&
|
|
124
144
|
site &&
|