Compare commits

...

16 commits

Author SHA1 Message Date
7f1f0caeed Merge branch 'master' into blog-content
Some checks failed
Deploy Astro Blog / build-and-deploy (push) Has been cancelled
2026-04-04 21:25:20 +03:00
e79bd0115b Update doc to remark42 related
added changelog
2026-04-04 21:24:55 +03:00
abc8462c32 update env example 2026-04-04 20:58:49 +03:00
978ae2f363 update remark link to env 2026-04-04 20:57:22 +03:00
f1f6e42ad2 Merge branch 'master' into blog-content 2026-04-04 13:14:36 +03:00
3f23b930d3 fix dark mode 2026-04-04 13:14:25 +03:00
81ec3a07ac Merge branch 'master' into blog-content 2026-04-04 13:08:02 +03:00
c8fbea7b07 update darkmode 2026-04-04 13:07:50 +03:00
18c9aceaa7 updated fonts 2026-04-04 12:59:59 +03:00
9fcbc3e753 Merge branch 'master' into blog-content 2026-04-04 12:59:25 +03:00
eaad8a3080 update font 2026-04-04 12:59:06 +03:00
fec9f85630 Merge branch 'master' into blog-content 2026-04-04 12:48:48 +03:00
a0eab19f31 refactor(comments): refactor comments from giscus to remark42 2026-04-04 12:48:25 +03:00
7d0bb1a973 udpate friend 2026-04-02 22:27:43 +03:00
706029d539 Merge branch 'master' into blog-content 2026-04-02 18:51:56 +03:00
31d12a4a06 update path in README 2026-04-02 18:51:30 +03:00
31 changed files with 742 additions and 818 deletions

View file

@ -1,5 +1,2 @@
GITHUB_TOKEN=
GISCUS_REPO_OWNER=
GISCUS_REPO_NAME=
GISCUS_CATEGORY_ID=
GISCUS_DATA_REPO_ID=
PUBLIC_REMARK42_HOST=
PUBLIC_REMARK42_SITE_ID=

View file

