@xexr/blog-platform 1.0.3 → 1.0.5

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={
@@ -197,12 +202,19 @@ export default function ArticlePage({
197
202
  )}
198
203
 
199
204
  {/* Article Content */}
200
- <div className="article-content mx-auto grid max-w-6xl grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_14rem] lg:gap-12">
205
+ <div
206
+ id="article-content"
207
+ className="article-content mx-auto grid max-w-6xl grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_14rem] lg:gap-12"
208
+ >
201
209
  <div className="min-w-0">
202
210
  <ArticleTableOfContents items={tocItems} variant="mobile" />
203
211
 
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} />
212
+ <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">
213
+ <MDXRemote
214
+ source={article.content}
215
+ options={articleMdxOptions}
216
+ components={{ img: ExpandableArticleImage }}
217
+ />
206
218
  </div>
207
219
  </div>
208
220
 
@@ -271,11 +283,20 @@ export default function ArticlePage({
271
283
  {navigation?.previousArticle && (
272
284
  <Card className="group hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)] cursor-pointer transition-colors">
273
285
  <Link href={navigation.previousArticle.url} className="block">
286
+ {navigation.previousArticle.featuredImage && (
287
+ <ArticleCardImage
288
+ src={navigation.previousArticle.featuredImage}
289
+ alt={
290
+ navigation.previousArticle.featuredImageAlt ??
291
+ `Featured image for ${navigation.previousArticle.title}`
292
+ }
293
+ />
294
+ )}
274
295
  <CardContent className="p-4">
275
296
  <div className="text-muted-foreground mb-2 text-sm font-medium">
276
297
  ← Previous Article
277
298
  </div>
278
- <div className="text-foreground group-hover:text-[var(--blog-accent)] transition-colors">
299
+ <div className="text-foreground group-hover:text-[var(--blog-text-accent)] transition-colors">
279
300
  {navigation.previousArticle.title}
280
301
  </div>
281
302
  </CardContent>
@@ -290,11 +311,20 @@ export default function ArticlePage({
290
311
  )}
291
312
  >
292
313
  <Link href={navigation.nextArticle.url} className="block">
314
+ {navigation.nextArticle.featuredImage && (
315
+ <ArticleCardImage
316
+ src={navigation.nextArticle.featuredImage}
317
+ alt={
318
+ navigation.nextArticle.featuredImageAlt ??
319
+ `Featured image for ${navigation.nextArticle.title}`
320
+ }
321
+ />
322
+ )}
293
323
  <CardContent className="p-4 md:text-right">
294
324
  <div className="text-muted-foreground mb-2 text-sm font-medium">
295
325
  Next Article →
296
326
  </div>
297
- <div className="text-foreground group-hover:text-[var(--blog-accent)] transition-colors">
327
+ <div className="text-foreground group-hover:text-[var(--blog-text-accent)] transition-colors">
298
328
  {navigation.nextArticle.title}
299
329
  </div>
300
330
  </CardContent>
@@ -342,7 +372,7 @@ function ArticleTableOfContents({
342
372
  <li key={item.id} className={item.level === 3 ? "pl-4" : undefined}>
343
373
  <a
344
374
  href={`#${item.id}`}
345
- className="text-muted-foreground hover:text-[var(--blog-accent)] block leading-snug transition-colors"
375
+ className="text-muted-foreground hover:text-[var(--blog-text-accent)] block leading-snug transition-colors"
346
376
  >
347
377
  {item.text}
348
378
  </a>
@@ -413,8 +443,8 @@ function ArticleShareLinks({ title, url }: { title: string; url: string }) {
413
443
  const encodedUrl = encodeURIComponent(url);
414
444
 
415
445
  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">
446
+ <div className="flex flex-wrap items-center justify-center gap-2 sm:gap-3">
447
+ <span className="text-muted-foreground inline-flex basis-full items-center justify-center gap-2 text-sm font-medium sm:basis-auto">
418
448
  <Share2 className="size-4" aria-hidden="true" />
419
449
  Share
420
450
  </span>
@@ -422,24 +452,30 @@ function ArticleShareLinks({ title, url }: { title: string; url: string }) {
422
452
  href={`https://x.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
423
453
  target="_blank"
424
454
  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"
455
+ aria-label="Share on X"
456
+ 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
457
  >
427
- X
458
+ <span className="text-sm font-semibold" aria-hidden="true">
459
+ X
460
+ </span>
428
461
  </a>
429
462
  <a
430
463
  href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`}
431
464
  target="_blank"
432
465
  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"
466
+ aria-label="Share on LinkedIn"
467
+ 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
468
  >
435
- LinkedIn
469
+ <span className="text-sm font-bold" aria-hidden="true">
470
+ in
471
+ </span>
436
472
  </a>
437
473
  <a
438
474
  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"
475
+ aria-label="Share by email"
476
+ 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
477
  >
441
- <Mail className="size-3.5" aria-hidden="true" />
442
- Email
478
+ <Mail className="size-4" aria-hidden="true" />
443
479
  </a>
444
480
  </div>
445
481
  );
@@ -462,16 +498,27 @@ function RelatedArticles({
462
498
  <Link
463
499
  key={article.id}
464
500
  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"
501
+ 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
502
  >
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>
503
+ {article.featuredImage && (
504
+ <ArticleCardImage
505
+ src={article.featuredImage}
506
+ alt={
507
+ article.featuredImageAlt ??
508
+ `Featured image for ${article.title}`
509
+ }
510
+ />
474
511
  )}
512
+ <div className="p-4">
513
+ <h3 className="text-foreground group-hover:text-[var(--blog-text-accent)] line-clamp-3 font-semibold transition-colors">
514
+ {article.title}
515
+ </h3>
516
+ {article.excerpt && (
517
+ <p className="text-muted-foreground mt-2 line-clamp-3 text-sm leading-relaxed">
518
+ {article.excerpt}
519
+ </p>
520
+ )}
521
+ </div>
475
522
  </Link>
476
523
  ))}
477
524
  </div>
@@ -479,6 +526,20 @@ function RelatedArticles({
479
526
  );
480
527
  }
481
528
 
529
+ function ArticleCardImage({ src, alt }: { src: string; alt: string }) {
530
+ return (
531
+ <div className="aspect-[16/9] overflow-hidden border-b border-border bg-[var(--blog-accent-bg)]">
532
+ <Image
533
+ src={src}
534
+ alt={alt}
535
+ width={480}
536
+ height={270}
537
+ className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
538
+ />
539
+ </div>
540
+ );
541
+ }
542
+
482
543
  /**
483
544
  * Generate metadata for the article page for SEO optimization.
484
545
  * 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". */