feat: add toc generation for astro articles. (#63)

This commit is contained in:
Yufan Sheng 2024-11-15 12:54:58 +08:00 committed by GitHub
parent 99dd5dc5d1
commit efbaffeb2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 4281 additions and 50 deletions

View File

@ -135,7 +135,7 @@ date: 2013/7/13 20:46:25
| Setting | Description | Required | Default |
|-------------|--------------------------------------|----------|----------------------|
| `id` | ID (unique), used as the permalink | true | Filename |
| `slug` | ID (unique), used as the permalink | true | Filename |
| `title` | Title | true | Filename |
| `date` | Published date | true | |
| `updated` | Updated date | false | Published date |
@ -145,18 +145,21 @@ date: 2013/7/13 20:46:25
| `summary` | Post summary in plain text | false | First 140 characters |
| `cover` | The cover image | false | `null` |
| `published` | Whether the post should be published | false | `true` |
| `toc` | Display the Table of Contents | false | `false` |
| `alias` | The alternatives slugs for post | false | `[]` |
### Pages Front Matter Settings
| Setting | Description | Required | Default |
|-------------|--------------------------------------|----------|----------------|
| `id` | ID (unique), used as the permalink | true | Filename |
| `slug` | ID (unique), used as the permalink | true | Filename |
| `title` | Title | true | Filename |
| `date` | Published date | true | |
| `updated` | Updated date | false | Published date |
| `comments` | Enables comment feature for the post | false | `true` |
| `cover` | The cover image | false | `null` |
| `published` | Whether the post should be published | false | `true` |
| `toc` | Display the Table of Contents | false | `false` |
## Weblog Design

View File

@ -74,6 +74,10 @@ const Options = z
size: z.number(),
}),
}),
toc: z.object({
minHeadingLevel: z.number().optional().default(2),
maxHeadingLevel: z.number().optional().default(3),
}),
}),
thumbnail: z
.function()
@ -92,7 +96,11 @@ const Options = z
assetsPrefix,
defaultOpenGraph: (): string => `${assetsPrefix()}/images/open-graph.png`,
};
});
})
.refine(
(options) => options.settings.toc.minHeadingLevel <= options.settings.toc.maxHeadingLevel,
'Invalid toc setting, the minHeadingLevel should bellow the maxHeadingLevel',
);
const options: z.input<typeof Options> = {
local: {
@ -191,6 +199,10 @@ const options: z.input<typeof Options> = {
size: 120,
},
},
toc: {
minHeadingLevel: 2,
maxHeadingLevel: 3,
},
},
thumbnail: ({ src, width, height }) => {
if (src.endsWith('.svg')) {

12
package-lock.json generated
View File

@ -3896,9 +3896,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.58",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.58.tgz",
"integrity": "sha512-al2l4r+24ZFL7WzyPTlyD0fC33LLzvxqLCwurtBibVPghRGO9hSTl+tis8t1kD7biPiH/en4U0I7o/nQbYeoVA==",
"version": "1.5.59",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.59.tgz",
"integrity": "sha512-faAXB6+gEbC8FsiRdpOXgOe4snP49YwjiXynEB8Mp7sUx80W5eN+BnnBHJ/F7eIeLzs+QBfDD40bJMm97oEFcw==",
"license": "ISC"
},
"node_modules/emmet": {
@ -4621,9 +4621,9 @@
}
},
"node_modules/hast-util-raw": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.4.tgz",
"integrity": "sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==",
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
"integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",

View File

@ -292,6 +292,15 @@ for (const anchor of document.querySelectorAll('a[href^="#"]')) {
});
}
const tocToggle = document.querySelector('.toggle-menu-tree');
if (typeof tocToggle !== 'undefined' && tocToggle !== null) {
tocToggle.addEventListener('click', () => {
const body = document.querySelector('body');
const displayToc = !body.classList.contains('display-menu-tree');
body.classList.toggle('display-menu-tree', displayToc);
});
}
// Add like button for updating likes.
const likeButton = document.querySelector('button.post-like');

View File