@ -1,5 +1,5 @@
# SanYeCao-Blog
<p align="right">[<a href="README.md">中文</a> | <a href="README-en.md">English</a>]</p>
<p align="right">[<a href="./README.md">中文</a> | <a href="./README-en.md">English</a>]</p>
<p align="center"><br>
✨SanYeCao Blog✨<br>
🌊Lightweight / Fast / Beautiful🌊<br><br>
@ -68,6 +68,18 @@ For details, see: <b>[GithubActions-en.md](docs/GithubActions-en.md)</b>.
Upload the generated `dist/` directory to your server, and configure `NGINX` to point to `index.html`.
### 4. Comments
This project uses self-hosted <b>[Remark42](https://remark42.com/)</b> as its comment system.
For installation and deployment details, please refer to the official documentation: <b>[Installation | Remark42](https://remark42.com/docs/getting-started/installation/)</b>
<br>
### 5. Changelog
For details, see: <b>[ChangeLog-en.md](./docs/ChangeLog-en.md)</b>
<hr>
## ⚖️ License

View file

@ -1,6 +1,6 @@
# SanYeCao-Blog
<p align="right">[<a href="README.md">中文</a> | <a href="README-en.md">English</a>]</p>
<p align="right">[<a href="./README.md">中文</a> | <a href="./README-en.md">English</a>]</p>
<p align="center"><br>
✨三叶草Blog✨<br>
🌊轻度/快速/美观🌊<br><br>
@ -69,6 +69,18 @@
将生成的`dist/`目录上传至你的服务器,使用`NGINX`指向index.html。
### 4. 评论区
项目使用自托管 <b>[Remark42](https://remark42.com/)</b> 作为评论区系统。
安装部署详情请查看官方文档:<b>[Installation | Remark42](https://remark42.com/docs/getting-started/installation/)</b>
<br>
### 5. 更新日志
详情请看:<b>[ChangeLog.md](./docs/ChangeLog.md)</b>
<hr>
## ⚖️ 许可

View file

@ -1,6 +1,7 @@
// @ts-check
import { defineConfig } from 'astro/config';
import sitemap from "@astrojs/sitemap";
import svelte from "@astrojs/svelte";
// https://astro.build/config
import { fileURLToPath } from 'node:url'
@ -16,5 +17,5 @@ export default defineConfig({
redirects: {
"/": "/zh",
},
integrations: [sitemap()],
integrations: [sitemap(), svelte()],
})

14
docs/ChangeLog-en.md Normal file
View file

@ -0,0 +1,14 @@
## v1.1.0 Update
Added improved locale support, with support for `zh-CN` and `en-US`. When uploading articles, it is recommended to upload both language versions to `src/blog/zh` and `src/blog/en`.
- Refactored the original `src/blog/posts.md` into `src/blog/zh/posts.md` and `src/blog/en/posts.md`
- When users click the language switch icon, the article list will automatically switch between Chinese and English
## v1.2.0 Update
Replaced the original `GISCUS`-based comment system with a self-hosted `Remark42`-based comment system.
- Removed `GISCUS`-related components
- Removed GitHub- and Giscus-related fields from `.env`
- Added `Remark42`-related components

14
docs/ChangeLog.md Normal file
View file

@ -0,0 +1,14 @@
## v1.1.0更新
添加完善的Locale支持支持 `zh-CN``en-US`,上传文章时建议同时上传两种语言的文章到 `src/blog/zh``src/blog/en` 中。
- 将原本的`src/blog/posts.md` 重构为`src/blog/zh/posts.md`和`src/blog/en/posts.md`
- 在用户点击切换中英文图标时,文章列表会自动切换
## v1.2.0 更新
将原本基于`GISCUS`的评论区更新为基于自托管`Remark42`
- 删去`GISCUS`相关组件
- 删去`.env`中github和giscus相关字段
- 新增`Remark42`相关组件

View file

@ -4,42 +4,17 @@
The following environment variables are listed in the project's `.env.example` file:
| Variable Name | Description |
| --------------------- | ----------- |
| `GITHUB_TOKEN` | Enter your <b>[Personal access token](https://github.com/settings/personal-access-tokens)</b> |
| `GISCUS_REPO_OWNER` | Your GitHub username, for example `ClovertaTheTrilobita` |
| `GISCUS_REPO_NAME` | Your code repository name, for example `SanYeCao-blog` |
| `GISCUS_CATEGORY_ID` | The category ID of `GISCUS`, see the explanation below |
| `GISCUS_DATA_REPO_ID` | The repository ID of `GISCUS`, see the explanation below |
| Variable Name | Description |
| ------------------------- | ---------------------------------------- |
| `PUBLIC_REMARK42_HOST` | The domain of the server hosting `Remark42` |
| `PUBLIC_REMARK42_SITE_ID` | Your custom `Remark42` site ID |
### 1. `GITHUB_TOKEN`
### 1. `PUBLIC_REMARK42_HOST`
It can greatly increase your GitHub API rate limit. Open this link: <b>[Personal access tokens](https://github.com/settings/personal-access-tokens)</b>
For example, if you deployed the `Remark42` Docker container on a server at `192.168.1.1` and pointed your DNS record to it, then fill in the resolved domain name (such as `https://comments.example.com`) in this field.
Choose <b>`Generate new token`</b>.
Do not add a trailing slash `/` at the end of the URL.
Then copy the generated token into the corresponding place in your `.env` file.
### 2. `PUBLIC_REMARK42_SITE_ID`
### 2. `GISCUS`
This blog uses a comment section based on the `GISCUS API`, which can map part of the GitHub repository's Discussions section onto your webpage as a comment area.
#### ① Enable Discussions in your repository
Go to <b>`Settings > General > Features`</b> in your repository and check <b>`Discussions`</b> to enable it.
Then go to the <b>`Discussions`</b> page, click the pencil icon next to <b>`Categories`</b> on the left side of the page, and then click <b>`New category`</b> to create a new category named <b>`Comments`</b>.
#### ② Install the Giscus GitHub App
Open this link: <b>[GitHub App - giscus](https://github.com/apps/giscus)</b>
Install it into the blog code repository you forked.
Then go to: <b>[giscus.app](https://giscus.app)</b>
In the <b>`Repository`</b> field, enter your repository address, and in <b>`Page ↔️ Discussions Mapping`</b>, choose <b>`Discussion title contains a specific term`</b>.
In <b>`Discussion Category`</b>, select the <b>`Comments`</b> category you just created.
Finally, in the generated code below, find <b>`data-category-id`</b> and <b>`data-repo-id`</b>, and fill them into the environment variables.
This is your custom site ID. For example, if you defined `SITE=sanyecao-blog` in your `Remark42` `docker-compose.yaml`, then you should also fill in `sanyecao-blog` in this field.

View file

@ -4,43 +4,17 @@
在项目的`.env.example`中,列举了如下环境变量
| 变量名 | 内容 |
| --------------------- | ------------------------------------------------------------ |
| `GITHUB_TOKEN` | 填写你的 <b>[Personal access tokens](https://github.com/settings/personal-access-tokens)</b> |
| `GISCUS_REPO_OWNER` | 你的Github账号名如`ClovertaTheTrilobita` |
| `GISCUS_REPO_NAME` | 你的代码仓库,如`SanYeCao-blog` |
| `GISCUS_CATEGORY_ID` | `GISCUS`的栏目ID,详见下方说明 |
| `GISCUS_DATA_REPO_ID` | `GISCUS`的仓库ID,详见下方说明 |
| 变量名 | 内容 |
| ------------------------- | -------------------------- |
| `PUBLIC_REMARK42_HOST` | 托管`Remark42`的服务器域名 |
| `PUBLIC_REMARK42_SITE_ID` | 你的自定义`Remark42`站点ID |
### 1. `GITHUB_TOKEN`
### 1. `PUBLIC_REMARK42_HOST`
它可以大幅增加你的Github API访问限度打开此链接<b>[Personal access tokens](https://github.com/settings/personal-access-tokens)</b>
例如你在`192.168.1.1`这台服务器上部署了`Remark42`的docker容器并将DNS解析指向他。将解析的域名如`https://comments.example.com`)填写在这个字段
选择 <b>`Generate new token`</b>
注意URL末尾不要添加反斜杠`/`
并将生成的Token复制到.env相应位置。
### 2. `GISCUS`
博客使用基于`GISCUS API`的评论区它可以将github仓库的Discussion区域部分映射到网页中以作为评论区使用。
#### ①启用你仓库的Discussion
在仓库的<b>`Settings > General > Features`</b>中找到<b>`Discussions`</b>勾选以启用它。
之后进入<b>`Discussion`</b>页面,点击页面左边<b>`Categories`</b>旁边的铅笔按钮,随后点击<b>`New category`</b>,新建一个名为<b>`Comments`</b>的栏目。
#### ②安装Giscus的Github App
点击此链接:<b>[Github App - giscus](https://github.com/apps/giscus)</b>
将其安装到你Fork的博客代码仓库中。
随后进入:<b>[giscus.app](https://giscus.app)</b>
<b>`Repository`</b>栏中填写你的仓库地址,并在<b>`Page ↔️ Discussions Mapping`</b>中选择<b>`Discussion title contains a specific term`</b>
<b>`Discussion Category`</b>处选择我们刚刚新建的<b>`Comments`</b>
最后在下面生成的代码中找到<b>`data-category-id`</b><b>`data-repo-id`</b>,将其填写到环境变量中。
### 2. `PUBLIC_REMARK42_SITE_ID`
你自定义的一个站点ID, 例如在你的`Remark42`的`docker-compose.yaml`中定义了`SITE=sanyecao-blog`,那请在这个字段中也填写`sanyecao-blog`

View file

@ -76,11 +76,8 @@ This variable is used to generate the `.env` file required for building.
If you have already configured your `.env`, it should contain the following:
```env
GITHUB_TOKEN=
GISCUS_REPO_OWNER=
GISCUS_REPO_NAME=
GISCUS_CATEGORY_ID=
GISCUS_DATA_REPO_ID=
PUBLIC_REMARK42_HOST=
PUBLIC_REMARK42_SITE_ID=
```
If you have not configured these yet or do not know what they mean, please see: <b>[EnvVariables-en.md](EnvVariables-en.md)</b>

View file

@ -76,11 +76,8 @@ cat ~/.ssh/github_actions_deploy
如果你已经设置好`.env`,它里面应该有如下内容
```env
GITHUB_TOKEN=
GISCUS_REPO_OWNER=
GISCUS_REPO_NAME=
GISCUS_CATEGORY_ID=
GISCUS_DATA_REPO_ID=
PUBLIC_REMARK42_HOST=
PUBLIC_REMARK42_SITE_ID=
```
如果你还没配置好或不知道这些是干什么的,请详见:<b>[EnvVariables.md](EnvVariables.md)</b>

273
package-lock.json generated
View file

@ -10,6 +10,7 @@
"dependencies": {
"@astrojs/rss": "^4.0.17",
"@astrojs/sitemap": "^3.7.1",
"@astrojs/svelte": "^8.0.4",
"astro": "^6.0.8",
"url": "^0.11.4"
},
@ -98,6 +99,25 @@
"zod": "^4.3.6"
}
},
"node_modules/@astrojs/svelte": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/@astrojs/svelte/-/svelte-8.0.4.tgz",
"integrity": "sha512-c5m3chjtgxBE3BzsE/bZbCFBkLPhq041rm2WJFaTIKGwt/3xNm/5efYCj23reuAcBsl4iYS8n2UwkAHQJzhkZA==",
"license": "MIT",
"dependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"svelte2tsx": "^0.7.52",
"vite": "^7.3.1"
},
"engines": {
"node": ">=22.12.0"
},
"peerDependencies": {
"astro": "^6.0.0",
"svelte": "^5.43.6",
"typescript": "^5.3.3"
}
},
"node_modules/@astrojs/telemetry": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz",
@ -1155,12 +1175,55 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@npmcli/fs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz",
@ -1682,6 +1745,53 @@
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
"node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"acorn": "^8.9.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
"license": "MIT",
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
"deepmerge": "^4.3.1",
"magic-string": "^0.30.21",
"obug": "^2.1.0",
"vitefu": "^1.1.1"
},
"engines": {
"node": "^20.19 || ^22.12 || >=24"
},
"peerDependencies": {
"svelte": "^5.0.0",
"vite": "^6.3.0 || ^7.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
"license": "MIT",
"dependencies": {
"obug": "^2.1.0"
},
"engines": {
"node": "^20.19 || ^22.12 || >=24"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.0.0-next.0",
"svelte": "^5.0.0",
"vite": "^6.3.0 || ^7.0.0"
}
},
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@ -1782,12 +1892,33 @@
"@types/node": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"peer": true
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@typescript-eslint/types": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz",
"integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==",
"license": "MIT",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@ -1811,6 +1942,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -2760,6 +2904,21 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/dedent-js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz",
"integrity": "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==",
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@ -3134,6 +3293,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/esm-env": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"license": "MIT",
"peer": true
},
"node_modules/esrap": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz",
"integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15",
"@typescript-eslint/types": "^8.2.0"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
@ -4117,6 +4294,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "^1.0.6"
}
},
"node_modules/is-relative": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
@ -4336,6 +4523,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/locate-character": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"license": "MIT",
"peer": true
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@ -6662,6 +6856,12 @@
"node": ">=11.0.0"
}
},
"node_modules/scule": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@ -7117,6 +7317,58 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svelte": {
"version": "5.55.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.1.tgz",
"integrity": "sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.4",
"esm-env": "^1.2.1",
"esrap": "^2.2.4",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/svelte/node_modules/aria-query": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/svelte2tsx": {
"version": "0.7.53",
"resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.53.tgz",
"integrity": "sha512-ljVSwmnYRDHRm8+7ICP6QoAN7U7vgOFfPBLN6T745YWNYqRRSzHxlrzUVqMjYls2Un8MzJissfziy/38e6Deeg==",
"license": "MIT",
"dependencies": {
"dedent-js": "^1.0.1",
"scule": "^1.3.0"
},
"peerDependencies": {
"svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0",
"typescript": "^4.9.4 || ^5.0.0"
}
},
"node_modules/svgo": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz",
@ -7400,6 +7652,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/ufo": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
@ -8186,6 +8452,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zimmerframe": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"license": "MIT",
"peer": true
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",

View file

@ -15,6 +15,7 @@
"dependencies": {
"@astrojs/rss": "^4.0.17",
"@astrojs/sitemap": "^3.7.1",
"@astrojs/svelte": "^8.0.4",
"astro": "^6.0.8",
"url": "^0.11.4"
},

View file

@ -1,341 +0,0 @@
---
import { getLangFromUrl } from "@/i18n";
const limit = Number(Astro.props.limit ?? 5);
const lang = getLangFromUrl(Astro.url);
function formatDate(iso: string) {
return new Date(iso).toISOString().slice(0, 10);
}
---
<section
id="latest-comments"
class="latest-comments"
data-lang={lang}
data-limit={limit}
>
<h2>{lang === "zh" ? "最新评论" : "Latest Comments"}</h2>
<div class="comments-loading" id="comments-loading">
<div class="loading-card"></div>
<div class="loading-card"></div>
<div class="loading-card"></div>
</div>
<ul class="comment-list" id="comment-list" style="display: none;"></ul>
<p class="empty" id="comments-empty" style="display: none;">
{lang === "zh" ? "还没有评论。" : "No comments yet."}
</p>
<p class="empty" id="comments-error" style="display: none;">
{lang === "zh" ? "评论加载失败。" : "Failed to load comments."}
</p>
</section>
<script>
type CommentAuthor = {
login?: string;
avatarUrl?: string;
url?: string;
};
type LatestCommentItem = {
id: string;
body?: string;
updatedAt: string;
author?: CommentAuthor | null;
postId: string;
postTitle: string;
localUrl: string;
isReply: boolean;
replyTo?: string;
};
type CommentsResponse = {
comments?: LatestCommentItem[];
error?: string;
};
const root = document.getElementById(
"latest-comments",
) as HTMLElement | null;
if (root) {
const lang = root.dataset.lang || "zh";
const limit = root.dataset.limit || "5";
const loadingEl = root.querySelector("#comments-loading");
const listEl = root.querySelector("#comment-list");
const emptyEl = root.querySelector("#comments-empty");
const errorEl = root.querySelector("#comments-error");
if (
!(loadingEl instanceof HTMLElement) ||
!(listEl instanceof HTMLElement) ||
!(emptyEl instanceof HTMLElement) ||
!(errorEl instanceof HTMLElement)
) {
throw new Error("Comments elements not found");
}
const formatDate = (iso: string) =>
new Date(iso).toISOString().slice(0, 10);
const renderComment = (comment: LatestCommentItem) => {
console.log("Start render");
const li = document.createElement("li");
li.className = `comment-card ${comment.isReply ? "is-reply" : ""}`;
li.innerHTML = `
<a href="${comment.localUrl}" class="comment-link" data-astro-reload>
<div class="comment-top">
${
comment.author?.avatarUrl
? `<img
src="${comment.author.avatarUrl}"
alt="${comment.author?.login ?? "author"}"
class="comment-avatar"
/>`
: ""
}
<div class="comment-meta">
<span class="comment-author">${comment.author?.login ?? "Unknown"}</span>
<span class="comment-date">${formatDate(comment.updatedAt)}</span>
</div>
<span class="comment-post-title" title="${comment.postTitle}">
${comment.postTitle}
</span>
</div>
${
comment.isReply
? `<p class="comment-kind">${
lang === "zh"
? `回复 @${comment.replyTo ?? "Unknown"}`
: `Reply to @${comment.replyTo ?? "Unknown"}`
}</p>`
: ""
}
<p class="comment-body">${comment.body ?? ""}</p>
</a>
`;
return li;
};
fetch(`/api/comments.json?lang=${lang}&limit=${limit}`)
.then((res) => {
if (!res.ok) throw new Error("Request failed");
return res.json() as Promise<CommentsResponse>;
})
.then((data) => {
loadingEl.style.display = "none";
const comments = data.comments ?? [];
if (!comments.length) {
emptyEl.style.display = "block";
return;
}
listEl.style.display = "grid";
comments.forEach((comment) => {
listEl.appendChild(renderComment(comment));
});
})
.catch((err) => {
console.error("Comments fetch failed:", err);
loadingEl.style.display = "none";
errorEl.style.display = "block";
});
}
</script>
<style is:global>
.latest-comments {
margin-top: 1.5rem;
}
.latest-comments h2 {
margin-bottom: 0.8rem;
}
.comment-list {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.9rem;
}
.comment-card {
margin: 0;
}
.comment-link {
display: block;
padding: 0.9rem 1rem;
text-decoration: none;
color: inherit;
border: 1px dashed #aeb8c2;
background: rgba(160, 175, 190, 0.06);
}
.comment-card.is-reply .comment-link {
border-style: dashed;
background: rgba(160, 175, 190, 0.1);
}
.comment-top {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.comment-meta {
display: flex;
flex-direction: column;
min-width: 0;
}
.comment-post-title {
margin-left: auto;
text-align: right;
font-size: 0.92rem;
font-weight: 700;
color: #6f8090;
max-width: 45%;
line-height: 1.35;
overflow-wrap: anywhere;
}
.comment-avatar {
width: 42px;
height: 42px;
object-fit: cover;
display: block;
}
.comment-author {
font-weight: 700;
line-height: 1.3;
}
.comment-date {
font-size: 0.9rem;
color: #6f8090;
}
.comment-kind {
margin: 0 0 0.35rem 0;
font-size: 0.82rem;
color: #8c8c8c;
font-style: italic;
}
.comment-body {
margin: 0;
color: #555;
line-height: 1.6;
font-style: italic;
}
.empty {
color: #6f8090;
}
.comments-loading {
display: grid;
gap: 0.9rem;
}
.loading-card {
height: 92px;
border: 1px dashed #aeb8c2;
background: linear-gradient(
90deg,
rgba(160, 175, 190, 0.06) 25%,
rgba(160, 175, 190, 0.16) 50%,
rgba(160, 175, 190, 0.06) 75%
);
background-size: 200% 100%;
animation: shimmer 1.2s infinite linear;
}
@keyframes shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
:global(.dark) .comment-link {
border-color: #7f8c97;
background: rgba(180, 190, 200, 0.06);
}
:global(.dark) .comment-card.is-reply .comment-link {
background: rgba(180, 190, 200, 0.1);
}
:global(.dark) .comment-post-title {
color: #c8d2dc;
}
:global(.dark) .comment-date,
:global(.dark) .empty {
color: #aab7c4;
}
:global(.dark) .comment-kind {
color: #b8b8b8;
}
:global(.dark) .comment-body {
color: #d3d7db;
}
:global(.dark) .loading-card {
border-color: #7f8c97;
background: linear-gradient(
90deg,
rgba(180, 190, 200, 0.06) 25%,
rgba(180, 190, 200, 0.16) 50%,
rgba(180, 190, 200, 0.06) 75%
);
background-size: 200% 100%;
}
@media (max-width: 640px) {
.comment-top {
align-items: flex-start;
flex-wrap: wrap;
}
.comment-post-title {
margin-left: 0;
max-width: 100%;
width: 100%;
text-align: left;
}
}
</style>

View file

@ -1,67 +0,0 @@
---
const { term } = Astro.props;
const repoOwner = import.meta.env.GISCUS_REPO_OWNER;
const repoName = import.meta.env.GISCUS_REPO_NAME;
const categoryId = import.meta.env.GISCUS_CATEGORY_ID;
const dataRepoId = import.meta.env.GISCUS_DATA_REPO_ID;
const repo = `${repoOwner}/${repoName}`;
---
<section class="comments">
<h2>Comments</h2>
<script
src="https://giscus.app/client.js"
data-repo={repo}
data-repo-id={dataRepoId}
data-category="Comments"
data-category-id={categoryId}
data-mapping="specific"
data-term={term}
data-strict="1"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="preferred_color_scheme"
data-lang="en"
data-loading="lazy"
crossorigin="anonymous"
async></script>
<script is:inline>
function setGiscusTheme(theme) {
const iframe = document.querySelector("iframe.giscus-frame");
if (!iframe) return;
iframe.contentWindow?.postMessage(
{
giscus: {
setConfig: {
theme,
},
},
},
"https://giscus.app",
);
}
function syncGiscusTheme() {
const isDark = document.documentElement.classList.contains("dark");
setGiscusTheme(isDark ? "dark" : "light");
}
const observer = new MutationObserver(() => {
syncGiscusTheme();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
window.addEventListener("load", () => {
syncGiscusTheme();
});
</script>
</section>

View file

@ -1,5 +1,6 @@
---
import "@/styles/global.css";
import Remark42Count from "@/components/remark42-counter.svelte";
const data = Astro.props;
---
@ -13,8 +14,9 @@ const data = Astro.props;
<span class="post-date">{data.date}</span>
<span class="post-stats">
<span class="post-stat">💬 {data.commentCount ?? 0}</span>
<span class="post-stat">❤ {data.reactionCount ?? 0}</span>
<span class="post-stat"
>💬 <Remark42Count url=`${data.postPath}` client:idle /></span
>
</span>
</div>
</div>

View file

@ -3,77 +3,9 @@ import { getCollection } from "astro:content";
import PostItem from "./PostItem.astro";
import { getLangFromUrl } from "@/i18n";
const token = import.meta.env.GITHUB_TOKEN;
const owner = import.meta.env.GISCUS_REPO_OWNER;
const name = import.meta.env.GISCUS_REPO_NAME;
const categoryId = import.meta.env.GISCUS_CATEGORY_ID;
const lang = getLangFromUrl(Astro.url);
const allPosts = await getCollection("blog");
type ReactionGroup = {
users?: {
totalCount?: number;
};
};
type DiscussionNode = {
title: string;
comments?: {
totalCount?: number;
};
reactionGroups?: ReactionGroup[];
};
function normalizePath(path: string) {
return path.replace(/^\/+|\/+$/g, "");
}
async function fetchDiscussionStats(): Promise<DiscussionNode[]> {
// const token = import.meta.env.GITHUB_TOKEN;
if (!token) return [];
const query = `
query($owner: String!, $name: String!, $categoryId: ID!) {
repository(owner: $owner, name: $name) {
discussions(first: 100, categoryId: $categoryId) {
nodes {
title
comments(first: 0) {
totalCount
}
reactionGroups {
users {
totalCount
}
}
}
}
}
}
`;
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query,
variables: {
owner: owner,
name: name,
categoryId: categoryId,
},
}),
});
const json = await res.json();
return (json?.data?.repository?.discussions?.nodes ?? []) as DiscussionNode[];
}
const discussions = await fetchDiscussionStats();
const sortedPosts = [...allPosts].sort(
(a, b) =>
new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime(),
@ -97,30 +29,14 @@ const latestPosts = filteredPosts.slice(0, 5);
const [postLang, ...slugParts] = post.id.split("/");
const slug = slugParts.join("/");
const pathname = `/${slug}/`;
const matchedDiscussion = discussions.find((d: DiscussionNode) => {
return normalizePath(d.title) === normalizePath(pathname);
});
const reactionCount =
matchedDiscussion?.reactionGroups?.reduce(
(sum: number, group: ReactionGroup) =>
sum + (group.users?.totalCount ?? 0),
0,
) ?? 0;
const commentCount = matchedDiscussion?.comments?.totalCount ?? 0;
return (
<PostItem
url={`/${postLang}/posts/${slug}/`}
postPath={`/posts/${slug}/`}
title={post.data.title}
description={post.data.description}
date={formattedDate}
img={post.data.image.url}
commentCount={commentCount}
reactionCount={reactionCount}
/>
);
})

