@xexr/blog-platform 1.0.3 → 1.0.4
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/dist/app/blog/[slug]/page.tsx +83 -25
- package/dist/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -8
|
@@ -7,6 +7,7 @@ import { cn } from "../../../lib/utils";
|
|
|
7
7
|
import { ArrowLeft, ArrowUpRight, Mail, Share2 } from "lucide-react";
|
|
8
8
|
import { Badge } from "../../../components/ui/badge";
|
|
9
9
|
import { Card, CardContent } from "../../../components/ui/card";
|
|
10
|
+
import { ExpandableArticleImage } from "../../../components/ExpandableArticleImage";
|
|
10
11
|
import { Separator } from "../../../components/ui/separator";
|
|
11
12
|
import {
|
|
12
13
|
generateCompleteArticleStructuredData,
|
|
@@ -33,10 +34,14 @@ interface ArticlePageProps {
|
|
|
33
34
|
previousArticle?: {
|
|
34
35
|
url: string;
|
|
35
36
|
title: string;
|
|
37
|
+
featuredImage?: string;
|
|
38
|
+
featuredImageAlt?: string;
|
|
36
39
|
};
|
|
37
40
|
nextArticle?: {
|
|
38
41
|
url: string;
|
|
39
42
|
title: string;
|
|
43
|
+
featuredImage?: string;
|
|
44
|
+
featuredImageAlt?: string;
|
|
40
45
|
};
|
|
41
46
|
};
|
|
42
47
|
/** Public path where this blog is mounted, e.g. "/blog" or "/new-blog". */
|
|
@@ -124,13 +129,13 @@ export default function ArticlePage({
|
|
|
124
129
|
{/* Article Header */}
|
|
125
130
|
<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">
|
|
126
131
|
{/* Title */}
|
|
127
|
-
<h1 className="article-title text-
|
|
132
|
+
<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">
|
|
128
133
|
{article.title}
|
|
129
134
|
</h1>
|
|
130
135
|
|
|
131
136
|
{/* Excerpt */}
|
|
132
137
|
{article.excerpt && (
|
|
133
|
-
<p className="article-excerpt text-muted-foreground mx-auto mt-6 max-w-
|
|
138
|
+
<p className="article-excerpt text-muted-foreground mx-auto mt-6 max-w-[42rem] px-4 text-lg leading-relaxed text-balance sm:mt-8 sm:max-w-[46rem] sm:px-0 sm:text-xl md:max-w-[52rem] md:text-2xl lg:max-w-4xl lg:text-2xl">
|
|
134
139
|
{article.excerpt}
|
|
135
140
|
</p>
|
|
136
141
|
)}
|
|
@@ -182,7 +187,7 @@ export default function ArticlePage({
|
|
|
182
187
|
|
|
183
188
|
{/* Featured Image */}
|
|
184
189
|
{imageUrl && (
|
|
185
|
-
<div className="article-featured-image mx-auto my-8 aspect-[16/9] w-full max-w-4xl overflow-hidden rounded-
|
|
190
|
+
<div className="article-featured-image mx-auto my-8 aspect-[16/9] w-full max-w-4xl overflow-hidden rounded-3xl border border-[var(--blog-accent-border)] shadow-[0_24px_80px_rgb(168_85_247_/_0.14)] sm:my-12 lg:my-16">
|
|
186
191
|
<Image
|
|
187
192
|
src={imageUrl}
|
|
188
193
|
alt={
|
|
@@ -201,8 +206,12 @@ export default function ArticlePage({
|
|
|
201
206
|
<div className="min-w-0">
|
|
202
207
|
<ArticleTableOfContents items={tocItems} variant="mobile" />
|
|
203
208
|
|
|
204
|
-
<div 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-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-
|
|
205
|
-
<MDXRemote
|
|
209
|
+
<div 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">
|
|
210
|
+
<MDXRemote
|
|
211
|
+
source={article.content}
|
|
212
|
+
options={articleMdxOptions}
|
|
213
|
+
components={{ img: ExpandableArticleImage }}
|
|
214
|
+
/>
|
|
206
215
|
</div>
|
|
207
216
|
</div>
|
|
208
217
|
|
|
@@ -271,11 +280,20 @@ export default function ArticlePage({
|
|
|
271
280
|
{navigation?.previousArticle && (
|
|
272
281
|
<Card className="group hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)] cursor-pointer transition-colors">
|
|
273
282
|
<Link href={navigation.previousArticle.url} className="block">
|
|
283
|
+
{navigation.previousArticle.featuredImage && (
|
|
284
|
+
<ArticleCardImage
|
|
285
|
+
src={navigation.previousArticle.featuredImage}
|
|
286
|
+
alt={
|
|
287
|
+
navigation.previousArticle.featuredImageAlt ??
|
|
288
|
+
`Featured image for ${navigation.previousArticle.title}`
|
|
289
|
+
}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
274
292
|
<CardContent className="p-4">
|
|
275
293
|
<div className="text-muted-foreground mb-2 text-sm font-medium">
|
|
276
294
|
← Previous Article
|
|
277
295
|
</div>
|
|
278
|
-
<div className="text-foreground group-hover:text-[var(--blog-accent)] transition-colors">
|
|
296
|
+
<div className="text-foreground group-hover:text-[var(--blog-text-accent)] transition-colors">
|
|
279
297
|
{navigation.previousArticle.title}
|
|
280
298
|
</div>
|
|
281
299
|
</CardContent>
|
|
@@ -290,11 +308,20 @@ export default function ArticlePage({
|
|
|
290
308
|
)}
|
|
291
309
|
>
|
|
292
310
|
<Link href={navigation.nextArticle.url} className="block">
|
|
311
|
+
{navigation.nextArticle.featuredImage && (
|
|
312
|
+
<ArticleCardImage
|
|
313
|
+
src={navigation.nextArticle.featuredImage}
|
|
314
|
+
alt={
|
|
315
|
+
navigation.nextArticle.featuredImageAlt ??
|
|
316
|
+
`Featured image for ${navigation.nextArticle.title}`
|
|
317
|
+
}
|
|
318
|
+
/>
|
|
319
|
+
)}
|
|
293
320
|
<CardContent className="p-4 md:text-right">
|
|
294
321
|
<div className="text-muted-foreground mb-2 text-sm font-medium">
|
|
295
322
|
Next Article →
|
|
296
323
|
</div>
|
|
297
|
-
<div className="text-foreground group-hover:text-[var(--blog-accent)] transition-colors">
|
|
324
|
+
<div className="text-foreground group-hover:text-[var(--blog-text-accent)] transition-colors">
|
|
298
325
|
{navigation.nextArticle.title}
|
|
299
326
|
</div>
|
|
300
327
|
</CardContent>
|
|
@@ -342,7 +369,7 @@ function ArticleTableOfContents({
|
|
|
342
369
|
<li key={item.id} className={item.level === 3 ? "pl-4" : undefined}>
|
|
343
370
|
<a
|
|
344
371
|
href={`#${item.id}`}
|
|
345
|
-
className="text-muted-foreground hover:text-[var(--blog-accent)] block leading-snug transition-colors"
|
|
372
|
+
className="text-muted-foreground hover:text-[var(--blog-text-accent)] block leading-snug transition-colors"
|
|
346
373
|
>
|
|
347
374
|
{item.text}
|
|
348
375
|
</a>
|
|
@@ -413,8 +440,8 @@ function ArticleShareLinks({ title, url }: { title: string; url: string }) {
|
|
|
413
440
|
const encodedUrl = encodeURIComponent(url);
|
|
414
441
|
|
|
415
442
|
return (
|
|
416
|
-
<div className="flex flex-wrap items-center gap-3">
|
|
417
|
-
<span className="text-muted-foreground inline-flex items-center gap-2 text-sm font-medium">
|
|
443
|
+
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-3">
|
|
444
|
+
<span className="text-muted-foreground inline-flex basis-full items-center justify-center gap-2 text-sm font-medium sm:basis-auto">
|
|
418
445
|
<Share2 className="size-4" aria-hidden="true" />
|
|
419
446
|
Share
|
|
420
447
|
</span>
|
|
@@ -422,24 +449,30 @@ function ArticleShareLinks({ title, url }: { title: string; url: string }) {
|
|
|
422
449
|
href={`https://x.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
|
|
423
450
|
target="_blank"
|
|
424
451
|
rel="noopener noreferrer"
|
|
425
|
-
|
|
452
|
+
aria-label="Share on X"
|
|
453
|
+
className="text-muted-foreground hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)] hover:text-foreground inline-flex size-9 items-center justify-center rounded-full border border-border grayscale transition-colors"
|
|
426
454
|
>
|
|
427
|
-
|
|
455
|
+
<span className="text-sm font-semibold" aria-hidden="true">
|
|
456
|
+
X
|
|
457
|
+
</span>
|
|
428
458
|
</a>
|
|
429
459
|
<a
|
|
430
460
|
href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`}
|
|
431
461
|
target="_blank"
|
|
432
462
|
rel="noopener noreferrer"
|
|
433
|
-
|
|
463
|
+
aria-label="Share on LinkedIn"
|
|
464
|
+
className="text-muted-foreground hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)] hover:text-foreground inline-flex size-9 items-center justify-center rounded-full border border-border grayscale transition-colors"
|
|
434
465
|
>
|
|
435
|
-
|
|
466
|
+
<span className="text-sm font-bold" aria-hidden="true">
|
|
467
|
+
in
|
|
468
|
+
</span>
|
|
436
469
|
</a>
|
|
437
470
|
<a
|
|
438
471
|
href={`mailto:?subject=${encodedTitle}&body=${encodedUrl}`}
|
|
439
|
-
|
|
472
|
+
aria-label="Share by email"
|
|
473
|
+
className="text-muted-foreground hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)] hover:text-foreground inline-flex size-9 items-center justify-center rounded-full border border-border grayscale transition-colors"
|
|
440
474
|
>
|
|
441
|
-
<Mail className="size-
|
|
442
|
-
Email
|
|
475
|
+
<Mail className="size-4" aria-hidden="true" />
|
|
443
476
|
</a>
|
|
444
477
|
</div>
|
|
445
478
|
);
|
|
@@ -462,16 +495,27 @@ function RelatedArticles({
|
|
|
462
495
|
<Link
|
|
463
496
|
key={article.id}
|
|
464
497
|
href={buildBlogPath(basePath, article.slug)}
|
|
465
|
-
className="group rounded-2xl border border-border
|
|
498
|
+
className="group overflow-hidden rounded-2xl border border-border transition-colors hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)] hover:no-underline"
|
|
466
499
|
>
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
500
|
+
{article.featuredImage && (
|
|
501
|
+
<ArticleCardImage
|
|
502
|
+
src={article.featuredImage}
|
|
503
|
+
alt={
|
|
504
|
+
article.featuredImageAlt ??
|
|
505
|
+
`Featured image for ${article.title}`
|
|
506
|
+
}
|
|
507
|
+
/>
|
|
474
508
|
)}
|
|
509
|
+
<div className="p-4">
|
|
510
|
+
<h3 className="text-foreground group-hover:text-[var(--blog-text-accent)] line-clamp-3 font-semibold transition-colors">
|
|
511
|
+
{article.title}
|
|
512
|
+
</h3>
|
|
513
|
+
{article.excerpt && (
|
|
514
|
+
<p className="text-muted-foreground mt-2 line-clamp-3 text-sm leading-relaxed">
|
|
515
|
+
{article.excerpt}
|
|
516
|
+
</p>
|
|
517
|
+
)}
|
|
518
|
+
</div>
|
|
475
519
|
</Link>
|
|
476
520
|
))}
|
|
477
521
|
</div>
|
|
@@ -479,6 +523,20 @@ function RelatedArticles({
|
|
|
479
523
|
);
|
|
480
524
|
}
|
|
481
525
|
|
|
526
|
+
function ArticleCardImage({ src, alt }: { src: string; alt: string }) {
|
|
527
|
+
return (
|
|
528
|
+
<div className="aspect-[16/9] overflow-hidden border-b border-border bg-[var(--blog-accent-bg)]">
|
|
529
|
+
<Image
|
|
530
|
+
src={src}
|
|
531
|
+
alt={alt}
|
|
532
|
+
width={480}
|
|
533
|
+
height={270}
|
|
534
|
+
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
482
540
|
/**
|
|
483
541
|
* Generate metadata for the article page for SEO optimization.
|
|
484
542
|
* This function should be used in the Next.js metadata API.
|
package/dist/index.d.mts
CHANGED
|
@@ -13,6 +13,8 @@ import { LibSQLDatabase } from 'drizzle-orm/libsql';
|
|
|
13
13
|
interface BlogTheme {
|
|
14
14
|
accentColor?: string;
|
|
15
15
|
accentHoverColor?: string;
|
|
16
|
+
textAccentColor?: string;
|
|
17
|
+
textAccentHoverColor?: string;
|
|
16
18
|
accentBackgroundColor?: string;
|
|
17
19
|
accentBorderColor?: string;
|
|
18
20
|
}
|
|
@@ -30,6 +32,8 @@ interface ArticleDateMeta {
|
|
|
30
32
|
type BlogThemeStyle = CSSProperties & {
|
|
31
33
|
"--blog-accent": string;
|
|
32
34
|
"--blog-accent-hover": string;
|
|
35
|
+
"--blog-text-accent": string;
|
|
36
|
+
"--blog-text-accent-hover": string;
|
|
33
37
|
"--blog-accent-bg": string;
|
|
34
38
|
"--blog-accent-border": string;
|
|
35
39
|
};
|
|
@@ -107,10 +111,14 @@ interface ArticlePageProps {
|
|
|
107
111
|
previousArticle?: {
|
|
108
112
|
url: string;
|
|
109
113
|
title: string;
|
|
114
|
+
featuredImage?: string;
|
|
115
|
+
featuredImageAlt?: string;
|
|
110
116
|
};
|
|
111
117
|
nextArticle?: {
|
|
112
118
|
url: string;
|
|
113
119
|
title: string;
|
|
120
|
+
featuredImage?: string;
|
|
121
|
+
featuredImageAlt?: string;
|
|
114
122
|
};
|
|
115
123
|
};
|
|
116
124
|
/** Public path where this blog is mounted, e.g. "/blog" or "/new-blog". */
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { LibSQLDatabase } from 'drizzle-orm/libsql';
|
|
|
13
13
|
interface BlogTheme {
|
|
14
14
|
accentColor?: string;
|
|
15
15
|
accentHoverColor?: string;
|
|
16
|
+
textAccentColor?: string;
|
|
17
|
+
textAccentHoverColor?: string;
|
|
16
18
|
accentBackgroundColor?: string;
|
|
17
19
|
accentBorderColor?: string;
|
|
18
20
|
}
|
|
@@ -30,6 +32,8 @@ interface ArticleDateMeta {
|
|
|
30
32
|
type BlogThemeStyle = CSSProperties & {
|
|
31
33
|
"--blog-accent": string;
|
|
32
34
|
"--blog-accent-hover": string;
|
|
35
|
+
"--blog-text-accent": string;
|
|
36
|
+
"--blog-text-accent-hover": string;
|
|
33
37
|
"--blog-accent-bg": string;
|
|
34
38
|
"--blog-accent-border": string;
|
|
35
39
|
};
|
|
@@ -107,10 +111,14 @@ interface ArticlePageProps {
|
|
|
107
111
|
previousArticle?: {
|
|
108
112
|
url: string;
|
|
109
113
|
title: string;
|
|
114
|
+
featuredImage?: string;
|
|
115
|
+
featuredImageAlt?: string;
|
|
110
116
|
};
|
|
111
117
|
nextArticle?: {
|
|
112
118
|
url: string;
|
|
113
119
|
title: string;
|
|
120
|
+
featuredImage?: string;
|
|
121
|
+
featuredImageAlt?: string;
|
|
114
122
|
};
|
|
115
123
|
};
|
|
116
124
|
/** Public path where this blog is mounted, e.g. "/blog" or "/new-blog". */
|