@ -1424,14 +1424,6 @@ textarea.form-control {
padding: 0.75rem 1rem 1rem;
}
.list-bookmarks .list-favicon {
background-size: cover;
display: inline-block;
margin-left: 8px;
width: 12px;
height: 12px;
}
@media (max-width: 767.98px) {
.list-bookmarks {
margin: 2.5rem 0 0;
@ -3266,3 +3258,282 @@ blockquote p:last-child {
margin: 0 2rem 1.25rem;
}
}
/** Table of Contents ------------------------------------ Default */
.toggle-menu-tree {
top: 0;
bottom: 0;
font-size: 1.375rem;
position: fixed;
right: 0;
margin-right: -5rem;
margin-bottom: auto;
margin-top: auto;
padding-left: 0.35rem;
color: #555;
width: 6.25rem;
height: 6.25rem;
background-color: rgba(255, 255, 255, 0.9);
cursor: pointer;
line-height: 6.25rem;
border-radius: 6.25rem;
border: 0.0625rem solid #f0f0f0;
opacity: 1;
font-family: -apple-system;
z-index: 890;
-webkit-transition: 0.5s ease all;
-moz-transition: 0.5s ease all;
-ms-transition: 0.5s ease all;
-o-transition: 0.5s ease all;
transition: 0.5s ease all;
-webkit-box-shadow: 0 0.125rem 0.3125rem rgba(0, 0, 0, 0.117);
-moz-box-shadow: 0 0.125rem 0.3125rem rgba(0, 0, 0, 0.117);
box-shadow: 0 0.125rem 0.3125rem rgba(0, 0, 0, 0.117);
}
.toggle-menu-tree i {
-webkit-transition: 0.5s ease all;
-moz-transition: 0.5s ease all;
-ms-transition: 0.5s ease all;
-o-transition: 0.5s ease all;
transition: 0.5s ease all;
}
.toggle-menu-tree:hover {
background: #fafafa;
-webkit-transform: translateX(-1.25rem);
-moz-transform: translateX(-1.25rem);
-ms-transform: translateX(-1.25rem);
-o-transform: translateX(-1.25rem);
transform: translateX(-1.25rem);
}
.post-menu {
position: fixed;
display: table;
top: 0;
right: -18.125rem;
bottom: 0;
width: 17.5rem;
height: 100%;
background-color: #fafafa;
border-left: 0.0625rem solid #f0f0f0;
opacity: 1;
z-index: 880;
font-weight: 400;
-webkit-transition: 0.5s ease all;
-moz-transition: 0.5s ease all;
-ms-transition: 0.5s ease all;
-o-transition: 0.5s ease all;
transition: 0.5s ease all;
}
.post-menu::-webkit-scrollbar {
height: 8px;
width: 4px;
}
.post-menu::-webkit-scrollbar-button {
height: 0;
width: 0;
}
.post-menu::-webkit-scrollbar-button:start:decrement,
.post-menu::-webkit-scrollbar-button:end:increment {
display: block;
}
.post-menu::-webkit-scrollbar-button:vertical:start:increment,
.post-menu::-webkit-scrollbar-button:vertical:end:decrement {
display: none;
}
.post-menu::-webkit-scrollbar-track:vertical,
.post-menu::-webkit-scrollbar-track:horizontal {
background-clip: padding-box;
background-color: #191919;
border: 0 solid transparent;
}
.post-menu::-webkit-scrollbar-track:hover {
background-color: #191919;
-webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.1);
-moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.1);
}
.post-menu::-webkit-scrollbar-track:active {
background-color: #191919;
-webkit-box-shadow:
inset 1px 0 0 rgba(0, 0, 0, 0.14),
inset -1px -1px 0 rgba(0, 0, 0, 0.07);
-moz-box-shadow:
inset 1px 0 0 rgba(0, 0, 0, 0.14),
inset -1px -1px 0 rgba(0, 0, 0, 0.07);
box-shadow:
inset 1px 0 0 rgba(0, 0, 0, 0.14),
inset -1px -1px 0 rgba(0, 0, 0, 0.07);
}
.post-menu::-webkit-scrollbar-thumb {
background-clip: padding-box;
background-color: rgba(255, 255, 255, 0.3);
min-height: 40px;
padding-top: 100px;
border-radius: 2px;
-webkit-box-shadow:
inset 1px 1px 0 rgba(0, 0, 0, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.07);
-moz-box-shadow:
inset 1px 1px 0 rgba(0, 0, 0, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.07);
box-shadow:
inset 1px 1px 0 rgba(0, 0, 0, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.07);
}
.post-menu::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.4);
-webkit-box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
-moz-box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
}
.post-menu::-webkit-scrollbar-thumb:active {
background-color: rgba(255, 255, 255, 0.5);
-webkit-box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35);
-moz-box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35);
}
.post-menu::-webkit-scrollbar-thumb:vertical,
.post-menu::-webkit-scrollbar-thumb:horizontal {
border: 0 solid transparent;
}
.toc-wrap {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: -3rem;
overflow-x: hidden;
overflow-y: auto;
--webkit-overflow-scrolling: touch;
}
.toc-content {
margin-right: 3rem;
padding-top: 2.875rem;
-webkit-transition: 0.5s ease all;
-moz-transition: 0.5s ease all;
-ms-transition: 0.5s ease all;
-o-transition: 0.5s ease all;
transition: 0.5s ease all;
}
.post-menu-title {
font-size: 1.5rem;
text-align: left;
line-height: 3.6rem;
width: 100%;
font-weight: 700;
padding: 0 2.5rem;
color: #202020;
}
.index-menu {
padding-top: 2rem;
}
.index-menu-list {
line-height: 1.8em;
list-style: none;
padding: 0;
}
.index-menu-item {
overflow: hidden;
text-overflow: ellipsis;
}
.index-menu-item > .index-menu-list span.menu-content {
padding-left: 2rem;
}
.index-menu-item > .index-menu-list > .index-menu-item > .index-menu-list span.menu-content {
padding-left: 4rem;
}
.index-menu-item.current > a.index-menu-link {
background: #f5f5f5;
color: #1abc9c;
font-weight: 700;
}
a.index-menu-link {
color: #555;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.85rem;
padding: 0.375rem 2.5rem;
position: relative;
display: block;
}
a.index-menu-link:hover {
background: #efefef;
color: #333;
}
.post-menu-overlay {
visibility: hidden;
pointer-events: none;
}
/** Table of Contents ------------------------------------ Display */
.display-menu-tree .post-menu {
-webkit-transform: translateX(-18.125rem);
-moz-transform: translateX(-18.125rem);
-ms-transform: translateX(-18.125rem);
-o-transform: translateX(-18.125rem);
transform: translateX(-18.125rem);
z-index: 1000;
}
.display-menu-tree .toggle-menu-tree {
background: #fafafa;
padding-left: 0;
width: 3.125rem;
height: 3.125rem;
line-height: 3.125rem;
text-align: center;
margin-right: -1.5625rem;
-webkit-transform: translateX(-17.5rem);
-moz-transform: translateX(-17.5rem);
-ms-transform: translateX(-17.5rem);
-o-transform: translateX(-17.5rem);
transform: translateX(-17.5rem);
z-index: 1500;
}
.display-menu-tree .toggle-menu-tree i {
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);
}
.display-menu-tree .post-menu-overlay {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(8, 15, 25, 0.3);
visibility: visible;
pointer-events: initial;
z-index: 500;
}