View file

@ -0,0 +1,38 @@
<script>
import { onMount } from "svelte";
export let url;
const host = import.meta.env.PUBLIC_REMARK42_HOST;
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
onMount(() => {
const remark_config = {
host,
site_id: siteId,
components: ["counter"],
show_rss_subscription: false,
theme: localStorage.getItem("color-theme") ?? "light",
};
window.remark_config = remark_config;
for (const name of remark_config.components || ["embed"]) {
const script = document.createElement("script");
let ext = ".js";
if ("noModule" in script) {
script.type = "module";
ext = ".mjs";
} else {
script.async = true;
}
script.defer = true;
script.src = `${remark_config.host}/web/${name}${ext}`;
document.head.appendChild(script);
}
});
</script>
<span class="remark42__counter" data-url={url}></span>

View file

@ -0,0 +1,75 @@
<script>
import { onMount } from "svelte";
export let slug;
let pagePath = `/posts/${slug}/`;
function getTheme() {
return document.documentElement.classList.contains("dark")
? "dark"
: "light";
}
function applyRemark42Theme() {
if (window.REMARK42 && typeof window.REMARK42.changeTheme === "function") {
window.REMARK42.changeTheme(getTheme());
return true;
}
return false;
}
onMount(() => {
const scriptId = "remark42-script";
window.remark_config = {
host: import.meta.env.PUBLIC_REMARK42_HOST,
site_id: import.meta.env.PUBLIC_REMARK42_SITE_ID,
components: ["embed"],
show_rss_subscription: false,
theme: getTheme(),
url: pagePath,
page_id: pagePath,
};
if (!document.getElementById(scriptId)) {
const script = document.createElement("script");
script.id = scriptId;
script.async = true;
script.innerHTML = `
!function(e,n){
for(var o=0;o<e.length;o++){
var r=n.createElement("script"),c=".js",d=n.head||n.body;
"noModule"in r?(r.type="module",c=".mjs"):r.async=!0;
r.defer=!0;
r.src=window.remark_config.host+"/web/"+e[o]+c;
d.appendChild(r);
}
}(window.remark_config.components||["embed"],document);
`;
document.body.appendChild(script);
}
const applyWhenReady = setInterval(() => {
if (applyRemark42Theme()) {
clearInterval(applyWhenReady);
}
}, 200);
const observer = new MutationObserver(() => {
applyRemark42Theme();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => {
clearInterval(applyWhenReady);
observer.disconnect();
};
});
</script>
<div id="remark42"></div>

View file

@ -0,0 +1,252 @@
<script>
import { onMount } from "svelte";
export let lang = "zh";
let loaded = false;
function rewriteLinks() {
const links = document.querySelectorAll(
".latest-comments .comment__title-link",
);
links.forEach((link) => {
const href = link.getAttribute("href");
if (!href) return;
if (href.startsWith("/posts/")) {
link.setAttribute("href", `/${lang}${href}`);
}
});
}
onMount(() => {
window.remark_config = {
host: import.meta.env.PUBLIC_REMARK42_HOST,
site_id: import.meta.env.PUBLIC_REMARK42_SITE_ID,
components: ["last-comments"],
theme: localStorage.getItem("color-theme") ?? "light",
max_last_comments: 10,
no_footer: true,
};
const loaderId = "remark42-last-comments-loader";
if (!document.getElementById(loaderId)) {
const init = document.createElement("script");
init.id = loaderId;
init.innerHTML = `
!function(e,n){
for(var o=0;o<e.length;o++){
var r=n.createElement("script"),c=".js",d=n.head||n.body;
"noModule"in r?(r.type="module",c=".mjs"):r.async=!0;
r.defer=!0;
r.src=window.remark_config.host+"/web/"+e[o]+c;
d.appendChild(r);
}
}(window.remark_config.components||["last-comments"],document);
`;
document.body.appendChild(init);
}
const timer = setInterval(() => {
const root = document.querySelector(".remark42__last-comments");
if (root && root.children.length > 0) {
rewriteLinks();
loaded = true;
clearInterval(timer);
}
}, 200);
return () => clearInterval(timer);
});
</script>
<section class="latest-comments">
<h2>最新评论</h2>
{#if !loaded}
<div class="comments-loading" aria-hidden="true">
<div class="loading-card"></div>
<div class="loading-card"></div>
<div class="loading-card"></div>
</div>
{/if}
<div class="remark42__last-comments" data-max="10"></div>
</section>
<style>
.loading-card {
height: 92px;
margin: 0 0 14px 0;
padding: 0.9rem 1rem;
border: 1px dashed #aeb8c2;
background: linear-gradient(
90deg,
rgba(160, 175, 190, 0.06) 25%,
rgba(160, 175, 190, 0.16) 50%,
rgba(160, 175, 190, 0.06) 75%
);
background-size: 200% 100%;
animation: shimmer 1.2s infinite linear;
}
@keyframes shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
.latest-comments {
margin-top: 1.5rem;
}
.latest-comments h2 {
margin-bottom: 0.8rem;
}
.latest-comments :global(.remark42__last-comments) {
display: block;
}
.latest-comments :global(article.comment.list-comments__item) {
display: block;
margin: 0 0 14px 0;
padding: 0.9rem 1rem;
border: 1px dashed #aeb8c2;
background: rgba(160, 175, 190, 0.06);
}
.latest-comments :global(article.comment.list-comments__item:last-child) {
margin-bottom: 0;
}
:global(.dark)
.latest-comments
:global(article.comment.list-comments__item) {
background: #2a3138;
border-color: #7f8c97;
}
.latest-comments :global(.comment__body) {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
grid-template-areas:
"info title"
"text title";
gap: 0.35rem 1rem;
align-items: start;
}
.latest-comments :global(.comment__title) {
grid-area: title;
text-align: right;
min-width: 0;
white-space: normal;
}
.latest-comments :global(.comment__title-link) {
color: #6f8090;
text-decoration: none;
font-size: 0.87rem;
font-weight: 700;
line-height: 1.45;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.latest-comments :global(.comment__title-link:hover) {
text-decoration: underline;
}
.latest-comments :global(.comment__info) {
grid-area: info;
font-weight: 700;
line-height: 1.3;
color: inherit;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
font-style: italic;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
}
.latest-comments :global(.comment__info)::before {
content: "@";
margin-right: 0.08em;
opacity: 0.85;
}
.latest-comments :global(.comment__text) {
grid-area: text;
margin: 0;
color: #555;
line-height: 1.7;
font-size: 1.03rem;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
white-space: pre-wrap;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
}
:global(.dark)
.latest-comments
:global(article.comment.list-comments__item) {
border-color: #7f8c97;
background: rgba(180, 190, 200, 0.06);
}
:global(.dark) .latest-comments :global(.comment__title-link) {
color: #c8d2dc;
}
:global(.dark) .latest-comments :global(.comment__text) {
color: #d3d7db;
}
@media (max-width: 640px) {
.latest-comments :global(.comment__body) {
grid-template-columns: 1fr;
grid-template-areas:
"info"
"title"
"text";
}
.latest-comments :global(.comment__title) {
text-align: left;
min-width: 0;
}
.latest-comments :global(.comment__title-link) {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>

6
src/friends/closelib.md Normal file
View file

@ -0,0 +1,6 @@
---
name: '極限風味 Blog'
description: "邪惡與極限伴您一生……🥰"
url: "https://ex-tasty.com"
avatar: 'https://files.seeusercontent.com/2026/04/02/dkJ4/pasted-image-1775158039506.webp'
---

View file

@ -1,10 +1,11 @@
---
import BaseLayout from "./BaseLayout.astro";
import Giscus from "@/components/Giscus.astro";
import Remark42Embed from "@/components/remark42-embed.svelte";
import { getLangFromUrl, getTranslations } from "@/i18n";
import "@/styles/global.css";
const { frontmatter, lang, postId } = Astro.props;
const comments = lang === "zh" ? "评论区" : "comments";
const t = getTranslations(lang);
---
@ -47,7 +48,8 @@ const t = getTranslations(lang);
<slot />
</div>
<Giscus term={postId} />
<h2>{comments}</h2>
<Remark42Embed slug={postId} client:load />
</article>
</BaseLayout>

View file

@ -1,7 +1,7 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import PostList from "@/components/Posts/PostList.astro";
import Comments from "@/components/Comments.astro";
import Remark42LatestComments from "@/components/remark42-latest-comments.svelte";
import { getLangFromUrl, getTranslations } from "@/i18n";
import "@/styles/global.css";
@ -50,7 +50,7 @@ const pageTitle = t.home.title;
<div class="section-divider"></div>
<Comments limit={7} />
<Remark42LatestComments lang={lang} client:load />
</BaseLayout>
<style>

View file

@ -1,231 +0,0 @@
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
const token = import.meta.env.GITHUB_TOKEN;
const owner = import.meta.env.GISCUS_REPO_OWNER ?? "ClovertaTheTrilobita";
const name = import.meta.env.GISCUS_REPO_NAME ?? "SanYeCao-blog";
const categoryId = import.meta.env.GISCUS_CATEGORY_ID ?? "DIC_kwDORvuVpM4C5MDE";
export function getStaticPaths() {
return [{ params: { lang: "zh" } }, { params: { lang: "en" } }];
}
type CommentAuthor = {
login?: string;
avatarUrl?: string;
url?: string;
};
type RawReply = {
id: string;
body?: string;
updatedAt: string;
author?: CommentAuthor | null;
};
type RawComment = {
id: string;
body?: string;
updatedAt: string;
author?: CommentAuthor | null;
replies?: {
nodes?: RawReply[];
};
};
type DiscussionNode = {
title: string;
comments?: {
nodes?: RawComment[];
};
};
type LatestCommentItem = {
id: string;
body?: string;
updatedAt: string;
author?: CommentAuthor | null;
postId: string;
postTitle: string;
localUrl: string;
isReply: boolean;
replyTo?: string;
};
function normalizePath(path: string) {
return path.replace(/^\/+|\/+$/g, "");
}
function excerpt(text = "", max = 110) {
const plain = text.replace(/\s+/g, " ").trim();
if (plain.length <= max) return plain;
return plain.slice(0, max) + "…";
}
async function fetchLatestComments(lang: string, limit: number): Promise<LatestCommentItem[]> {
if (!token) return [];
const allPosts = await getCollection("blog");
const query = `
query($owner: String!, $name: String!, $categoryId: ID!) {
repository(owner: $owner, name: $name) {
discussions(
first: 20
categoryId: $categoryId
orderBy: { field: UPDATED_AT, direction: DESC }
) {
nodes {
title
comments(last: 10) {
nodes {
id
body
updatedAt
author {
... on User {
login
avatarUrl
url
}
... on Organization {
login
avatarUrl
url
}
}
replies(first: 10) {
nodes {
id
body
updatedAt
author {
... on User {
login
avatarUrl
url
}
... on Organization {
login
avatarUrl
url
}
}
}
}
}
}
}
}
}
}
`;
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query,
variables: { owner, name, categoryId },
}),
});
const json = await res.json();
const discussions = (json?.data?.repository?.discussions?.nodes ?? []) as DiscussionNode[];
const postMap = new Map(
allPosts.map((post: any) => [
normalizePath(post.id),
{
title: post.data.title,
url: `/${lang}/posts/${post.id}/#comments`,
},
]),
);
const comments: LatestCommentItem[] = [];
for (const discussion of discussions) {
const discussionKey = normalizePath(discussion.title);
let matchedPostId: string | undefined;
if (postMap.has(discussionKey)) {
matchedPostId = discussionKey;
} else {
matchedPostId = [...postMap.keys()].find(
(postId) =>
discussionKey === postId ||
discussionKey.includes(postId) ||
postId.includes(discussionKey),
);
}
if (!matchedPostId) continue;
const postInfo = postMap.get(matchedPostId);
if (!postInfo) continue;
for (const comment of discussion.comments?.nodes ?? []) {
comments.push({
id: comment.id,
body: excerpt(comment.body),
updatedAt: comment.updatedAt,
author: comment.author,
postId: matchedPostId,
postTitle: postInfo.title,
localUrl: postInfo.url,
isReply: false,
});
for (const reply of comment.replies?.nodes ?? []) {
comments.push({
id: reply.id,
body: excerpt(reply.body),
updatedAt: reply.updatedAt,
author: reply.author,
postId: matchedPostId,
postTitle: postInfo.title,
localUrl: postInfo.url,
isReply: true,
replyTo: comment.author?.login ?? "Unknown",
});
}
}
}
return comments
.sort(
(a, b) =>
new Date(b.updatedAt).getTime() -
new Date(a.updatedAt).getTime(),
)
.slice(0, limit);
}
export const GET: APIRoute = async ({ params, url }) => {
try {
const lang = params.lang === "en" ? "en" : "zh";
const limit = Number(url.searchParams.get("limit") ?? 5);
const comments = await fetchLatestComments(lang, limit);
return new Response(JSON.stringify({ comments }), {
status: 200,
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "public, max-age=300",
},
});
} catch (error) {
return new Response(JSON.stringify({ comments: [], error: "Failed to load comments" }), {
status: 500,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
});
}
};

View file

@ -85,7 +85,7 @@ html.dark body::after {
background-image: url("https://files.seeusercontent.com/2026/03/30/vd8W/touhou___alice_margatroid_x_kiri.webp");
}
@media (max-width: 768px) {
@media (max-width: 900px) {
body::after {
opacity: 0.06;
}

5
svelte.config.js Normal file
View file

@ -0,0 +1,5 @@
import { vitePreprocess } from "@astrojs/svelte";
export default {
preprocess: vitePreprocess(),
};