@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.
@@ -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-foreground 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">
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-xs px-4 text-base leading-relaxed text-balance sm:mt-8 sm:max-w-lg sm:px-0 sm:text-lg md:max-w-2xl md:text-xl lg:max-w-4xl lg:text-2xl">
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-2xl sm:my-12 lg:my-16">
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-img:mx-auto prose-img:rounded-2xl prose-a:text-[var(--blog-accent)] prose-a:transition-colors hover:prose-a:text-[var(--blog-accent-hover)] max-w-none">
205
- <MDXRemote source={article.content} options={articleMdxOptions} />
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
- className="text-muted-foreground hover:border-[var(--blog-accent-border)] hover:text-[var(--blog-accent)] rounded-full border border-border px-3 py-1.5 text-sm transition-colors"
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
- X
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
- className="text-muted-foreground hover:border-[var(--blog-accent-border)] hover:text-[var(--blog-accent)] rounded-full border border-border px-3 py-1.5 text-sm transition-colors"
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
- LinkedIn
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
- className="text-muted-foreground hover:border-[var(--blog-accent-border)] hover:text-[var(--blog-accent)] inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1.5 text-sm transition-colors"
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-3.5" aria-hidden="true" />
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 p-4 transition-colors hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)] hover:no-underline"
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
- <h3 className="text-foreground group-hover:text-[var(--blog-accent)] line-clamp-3 font-semibold transition-colors">
468
- {article.title}
469
- </h3>
470
- {article.excerpt && (
471
- <p className="text-muted-foreground mt-2 line-clamp-3 text-sm leading-relaxed">
472
- {article.excerpt}
473
- </p>
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". */