File diff suppressed because it is too large Load Diff

271
src/assets/styles/toc.css Normal file
View File

@ -0,0 +1,271 @@
/* Table of Contents ----------------------------------- */
.post-menu {
position: fixed;
display: table;
top: 0;
right: -18.125rem;
bottom: 0;
width: 17.5rem;
height: 100%;
background-color: #fafafa;
border-left: 0.0625rem solid #f0f0f0;
opacity: 1;
z-index: 880;
font-weight: 400;
-webkit-transition: 0.5s ease all;
-moz-transition: 0.5s ease all;
-ms-transition: 0.5s ease all;
-o-transition: 0.5s ease all;
transition: 0.5s ease all;
}
.display-menu-tree .post-menu {
-webkit-transform: translateX(-18.125rem);
-moz-transform: translateX(-18.125rem);
-ms-transform: translateX(-18.125rem);
-o-transform: translateX(-18.125rem);
transform: translateX(-18.125rem);
}
.post-menu::-webkit-scrollbar {
height: 8px;
width: 4px;
}
.post-menu::-webkit-scrollbar-button {
height: 0;
width: 0;
}
.post-menu::-webkit-scrollbar-button:start:decrement,
.post-menu::-webkit-scrollbar-button:end:increment {
display: block;
}
.post-menu::-webkit-scrollbar-button:vertical:start:increment,
.post-menu::-webkit-scrollbar-button:vertical:end:decrement {
display: none;
}
.post-menu::-webkit-scrollbar-track:vertical,
.post-menu::-webkit-scrollbar-track:horizontal {
background-clip: padding-box;
background-color: #191919;
border: 0 solid transparent;
}
.post-menu::-webkit-scrollbar-track:hover {
background-color: #191919;
-webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.1);
-moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.1);
}
.post-menu::-webkit-scrollbar-track:active {
background-color: #191919;
-webkit-box-shadow:
inset 1px 0 0 rgba(0, 0, 0, 0.14),
inset -1px -1px 0 rgba(0, 0, 0, 0.07);
-moz-box-shadow:
inset 1px 0 0 rgba(0, 0, 0, 0.14),
inset -1px -1px 0 rgba(0, 0, 0, 0.07);
box-shadow:
inset 1px 0 0 rgba(0, 0, 0, 0.14),
inset -1px -1px 0 rgba(0, 0, 0, 0.07);
}
.post-menu::-webkit-scrollbar-thumb {
background-clip: padding-box;
background-color: rgba(255, 255, 255, 0.3);
min-height: 40px;
padding-top: 100px;
border-radius: 2px;
-webkit-box-shadow:
inset 1px 1px 0 rgba(0, 0, 0, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.07);
-moz-box-shadow:
inset 1px 1px 0 rgba(0, 0, 0, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.07);
box-shadow:
inset 1px 1px 0 rgba(0, 0, 0, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.07);
}
.post-menu::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.4);
-webkit-box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
-moz-box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
}
.post-menu::-webkit-scrollbar-thumb:active {
background-color: rgba(255, 255, 255, 0.5);
-webkit-box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35);
-moz-box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35);
}
.post-menu::-webkit-scrollbar-thumb:vertical,
.post-menu::-webkit-scrollbar-thumb:horizontal {
border: 0 solid transparent;
}
.toggle-menu-tree {
top: 0;
bottom: 0;
font-size: 1.375rem;
position: fixed;
right: 0;
margin-right: -5rem;
margin-bottom: auto;
margin-top: auto;
padding-left: 0.35rem;
color: #555;
width: 6.25rem;
height: 6.25rem;
background-color: rgba(255, 255, 255, 0.9);
cursor: pointer;
line-height: 6.25rem;
border-radius: 6.25rem;
border: 0.0625rem solid #f0f0f0;
opacity: 1;
font-family: -apple-system;
z-index: 890;
-webkit-transition: 0.5s ease all;
-moz-transition: 0.5s ease all;
-ms-transition: 0.5s ease all;
-o-transition: 0.5s ease all;
transition: 0.5s ease all;
-webkit-box-shadow: 0 0.125rem 0.3125rem rgba(0, 0, 0, 0.117);
-moz-box-shadow: 0 0.125rem 0.3125rem rgba(0, 0, 0, 0.117);
box-shadow: 0 0.125rem 0.3125rem rgba(0, 0, 0, 0.117);
}
.display-menu-tree .toggle-menu-tree {
background: #fafafa;
padding-left: 0;
width: 3.125rem;
height: 3.125rem;
line-height: 3.125rem;
text-align: center;
margin-right: -1.5625rem;
-webkit-transform: translateX(-17.5rem);
-moz-transform: translateX(-17.5rem);
-ms-transform: translateX(-17.5rem);
-o-transform: translateX(-17.5rem);
transform: translateX(-17.5rem);
}
.toggle-menu-tree i {
-webkit-transition: 0.5s ease all;
-moz-transition: 0.5s ease all;
-ms-transition: 0.5s ease all;
-o-transition: 0.5s ease all;
transition: 0.5s ease all;
}
.display-menu-tree .toggle-menu-tree i {
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);
}
.toggle-menu-tree:hover {
background: #fafafa;
-webkit-transform: translateX(-1.25rem);
-moz-transform: translateX(-1.25rem);
-ms-transform: translateX(-1.25rem);
-o-transform: translateX(-1.25rem);
transform: translateX(-1.25rem);
}
.toggle-menu-tree.show {
right: 0;
}
.toggle-menu-tree.hide {
right: -5.3125rem;
}
.display-menu-tree .toggle-menu-tree.hide {
right: 0;
}
.toc-wrap {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: -3rem;
overflow-x: hidden;
overflow-y: auto;
--webkit-overflow-scrolling: touch;
}
.toc-content {
margin-right: 3rem;
padding-top: 2.875rem;
-webkit-transition: 0.5s ease all;
-moz-transition: 0.5s ease all;
-ms-transition: 0.5s ease all;
-o-transition: 0.5s ease all;
transition: 0.5s ease all;
}
.post-menu-title {
font-size: 1.5rem;
text-align: left;
line-height: 3.6rem;
width: 100%;
font-weight: 700;
padding: 0 2.5rem;
color: #202020;
}
.index-menu {
padding-top: 2rem;
}
.index-menu-list {
line-height: 1.8em;
list-style: none;
padding: 0;
}
.index-menu-item {
overflow: hidden;
text-overflow: ellipsis;
}
.index-menu-item > .index-menu-list span.menu-content {
padding-left: 2rem;
}
.index-menu-item > .index-menu-list > .index-menu-item > .index-menu-list span.menu-content {
padding-left: 4rem;
}
.index-menu-item.current > a.index-menu-link {
background: #f5f5f5;
color: #1abc9c;
font-weight: 700;
}
a.index-menu-link {
color: #555;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.85rem;
padding: 0.375rem 2.5rem;
position: relative;
display: block;
}
a.index-menu-link:hover {
background: #efefef;
color: #333;
}

