Skip to content

Commit 2908baf

Browse files
committed
adds table-of-content
1 parent 5da6e05 commit 2908baf

23 files changed

+886
-86
lines changed

package-lock.json

+508-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module "markdown-toc-unlazy";

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"gray-matter": "^4.0.3",
1313
"js-yaml": "^4.1.0",
14+
"markdown-toc-unlazy": "^1.0.1",
1415
"next": "14.2.10",
1516
"next-themes": "^0.3.0",
1617
"pinyin": "^4.0.0-alpha.2",

src/app/[category]/[slug]/page.module.css

+31
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
1+
.pageWrapper {
2+
display: flex;
3+
justify-content: center;
4+
}
5+
6+
.postWrapper {
7+
@media (max-width: 1340px) {
8+
max-width: 720px;
9+
}
10+
@media (max-width: 1200px) {
11+
max-width: 640px;
12+
}
13+
}
14+
115
.postHeader {
216
border-bottom: 1px solid var(--quote-color);
317
}
418

19+
.postHeader :global(#toc-title)::before {
20+
display: block;
21+
content: " ";
22+
margin-top: -72px;
23+
height: 72px;
24+
visibility: hidden;
25+
pointer-events: none;
26+
}
27+
528
.postTitle {
629
margin: 48px 0 12px 0;
730
display: block;
@@ -34,4 +57,12 @@
3457
}
3558
.postTime a {
3659
color: #808080;
60+
}
61+
62+
/* for table-of-content */
63+
.toc {
64+
flex-grow: 1;
65+
@media (max-width: 1080px) {
66+
display: none;
67+
}
3768
}

src/app/[category]/[slug]/page.tsx

+61-38
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { Metadata } from "next";
22
import Link from "next/link";
33
import { notFound } from "next/navigation";
4+
import toc from "markdown-toc-unlazy";
45
import { getPostBySlug, getPostsByCategory } from "@/lib/posts";
6+
import { Itoc } from "@/interfaces/Post";
57
import styles from "./page.module.css";
68
import ReactMarkdown from "@/components/ReactMarkdown";
9+
import TOC from "@/components/TOC";
710
import Comments from "@/components/Comments";
811
import { SITE_CONFIG } from "@/app/site.config";
912

