hd-garden 0.10.0 → 0.10.2
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/app/about/page.tsx +1 -1
- package/app/blog/[...path]/page.tsx +11 -80
- package/app/blog/page.tsx +5 -4
- package/app/columns/[column]/[...path]/page.tsx +31 -93
- package/app/columns/page.tsx +6 -4
- package/app/components/HandbookView.tsx +6 -6
- package/app/components/MarkdownArticle.tsx +10 -3
- package/app/components/PageIntro.tsx +9 -4
- package/app/components/PostReadingView.tsx +133 -0
- package/app/components/SeriesEdgeNav.tsx +119 -0
- package/app/layout.tsx +3 -3
- package/app/lib/i18n.ts +6 -2
- package/app/lib/remark-link-card.mjs +11 -12
- package/app/page.tsx +3 -1
- package/app/styles/a2.css +204 -0
- package/app/thoughts/page/[n]/page.tsx +1 -1
- package/app/thoughts/page.tsx +1 -1
- package/docs/handbook/components/link-card.md +5 -5
- package/docs/handbook/en/components/link-card.md +5 -5
- package/package.json +1 -1
- package/public/og.png +0 -0
- package/scripts/build-content.mjs +2 -3
- package/scripts/link-assets.mjs +7 -5
- package/scripts/link-meta-core.mjs +40 -3
- package/ui/garden/markdown-links.mjs +44 -8
- package/ui/garden/site-nav.mjs +3 -1
package/app/about/page.tsx
CHANGED
|
@@ -1,34 +1,18 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { CoverImage } from "../../components/CoverImage";
|
|
3
1
|
import type { Metadata } from "next";
|
|
4
2
|
import { FEED_ALTERNATES } from "../../lib/page-alternates";
|
|
5
3
|
import { notFound } from "next/navigation";
|
|
6
4
|
import {
|
|
7
|
-
extractHeadings,
|
|
8
|
-
estimateWordCount,
|
|
9
5
|
findPost,
|
|
10
|
-
formatDate,
|
|
11
6
|
postHref,
|
|
12
7
|
posts,
|
|
13
8
|
} from "../../lib/content";
|
|
14
|
-
import {
|
|
15
|
-
import { ArticleReferences } from "../../components/ArticleReferences";
|
|
16
|
-
import { ArticleTitle, originalUrl } from "../../components/ArticleTitle";
|
|
17
|
-
import { MarkdownArticle } from "../../components/MarkdownArticle";
|
|
18
|
-
import { TableOfContents } from "../../components/TableOfContents";
|
|
19
|
-
import { Comments } from "../../components/Comments";
|
|
20
|
-
import { Webmentions } from "../../components/Webmentions";
|
|
21
|
-
import { ArticleTags } from "../../components/ArticleTags";
|
|
22
|
-
import { ArticlePager } from "../../components/ArticlePager";
|
|
9
|
+
import { PostReadingView } from "../../components/PostReadingView";
|
|
23
10
|
import { adjacentByDate } from "../../../ui/garden/adjacent.mjs";
|
|
24
11
|
import {
|
|
25
12
|
absoluteUrl,
|
|
26
13
|
getSiteUrl,
|
|
27
|
-
webmentionApiEndpoint,
|
|
28
14
|
} from "../../lib/site";
|
|
29
|
-
import { githubUrl, siteConfig } from "../../site.config";
|
|
30
15
|
import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../ui/garden/static-params.mjs";
|
|
31
|
-
import { siteLang, ui } from "../../lib/i18n";
|
|
32
16
|
|
|
33
17
|
export function generateStaticParams() {
|
|
34
18
|
return staticParams(posts.map((post) => ({ path: post.path.split("/") })), { path: [STATIC_PLACEHOLDER] });
|
|
@@ -61,10 +45,10 @@ export async function generateMetadata({
|
|
|
61
45
|
images: post.cover ? [{ url: post.cover, alt: post.title }] : undefined,
|
|
62
46
|
},
|
|
63
47
|
twitter: {
|
|
64
|
-
card: "summary_large_image",
|
|
48
|
+
card: post.cover ? "summary_large_image" : "summary",
|
|
65
49
|
title: post.title,
|
|
66
50
|
description: post.description,
|
|
67
|
-
images: post.cover ? [post.cover] :
|
|
51
|
+
images: post.cover ? [post.cover] : undefined,
|
|
68
52
|
},
|
|
69
53
|
} satisfies Metadata;
|
|
70
54
|
}
|
|
@@ -80,72 +64,19 @@ export default async function PostPage({
|
|
|
80
64
|
if (isStaticPlaceholder(path)) return null;
|
|
81
65
|
notFound();
|
|
82
66
|
}
|
|
83
|
-
const headings = extractHeadings(post.content, { numbering: post.numbering });
|
|
84
67
|
const baseUrl = await getSiteUrl();
|
|
85
68
|
const pathname = postHref(post);
|
|
86
69
|
const canonicalUrl = absoluteUrl(pathname, baseUrl);
|
|
87
|
-
const wordCount = estimateWordCount(post.content);
|
|
88
70
|
const { prev, next } = adjacentByDate(posts, (item: { path: string }) => item.path === post.path);
|
|
89
|
-
const t = ui();
|
|
90
71
|
|
|
91
72
|
return (
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
rel="author"
|
|
101
|
-
>
|
|
102
|
-
{siteConfig.author.name}
|
|
103
|
-
</a>
|
|
104
|
-
<header className="article-header article-summary-row hero">
|
|
105
|
-
<ArticleTitle title={post.title} />
|
|
106
|
-
<ul className="meta article-meta">
|
|
107
|
-
<li>{siteConfig.author.name}</li>
|
|
108
|
-
<li>
|
|
109
|
-
<time className="dt-published" dateTime={post.date}>
|
|
110
|
-
{formatDate(post.date)}
|
|
111
|
-
</time>
|
|
112
|
-
</li>
|
|
113
|
-
{post.updated ? <li>{siteLang === "en" ? t.updatedOn(formatDate(post.updated)) : <>更新于 {formatDate(post.updated)}</>}</li> : null}
|
|
114
|
-
<li>{t.wordCount(wordCount)}</li>
|
|
115
|
-
{originalUrl(post) ? (
|
|
116
|
-
<li>
|
|
117
|
-
<a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer">{t.originalLink}</a>
|
|
118
|
-
</li>
|
|
119
|
-
) : null}
|
|
120
|
-
</ul>
|
|
121
|
-
<ArticleTags tags={post.tags} />
|
|
122
|
-
{post.cover && (
|
|
123
|
-
<figure className="article-hero article-cover cover-card cover-img">
|
|
124
|
-
<CoverImage className="u-photo" src={post.cover} image={post.coverImage} alt={t.coverLabel(post.title)} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
|
|
125
|
-
</figure>
|
|
126
|
-
)}
|
|
127
|
-
</header>
|
|
128
|
-
<div className="article-reading-layout">
|
|
129
|
-
<div className="article-reading-column">
|
|
130
|
-
<ArticleMeasure />
|
|
131
|
-
<div className="e-content">
|
|
132
|
-
{post.description ? <p className="lead quote-plain p-summary">{post.description}</p> : null}
|
|
133
|
-
<MarkdownArticle content={post.content} numbering={post.numbering} />
|
|
134
|
-
</div>
|
|
135
|
-
<ArticleReferences references={post.references} />
|
|
136
|
-
<Webmentions
|
|
137
|
-
target={canonicalUrl}
|
|
138
|
-
endpoint={webmentionApiEndpoint()}
|
|
139
|
-
/>
|
|
140
|
-
<ArticlePager
|
|
141
|
-
prev={prev ? { href: postHref(prev), title: prev.title } : null}
|
|
142
|
-
next={next ? { href: postHref(next), title: next.title } : null}
|
|
143
|
-
/>
|
|
144
|
-
<Comments slug={post.path} />
|
|
145
|
-
</div>
|
|
146
|
-
<TableOfContents headings={headings} />
|
|
147
|
-
</div>
|
|
148
|
-
</article>
|
|
149
|
-
</main>
|
|
73
|
+
<PostReadingView
|
|
74
|
+
post={post}
|
|
75
|
+
pathname={pathname}
|
|
76
|
+
canonicalUrl={canonicalUrl}
|
|
77
|
+
commentsSlug={post.path}
|
|
78
|
+
prev={prev ? { href: postHref(prev), title: prev.title } : null}
|
|
79
|
+
next={next ? { href: postHref(next), title: next.title } : null}
|
|
80
|
+
/>
|
|
150
81
|
);
|
|
151
82
|
}
|
package/app/blog/page.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Suspense } from "react";
|
|
2
2
|
import { siteConfig, siteDocumentTitle } from "../site.config";
|
|
3
3
|
import { BlogArchive, BlogArchiveFallback } from "../components/BlogArchive";
|
|
4
|
+
import { PageIntro } from "../components/PageIntro";
|
|
4
5
|
import { posts } from "../lib/content";
|
|
5
6
|
import { archivePosts, archiveSeries } from "../lib/archive";
|
|
6
7
|
import { pageAlternates } from "../lib/page-alternates";
|
|
@@ -16,12 +17,12 @@ export default function BlogIndex() {
|
|
|
16
17
|
const t = ui();
|
|
17
18
|
const list = archivePosts();
|
|
18
19
|
const series = archiveSeries();
|
|
20
|
+
const deck =
|
|
21
|
+
siteConfig.pages.blog.subtitle?.trim() ||
|
|
22
|
+
(siteLang === "en" ? t.blogDeck(posts.length) : `阶段性的完整沉淀,关于工程、架构与技术思考。共 ${posts.length} 篇,按年份归档。`);
|
|
19
23
|
return (
|
|
20
24
|
<main className="sh-main" id="content">
|
|
21
|
-
<
|
|
22
|
-
<h1 id="sh-title">{t.blogHeading}</h1>
|
|
23
|
-
<p className="sh-deck">{siteLang === "en" ? t.blogDeck(posts.length) : <>阶段性的完整沉淀,关于工程、架构与技术思考。共 {posts.length} 篇,按年份归档。</>}</p>
|
|
24
|
-
</div>
|
|
25
|
+
<PageIntro eyebrow={t.blogEyebrow} title={t.blogHeading} subtitle={deck} />
|
|
25
26
|
<Suspense fallback={<BlogArchiveFallback posts={list} series={series} />}>
|
|
26
27
|
<BlogArchive posts={list} series={series} />
|
|
27
28
|
</Suspense>
|
|
@@ -1,35 +1,21 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { ArticlePager } from "../../../components/ArticlePager";
|
|
3
|
-
import { CoverImage } from "../../../components/CoverImage";
|
|
4
1
|
import type { Metadata } from "next";
|
|
5
2
|
import { FEED_ALTERNATES } from "../../../lib/page-alternates";
|
|
6
3
|
import { notFound } from "next/navigation";
|
|
7
|
-
import { ArrowLeft } from "@phosphor-icons/react/ssr";
|
|
8
4
|
import {
|
|
9
5
|
columns,
|
|
10
|
-
extractHeadings,
|
|
11
6
|
findColumnEntry,
|
|
12
|
-
formatDate,
|
|
13
7
|
columnHref,
|
|
14
8
|
columnEntryPath,
|
|
15
9
|
} from "../../../lib/content";
|
|
16
|
-
import {
|
|
17
|
-
import { ArticleReferences } from "../../../components/ArticleReferences";
|
|
18
|
-
import { ArticleTitle, originalUrl } from "../../../components/ArticleTitle";
|
|
19
|
-
import { MarkdownArticle } from "../../../components/MarkdownArticle";
|
|
20
|
-
import { TableOfContents } from "../../../components/TableOfContents";
|
|
21
|
-
import { Comments } from "../../../components/Comments";
|
|
22
|
-
import { Webmentions } from "../../../components/Webmentions";
|
|
23
|
-
import { ArticleTags } from "../../../components/ArticleTags";
|
|
10
|
+
import { PostReadingView } from "../../../components/PostReadingView";
|
|
24
11
|
import {
|
|
25
12
|
absoluteUrl,
|
|
26
13
|
getSiteUrl,
|
|
27
|
-
webmentionApiEndpoint,
|
|
28
14
|
} from "../../../lib/site";
|
|
29
|
-
import { githubUrl, siteConfig } from "../../../site.config";
|
|
30
|
-
import { siteHref } from "../../../lib/site-href";
|
|
31
15
|
import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../../ui/garden/static-params.mjs";
|
|
32
|
-
import { columnStatusLabel,
|
|
16
|
+
import { columnStatusLabel, ui } from "../../../lib/i18n";
|
|
17
|
+
|
|
18
|
+
const stripColumnIndex = (title: string) => title.replace(/^\d+\s*·\s*/u, "");
|
|
33
19
|
|
|
34
20
|
export function generateStaticParams() {
|
|
35
21
|
return staticParams(
|
|
@@ -51,14 +37,15 @@ export async function generateMetadata({
|
|
|
51
37
|
if (!entry) return {};
|
|
52
38
|
const baseUrl = await getSiteUrl();
|
|
53
39
|
const pathname = columnHref(entry);
|
|
40
|
+
const title = stripColumnIndex(entry.title);
|
|
54
41
|
return {
|
|
55
|
-
title
|
|
42
|
+
title,
|
|
56
43
|
description: entry.description,
|
|
57
44
|
alternates: { canonical: pathname, types: FEED_ALTERNATES },
|
|
58
45
|
openGraph: {
|
|
59
46
|
type: "article",
|
|
60
47
|
url: absoluteUrl(pathname, baseUrl),
|
|
61
|
-
title
|
|
48
|
+
title,
|
|
62
49
|
description: entry.description,
|
|
63
50
|
publishedTime: new Date(entry.date).toISOString(),
|
|
64
51
|
modifiedTime: entry.updated
|
|
@@ -68,10 +55,10 @@ export async function generateMetadata({
|
|
|
68
55
|
images: entry.cover ? [{ url: entry.cover, alt: entry.title }] : undefined,
|
|
69
56
|
},
|
|
70
57
|
twitter: {
|
|
71
|
-
card: "summary_large_image",
|
|
72
|
-
title
|
|
58
|
+
card: entry.cover ? "summary_large_image" : "summary",
|
|
59
|
+
title,
|
|
73
60
|
description: entry.description,
|
|
74
|
-
images: entry.cover ? [entry.cover] :
|
|
61
|
+
images: entry.cover ? [entry.cover] : undefined,
|
|
75
62
|
},
|
|
76
63
|
} satisfies Metadata;
|
|
77
64
|
}
|
|
@@ -92,81 +79,32 @@ export default async function ColumnEntryPage({
|
|
|
92
79
|
const currentIndex = series.findIndex((item) => item.path === entry.path);
|
|
93
80
|
const prev = series[currentIndex - 1] ?? null;
|
|
94
81
|
const next = series[currentIndex + 1] ?? null;
|
|
95
|
-
const headings = extractHeadings(entry.content, { numbering: entry.numbering });
|
|
96
82
|
const baseUrl = await getSiteUrl();
|
|
97
83
|
const pathname = columnHref(entry);
|
|
98
84
|
const canonicalUrl = absoluteUrl(pathname, baseUrl);
|
|
99
85
|
const t = ui();
|
|
86
|
+
const displayTitle = stripColumnIndex(entry.title);
|
|
100
87
|
|
|
101
88
|
return (
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{siteLang === "en" ? t.permalinkLabel(entry.title) : <>{entry.title} 永久链接</>}
|
|
123
|
-
</Link>
|
|
124
|
-
<a
|
|
125
|
-
className="p-author h-card sr-only"
|
|
126
|
-
href={githubUrl}
|
|
127
|
-
rel="author"
|
|
128
|
-
>
|
|
129
|
-
{siteConfig.author.name}
|
|
130
|
-
</a>
|
|
131
|
-
<header className="article-header hero">
|
|
132
|
-
<ArticleTitle title={entry.title.replace(/^\d+\s*·\s*/u, "")} />
|
|
133
|
-
<ul className="meta article-meta">
|
|
134
|
-
<li>{siteConfig.author.name}</li>
|
|
135
|
-
<li>
|
|
136
|
-
<time className="dt-published" dateTime={entry.date}>
|
|
137
|
-
{formatDate(entry.date)}
|
|
138
|
-
</time>
|
|
139
|
-
</li>
|
|
140
|
-
{originalUrl(entry) ? (
|
|
141
|
-
<li><a className="orig ext" href={originalUrl(entry)} target="_blank" rel="noopener noreferrer">{t.originalLink}</a></li>
|
|
142
|
-
) : null}
|
|
143
|
-
</ul>
|
|
144
|
-
<ArticleTags tags={entry.tags} />
|
|
145
|
-
{entry.cover && (
|
|
146
|
-
<figure className="article-hero article-cover cover-img">
|
|
147
|
-
<CoverImage className="u-photo" src={entry.cover} image={entry.coverImage} alt={siteLang === "en" ? t.coverLabel(entry.title) : `${entry.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
|
|
148
|
-
</figure>
|
|
149
|
-
)}
|
|
150
|
-
</header>
|
|
151
|
-
<ArticleMeasure />
|
|
152
|
-
<div className="e-content">
|
|
153
|
-
{entry.description ? <p className="lead p-summary">{entry.description}</p> : null}
|
|
154
|
-
<MarkdownArticle content={entry.content} numbering={entry.numbering} />
|
|
155
|
-
</div>
|
|
156
|
-
<ArticleReferences references={entry.references} />
|
|
157
|
-
<Webmentions
|
|
158
|
-
target={canonicalUrl}
|
|
159
|
-
endpoint={webmentionApiEndpoint()}
|
|
160
|
-
/>
|
|
161
|
-
<ArticlePager
|
|
162
|
-
label={ui().columnPagerLabel}
|
|
163
|
-
prev={prev ? { href: columnHref(prev), title: prev.title } : null}
|
|
164
|
-
next={next ? { href: columnHref(next), title: next.title } : null}
|
|
165
|
-
/>
|
|
166
|
-
<Comments slug={`${column}:${entry.path}`} />
|
|
167
|
-
</article>
|
|
168
|
-
|
|
169
|
-
<TableOfContents headings={headings} />
|
|
170
|
-
</main>
|
|
89
|
+
<PostReadingView
|
|
90
|
+
post={entry}
|
|
91
|
+
pathname={pathname}
|
|
92
|
+
canonicalUrl={canonicalUrl}
|
|
93
|
+
title={displayTitle}
|
|
94
|
+
commentsSlug={`${column}:${entry.path}`}
|
|
95
|
+
pagerLabel={t.columnPagerLabel}
|
|
96
|
+
prev={prev ? { href: columnHref(prev), title: stripColumnIndex(prev.title) } : null}
|
|
97
|
+
next={next ? { href: columnHref(next), title: stripColumnIndex(next.title) } : null}
|
|
98
|
+
series={{
|
|
99
|
+
columnTitle: entry.columnTitle ?? column,
|
|
100
|
+
statusLabel: t.columnSideCount(series.length, columnStatusLabel(entry.columnStatus)),
|
|
101
|
+
items: listed.map((item) => ({
|
|
102
|
+
path: item.path,
|
|
103
|
+
href: columnHref(item),
|
|
104
|
+
title: stripColumnIndex(item.title),
|
|
105
|
+
current: item.path === entry.path,
|
|
106
|
+
})),
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
171
109
|
);
|
|
172
110
|
}
|
package/app/columns/page.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { columnHref, columns } from "../lib/content";
|
|
4
4
|
import { ContentEmptyState } from "../components/ContentEmptyState";
|
|
5
|
+
import { PageIntro } from "../components/PageIntro";
|
|
5
6
|
import { siteConfig, siteDocumentTitle } from "../site.config";
|
|
6
7
|
import { pageAlternates } from "../lib/page-alternates";
|
|
7
8
|
import { columnStatusLabel, ui } from "../lib/i18n";
|
|
@@ -40,10 +41,11 @@ export default function ColumnsPage() {
|
|
|
40
41
|
|
|
41
42
|
return (
|
|
42
43
|
<main className="sh-main" id="content">
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
{siteConfig.pages.columns.
|
|
46
|
-
|
|
44
|
+
<PageIntro
|
|
45
|
+
eyebrow={t.columnsEyebrow}
|
|
46
|
+
title={siteConfig.pages.columns.title || t.homeColumns}
|
|
47
|
+
subtitle={siteConfig.pages.columns.subtitle}
|
|
48
|
+
/>
|
|
47
49
|
{grouped.length ? grouped.map(({ slug, first, entries }) => (
|
|
48
50
|
<section className={first.columnCover ? "sh-series" : "sh-series no-cover"} aria-labelledby={slug} key={slug}>
|
|
49
51
|
{first.columnCover ? (
|
|
@@ -64,8 +64,8 @@ export async function handbookOverviewMetadata(lang: HandbookLang, indexDoc: Gar
|
|
|
64
64
|
images: [
|
|
65
65
|
{
|
|
66
66
|
url: "/og.png",
|
|
67
|
-
width:
|
|
68
|
-
height:
|
|
67
|
+
width: 1200,
|
|
68
|
+
height: 630,
|
|
69
69
|
alt: imageAlt,
|
|
70
70
|
},
|
|
71
71
|
],
|
|
@@ -94,8 +94,8 @@ export async function handbookDocMetadata(lang: HandbookLang, doc: GardenDocEntr
|
|
|
94
94
|
images: [
|
|
95
95
|
{
|
|
96
96
|
url: "/og.png",
|
|
97
|
-
width:
|
|
98
|
-
height:
|
|
97
|
+
width: 1200,
|
|
98
|
+
height: 630,
|
|
99
99
|
alt: fullTitle,
|
|
100
100
|
},
|
|
101
101
|
],
|
|
@@ -125,8 +125,8 @@ export async function handbookDocMetadata(lang: HandbookLang, doc: GardenDocEntr
|
|
|
125
125
|
images: [
|
|
126
126
|
{
|
|
127
127
|
url: "/og.png",
|
|
128
|
-
width:
|
|
129
|
-
height:
|
|
128
|
+
width: 1200,
|
|
129
|
+
height: 630,
|
|
130
130
|
alt: `${siteConfig.name} · ${docTitle}`,
|
|
131
131
|
},
|
|
132
132
|
],
|
|
@@ -292,7 +292,7 @@ export function MarkdownArticle({ content, numbering, lang, fieldTables, refDemo
|
|
|
292
292
|
<FaviconProvider favicons={linkAssets?.favicons}>
|
|
293
293
|
<div className="markdown-body prose">
|
|
294
294
|
<ReactMarkdown
|
|
295
|
-
remarkPlugins={[...(refDemos ? [remarkRefDemos] : []), ...markdownPlugins,
|
|
295
|
+
remarkPlugins={[...(refDemos ? [remarkRefDemos] : []), ...markdownPlugins, remarkLinkCard, [remarkA2, { imageSize: (src: string) => lookupImageSize(imageSizes, src), lang: lang ?? siteLang }]]}
|
|
296
296
|
rehypePlugins={[
|
|
297
297
|
rehypeRaw,
|
|
298
298
|
rehypeKatex,
|
|
@@ -311,11 +311,18 @@ export function MarkdownArticle({ content, numbering, lang, fieldTables, refDemo
|
|
|
311
311
|
const only = nodes.length === 1 && isValidElement<{ href?: string; children?: ReactNode }>(nodes[0]) ? nodes[0] : null;
|
|
312
312
|
const href = only?.props.href;
|
|
313
313
|
const text = only ? headingText(only.props.children).trim() : "";
|
|
314
|
-
|
|
314
|
+
// Bare http(s) URL alone in a paragraph → link card (same path for every jump link).
|
|
315
|
+
if (href && text === href && /^https?:\/\//iu.test(href)) return <LinkCard href={href} />;
|
|
315
316
|
return <p {...domProps}>{children}</p>;
|
|
316
317
|
},
|
|
317
318
|
a: ({ href: written, children, ...props }) => {
|
|
318
|
-
const
|
|
319
|
+
const label = headingText(children).trim();
|
|
320
|
+
// Bare URL jump links stay absolute so the paragraph card path is one pipeline
|
|
321
|
+
// for same-site and external alike. Titled same-site prose still uses Link.
|
|
322
|
+
const bareJump = Boolean(written && label === written && /^https?:\/\//iu.test(written));
|
|
323
|
+
const { href, external } = bareJump
|
|
324
|
+
? { href: written as string, external: true }
|
|
325
|
+
: articleLink(written, sameSite);
|
|
319
326
|
const domProps = { ...props };
|
|
320
327
|
Reflect.deleteProperty(domProps, "node");
|
|
321
328
|
// A link to another page of the site navigates in place, like the header and lists do.
|
|
@@ -7,15 +7,20 @@ type PageIntroProps = {
|
|
|
7
7
|
children?: ReactNode;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Shared heading for top-level list pages (blog / thoughts / columns).
|
|
12
|
+
* Same eyebrow + h1 + deck typography and padding so tab switches do not jump.
|
|
13
|
+
* Empty subtitle reserves no space.
|
|
14
|
+
*/
|
|
11
15
|
export function PageIntro({
|
|
12
16
|
eyebrow,
|
|
13
17
|
title,
|
|
14
18
|
subtitle,
|
|
15
19
|
children,
|
|
16
20
|
}: PageIntroProps) {
|
|
21
|
+
const deck = subtitle?.trim();
|
|
17
22
|
return (
|
|
18
|
-
<header className="page-intro">
|
|
23
|
+
<header className="page-intro sh-head">
|
|
19
24
|
{eyebrow ? <p className="eyebrow">{eyebrow}</p> : null}
|
|
20
25
|
<h1>
|
|
21
26
|
{title.split("\n").map((line, index) => (
|
|
@@ -25,8 +30,8 @@ export function PageIntro({
|
|
|
25
30
|
</span>
|
|
26
31
|
))}
|
|
27
32
|
</h1>
|
|
28
|
-
{
|
|
29
|
-
{children
|
|
33
|
+
{deck ? <p className="sh-deck">{deck}</p> : null}
|
|
34
|
+
{children ? <div className="page-intro-actions">{children}</div> : null}
|
|
30
35
|
</header>
|
|
31
36
|
);
|
|
32
37
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { CoverImage } from "./CoverImage";
|
|
3
|
+
import { ArticleMeasure } from "./ArticleMeasure";
|
|
4
|
+
import { ArticleReferences } from "./ArticleReferences";
|
|
5
|
+
import { ArticleTitle, originalUrl } from "./ArticleTitle";
|
|
6
|
+
import { MarkdownArticle } from "./MarkdownArticle";
|
|
7
|
+
import { TableOfContents } from "./TableOfContents";
|
|
8
|
+
import { Comments } from "./Comments";
|
|
9
|
+
import { Webmentions } from "./Webmentions";
|
|
10
|
+
import { ArticleTags } from "./ArticleTags";
|
|
11
|
+
import { ArticlePager } from "./ArticlePager";
|
|
12
|
+
import { SeriesEdgeNav, type SeriesEdgeItem } from "./SeriesEdgeNav";
|
|
13
|
+
import { formatDate, estimateWordCount, extractHeadings } from "../lib/content";
|
|
14
|
+
import { githubUrl, siteConfig } from "../site.config";
|
|
15
|
+
import { siteLang, ui } from "../lib/i18n";
|
|
16
|
+
import { webmentionApiEndpoint } from "../lib/site";
|
|
17
|
+
import type { ContentEntry } from "../lib/content";
|
|
18
|
+
|
|
19
|
+
export type PostReadingPagerLink = { href: string; title: string } | null;
|
|
20
|
+
|
|
21
|
+
export type PostReadingSeries = {
|
|
22
|
+
columnTitle: string;
|
|
23
|
+
statusLabel?: string;
|
|
24
|
+
items: SeriesEdgeItem[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Shared article body for /blog/... and /columns/... .
|
|
29
|
+
* Column routes only differ by mounting SeriesEdgeNav on the left.
|
|
30
|
+
*/
|
|
31
|
+
export function PostReadingView({
|
|
32
|
+
post,
|
|
33
|
+
pathname,
|
|
34
|
+
canonicalUrl,
|
|
35
|
+
title,
|
|
36
|
+
commentsSlug,
|
|
37
|
+
prev,
|
|
38
|
+
next,
|
|
39
|
+
pagerLabel,
|
|
40
|
+
series,
|
|
41
|
+
}: {
|
|
42
|
+
post: ContentEntry;
|
|
43
|
+
pathname: string;
|
|
44
|
+
canonicalUrl: string;
|
|
45
|
+
/** Display title; defaults to post.title */
|
|
46
|
+
title?: string;
|
|
47
|
+
commentsSlug: string;
|
|
48
|
+
prev?: PostReadingPagerLink;
|
|
49
|
+
next?: PostReadingPagerLink;
|
|
50
|
+
pagerLabel?: string;
|
|
51
|
+
series?: PostReadingSeries;
|
|
52
|
+
}) {
|
|
53
|
+
const displayTitle = title ?? post.title;
|
|
54
|
+
const numbering = (post as ContentEntry & { numbering?: boolean }).numbering;
|
|
55
|
+
const headings = extractHeadings(post.content, { numbering });
|
|
56
|
+
const wordCount = estimateWordCount(post.content);
|
|
57
|
+
const t = ui();
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<main className="reading-page post-reading-page">
|
|
61
|
+
{series ? (
|
|
62
|
+
<SeriesEdgeNav
|
|
63
|
+
columnTitle={series.columnTitle}
|
|
64
|
+
statusLabel={series.statusLabel}
|
|
65
|
+
items={series.items}
|
|
66
|
+
/>
|
|
67
|
+
) : null}
|
|
68
|
+
<article className="post-article h-entry">
|
|
69
|
+
<Link className="u-url sr-only" href={pathname}>
|
|
70
|
+
{siteLang === "en" ? t.permalinkLabel(displayTitle) : <>{displayTitle} 永久链接</>}
|
|
71
|
+
</Link>
|
|
72
|
+
<a className="p-author h-card sr-only" href={githubUrl} rel="author">
|
|
73
|
+
{siteConfig.author.name}
|
|
74
|
+
</a>
|
|
75
|
+
<header className="article-header article-summary-row hero">
|
|
76
|
+
<ArticleTitle title={displayTitle} />
|
|
77
|
+
<ul className="meta article-meta">
|
|
78
|
+
<li>{siteConfig.author.name}</li>
|
|
79
|
+
<li>
|
|
80
|
+
<time className="dt-published" dateTime={post.date}>
|
|
81
|
+
{formatDate(post.date)}
|
|
82
|
+
</time>
|
|
83
|
+
</li>
|
|
84
|
+
{post.updated ? (
|
|
85
|
+
<li>
|
|
86
|
+
{siteLang === "en" ? t.updatedOn(formatDate(post.updated)) : <>更新于 {formatDate(post.updated)}</>}
|
|
87
|
+
</li>
|
|
88
|
+
) : null}
|
|
89
|
+
<li>{t.wordCount(wordCount)}</li>
|
|
90
|
+
{originalUrl(post) ? (
|
|
91
|
+
<li>
|
|
92
|
+
<a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer">
|
|
93
|
+
{t.originalLink}
|
|
94
|
+
</a>
|
|
95
|
+
</li>
|
|
96
|
+
) : null}
|
|
97
|
+
</ul>
|
|
98
|
+
<ArticleTags tags={post.tags} />
|
|
99
|
+
{post.cover ? (
|
|
100
|
+
<figure className="article-hero article-cover cover-card cover-img">
|
|
101
|
+
<CoverImage
|
|
102
|
+
className="u-photo"
|
|
103
|
+
src={post.cover}
|
|
104
|
+
image={post.coverImage}
|
|
105
|
+
alt={t.coverLabel(displayTitle)}
|
|
106
|
+
sizes="(max-width: 852px) calc(100vw - 40px), 812px"
|
|
107
|
+
priority
|
|
108
|
+
/>
|
|
109
|
+
</figure>
|
|
110
|
+
) : null}
|
|
111
|
+
</header>
|
|
112
|
+
<div className="article-reading-layout">
|
|
113
|
+
<div className="article-reading-column">
|
|
114
|
+
<ArticleMeasure />
|
|
115
|
+
<div className="e-content">
|
|
116
|
+
{post.description ? <p className="lead quote-plain p-summary">{post.description}</p> : null}
|
|
117
|
+
<MarkdownArticle content={post.content} numbering={numbering} />
|
|
118
|
+
</div>
|
|
119
|
+
<ArticleReferences references={post.references} />
|
|
120
|
+
<Webmentions target={canonicalUrl} endpoint={webmentionApiEndpoint()} />
|
|
121
|
+
<ArticlePager
|
|
122
|
+
label={pagerLabel}
|
|
123
|
+
prev={prev ?? null}
|
|
124
|
+
next={next ?? null}
|
|
125
|
+
/>
|
|
126
|
+
<Comments slug={commentsSlug} />
|
|
127
|
+
</div>
|
|
128
|
+
<TableOfContents headings={headings} />
|
|
129
|
+
</div>
|
|
130
|
+
</article>
|
|
131
|
+
</main>
|
|
132
|
+
);
|
|
133
|
+
}
|