View File

@ -26,15 +26,7 @@ const list = _.shuffle(friends);
</div>
<div class="list-content">
<div class="list-body">
<div class="list-title h6 h-1x">
{friend.website}
<div
class="list-favicon"
style={{
backgroundImage: `url('${friend.favicon}')`,
}}
/>
</div>
<div class="list-title h6 h-1x">{friend.website}</div>
<div class="text-sm text-secondary h-2x mt-1">{friend.description ? friend.description : ' '}</div>
</div>
</div>

View File

@ -0,0 +1,34 @@
---
import TocItems from '@/components/page/toc/TocItems.astro';
import { generateToC, type TocOpts } from '@/helpers/toc';
import type { MarkdownHeading } from 'astro';
interface Props {
headings: MarkdownHeading[];
toc: TocOpts | false;
}
const { headings, toc } = Astro.props;
const items = generateToC(headings, toc);
---
{
items.length > 0 && (
<Fragment>
<a class="toggle-menu-tree">
<i class="text-md iconfont icon-left" />
</a>
<div class="post-menu">
<div class="toc-wrap">
<div class="toc-content">
<h2 class="post-menu-title">文章目录</h2>
<div class="index-menu">
<TocItems {items} />
</div>
</div>
</div>
</div>
<div class="post-menu-overlay" />
</Fragment>
)
}