@@ -14,52 +17,72 @@ export default function Page({ params }: { params: { category: string; slug: str
1417
const { title, description, tags } = frontMatter;
1518
const createdDate = new Date(frontMatter.date);
1619
const updatedDate = frontMatter.updated ? new Date(frontMatter.updated) : null;
20+
const tocContent = handleTocHeader(
21+
toc(content).json.filter((header: Itoc) => header.lvl <= SITE_CONFIG.tocMaxHeader),
22+
);
1723
return (
18-
<>
19-
<div className={styles.postHeader}>
20-
<div className={styles.postTitle}>{title}</div>
21-
<div className={styles.postDesc}>{description}</div>
22-
{tags && (
23-
<div>
24-
{tags.map((tag, index) => (
25-
<div key={index.toString()} className="flexItem">
26-
<Link href={`/tag/${tag}`} className={styles.postTag}>
27-
# {tag}
28-
</Link>
29-
</div>
30-
))}
24+
<div className={styles.pageWrapper}>
25+
<TOC tocContent={tocContent} style={{ visibility: "hidden" }} className={styles.toc} />
26+
<div className={`${styles.postWrapper} container`}>
27+
<div className={styles.postHeader}>
28+
<div className={styles.postTitle} id="toc-title">
29+
{title}
3130
</div>
32-
)}
33-
<div className={styles.postTime}>
34-
<span>
35-
发布于 {createdDate.getFullYear()}{createdDate.getMonth() + 1}{createdDate.getDate()}
36-
</span>
37-
{updatedDate && (
38-
<>
39-
<span>|</span>
40-
<span>
41-
更新于 {updatedDate.getFullYear()}{updatedDate.getMonth() + 1}{updatedDate.getDate()}
42-
</span>
43-
</>
31+
<div className={styles.postDesc}>{description}</div>
32+
{tags && (
33+
<div>
34+
{tags.map((tag, index) => (
35+
<div key={index.toString()} className="flexItem">
36+
<Link href={`/tag/${tag}`} className={styles.postTag}>
37+
# {tag}
38+
</Link>
39+
</div>
40+
))}
41+
</div>
4442
)}
45-
<span>|</span>
46-
<span>
47-
遵循{" "}
48-
<Link href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">
49-
CC BY-NC-SA 4.0
50-
</Link>{" "}
51-
许可
52-
</span>
43+
<div className={styles.postTime}>
44+
<span>
45+
发布于 {createdDate.getFullYear()}{createdDate.getMonth() + 1}{createdDate.getDate()}
46+
</span>
47+
{updatedDate && (
48+
<>
49+
<span>|</span>
50+
<span>
51+
更新于 {updatedDate.getFullYear()}{updatedDate.getMonth() + 1}{updatedDate.getDate()}
52+
</span>
53+
</>
54+
)}
55+
<span>|</span>
56+
<span>
57+
遵循{" "}
58+
<Link href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">
59+
CC BY-NC-SA 4.0
60+
</Link>{" "}
61+
许可
62+
</span>
63+
</div>
5364
</div>
65+
<div>
66+
<ReactMarkdown abbrlink={frontMatter.abbrlink!}>{content}</ReactMarkdown>
67+
</div>
68+
<Comments id="toc-comments" />
5469
</div>
55-
<div>
56-
<ReactMarkdown abbrlink={frontMatter.abbrlink!}>{content}</ReactMarkdown>
57-
</div>
58-
<Comments />
59-
</>
70+
<TOC tocContent={tocContent} className={styles.toc} />
71+
</div>
6072
);
6173
}
6274

75+
const handleTocHeader = (tocContent: Array<Itoc>) => {
76+
const minLvl = Math.min(...tocContent.map((header) => header.lvl));
77+
return tocContent.map((header) => {
78+
const { lvl, ...rest } = header;
79+
return {
80+
lvl: lvl - minLvl,
81+
...rest,
82+
};
83+
});
84+
};
85+
6386
export function generateMetadata({ params }: { params: { slug: string } }): Metadata {
6487
const post = getPostBySlug(params.slug);
6588
if (!post) return notFound();

src/app/[category]/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ export default function postArchive({ params }: { params: { category: string } }
99
if (posts.length <= 0) return notFound();
1010

1111
return (
12-
<>
12+
<div className="container">
1313
<div className={styles.siteTitle}>
1414
{SITE_CONFIG.categories.filter((item) => item.url === params.category)[0].name}
1515
</div>
1616
<div>共归档 {posts.length} 篇文章。</div>
1717
<Pagination posts={posts} pageSize={SITE_CONFIG.categoryPaginationSize} />
18-
</>
18+
</div>
1919
);
2020
}
2121

src/app/about/page.module.css

+22
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
.pageWrapper {
2+
display: flex;
3+
justify-content: center;
4+
}
5+
6+
.postWrapper {
7+
@media (max-width: 1340px) {
8+
max-width: 720px;
9+
}
10+
@media (max-width: 1200px) {
11+
max-width: 640px;
12+
}
13+
}
14+
115
.header {
216
padding-top: 32px;
317
display: flex;
@@ -38,3 +52,11 @@
3852
.avatarContainer img {
3953
border-radius: 50%;
4054
}
55+
56+
/* for table-of-content */
57+
.toc {
58+
flex-grow: 1;
59+
@media (max-width: 1080px) {
60+
display: none;
61+
}
62+
}

src/app/about/page.tsx

+46-25
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { notFound } from "next/navigation";
22
import Image from "next/image";
3+
import { Metadata } from "next";
4+
import toc from "markdown-toc-unlazy";
35
import { getAboutPost } from "@/lib/posts";
46
import ReactMarkdown from "@/components/ReactMarkdown";
57
import Comments from "@/components/Comments";
8+
import TOC from "@/components/TOC";
9+
import { Itoc } from "@/interfaces/Post";
610
import avatar from "@/assets/avatar.jpg";
711
import styles from "./page.module.css";
8-
import { Metadata } from "next";
912
import { SITE_CONFIG } from "../site.config";
1013

1114
export const metadata: Metadata = {
@@ -19,33 +22,51 @@ export default function AboutPage() {
1922
const { content, ...frontMatter } = post;
2023
const createdDate = new Date(frontMatter.date);
2124
const updatedDate = frontMatter.updated ? new Date(frontMatter.updated) : null;
25+
const tocContent = handleTocHeader(
26+
toc(content).json.filter((header: Itoc) => header.lvl <= SITE_CONFIG.tocMaxHeader),
27+
);
2228
return (
23-
<>
24-
<div className={styles.header}>
25-
<div className={styles.titleContainer}>
26-
<div className={styles.title}>{frontMatter.title}</div>
27-
<div className={styles.description}>{frontMatter.description}</div>
28-
<div className={styles.postTime}>
29-
<span>
30-
发布于 {createdDate.getFullYear()}{createdDate.getMonth() + 1}{createdDate.getDate()}
31-
</span>
32-
{updatedDate && (
33-
<>
34-
<span>|</span>
35-
<span>
36-
更新于 {updatedDate.getFullYear()}{updatedDate.getMonth() + 1}{updatedDate.getDate()}
37-
</span>
38-
</>
39-
)}
29+
<div className={styles.pageWrapper}>
30+
<TOC tocContent={tocContent} style={{ visibility: "hidden" }} className={styles.toc} />
31+
<div className={`${styles.postWrapper} container`}>
32+
<div className={styles.header}>
33+
<div className={styles.titleContainer}>
34+
<div className={styles.title}>{frontMatter.title}</div>
35+
<div className={styles.description}>{frontMatter.description}</div>
36+
<div className={styles.postTime}>
37+
<span>
38+
发布于 {createdDate.getFullYear()}{createdDate.getMonth() + 1}{createdDate.getDate()}
39+
</span>
40+
{updatedDate && (
41+
<>
42+
<span>|</span>
43+
<span>
44+
更新于 {updatedDate.getFullYear()}{updatedDate.getMonth() + 1}{updatedDate.getDate()}
45+
</span>
46+
</>
47+
)}
48+
</div>
49+
</div>
50+
<div className={styles.avatarContainer}>
51+
<Image src={avatar} width={160} height={160} alt="avatar" className={styles.avatar} />
4052
</div>
4153
</div>
42-
<div className={styles.avatarContainer}>
43-
<Image src={avatar} width={160} height={160} alt="avatar" className={styles.avatar} />
44-
</div>
45-
</div>
4654

47-
<ReactMarkdown abbrlink={frontMatter.abbrlink!}>{content}</ReactMarkdown>
48-
<Comments />
49-
</>
55+
<ReactMarkdown abbrlink={frontMatter.abbrlink!}>{content}</ReactMarkdown>
56+
<Comments />
57+
</div>
58+
<TOC tocContent={tocContent} className={styles.toc} />
59+
</div>
5060
);
5161
}
62+
63+
const handleTocHeader = (tocContent: Array<Itoc>) => {
64+
const minLvl = Math.min(...tocContent.map((header) => header.lvl));
65+
return tocContent.map((header) => {
66+
const { lvl, ...rest } = header;
67+
return {
68+
lvl: lvl - minLvl,
69+
...rest,
70+
};
71+
});
72+
};

src/app/global.css

+3-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ html {
3838
"WenQuanYi Micro Hei",
3939
sans-serif;
4040
font-size: 18px;
41+
scroll-behavior: smooth;
4142
}
4243

4344
html a {
@@ -53,8 +54,6 @@ body {
5354

5455
.main {
5556
min-height: 360px;
56-
width: -moz-available;
57-
width: -webkit-fill-available;
5857
display: flex;
5958
flex-direction: column;
6059
flex-grow: 1;
@@ -66,6 +65,8 @@ body {
6665
padding-right: 32px;
6766
margin-left: auto;
6867
margin-right: auto;
68+
width: -moz-available;
69+
width: -webkit-fill-available;
6970
@media (max-width: 640px) {
7071
padding: 0 16px;
7172
}

src/app/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => {
1111
<body>
1212
<ThemeProvider defaultTheme="system">
1313
<Navbar />
14-
<main className="main container">{children}</main>
14+
<main className="main">{children}</main>
1515
<Footer />
1616
</ThemeProvider>
1717
</body>

src/app/not-found.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import styles from "./not-found.module.css";
55

66
export default function NotFound() {
77
return (
8-
<div className={styles.notfound}>
8+
<div className={`${styles.notfound} container`}>
99
<div className={styles.title}>Oops!</div>
1010
<div>This page could not be found.</div>
1111
</div>

src/app/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const Index = () => {
2222
const recentNotes = getPostsByCategory("note");
2323
const recentArticles = getPostsByCategory("article");
2424
return (
25-
<section>
25+
<section className="container">
2626
<div id="introduction">
2727
<div className={styles.siteTitle}>你好,来访者。</div>
2828
<div className={styles.siteDesc}>这里是Palemoons的网络存档点。</div>

src/app/site.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const SITE_CONFIG = {
77
commentRepo: "palemoons/blog-comment",
88
landingPageListSize: 5,
99
categoryPaginationSize: 20,
10+
tocMaxHeader: 3,
1011

1112
categories: [
1213
{ name: "技术笔记", url: "note" },

src/app/tag/[slug]/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ export default function TagPage({ params }: { params: { slug: string } }) {
88
const posts = getPostsByTag(decodeURI(params.slug));
99
if (posts.length <= 0) return notFound();
1010
return (
11-
<>
11+
<div className="container">
1212
<div className={styles.siteTitle}>
1313
<span className={styles.tag}>#{decodeURI(params.slug)}</span>
1414
</div>
1515
<div>共归档 {posts.length} 篇文章。</div>
1616
<Pagination posts={posts} />
17-
</>
17+
</div>
1818
);
1919
}
2020

0 commit comments

Comments
 (0)