@xexr/blog-platform 1.0.7 → 1.0.8
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/CHANGELOG.md +7 -0
- package/dist/app/blog/[slug]/page.d.ts +68 -0
- package/dist/app/blog/[slug]/page.tsx +155 -113
- package/dist/components/ArticleReadingProgress.tsx +208 -0
- package/dist/components/ArticleTopActions.tsx +179 -0
- package/dist/components/AuthorByline.tsx +569 -0
- package/dist/components/ExpandableArticleImage.tsx +79 -0
- package/dist/components/Pagination.tsx +236 -0
- package/dist/components/TopicArchivePages.tsx +204 -0
- package/dist/components/ui/badge.tsx +36 -0
- package/dist/components/ui/button.tsx +59 -0
- package/dist/components/ui/card.tsx +92 -0
- package/dist/components/ui/pagination.tsx +127 -0
- package/dist/components/ui/separator.tsx +29 -0
- package/dist/index.d.mts +59 -69
- package/dist/index.d.ts +59 -69
- package/dist/index.js +83 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +83 -8
- package/dist/index.mjs.map +1 -1
- package/dist/lib/article-ui.ts +158 -0
- package/dist/lib/auth.ts +354 -0
- package/dist/lib/cors.ts +243 -0
- package/dist/lib/database.ts +277 -0
- package/dist/lib/init.ts +186 -0
- package/dist/lib/mdx.ts +73 -0
- package/dist/lib/metadata.ts +558 -0
- package/dist/lib/ratelimit.ts +224 -0
- package/dist/lib/security-headers.ts +322 -0
- package/dist/lib/seo.ts +295 -0
- package/dist/lib/sitemap.ts +719 -0
- package/dist/lib/storage/drizzle.ts +400 -0
- package/dist/lib/structured-data.ts +608 -0
- package/dist/lib/topics.ts +56 -0
- package/dist/lib/urls.ts +29 -0
- package/dist/lib/utils.ts +6 -0
- package/dist/lib/version.ts +154 -0
- package/dist/types/contracts.ts +256 -0
- package/dist/types/index.d.mts +18 -1
- package/dist/types/index.d.ts +18 -1
- package/dist/types/index.ts +16 -0
- package/package.json +14 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add reading progress and active table-of-contents state to public article pages.
|
|
8
|
+
- Include route runtime dependencies in the package build output for subpath app imports.
|
|
9
|
+
|
|
3
10
|
## 1.0.1
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { PublishedArticle } from "../../../types/contracts";
|
|
3
|
+
import type { BlogTheme } from "../../../lib/article-ui";
|
|
4
|
+
|
|
5
|
+
interface ArticlePageProps {
|
|
6
|
+
article: PublishedArticle;
|
|
7
|
+
className?: string;
|
|
8
|
+
navigation?: {
|
|
9
|
+
backToHome?: {
|
|
10
|
+
url: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
};
|
|
13
|
+
previousArticle?: {
|
|
14
|
+
url: string;
|
|
15
|
+
title: string;
|
|
16
|
+
featuredImage?: string;
|
|
17
|
+
featuredImageAlt?: string;
|
|
18
|
+
};
|
|
19
|
+
nextArticle?: {
|
|
20
|
+
url: string;
|
|
21
|
+
title: string;
|
|
22
|
+
featuredImage?: string;
|
|
23
|
+
featuredImageAlt?: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
basePath?: string;
|
|
27
|
+
theme?: BlogTheme;
|
|
28
|
+
postArticleCta?: {
|
|
29
|
+
title: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
href: string;
|
|
32
|
+
label: string;
|
|
33
|
+
};
|
|
34
|
+
relatedArticles?: PublishedArticle[];
|
|
35
|
+
showShareLinks?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function ArticlePage(props: ArticlePageProps): ReactElement;
|
|
39
|
+
|
|
40
|
+
export function generateArticleMetadata(
|
|
41
|
+
article: PublishedArticle,
|
|
42
|
+
basePath?: string,
|
|
43
|
+
): {
|
|
44
|
+
title: string;
|
|
45
|
+
description: string;
|
|
46
|
+
keywords: string;
|
|
47
|
+
authors: { name: string }[] | undefined;
|
|
48
|
+
openGraph: {
|
|
49
|
+
title: string;
|
|
50
|
+
description: string;
|
|
51
|
+
type: string;
|
|
52
|
+
url: string;
|
|
53
|
+
images: { url: string; alt: string }[] | undefined;
|
|
54
|
+
publishedTime: string;
|
|
55
|
+
modifiedTime: string;
|
|
56
|
+
authors: string[] | undefined;
|
|
57
|
+
siteName: string;
|
|
58
|
+
};
|
|
59
|
+
twitter: {
|
|
60
|
+
card: string;
|
|
61
|
+
title: string;
|
|
62
|
+
description: string;
|
|
63
|
+
images: string[] | undefined;
|
|
64
|
+
};
|
|
65
|
+
alternates: {
|
|
66
|
+
canonical: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -7,10 +7,13 @@ import {
|
|
|
7
7
|
type PublishedAuthor,
|
|
8
8
|
} from "../../../types/contracts";
|
|
9
9
|
import { cn } from "../../../lib/utils";
|
|
10
|
-
import { ArrowLeft, ArrowUpRight,
|
|
10
|
+
import { ArrowLeft, ArrowUpRight, Globe, Mail } from "lucide-react";
|
|
11
|
+
import { FaGithub, FaLinkedinIn, FaXTwitter } from "react-icons/fa6";
|
|
11
12
|
import { Badge } from "../../../components/ui/badge";
|
|
12
13
|
import { Card, CardContent } from "../../../components/ui/card";
|
|
14
|
+
import ArticleTopActions from "../../../components/ArticleTopActions";
|
|
13
15
|
import { ExpandableArticleImage } from "../../../components/ExpandableArticleImage";
|
|
16
|
+
import { ArticleReadingProgress } from "../../../components/ArticleReadingProgress";
|
|
14
17
|
import { Separator } from "../../../components/ui/separator";
|
|
15
18
|
import {
|
|
16
19
|
generateCompleteArticleStructuredData,
|
|
@@ -21,7 +24,6 @@ import { buildBlogPath, normalizeBlogBasePath } from "../../../lib/urls";
|
|
|
21
24
|
import {
|
|
22
25
|
type BlogTheme,
|
|
23
26
|
extractTableOfContents,
|
|
24
|
-
formatTableOfContentsSummary,
|
|
25
27
|
formatArticleDateMeta,
|
|
26
28
|
getBlogThemeStyle,
|
|
27
29
|
} from "../../../lib/article-ui";
|
|
@@ -91,11 +93,15 @@ export default function ArticlePage({
|
|
|
91
93
|
showShareLinks = true,
|
|
92
94
|
}: ArticlePageProps) {
|
|
93
95
|
const normalizedBasePath = normalizeBlogBasePath(basePath) || "/";
|
|
94
|
-
const dateMeta = formatArticleDateMeta(
|
|
96
|
+
const dateMeta = formatArticleDateMeta(
|
|
97
|
+
article.publishedAt,
|
|
98
|
+
article.updatedAt,
|
|
99
|
+
);
|
|
95
100
|
const readingTime = formatReadingTime(article.readingSeconds);
|
|
96
101
|
const tocItems = extractTableOfContents(article.content);
|
|
97
102
|
const articleUrl =
|
|
98
|
-
article.meta.canonicalUrl ??
|
|
103
|
+
article.meta.canonicalUrl ??
|
|
104
|
+
buildBlogPath(normalizedBasePath, article.slug);
|
|
99
105
|
|
|
100
106
|
// Use the featured image URL provided by Bloggy (hosted on Bloggy's domain)
|
|
101
107
|
const imageUrl = article.featuredImage;
|
|
@@ -113,7 +119,7 @@ export default function ArticlePage({
|
|
|
113
119
|
<header className="mx-auto mb-8 max-w-4xl sm:mb-12 lg:mb-16">
|
|
114
120
|
<Link
|
|
115
121
|
href={navigation.backToHome.url}
|
|
116
|
-
className="text-muted-foreground
|
|
122
|
+
className="text-muted-foreground group focus-visible:ring-ring inline-flex touch-manipulation items-center space-x-2 rounded-md px-2 py-1 transition-colors hover:text-[var(--blog-accent)] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none sm:space-x-3"
|
|
117
123
|
aria-label={navigation.backToHome.label ?? "Return to blog"}
|
|
118
124
|
>
|
|
119
125
|
<ArrowLeft
|
|
@@ -168,7 +174,9 @@ export default function ArticlePage({
|
|
|
168
174
|
Published {dateMeta.publishedDateLabel}
|
|
169
175
|
</time>
|
|
170
176
|
<span className="text-border">•</span>
|
|
171
|
-
<span className="font-medium">
|
|
177
|
+
<span className="font-medium">
|
|
178
|
+
{dateMeta.publishedRelativeLabel}
|
|
179
|
+
</span>
|
|
172
180
|
<span className="text-border">•</span>
|
|
173
181
|
<span className="font-medium">{readingTime}</span>
|
|
174
182
|
</div>
|
|
@@ -179,6 +187,14 @@ export default function ArticlePage({
|
|
|
179
187
|
</time>
|
|
180
188
|
)}
|
|
181
189
|
|
|
190
|
+
{showShareLinks && (
|
|
191
|
+
<ArticleTopActions
|
|
192
|
+
title={article.title}
|
|
193
|
+
url={articleUrl}
|
|
194
|
+
size="sm"
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
197
|
+
|
|
182
198
|
<div className="bg-border h-px w-6 sm:w-8" aria-hidden="true"></div>
|
|
183
199
|
|
|
184
200
|
{(article.topics ?? []).length > 0 ? (
|
|
@@ -190,10 +206,7 @@ export default function ArticlePage({
|
|
|
190
206
|
<Link
|
|
191
207
|
key={topic.id}
|
|
192
208
|
href={buildBlogPath(
|
|
193
|
-
(navigation?.backToHome?.url ?? "/blog").replace(
|
|
194
|
-
/\/$/,
|
|
195
|
-
"",
|
|
196
|
-
),
|
|
209
|
+
(navigation?.backToHome?.url ?? "/blog").replace(/\/$/, ""),
|
|
197
210
|
`topics/${topic.slug}`,
|
|
198
211
|
)}
|
|
199
212
|
className="rounded-full border border-[var(--blog-accent-border)] bg-[var(--blog-accent-bg)] px-3 py-1 text-xs font-medium text-[var(--blog-text-accent)] transition-colors hover:border-[var(--blog-text-accent)] hover:text-[var(--blog-text-accent-hover)]"
|
|
@@ -235,9 +248,16 @@ export default function ArticlePage({
|
|
|
235
248
|
className="article-content mx-auto grid max-w-6xl grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_14rem] lg:gap-12"
|
|
236
249
|
>
|
|
237
250
|
<div className="min-w-0">
|
|
238
|
-
<
|
|
251
|
+
<ArticleReadingProgress
|
|
252
|
+
items={tocItems}
|
|
253
|
+
contentId="article-body"
|
|
254
|
+
variant="mobile"
|
|
255
|
+
/>
|
|
239
256
|
|
|
240
|
-
<div
|
|
257
|
+
<div
|
|
258
|
+
id="article-body"
|
|
259
|
+
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"
|
|
260
|
+
>
|
|
241
261
|
<MDXRemote
|
|
242
262
|
source={article.content}
|
|
243
263
|
options={articleMdxOptions}
|
|
@@ -246,31 +266,39 @@ export default function ArticlePage({
|
|
|
246
266
|
</div>
|
|
247
267
|
</div>
|
|
248
268
|
|
|
249
|
-
<
|
|
269
|
+
<ArticleReadingProgress
|
|
270
|
+
items={tocItems}
|
|
271
|
+
contentId="article-body"
|
|
272
|
+
variant="desktop"
|
|
273
|
+
showProgress={false}
|
|
274
|
+
/>
|
|
250
275
|
</div>
|
|
251
276
|
|
|
252
277
|
{/* Author Bio */}
|
|
253
278
|
{article.author?.bio && (
|
|
254
|
-
<div className="author-bio mx-auto mt-12 max-w-4xl">
|
|
279
|
+
<div id="about-author" className="author-bio mx-auto mt-12 max-w-4xl">
|
|
255
280
|
<Separator className="mb-8" />
|
|
256
281
|
<Card className="border-[var(--blog-accent-border)] bg-[var(--blog-accent-bg)]">
|
|
257
282
|
<CardContent className="flex flex-col gap-4 p-5 sm:flex-row sm:items-start sm:p-6">
|
|
258
283
|
<div className="shrink-0">
|
|
259
284
|
<AuthorAvatar author={article.author} size="lg" />
|
|
260
285
|
</div>
|
|
261
|
-
<div className="min-w-0 space-y-2">
|
|
262
|
-
<div>
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
286
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
287
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
288
|
+
<div className="min-w-0">
|
|
289
|
+
<p className="text-muted-foreground text-sm font-medium">
|
|
290
|
+
Written by
|
|
291
|
+
</p>
|
|
292
|
+
<h2 className="text-foreground flex flex-wrap items-center gap-2 text-xl font-semibold">
|
|
293
|
+
{article.author.name}
|
|
294
|
+
{article.author.isGuest && (
|
|
295
|
+
<Badge variant="outline" className="text-xs">
|
|
296
|
+
Guest Author
|
|
297
|
+
</Badge>
|
|
298
|
+
)}
|
|
299
|
+
</h2>
|
|
300
|
+
</div>
|
|
301
|
+
<AuthorSocialLinks author={article.author} />
|
|
274
302
|
</div>
|
|
275
303
|
<p className="text-muted-foreground leading-relaxed">
|
|
276
304
|
{article.author.bio}
|
|
@@ -306,7 +334,7 @@ export default function ArticlePage({
|
|
|
306
334
|
<Separator className="mb-8" />
|
|
307
335
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
308
336
|
{navigation?.previousArticle && (
|
|
309
|
-
<Card className="group hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)]
|
|
337
|
+
<Card className="group cursor-pointer transition-colors hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)]">
|
|
310
338
|
<Link href={navigation.previousArticle.url} className="block">
|
|
311
339
|
{navigation.previousArticle.featuredImage && (
|
|
312
340
|
<ArticleCardImage
|
|
@@ -321,7 +349,7 @@ export default function ArticlePage({
|
|
|
321
349
|
<div className="text-muted-foreground mb-2 text-sm font-medium">
|
|
322
350
|
← Previous Article
|
|
323
351
|
</div>
|
|
324
|
-
<div className="text-foreground group-hover:text-[var(--blog-text-accent)]
|
|
352
|
+
<div className="text-foreground transition-colors group-hover:text-[var(--blog-text-accent)]">
|
|
325
353
|
{navigation.previousArticle.title}
|
|
326
354
|
</div>
|
|
327
355
|
</CardContent>
|
|
@@ -331,7 +359,7 @@ export default function ArticlePage({
|
|
|
331
359
|
{navigation?.nextArticle && (
|
|
332
360
|
<Card
|
|
333
361
|
className={cn(
|
|
334
|
-
"group hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)]
|
|
362
|
+
"group cursor-pointer transition-colors hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)]",
|
|
335
363
|
!navigation?.previousArticle && "md:col-start-2",
|
|
336
364
|
)}
|
|
337
365
|
>
|
|
@@ -349,7 +377,7 @@ export default function ArticlePage({
|
|
|
349
377
|
<div className="text-muted-foreground mb-2 text-sm font-medium">
|
|
350
378
|
Next Article →
|
|
351
379
|
</div>
|
|
352
|
-
<div className="text-foreground group-hover:text-[var(--blog-text-accent)]
|
|
380
|
+
<div className="text-foreground transition-colors group-hover:text-[var(--blog-text-accent)]">
|
|
353
381
|
{navigation.nextArticle.title}
|
|
354
382
|
</div>
|
|
355
383
|
</CardContent>
|
|
@@ -380,72 +408,6 @@ export default function ArticlePage({
|
|
|
380
408
|
);
|
|
381
409
|
}
|
|
382
410
|
|
|
383
|
-
function ArticleTableOfContents({
|
|
384
|
-
items,
|
|
385
|
-
variant,
|
|
386
|
-
}: {
|
|
387
|
-
items: ReturnType<typeof extractTableOfContents>;
|
|
388
|
-
variant: "mobile" | "desktop";
|
|
389
|
-
}) {
|
|
390
|
-
if (items.length === 0) {
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const list = (
|
|
395
|
-
<ul className="space-y-2 text-sm">
|
|
396
|
-
{items.map((item) => (
|
|
397
|
-
<li key={item.id} className={item.level === 3 ? "pl-4" : undefined}>
|
|
398
|
-
<a
|
|
399
|
-
href={`#${item.id}`}
|
|
400
|
-
className="text-muted-foreground hover:text-[var(--blog-text-accent)] block leading-snug transition-colors"
|
|
401
|
-
>
|
|
402
|
-
{item.text}
|
|
403
|
-
</a>
|
|
404
|
-
</li>
|
|
405
|
-
))}
|
|
406
|
-
</ul>
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
if (variant === "mobile") {
|
|
410
|
-
return (
|
|
411
|
-
<details className="group mb-6 rounded-2xl border border-[var(--blog-accent-border)] bg-[var(--blog-accent-bg)] lg:hidden">
|
|
412
|
-
<summary className="text-foreground flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 text-sm font-semibold [&::-webkit-details-marker]:hidden">
|
|
413
|
-
<span>On this page</span>
|
|
414
|
-
<span className="text-muted-foreground flex items-center gap-2 text-xs font-medium">
|
|
415
|
-
{formatTableOfContentsSummary(items.length)}
|
|
416
|
-
<span
|
|
417
|
-
className="text-[var(--blog-text-accent)] transition-transform group-open:rotate-180"
|
|
418
|
-
aria-hidden="true"
|
|
419
|
-
>
|
|
420
|
-
↓
|
|
421
|
-
</span>
|
|
422
|
-
</span>
|
|
423
|
-
</summary>
|
|
424
|
-
<nav
|
|
425
|
-
aria-label="Table of contents"
|
|
426
|
-
className="border-t border-[var(--blog-accent-border)] px-4 py-4"
|
|
427
|
-
>
|
|
428
|
-
{list}
|
|
429
|
-
</nav>
|
|
430
|
-
</details>
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return (
|
|
435
|
-
<aside className="hidden lg:block">
|
|
436
|
-
<nav
|
|
437
|
-
aria-label="Table of contents"
|
|
438
|
-
className="sticky top-24 max-h-[calc(100vh-8rem)] overflow-y-auto border-l border-border pl-5"
|
|
439
|
-
>
|
|
440
|
-
<p className="text-muted-foreground mb-3 text-xs font-semibold tracking-wider uppercase">
|
|
441
|
-
On this page
|
|
442
|
-
</p>
|
|
443
|
-
{list}
|
|
444
|
-
</nav>
|
|
445
|
-
</aside>
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
411
|
function AuthorAvatar({
|
|
450
412
|
author,
|
|
451
413
|
size,
|
|
@@ -482,6 +444,93 @@ function AuthorAvatar({
|
|
|
482
444
|
);
|
|
483
445
|
}
|
|
484
446
|
|
|
447
|
+
interface AuthorSocialLinkView {
|
|
448
|
+
key: string;
|
|
449
|
+
href: string;
|
|
450
|
+
label: string;
|
|
451
|
+
external: boolean;
|
|
452
|
+
icon: React.ReactNode;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function AuthorSocialLinks({ author }: { author: PublishedAuthor }) {
|
|
456
|
+
const links = getAuthorSocialLinks(author);
|
|
457
|
+
if (links.length === 0) return null;
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
<div className="flex flex-wrap items-center gap-1 sm:justify-end">
|
|
461
|
+
{links.map((link) => (
|
|
462
|
+
<a
|
|
463
|
+
key={link.key}
|
|
464
|
+
href={link.href}
|
|
465
|
+
target={link.external ? "_blank" : undefined}
|
|
466
|
+
rel={link.external ? "noopener noreferrer" : undefined}
|
|
467
|
+
aria-label={`${author.name} ${link.label}`}
|
|
468
|
+
className="text-muted-foreground hover:bg-background/60 hover:text-foreground inline-flex size-7 items-center justify-center rounded-full transition-colors"
|
|
469
|
+
>
|
|
470
|
+
{link.icon}
|
|
471
|
+
</a>
|
|
472
|
+
))}
|
|
473
|
+
</div>
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function getAuthorSocialLinks(author: PublishedAuthor): AuthorSocialLinkView[] {
|
|
478
|
+
const links = author.socialLinks;
|
|
479
|
+
if (!links) return [];
|
|
480
|
+
|
|
481
|
+
const socialLinks: Array<AuthorSocialLinkView | false | undefined> = [
|
|
482
|
+
links.website
|
|
483
|
+
? {
|
|
484
|
+
key: "website",
|
|
485
|
+
href: links.website,
|
|
486
|
+
label: "website",
|
|
487
|
+
external: true,
|
|
488
|
+
icon: <Globe className="size-4" aria-hidden="true" />,
|
|
489
|
+
}
|
|
490
|
+
: undefined,
|
|
491
|
+
links.x
|
|
492
|
+
? {
|
|
493
|
+
key: "x",
|
|
494
|
+
href: links.x,
|
|
495
|
+
label: "on X",
|
|
496
|
+
external: true,
|
|
497
|
+
icon: <FaXTwitter className="size-4" aria-hidden="true" />,
|
|
498
|
+
}
|
|
499
|
+
: undefined,
|
|
500
|
+
links.linkedin
|
|
501
|
+
? {
|
|
502
|
+
key: "linkedin",
|
|
503
|
+
href: links.linkedin,
|
|
504
|
+
label: "on LinkedIn",
|
|
505
|
+
external: true,
|
|
506
|
+
icon: <FaLinkedinIn className="size-4" aria-hidden="true" />,
|
|
507
|
+
}
|
|
508
|
+
: undefined,
|
|
509
|
+
links.github
|
|
510
|
+
? {
|
|
511
|
+
key: "github",
|
|
512
|
+
href: links.github,
|
|
513
|
+
label: "on GitHub",
|
|
514
|
+
external: true,
|
|
515
|
+
icon: <FaGithub className="size-4" aria-hidden="true" />,
|
|
516
|
+
}
|
|
517
|
+
: undefined,
|
|
518
|
+
links.email
|
|
519
|
+
? {
|
|
520
|
+
key: "email",
|
|
521
|
+
href: `mailto:${links.email}`,
|
|
522
|
+
label: "email",
|
|
523
|
+
external: false,
|
|
524
|
+
icon: <Mail className="size-4" aria-hidden="true" />,
|
|
525
|
+
}
|
|
526
|
+
: undefined,
|
|
527
|
+
];
|
|
528
|
+
|
|
529
|
+
return socialLinks.filter((link): link is AuthorSocialLinkView =>
|
|
530
|
+
Boolean(link),
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
485
534
|
function getAuthorInitials(name: string): string {
|
|
486
535
|
const initials = name
|
|
487
536
|
.trim()
|
|
@@ -528,36 +577,29 @@ function ArticleShareLinks({ title, url }: { title: string; url: string }) {
|
|
|
528
577
|
|
|
529
578
|
return (
|
|
530
579
|
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-3">
|
|
531
|
-
<
|
|
532
|
-
<Share2 className="size-4" aria-hidden="true" />
|
|
533
|
-
Share
|
|
534
|
-
</span>
|
|
580
|
+
<ArticleTopActions title={title} url={url} size="sm" />
|
|
535
581
|
<a
|
|
536
582
|
href={`https://x.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
|
|
537
583
|
target="_blank"
|
|
538
584
|
rel="noopener noreferrer"
|
|
539
585
|
aria-label="Share on X"
|
|
540
|
-
className="text-muted-foreground hover:border-
|
|
586
|
+
className="text-muted-foreground hover:text-foreground border-border inline-flex size-9 items-center justify-center rounded-full border grayscale transition-colors hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)]"
|
|
541
587
|
>
|
|
542
|
-
<
|
|
543
|
-
X
|
|
544
|
-
</span>
|
|
588
|
+
<FaXTwitter className="size-4" aria-hidden="true" />
|
|
545
589
|
</a>
|
|
546
590
|
<a
|
|
547
591
|
href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`}
|
|
548
592
|
target="_blank"
|
|
549
593
|
rel="noopener noreferrer"
|
|
550
594
|
aria-label="Share on LinkedIn"
|
|
551
|
-
className="text-muted-foreground hover:border-
|
|
595
|
+
className="text-muted-foreground hover:text-foreground border-border inline-flex size-9 items-center justify-center rounded-full border grayscale transition-colors hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)]"
|
|
552
596
|
>
|
|
553
|
-
<
|
|
554
|
-
in
|
|
555
|
-
</span>
|
|
597
|
+
<FaLinkedinIn className="size-4" aria-hidden="true" />
|
|
556
598
|
</a>
|
|
557
599
|
<a
|
|
558
600
|
href={`mailto:?subject=${encodedTitle}&body=${encodedUrl}`}
|
|
559
601
|
aria-label="Share by email"
|
|
560
|
-
className="text-muted-foreground hover:border-
|
|
602
|
+
className="text-muted-foreground hover:text-foreground border-border inline-flex size-9 items-center justify-center rounded-full border grayscale transition-colors hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)]"
|
|
561
603
|
>
|
|
562
604
|
<Mail className="size-4" aria-hidden="true" />
|
|
563
605
|
</a>
|
|
@@ -582,7 +624,7 @@ function RelatedArticles({
|
|
|
582
624
|
<Link
|
|
583
625
|
key={article.id}
|
|
584
626
|
href={buildBlogPath(basePath, article.slug)}
|
|
585
|
-
className="group overflow-hidden rounded-2xl border
|
|
627
|
+
className="group border-border overflow-hidden rounded-2xl border transition-colors hover:border-[var(--blog-accent-border)] hover:bg-[var(--blog-accent-bg)] hover:no-underline"
|
|
586
628
|
>
|
|
587
629
|
{article.featuredImage && (
|
|
588
630
|
<ArticleCardImage
|
|
@@ -594,7 +636,7 @@ function RelatedArticles({
|
|
|
594
636
|
/>
|
|
595
637
|
)}
|
|
596
638
|
<div className="p-4">
|
|
597
|
-
<h3 className="text-foreground group-hover:text-[var(--blog-text-accent)]
|
|
639
|
+
<h3 className="text-foreground line-clamp-3 font-semibold transition-colors group-hover:text-[var(--blog-text-accent)]">
|
|
598
640
|
{article.title}
|
|
599
641
|
</h3>
|
|
600
642
|
{article.excerpt && (
|
|
@@ -612,7 +654,7 @@ function RelatedArticles({
|
|
|
612
654
|
|
|
613
655
|
function ArticleCardImage({ src, alt }: { src: string; alt: string }) {
|
|
614
656
|
return (
|
|
615
|
-
<div className="aspect-[16/9] overflow-hidden border-b
|
|
657
|
+
<div className="border-border aspect-[16/9] overflow-hidden border-b bg-[var(--blog-accent-bg)]">
|
|
616
658
|
<Image
|
|
617
659
|
src={src}
|
|
618
660
|
alt={alt}
|