View File

@ -0,0 +1,22 @@
---
import type { TocItem } from '@/helpers/toc';
interface Props {
items: TocItem[];
}
const { items } = Astro.props;
---
<ul class="index-menu-list">
{
items.map((item) => (
<li class="index-menu-item">
<a data-scroll class="index-menu-link" href={`#${item.slug}`} title={item.text}>
<span class="menu-content">{item.text}</span>
</a>
{item.children.length > 0 && <Astro.self items={item.children} />}
</li>
))
}
</ul>

View File

@ -22,6 +22,30 @@ const image = (fallbackImage: string) =>
.default(fallbackImage)
.transform((file) => imageMetadata(file));
// The default toc heading level.
const toc = () =>
z
.union([
z.object({
// The level to start including headings at in the table of contents. Default: 2.
minHeadingLevel: z.number().int().min(1).max(6).optional().default(options.settings.toc.minHeadingLevel),
// The level to stop including headings at in the table of contents. Default: 3.
maxHeadingLevel: z.number().int().min(1).max(6).optional().default(options.settings.toc.maxHeadingLevel),
}),
z.boolean().transform((enabled) =>
enabled
? {
minHeadingLevel: options.settings.toc.minHeadingLevel,
maxHeadingLevel: options.settings.toc.maxHeadingLevel,
}
: false,
),
])
.default(false)
.refine((toc) => (toc ? toc.minHeadingLevel <= toc.maxHeadingLevel : true), {
message: 'minHeadingLevel must be less than or equal to maxHeadingLevel',
});
// Categories Collection
const categoriesCollection = defineCollection({
type: 'data',
@ -70,6 +94,7 @@ const postsCollection = defineCollection({
summary: z.string().optional().default(''),
cover: image(defaultCover),
published: z.boolean().optional().default(true),
toc: toc(),
}),
});
@ -84,6 +109,7 @@ const pagesCollection = defineCollection({
cover: image(defaultCover),
published: z.boolean().optional().default(true),
friend: z.boolean().optional().default(false),
toc: toc(),
}),
});

