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.
@@ -23,7 +23,7 @@ export const metadata = {
23
23
 
24
24
  export default function AboutPage() {
25
25
  return (
26
- <main className="page-shell about-page">
26
+ <main className="sh-main about-page" id="content">
27
27
  <PageIntro
28
28
  eyebrow={t.aboutEyebrow}
29
29
  title={about.title}
@@ -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 { ArticleMeasure } from "../../components/ArticleMeasure";
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] : ["/og.png"],
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
- <main className="reading-page post-reading-page">
93
- <article className="post-article h-entry">
94
- <Link className="u-url sr-only" href={pathname}>
95
- {siteLang === "en" ? t.permalinkLabel(post.title) : <>{post.title} 永久链接</>}
96
- </Link>
97
- <a
98
- className="p-author h-card sr-only"
99
- href={githubUrl}
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
- <div className="sh-head">
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 { ArticleMeasure } from "../../../components/ArticleMeasure";
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, siteLang, ui } from "../../../lib/i18n";
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: entry.title.replace(/^\d+\s*·\s*/u, ""),
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: entry.title.replace(/^\d+\s*·\s*/u, ""),
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: entry.title.replace(/^\d+\s*·\s*/u, ""),
58
+ card: entry.cover ? "summary_large_image" : "summary",
59
+ title,
73
60
  description: entry.description,
74
- images: entry.cover ? [entry.cover] : ["/og.png"],
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
- <main className="column-reading-page">
103
- <aside className="series-nav">
104
- <Link href={siteHref("/columns")} className="back-link">
105
- <ArrowLeft size={15} />
106
- {t.allColumnsBack}
107
- </Link>
108
- <p>{t.seriesEyebrow}</p>
109
- <h2>{entry.columnTitle}</h2>
110
- <ol>
111
- {listed.map((item) => (
112
- <li key={item.path} className={item.path === entry.path ? "active" : ""}>
113
- <Link href={columnHref(item)}>{item.title}</Link>
114
- </li>
115
- ))}
116
- </ol>
117
- <small>{ui().columnSideCount(series.length, columnStatusLabel(entry.columnStatus))}</small>
118
- </aside>
119
-
120
- <article className="article-column series-article h-entry">
121
- <Link className="u-url sr-only" href={pathname}>
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
  }
@@ -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
- <div className="sh-head">
44
- <h1>{siteConfig.pages.columns.title || t.homeColumns}</h1>
45
- {siteConfig.pages.columns.subtitle ? <p className="sh-deck">{siteConfig.pages.columns.subtitle}</p> : null}
46
- </div>
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: 1731,
68
- height: 909,
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: 1731,
98
- height: 909,
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: 1731,
129
- height: 909,
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, [remarkLinkCard, { sameSite }], [remarkA2, { imageSize: (src: string) => lookupImageSize(imageSizes, src), lang: lang ?? siteLang }]]}
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
- if (href && text === href && /^https?:\/\//iu.test(href) && !sameSite(href)) return <LinkCard href={href} />;
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 { href, external } = articleLink(written, sameSite);
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
- /** Shared heading treatment for every top-level public page. */
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
- {subtitle && <p>{subtitle}</p>}
29
- {children && <div className="page-intro-actions">{children}</div>}
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
+ }