View File

@ -6,7 +6,6 @@
description: Trying to light up the dark
homepage: https://cynosura.one
poster: /images/links/cynosura.one.jpg
favicon: https://cynosura.one/img/favicon.ico
- website: EEE.ME
description: 途方に暮れて
homepage: https://eee.me
@ -22,7 +21,6 @@
description: 这世界需要更多英雄
homepage: https://blog.mboker.cn
poster: /images/links/mboker.cn.jpg
favicon: data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text x=%22-0.125em%22 y=%22.9em%22 font-size=%2290%22>🌻</text></svg>
- website: 品味苏州
description: 吴侬软语、小桥流水的生活点滴
homepage: https://pwsz.com
@ -42,12 +40,11 @@
description: 茶盐观瑟丶行者自若
homepage: https://alexinea.com
poster: /images/links/alexinea.com.jpg
favicon: https://alexinea.com/wp-content/uploads/2016/09/cropped-20160812.2-32x32.png
- website: 贾顺名
description: 全沾码农的一些笔记
homepage: https://jiasm.github.io
poster: /images/links/jiasm.github.io.jpg
- website: 白丁轶事 - FMoran的自留地
- website: 白丁轶事
description: Everything should be hurry.
homepage: https://fmoran.me
poster: /images/links/fmoran.me.jpg
@ -55,22 +52,18 @@
description: 船长の部落格,记录有趣的事,分享技术经验。
homepage: https://captainofphb.me
poster: /images/links/captainofphb.me.jpg
favicon: https://captainofphb.me/favicon.svg
- website: 小萌博客
description: 互联网分享笔记
homepage: https://blog.luoli.net
poster: /images/links/luoli.net.jpg
favicon: https://blog.luoli.net/usr/themes/MUltraMoe/images/favicon.png
- website: LongLuo's Life Notes
description: 每一天都是奇迹
homepage: https://www.longluo.me
poster: /images/links/longluo.me.jpg
favicon: https://www.longluo.me/assets/logo/favicon-32x32.png
- website: 佛爷小窝
description: 一个基于内容分享,创作与灵感结合的折腾笔记博客。
homepage: https://www.lafoyer.com
poster: /images/links/lafoyer.com.jpg
favicon: https://www.boxmoe.com/file/100logo.png
- website: 白熊阿丸的小屋
description: 在这里可以看到一个真实的我,我会在这里书写我的一切
homepage: https://bxaw.name

View File

@ -7,6 +7,9 @@ tags:
- 博客
category: 编程
summary: 如你所见,当你访问这篇文章的时候,我已经把写了 13 年的博客程序从 WordPress 迁移到了自己用 Next.js 写的程序,这可能是我第 N 次尝试使用其他的方式写博客,但我想绝对不会是最后一次。
toc:
minHeadingLevel: 2
maxHeadingLevel: 3
cover: /images/2024/04/2024040719182310.jpg
---

View File

@ -8,6 +8,7 @@ tags:
- 期房
category: 杂谈
summary: 这篇文章试图从金融视角来期房销售的问题,也希望能回答相关投资理财遇到资损的问题。
toc: true
cover: /images/2024/06/2024063015135000.jpg
---

View File

@ -7,6 +7,7 @@ tags:
- 学习
category: 杂谈
summary: 于我们成年人而言,虽已不能像儿童一样掌握一门外语,但是我们可通过“使用”的方式来习得英语。
toc: true
cover: /images/2024/10/2024101621070600.jpg
---

View File

@ -1,6 +1,6 @@
import { defaultCover } from '@/content/config.ts';
import options from '@/options';
import { getCollection, getEntry, type Render } from 'astro:content';
import { getCollection, type Render } from 'astro:content';
// Import the collections from the astro content.
const categoriesCollection = await getCollection('categories');
@ -24,7 +24,6 @@ export type Post = (typeof postsCollection)[number]['data'] & {
slug: string;
permalink: string;
render: () => Render['.mdx'];
raw: () => Promise<string>;
};
export type Tag = (typeof tagsCollection)[number]['data'][number] & { counts: number; permalink: string };
@ -36,10 +35,7 @@ export const pages: Page[] = pagesCollection
.map((page) => ({
slug: page.slug,
permalink: `/${page.slug}`,
render: async () => {
const entry = await getEntry('pages', page.slug);
return entry.render();
},
render: page.render,
...page.data,
}));
export const posts: Post[] = postsCollection
@ -47,14 +43,7 @@ export const posts: Post[] = postsCollection
.map((post) => ({
slug: post.slug,
permalink: `/posts/${post.slug}`,
render: async () => {
const entry = await getEntry('posts', post.slug);
return entry.render();
},
raw: async () => {
const entry = await getEntry('posts', post.slug);
return entry.body;
},
render: post.render,
...post.data,
}))
.sort((left: Post, right: Post) => {

34
src/helpers/toc.ts Normal file
View File

@ -0,0 +1,34 @@
import type { MarkdownHeading } from 'astro';
export interface TocItem extends MarkdownHeading {
children: TocItem[];
}
export interface TocOpts {
minHeadingLevel: number;
maxHeadingLevel: number;
}
// Convert the flat headings array generated by Astro into a nested tree structure.
export function generateToC(headings: MarkdownHeading[], opts: TocOpts | false): TocItem[] {
if (opts === false) {
return [];
}
const { minHeadingLevel, maxHeadingLevel } = opts;
const toc: Array<TocItem> = [];
for (const heading of headings.filter(({ depth }) => depth >= minHeadingLevel && depth <= maxHeadingLevel)) {
injectChild(toc, { ...heading, children: [] });
}
return toc;
}
// Inject a ToC entry as deep in the tree as its `depth` property requires.
function injectChild(items: TocItem[], item: TocItem): void {
const lastItem = items.at(-1);
if (!lastItem || lastItem.depth >= item.depth) {
items.push(item);
} else {
injectChild(lastItem.children, item);
}
}

View File

@ -4,6 +4,7 @@ import Image from '@/components/image/Image.astro';
import LikeButton from '@/components/like/LikeButton.astro';
import PageMeta from '@/components/meta/PageMeta.astro';
import Friends from '@/components/page/friend/Friends.astro';
import TableOfContents from '@/components/page/toc/TableOfContents.astro';
import MusicPlayer from '@/components/player/MusicPlayer.astro';
import type { Page } from '@/helpers/schema';
import { urlJoin } from '@/helpers/tools';
@ -15,7 +16,7 @@ interface Props {
}
const { page } = Astro.props;
const { Content } = await page.render();
const { Content, headings } = await page.render();
---
<BaseLayout title={page.title}>
@ -25,6 +26,7 @@ const { Content } = await page.render();
<div class="container">
<div class="post">
<h1 class="post-title mb-3 mb-xl-4">{page.title}</h1>
<TableOfContents {headings} toc={page.toc} />
<div class="post-content">
<div class="nc-light-gallery">
<Content components={{ MusicPlayer: MusicPlayer, Image: Image }} />

View File

@ -4,6 +4,7 @@ import Image from '@/components/image/Image.astro';
import LikeButton from '@/components/like/LikeButton.astro';
import LikeShare from '@/components/like/LikeShare.astro';
import PostMeta from '@/components/meta/PostMeta.astro';
import TableOfContents from '@/components/page/toc/TableOfContents.astro';
import MusicPlayer from '@/components/player/MusicPlayer.astro';
import Sidebar from '@/components/sidebar/Sidebar.astro';
import { formatShowDate } from '@/helpers/formatter';
@ -17,7 +18,7 @@ interface Props {
}
const { post } = Astro.props;
const { Content } = await post.render();
const { Content, headings } = await post.render();
---
<BaseLayout title={post.title}>
@ -53,6 +54,7 @@ const { Content } = await post.render();
))
}
</div>
<TableOfContents {headings} toc={post.toc} />
<div class="post-content">
<div class="nc-light-gallery">
<Content components={{ MusicPlayer: MusicPlayer, Image: Image }} />