Compare commits

..

No commits in common. "master" and "v1.2.1" have entirely different histories.

54 changed files with 4545 additions and 4667 deletions

View file

@ -1,116 +1,44 @@
name: Sync and Deploy Astro Blog name: Deploy Astro Blog
on: on:
push: push:
branches: branches:
- master - blog-content
paths:
- 'src/blog/**'
- 'src/friends/**'
- 'package.json'
- 'package-lock.json'
- 'astro.config.mjs'
- 'astro.config.ts'
- 'public/**'
- '.github/workflows/deploy-blog.yml'
workflow_dispatch: workflow_dispatch:
discussion:
types: [created, category_changed]
discussion_comment:
types: [created, edited, deleted]
concurrency: concurrency:
group: deploy-blog group: deploy-blog
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
sync-blog-content:
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Checkout blog-content branch
run: |
git checkout blog-content
git pull origin blog-content
- name: Merge master into blog-content
run: |
git merge origin/master --no-edit
- name: Push blog-content
run: |
git push origin blog-content
build-and-deploy: build-and-deploy:
needs: sync-blog-content if: >
if: github.event_name == 'push' github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'discussion' &&
github.event.discussion.category.slug == 'comments'
) ||
(
github.event_name == 'discussion_comment' &&
github.event.discussion.category.slug == 'comments'
)
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment:
name: production
url: https://blog.cloverta.top
env:
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
DEPLOY_PORT: ${{ vars.DEPLOY_PORT }}
DEPLOY_PATH: ${{ vars.DEPLOY_PATH }}
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
steps:
- name: Checkout blog-content branch
uses: actions/checkout@v5
with:
ref: blog-content
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install chromium --with-deps
- name: Create .env file
run: |
cat > .env <<'EOF'
${{ secrets.ENV_FILE }}
EOF
- name: Build Astro site
run: npm run build
- name: Install rsync and ssh client
run: sudo apt-get update && sudo apt-get install -y rsync openssh-client
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Deploy to server
run: |
rsync -avz --delete \
--omit-dir-times \
--no-perms \
--no-owner \
--no-group \
-e "ssh -p ${DEPLOY_PORT}" \
dist/ \
${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/
manual-deploy:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
environment:
name: production
url: https://blog.cloverta.top
env: env:
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }} DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
@ -133,9 +61,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Install Playwright browsers
run: npx playwright install chromium --with-deps
- name: Create .env file - name: Create .env file
run: | run: |
cat > .env <<'EOF' cat > .env <<'EOF'

View file

@ -86,10 +86,6 @@ For installation and deployment details, please refer to the official documentat
For details, see: <b>[ChangeLog-en.md](./docs/ChangeLog-en.md)</b> For details, see: <b>[ChangeLog-en.md](./docs/ChangeLog-en.md)</b>
### 6. Older Versions
Older versions of GISCUS are available on the <b>[deprecated/giscus](https://github.com/ClovertaTheTrilobita/SanYeCao-blog/tree/deprecated/giscus)</b> branch, but they havent been updated in a while and may contain numerous issues.
<hr> <hr>
## ⚖️ License ## ⚖️ License

View file

@ -1,6 +1,6 @@
# SanYeCao-Blog # SanYeCao-Blog
<p align="right">[<a href="./README.md">中文</a> | <a href="./README-en.md">English</a>]</p> [中文](README.md) | [English](README-en.md)
<p align="center"><br> <p align="center"><br>
✨三叶草Blog✨<br> ✨三叶草Blog✨<br>
@ -88,10 +88,6 @@
详情请看:<b>[ChangeLog.md](./docs/ChangeLog.md)</b> 详情请看:<b>[ChangeLog.md](./docs/ChangeLog.md)</b>
### 6. 旧版本
支持GISCUS的旧版本在 <b>[deprecated/giscus](https://github.com/ClovertaTheTrilobita/SanYeCao-blog/tree/deprecated/giscus)</b> 分支上,但是它有些时候没更新了,可能会有诸多问题。
<hr> <hr>
## ⚖️ 许可 ## ⚖️ 许可

View file

@ -2,9 +2,6 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import svelte from "@astrojs/svelte"; import svelte from "@astrojs/svelte";
import rehypeMermaid from "rehype-mermaid";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
// https://astro.build/config // https://astro.build/config
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
@ -21,15 +18,4 @@ export default defineConfig({
"/": "/zh", "/": "/zh",
}, },
integrations: [sitemap(), svelte()], integrations: [sitemap(), svelte()],
markdown: {
syntaxHighlight: {
type: 'shiki',
excludeLangs: ['mermaid', 'math'],
},
rehypePlugins: [
rehypeMermaid,
rehypeKatex,
],
remarkPlugins: [remarkMath],
},
}) })

95
build/fontmin.js Normal file
View file

@ -0,0 +1,95 @@
import fs from "fs";
import path from "path";
import Fontmin from "fontmin";
function getFiles(dir) {
const results = [];
const list = fs.readdirSync(dir);
for (const file of list) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
results.push(...getFiles(filePath));
} else {
results.push(filePath);
}
}
return results;
}
function scanDirectory(dir) {
let set = new Set();
const files = getFiles(dir);
for (const file of files) {
const ignoredExtensions = [
".ttf",
".otf",
".woff",
".woff2",
".eot",
".png",
".jpg",
".jpeg",
".webp",
".gif",
".ico",
".pdf",
];
if (ignoredExtensions.some((ext) => file.endsWith(ext))) continue;
try {
const content = fs.readFileSync(file, "utf8");
const currentSet = new Set(content);
set = new Set([...set, ...currentSet]);
} catch {
// 跳过二进制等不可读文件
}
}
return set;
}
function subsetFont(src, text) {
return new Promise((resolve, reject) => {
const fontmin = new Fontmin()
.src(src)
.use(
Fontmin.glyph({
text,
hinting: false,
})
)
.use(Fontmin.ttf2woff2())
.dest("public/fonts/subset");
fontmin.run((err) => {
if (err) return reject(err);
resolve();
});
});
}
async function main() {
const baseChars =
"首页文章标签关于作者评论发布于切换主题,。!?:“”‘’()《》【】、—…·-_/\\'\"()[]{}<>:;.!? ";
const scanned = Array.from(scanDirectory("src")).join("");
const chars = Array.from(new Set((scanned + baseChars).split(""))).join("");
await Promise.all([
subsetFont("public/fonts/MapleMono-CN-Regular.ttf", chars),
subsetFont("public/fonts/MapleMono-CN-Bold.ttf", chars),
subsetFont("public/fonts/MapleMono-CN-Italic.ttf", chars),
]);
console.log(`中文子集字体生成完成,共收集 ${chars.length} 个字符`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

5537
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,23 +6,21 @@
"node": ">=22.12.0" "node": ">=22.12.0"
}, },
"scripts": { "scripts": {
"dev": "PUBLIC_GIT_COMMIT=$(git rev-parse HEAD) astro dev", "dev": "astro dev",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro",
"build": "PUBLIC_GIT_COMMIT=$(git rev-parse HEAD) astro build" "subset-font": "node build/fontmin.js",
"build": "npm run subset-font && astro build"
}, },
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.17", "@astrojs/rss": "^4.0.17",
"@astrojs/sitemap": "^3.7.1", "@astrojs/sitemap": "^3.7.1",
"@astrojs/svelte": "^8.0.4", "@astrojs/svelte": "^8.0.4",
"astro": "^6.0.8", "astro": "^6.0.8",
"katex": "^0.17.0", "url": "^0.11.4"
"playwright": "^1.59.1",
"rehype-katex": "^7.0.1",
"rehype-mermaid": "^3.0.0",
"remark-math": "^6.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^25.5.0" "@types/node": "^25.5.0",
"fontmin": "^1.1.1"
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1 +0,0 @@
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M957.1 183.2c-0.2-0.4-0.3-0.8-0.5-1.2-0.7-1.7-1.6-3.3-2.5-4.8-0.3-0.5-0.7-1.1-1-1.6-0.5-0.8-1.1-1.6-1.6-2.3-0.2-0.3-0.4-0.6-0.7-0.9-0.4-0.6-0.9-1.1-1.4-1.7-0.5-0.5-1-1.1-1.5-1.6-2.3-2.4-4.9-4.6-7.7-6.4-0.7-0.4-1.4-0.9-2.1-1.3-2.8-1.7-5.9-3-9-4l-0.6-0.2c-0.5-0.2-1.1-0.3-1.7-0.4-3.7-1-7.6-1.5-11.4-1.5H106.7l-0.9 0.1h-0.2C82.2 157 64 176.6 64.1 200.1c0 12.2 5 23.9 13.8 32.3l404.8 404.8c18 17.6 46.7 17.7 64.8 0.2l323.3-323.3v468.6h-717V648.2c0-24.7-20.1-44.8-44.8-44.8-24.7 0-44.8 20.1-44.8 44.8v179.3c0 24.7 20.1 44.8 44.8 44.8h806.6c24.7 0 44.8-20.1 44.8-44.8V200.1c-0.1-5.8-1.2-11.5-3.3-16.9z m-442 359.7l-298-298h596l-298 298z" fill="" /><path d="M108.9 513.8c24.7 0 44.8-20.1 44.8-44.8v-44.8c0-24.7-20.1-44.8-44.8-44.8-24.7 0-44.8 20.1-44.8 44.8V469c0 24.7 20 44.8 44.8 44.8z" fill="" /></svg>

Before

Width:  |  Height:  |  Size: 994 B

View file

@ -1 +0,0 @@
<svg class="svg-icon" style="width: 1em;height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M692.906667 938.666667a42.666667 42.666667 0 0 1-42.666667-42.666667v-110.933333a91.733333 91.733333 0 0 0-23.04-70.826667 42.666667 42.666667 0 0 1 26.026667-71.253333C757.333333 630.613333 853.333333 597.333333 853.333333 416.853333a170.666667 170.666667 0 0 0-28.586666-94.72 117.333333 117.333333 0 0 1-17.493334-87.893333 158.293333 158.293333 0 0 0 0-60.16 326.4 326.4 0 0 0-89.173333 46.506667 42.666667 42.666667 0 0 1-35.84 6.4 433.066667 433.066667 0 0 0-235.52 0 42.666667 42.666667 0 0 1-35.84-6.4 315.733333 315.733333 0 0 0-90.026667-46.506667 150.186667 150.186667 0 0 0 0 60.16 121.173333 121.173333 0 0 1-18.346666 88.746667 173.653333 173.653333 0 0 0-28.586667 95.146666c0 165.973333 80.213333 210.346667 200.533333 225.706667a42.666667 42.666667 0 0 1 34.986667 28.16 42.666667 42.666667 0 0 1-8.96 42.666667 87.893333 87.893333 0 0 0-23.466667 66.56V896a42.666667 42.666667 0 0 1-85.333333 0v-24.32a256 256 0 0 1-224.853333-89.173333 166.4 166.4 0 0 0-49.493334-37.546667 42.666667 42.666667 0 1 1 21.333334-82.773333 210.346667 210.346667 0 0 1 85.333333 58.026666c42.666667 42.666667 85.333333 80.213333 166.4 64.853334a165.973333 165.973333 0 0 1 9.813333-67.413334c-87.893333-22.186667-213.333333-85.333333-213.333333-298.666666a256 256 0 0 1 42.666667-142.08 36.266667 36.266667 0 0 0 5.546666-26.453334 242.773333 242.773333 0 0 1 14.08-136.96 42.666667 42.666667 0 0 1 26.88-24.32c14.506667-4.266667 66.56-12.8 165.12 51.2a518.826667 518.826667 0 0 1 242.773334 0c98.56-64 150.613333-55.893333 164.693333-51.2a42.666667 42.666667 0 0 1 26.88 24.32 243.626667 243.626667 0 0 1 14.08 137.386667 32 32 0 0 0 4.693333 24.32 256 256 0 0 1 42.666667 142.506667c0 216.32-124.586667 279.04-213.333333 298.666666a182.613333 182.613333 0 0 1 9.386666 71.253334V896a42.666667 42.666667 0 0 1-40.106666 42.666667z" /></svg>

Before

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,47 +0,0 @@
---
const rawHash = (import.meta.env.PUBLIC_GIT_COMMIT || "")
.replace(/[^a-fA-F0-9]/g, "")
.toLowerCase();
const safeHash = rawHash || "0000000000000000000000000000000000000000";
const blockCount = 6;
const hexPerBlock = 6;
const blocks: string[] = Array.from({ length: blockCount }, (_, i) => {
const start = i * hexPerBlock;
const part = safeHash.slice(start, start + hexPerBlock).padEnd(6, "0");
return `#${part}`;
});
---
<div
class="build-hash"
title={`Build ${safeHash}`}
aria-label={`Build ${safeHash}`}
>
<div class="build-hint">$build.onCommit.hash =</div>
{
blocks.map((color: string) => (
<span class="build-block" style={`background:${color}`} />
))
}
</div>
<style>
.build-hint {
margin-right: 5px;
font-size: 0.8rem;
}
.build-hash {
display: inline-flex;
align-items: center;
/* vertical-align: middle; */
}
.build-block {
width: 1rem;
height: 1rem;
display: inline-block;
}
</style>

View file

@ -1,224 +0,0 @@
<div class="floating-actions">
<button
id="theme-toggle-fab"
aria-label="Toggle theme"
title="Toggle theme"
>
<svg
class="theme-toggle-icon"
viewBox="0 0 24 24"
width="20"
height="20"
aria-hidden="true"
>
<path
class="sun"
fill-rule="evenodd"
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
></path>
<path
class="moon"
fill-rule="evenodd"
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
></path>
</svg>
</button>
<button id="back-to-top" aria-label="Back to top" title="Back to top">
<svg
viewBox="0 0 24 24"
width="20"
height="20"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M18 15l-6-6-6 6"></path>
</svg>
</button>
</div>
<style>
.floating-actions {
position: fixed;
right: 1.25rem;
bottom: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
z-index: 1000;
}
#theme-toggle-fab,
#back-to-top {
width: 3rem;
height: 3rem;
border: #a7a7a7 1.5px solid;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
color: #222;
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(6px);
}
#theme-toggle-fab {
position: relative;
}
#theme-toggle-fab {
position: relative;
}
.theme-toggle-icon {
width: 1.2rem;
height: 1.2rem;
fill: currentColor;
}
.theme-toggle-icon .sun,
.theme-toggle-icon .moon {
transform-origin: center;
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.theme-toggle-icon .sun {
opacity: 1;
transform: scale(1) rotate(0deg);
}
.theme-toggle-icon .moon {
opacity: 0;
transform: scale(0.75) rotate(-20deg);
}
:global(.dark) .theme-toggle-icon .sun {
opacity: 0;
transform: scale(0.75) rotate(20deg);
}
:global(.dark) .theme-toggle-icon .moon {
opacity: 1;
transform: scale(1) rotate(0deg);
}
#theme-toggle-fab,
#back-to-top {
/* opacity: 0; */
visibility: hidden;
transform: translateY(150px);
transition:
opacity 0.2s ease,
transform 0.27s ease,
visibility 0.2s ease;
}
#theme-toggle-fab.show,
#back-to-top.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
#theme-toggle-fab.show:hover,
#back-to-top.show:hover {
transform: translateY(-2px);
}
:global(.dark) #theme-toggle-fab,
:global(.dark) #back-to-top {
background: rgba(34, 34, 34, 0.92);
color: #f5f5f5;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
border: #515151 2px solid;
}
@media (max-width: 640px) {
.floating-actions {
right: 1rem;
bottom: 1rem;
gap: 0.65rem;
}
#theme-toggle-fab,
#back-to-top {
width: 2.75rem;
height: 2.75rem;
}
}
</style>
<script>
const initFloatingActions = () => {
const backToTop = document.getElementById("back-to-top");
const themeToggle = document.getElementById("theme-toggle-fab");
if (backToTop && backToTop.dataset.bound !== "true") {
const toggleBackToTop = () => {
if (window.scrollY > 300) {
backToTop.classList.add("show");
} else {
backToTop.classList.remove("show");
}
};
backToTop.addEventListener("click", () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
});
window.addEventListener("scroll", toggleBackToTop, {
passive: true,
});
toggleBackToTop();
backToTop.dataset.bound = "true";
}
if (themeToggle && themeToggle.dataset.bound !== "true") {
const togglethemeToggle = () => {
if (window.scrollY > 300) {
themeToggle.classList.add("show");
} else {
themeToggle.classList.remove("show");
}
};
const root = document.documentElement;
const applyTheme = (theme: string) => {
if (theme === "dark") {
root.classList.add("dark");
localStorage.setItem("color-theme", "dark");
} else {
root.classList.remove("dark");
localStorage.setItem("color-theme", "light");
}
};
themeToggle.addEventListener("click", () => {
const isDark = root.classList.contains("dark");
applyTheme(isDark ? "light" : "dark");
});
window.addEventListener("scroll", togglethemeToggle, {
passive: true,
});
togglethemeToggle();
themeToggle.dataset.bound = "true";
}
};
initFloatingActions();
document.addEventListener("astro:page-load", initFloatingActions);
</script>

View file

@ -1,85 +1,22 @@
--- ---
import { getLangFromUrl, getTranslations, type Lang } from "@/i18n"; import { getLangFromUrl, getTranslations, type Lang } from "@/i18n";
import BuildHashBlocks from "@/components/BuildHashBlocks.astro";
const commit = import.meta.env.PUBLIC_GIT_COMMIT || "unknown";
const lang = getLangFromUrl(Astro.url); const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang); const t = getTranslations(lang);
--- ---
<footer class="footer"> <footer class="footer">
<a <p set:html={t.footer.githubIntro} />
href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog" <p set:html={t.footer.repoIntro} />
class="github"
aria-label="GitHub repository"></a>
<a href="mailto:cloverta@petalmail.com" class="email" aria-label="Email"></a>
<p class="footer-content-pc">
© 2026 ClovertaTheTrilobita · <a
href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog"
>Source code</a
> licensed under MIT. Content rights reserved.
<!-- <code class="build-commit">{commit.slice(0, 7)}</code> -->
</p>
<div class="footer-content-mobile">
<p>© 2026 ClovertaTheTrilobita</p>
<p>Content rights reserved.</p>
</div>
<BuildHashBlocks />
</footer> </footer>
<style> <style>
.github {
display: inline-block;
width: 30px;
height: 30px;
background-color: #1f2328;
-webkit-mask: url("/images/github-outline-7.svg") no-repeat center / contain;
mask: url("/images/github-outline-7.svg") no-repeat center / contain;
vertical-align: middle;
margin-bottom: 0.5rem;
margin-right: 0.5rem;
transition:
transform 0.18s ease,
opacity 0.18s ease;
}
.email {
display: inline-block;
width: 30px;
height: 30px;
background-color: #1f2328;
-webkit-mask: url("/images/email.svg") no-repeat center / contain;
mask: url("/images/email.svg") no-repeat center / contain;
vertical-align: middle;
margin-bottom: 0.5rem;
transition:
transform 0.18s ease,
opacity 0.18s ease;
}
.github:hover {
transform: translateY(-2px);
}
.email:hover {
transform: translateY(-2px);
}
:global(html.dark .github) {
background-color: #e6e6e6;
}
:global(html.dark .email) {
background-color: #e6e6e6;
}
.footer { .footer {
margin-top: 1rem; margin-top: 1rem;
padding: 1.5rem 0 0; padding: 1.5rem 0 0;
font-size: 0.95rem; font-size: 0.95rem;
opacity: 0.9; opacity: 0.9;
position: relative; position: relative;
text-align: center;
} }
.footer::before { .footer::before {
@ -90,22 +27,22 @@ const t = getTranslations(lang);
margin-bottom: 1rem; margin-bottom: 1rem;
background: repeating-linear-gradient( background: repeating-linear-gradient(
-45deg, -45deg,
var(--deep-red) 0 14px, #e96b6b 0 14px,
transparent 14px 28px, transparent 14px 28px,
var(--deep-blue) 28px 42px, #7da2ff 28px 42px,
transparent 42px 56px transparent 42px 56px
); );
pointer-events: none; pointer-events: none;
} }
.footer-content-mobile {
display: none;
}
.footer p { .footer p {
margin: 0.4rem 0; margin: 0.4rem 0;
font-size: 0.8rem; }
/* display: inline-flex; */
.footer a {
color: #7fb3ff;
font-weight: 700;
text-decoration: underline;
} }
:global(.dark) .footer::before { :global(.dark) .footer::before {
@ -117,14 +54,4 @@ const t = getTranslations(lang);
transparent 42px 56px transparent 42px 56px
); );
} }
@media (max-width: 700px) {
.footer-content-pc {
display: none;
}
.footer-content-mobile {
display: block;
}
}
</style> </style>

View file

@ -23,8 +23,6 @@ const links = await getCollection("friends");
<style> <style>
.friendly-link-list { .friendly-link-list {
max-width: 770px;
margin: 0 auto;
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem; gap: 1rem;

View file

@ -1,108 +0,0 @@
---
import { getLangFromUrl, getTranslations } from "@/i18n";
import "@/styles/global.css";
const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang);
---
<div class="description">
<div class="image-wrap">
<img
src="https://files.seeusercontent.com/2026/04/17/nOg4/7tx5GS3nAtOXM577XE9VOe0LUU8.webp"
alt="nichijo.webp"
/>
<p class="caption">{t.friends.imgDesc}</p>
</div>
<h1>{t.friends.title}</h1>
<div class="content">
{t.friends.content.map((line: string) => <p set:html={line} />)}
</div>
<div class="friend-code-block">
<pre><code>---
name: 'CLoverta的博客'
description: "欢迎光临,请进门左转"
url: "https://blog.cloverta.top"
avatar: 'https://s2.loli.net/2025/11/22/tiDKuzdqycx1v9B.png'
---</code></pre>
</div>
<!-- <div class="player">
<iframe
data-testid="embed-iframe"
style="border-radius:2px"
src="https://open.spotify.com/embed/track/2vuSiplO7RnGkoyw2gH96M?utm_source=generator"
width="450px"
height="80"
frameborder="0"
allowfullscreen=""
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"></iframe>
</div> -->
</div>
<style>
.player {
margin-top: 1.5rem;
display: flex;
justify-content: center;
}
.content p {
line-height: 1.8;
margin: 1rem 0;
}
.description {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
}
.friend-code-block {
background: #fbf5f2;
border: 1px solid #8fa1ad;
border-radius: 3px;
padding: 0rem;
overflow-x: auto;
}
.friend-code-block pre {
margin: 0;
}
.friend-code-block code {
font-family: monospace;
white-space: pre;
}
.image-wrap {
width: fit-content;
margin: 0 auto;
}
.image-wrap img {
width: calc((3.8em * 4) * 16 / 5);
height: calc(3.4em * 4);
aspect-ratio: 16 / 5;
object-fit: cover;
object-position: center;
display: block;
margin: 0 auto;
border: 2px dashed gray;
filter: sepia(0.3) saturate(1.08) hue-rotate(-12deg) brightness(0.97);
}
.image-wrap .caption {
margin-top: 0.3rem;
text-align: right;
color: #888;
font-style: italic;
font-size: 0.9rem;
}
@media (max-width: 600px) {
.description img {
height: calc(2.6em * 4);
}
}
</style>

View file

@ -1,299 +0,0 @@
---
const { lang = "zh" } = Astro.props;
const host = import.meta.env.PUBLIC_REMARK42_HOST;
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
const max = 10;
const heading = lang === "zh" ? "最新评论" : "Latest comments";
---
<section
class="latest-comments"
data-lang={lang}
data-host={host}
data-site-id={siteId}
data-max={max}
>
<h2>{heading}</h2>
<div class="comments-loading" aria-hidden="true">
<div class="loading-card"></div>
<div class="loading-card"></div>
<div class="loading-card"></div>
</div>
<div class="latest-comments-list" aria-live="polite"></div>
</section>
<script is:inline data-astro-rerun>
function escapeHtml(str) {
return String(str ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function formatTime(input, lang) {
try {
const date = new Date(input);
return new Intl.DateTimeFormat(lang === "zh" ? "zh-CN" : "en", {
year: "numeric",
month: "short",
day: "numeric",
}).format(date);
} catch {
return "";
}
}
function rewritePostUrl(url, lang) {
if (!url) return "#";
if (url.startsWith("/posts/")) {
return `/${lang}${url}`;
}
try {
const u = new URL(url, window.location.origin);
if (u.pathname.startsWith("/posts/")) {
return `/${lang}${u.pathname}${u.search}${u.hash}`;
}
return u.pathname + u.search + u.hash;
} catch {
return url;
}
}
function getAvatar(item) {
return (
item?.user?.picture || item?.user?.photo || item?.user?.avatar || ""
);
}
function getUserName(item, lang) {
return item?.user?.name || (lang === "zh" ? "匿名用户" : "Anonymous");
}
function normalizePath(url) {
if (!url) return "";
try {
const u = new URL(url, window.location.origin);
let path = u.pathname;
if (!path.startsWith("/")) path = "/" + path;
if (!path.endsWith("/")) path += "/";
return path;
} catch {
let path = String(url);
path = path.replace(/^https?:\/\/[^/]+/i, "");
if (!path.startsWith("/")) path = "/" + path;
if (!path.endsWith("/")) path += "/";
return path;
}
}
function buildUrlCandidates(rawUrl) {
const path = normalizePath(rawUrl);
if (!path) return [];
const noSlash = path.endsWith("/") ? path.slice(0, -1) : path;
const candidates = [
path,
noSlash,
`/zh${path}`,
`/zh${noSlash}`,
`/en${path}`,
`/en${noSlash}`,
`${window.location.origin}${path}`,
`${window.location.origin}${noSlash}`,
];
return [...new Set(candidates.filter(Boolean))];
}
async function fetchPostComments(host, siteId, rawUrl) {
const candidates = buildUrlCandidates(rawUrl);
for (const candidate of candidates) {
try {
const res = await fetch(
`${host}/api/v1/find?site=${encodeURIComponent(siteId)}&url=${encodeURIComponent(candidate)}&format=plain`,
{ credentials: "omit" },
);
if (!res.ok) continue;
const data = await res.json();
const comments = Array.isArray(data)
? data
: Array.isArray(data?.comments)
? data.comments
: [];
if (comments.length > 0) {
return comments;
}
} catch {}
}
return [];
}
async function enrichReplyTargets(host, siteId, comments) {
const urls = [
...new Set(
comments.map((item) => item?.locator?.url).filter(Boolean),
),
];
const postMap = new Map();
await Promise.all(
urls.map(async (url) => {
const postComments = await fetchPostComments(host, siteId, url);
postMap.set(url, postComments);
}),
);
return comments.map((item) => {
if (!item?.pid) {
return { ...item, replyToName: "" };
}
const postComments = postMap.get(item?.locator?.url) || [];
const parent = postComments.find(
(c) => String(c?.id) === String(item?.pid),
);
return {
...item,
replyToName: parent?.user?.name || "",
};
});
}
function renderComments(root, comments, lang) {
const list = root.querySelector(".latest-comments-list");
const loading = root.querySelector(".comments-loading");
if (!list || !loading) return;
if (!Array.isArray(comments) || comments.length === 0) {
loading.style.display = "none";
list.innerHTML = `<p class="latest-comments-empty">${
lang === "zh" ? "还没有评论" : "No comments yet"
}</p>`;
return;
}
list.innerHTML = comments
.map((item) => {
const rawAuthor = getUserName(item, lang);
const author = escapeHtml(rawAuthor);
const fallbackInitial = escapeHtml(
rawAuthor.slice(0, 1).toUpperCase(),
);
const avatar = getAvatar(item);
const safeAvatar = escapeHtml(avatar || "");
const title = escapeHtml(
item?.title ||
(lang === "zh" ? "未命名文章" : "Untitled post"),
);
const href = rewritePostUrl(item?.locator?.url || "", lang);
// Remark42 返回的 text 是处理后的 HTML适合直接渲染
const html = item?.text || "";
const time = formatTime(item?.time, lang);
const replyTo = escapeHtml(item?.replyToName || "");
const authorLine = replyTo
? `<span class="comment-author">@${author}</span><span class="comment-reply-sep">${
lang === "zh" ? "回复" : "replied to"
}</span><span class="comment-reply-to">@${replyTo}</span>`
: `<span class="comment-author">@${author}</span>`;
const avatarHtml = avatar
? `<img class="comment-avatar-img" src="${safeAvatar}" alt="${author}" loading="lazy" referrerpolicy="no-referrer" />`
: `<div class="comment-avatar-fallback" aria-hidden="true">${fallbackInitial}</div>`;
return `
<article class="comment-card">
<div class="comment-card-body">
<div class="comment-card-info">
<div class="comment-avatar">
${avatarHtml}
</div>
<div class="comment-meta">
<div class="comment-author-row">
${authorLine}
${time ? `<span class="comment-time">${escapeHtml(time)}</span>` : ""}
</div>
</div>
</div>
<div class="comment-card-title">
<a class="comment-title-link" href="${href}">${title}</a>
</div>
<div class="comment-card-text">${html}</div>
</div>
</article>
`;
})
.join("");
loading.style.display = "none";
}
async function loadLatestComments(section) {
if (!section) return;
const host = section.dataset.host;
const siteId = section.dataset.siteId;
const max = section.dataset.max || "10";
const lang = section.dataset.lang || "zh";
const list = section.querySelector(".latest-comments-list");
const loading = section.querySelector(".comments-loading");
if (!host || !siteId || !list || !loading) return;
loading.style.display = "";
list.innerHTML = "";
const url = `${host}/api/v1/last/${encodeURIComponent(max)}?site=${encodeURIComponent(siteId)}`;
try {
const res = await fetch(url, { credentials: "omit" });
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
const enriched = await enrichReplyTargets(
host,
siteId,
Array.isArray(data) ? data : [],
);
renderComments(section, enriched, lang);
} catch (err) {
loading.style.display = "none";
list.innerHTML = `<p class="latest-comments-empty">${
lang === "zh"
? "最新评论加载失败"
: "Failed to load latest comments"
}</p>`;
console.error("latest comments load failed:", err);
}
}
document.querySelectorAll(".latest-comments").forEach(loadLatestComments);
</script>

View file

@ -7,7 +7,7 @@ const t = getTranslations(lang);
<nav class="site-nav" aria-label="Site navigation"> <nav class="site-nav" aria-label="Site navigation">
<div class="site-nav-desktop"> <div class="site-nav-desktop">
<a href={`/${lang}/`}>{t.nav.home}</a> <a href={`/${lang}/`} data-astro-reload>{t.nav.home}</a>
<a href={`/${lang}/about/`}>{t.nav.about}</a> <a href={`/${lang}/about/`}>{t.nav.about}</a>
<a href={`/${lang}/tags/`}>{t.nav.tags}</a> <a href={`/${lang}/tags/`}>{t.nav.tags}</a>
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a> <a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
@ -17,7 +17,7 @@ const t = getTranslations(lang);
<details class="site-nav-mobile"> <details class="site-nav-mobile">
<summary>☰ {lang === "zh" ? "导航栏" : "Menu"}</summary> <summary>☰ {lang === "zh" ? "导航栏" : "Menu"}</summary>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a href={`/${lang}/`}>{t.nav.home}</a> <a href={`/${lang}/`} data-astro-reload>{t.nav.home}</a>
<a href={`/${lang}/about/`}>{t.nav.about}</a> <a href={`/${lang}/about/`}>{t.nav.about}</a>
<a href={`/${lang}/tags/`}>{t.nav.tags}</a> <a href={`/${lang}/tags/`}>{t.nav.tags}</a>
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a> <a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
@ -26,20 +26,6 @@ const t = getTranslations(lang);
</details> </details>
</nav> </nav>
<script>
document.addEventListener("click", (event) => {
const nav = document.querySelector(".site-nav-mobile");
if (!nav) return;
const target = event.target as Node;
if (!nav.contains(target)) {
nav.removeAttribute("open");
}
});
</script>
<style> <style>
.site-nav { .site-nav {
margin: 1rem 0; margin: 1rem 0;
@ -56,9 +42,9 @@ const t = getTranslations(lang);
height: 12px; height: 12px;
background: repeating-linear-gradient( background: repeating-linear-gradient(
-45deg, -45deg,
var(--deep-red) 0 14px, #e96b6b 0 14px,
transparent 14px 28px, transparent 14px 28px,
var(--deep-blue) 28px 42px, #7da2ff 28px 42px,
transparent 42px 56px transparent 42px 56px
); );
pointer-events: none; pointer-events: none;
@ -157,7 +143,7 @@ const t = getTranslations(lang);
color: white; color: white;
} }
@media (max-width: 700px) { @media (max-width: 768px) {
.site-nav-desktop { .site-nav-desktop {
display: none; display: none;
} }

View file

@ -1,118 +0,0 @@
<dialog id="image-preview-dialog" class="image-preview-dialog">
<button class="image-preview-close" aria-label="关闭图片预览">×</button>
<img id="image-preview-img" alt="" />
<a
id="image-preview-download"
class="image-preview-download"
href="#"
aria-label="下载图片"
title="下载图片"
>
<svg
width="26"
height="26"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d="M12 3v12m0 0 5-5m-5 5-5-5M5 21h14"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
</a>
</dialog>
<script is:inline>
function initImagePreview() {
const dialog = document.getElementById("image-preview-dialog");
const previewImg = document.getElementById("image-preview-img");
const closeBtn = dialog?.querySelector(".image-preview-close");
const downloadBtn = document.getElementById("image-preview-download");
if (!dialog || !previewImg || !closeBtn || !downloadBtn) return;
let currentImageUrl = "";
function getFileNameFromUrl(url) {
try {
const pathname = new URL(url, window.location.href).pathname;
return pathname.split("/").pop() || "image";
} catch {
return "image";
}
}
async function downloadImage(url) {
const response = await fetch(url, {
mode: "cors",
credentials: "omit",
});
if (!response.ok) {
throw new Error("Failed to fetch image");
}
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = getFileNameFromUrl(url);
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(blobUrl);
}
document.querySelectorAll("article img").forEach((img) => {
if (img.dataset.previewBound === "true") return;
img.dataset.previewBound = "true";
img.addEventListener("click", () => {
const imageUrl = img.currentSrc || img.src;
currentImageUrl = imageUrl;
previewImg.src = imageUrl;
previewImg.alt = img.alt || "";
dialog.showModal();
});
});
downloadBtn.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
if (!currentImageUrl) return;
try {
await downloadImage(currentImageUrl);
} catch (error) {
// 如果 fetch 因为跨域失败,就退回到直接打开图片
window.open(currentImageUrl, "_blank", "noopener,noreferrer");
}
});
function closePreview() {
dialog.close();
previewImg.removeAttribute("src");
currentImageUrl = "";
}
closeBtn.addEventListener("click", closePreview);
dialog.addEventListener("click", (event) => {
if (event.target === dialog) {
closePreview();
}
});
}
document.addEventListener("DOMContentLoaded", initImagePreview);
document.addEventListener("astro:page-load", initImagePreview);
</script>

View file

@ -1,123 +1,38 @@
--- ---
import "@/styles/global.css"; import "@/styles/global.css";
import Remark42Counter from "@/components/Remark42Counter.astro"; import Remark42Count from "@/components/remark42-counter.svelte";
import { getLangFromUrl } from "@/i18n";
const lang = getLangFromUrl(Astro.url);
const data = Astro.props; const data = Astro.props;
const tags = data.tags;
--- ---
<li class="post-card"> <li class="post-card">
<div class="post-link"> <a href={data.url} class="post-link" data-astro-reload>
<div class="post-text"> <div class="post-text">
<a href={data.url} class="post-title-link"> <span class="post-title">{data.title}</span>
<h2 class="post-title" transition:name={`post-title-${data.postId}`}> <span class="post-description">{data.description}</span>
{data.title}
</h2>
</a>
<a href={data.url} class="post-description">
{data.description}
</a>
<div class="tags" transition:name={`post-tags-${data.postId}`}>
{
tags.map((tag: string) => (
<p class="tag">
<a href={`/${lang}/tags/${tag}`}>{tag}</a>
</p>
))
}
</div>
<div class="post-meta-row"> <div class="post-meta-row">
<span class="post-date">{data.date}</span> <span class="post-date">{data.date}</span>
<span class="post-stats"> <span class="post-stats">
<span class="post-stat"> <span class="post-stat">
💬 <Remark42Counter url={data.postPath} /> 💬 <Remark42Count url={data.postPath} client:idle />
</span> </span>
</span> </span>
</div> </div>
</div> </div>
<a href={data.url} class="post-image-link"> <img src={data.img} alt={data.title} class="post-image" loading="lazy" />
<img </a>
src={data.img}
alt={data.title}
class="post-image"
loading="lazy"
transition:name={`post-image-${data.postId}`}
/>
</a>
</div>
</li> </li>
<div class="section-divider"></div> <div class="section-divider"></div>
<style> <style>
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.tag {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
margin: 0;
padding: 0.22em 0.65em;
font-size: 1rem;
line-height: 1.2;
border: 1px dashed #8b6b4a;
border-radius: 0;
background-color: transparent;
}
.tag a {
color: #6f4e37;
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
}
.tag a:hover {
text-decoration: underline;
}
:global(.dark) .tag {
border-color: #d8c7a1;
}
:global(.dark) .tag a {
color: #e6d8b8;
}
:global(.dark) .tag {
border-color: #d8c7a1;
}
:global(.dark) .tag a {
color: #e6d8b8;
}
.post-card { .post-card {
list-style: none; list-style: none;
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
padding-left: 0; padding-left: 0;
padding-bottom: 0.9rem; padding-bottom: 0.2rem;
padding-top: 0.9rem; padding-top: 0.2rem;
} }
.post-link { .post-link {
@ -132,7 +47,6 @@ const tags = data.tags;
.post-text { .post-text {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
max-width: 40rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
@ -140,15 +54,10 @@ const tags = data.tags;
min-height: calc(1.6em * 4); min-height: calc(1.6em * 4);
} }
.post-title-link {
text-decoration: none;
display: block;
}
.post-title { .post-title {
margin: 0;
font-family: font-family:
system-ui, system-ui,
sans-serif,
-apple-system, -apple-system,
BlinkMacSystemFont, BlinkMacSystemFont,
"Segoe UI", "Segoe UI",
@ -157,9 +66,9 @@ const tags = data.tags;
Ubuntu, Ubuntu,
Cantarell, Cantarell,
"Open Sans", "Open Sans",
"Helvetica Neue", "Helvetica Neue";
sans-serif; color: inherit;
font-weight: 500; font-weight: 700;
font-size: 1.34rem; font-size: 1.34rem;
line-height: 1.6; line-height: 1.6;
display: -webkit-box; display: -webkit-box;
@ -170,10 +79,8 @@ const tags = data.tags;
.post-description { .post-description {
color: black; color: black;
text-decoration: none;
font-weight: 400; font-weight: 400;
font-size: 0.92rem; font-style: italic;
/* font-style: italic; */
} }
.post-meta-row { .post-meta-row {
@ -207,12 +114,12 @@ const tags = data.tags;
} }
.post-image { .post-image {
width: calc((1.6em * 4) * 16 / 10); width: calc((1.6em * 4) * 16 / 9);
height: calc(1.6em * 4); height: calc(1.6em * 4);
aspect-ratio: 16 / 10; aspect-ratio: 16 / 9;
object-fit: cover; object-fit: cover;
object-position: center; object-position: center;
border: 1.5px #94a0ab dashed; border-radius: 0.5rem;
flex-shrink: 0; flex-shrink: 0;
display: block; display: block;
} }
@ -225,14 +132,13 @@ const tags = data.tags;
color: #aab7c4; color: #aab7c4;
} }
@media (max-width: 600px) { @media (max-width: 640px) {
.post-link { .post-link {
display: grid; display: grid;
grid-template-columns: 1fr 145px; grid-template-columns: 1fr 120px;
grid-template-areas: grid-template-areas:
"title title" "title title"
"desc image" "desc image"
"tags image"
"meta image"; "meta image";
column-gap: 0.75rem; column-gap: 0.75rem;
row-gap: 0.2rem; row-gap: 0.2rem;
@ -244,11 +150,8 @@ const tags = data.tags;
min-height: auto; min-height: auto;
} }
.post-title-link {
grid-area: title;
}
.post-title { .post-title {
grid-area: title;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
} }
@ -262,24 +165,9 @@ const tags = data.tags;
} }
.post-image { .post-image {
/* border: none; */
grid-area: image; grid-area: image;
width: 100%; width: 120px;
height: calc(145px * 10 / 16); height: calc(120px * 9 / 16);
align-self: center;
}
.post-image-link {
grid-area: image;
align-self: center;
display: block;
width: 145px;
text-decoration: none;
}
.post-card {
padding-bottom: 0.2rem;
padding-top: 0.2rem;
} }
} }
</style> </style>

View file

@ -16,7 +16,7 @@ const filteredPosts = sortedPosts.filter((post: any) => {
return postLang === lang; return postLang === lang;
}); });
const latestPosts = filteredPosts.slice(0, 7); const latestPosts = filteredPosts.slice(0, 5);
--- ---
<ul> <ul>
@ -37,9 +37,6 @@ const latestPosts = filteredPosts.slice(0, 7);
description={post.data.description} description={post.data.description}
date={formattedDate} date={formattedDate}
img={post.data.image.url} img={post.data.image.url}
tags={post.data.tags}
slug={slug}
postId={post.id}
/> />
); );
}) })

View file

@ -1,403 +0,0 @@
---
interface Heading {
depth: number;
slug: string;
text: string;
}
interface Props {
headings: Heading[];
}
const { headings = [] } = Astro.props;
const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
---
{
tocHeadings.length > 0 && (
<>
<button
class="post-menu-mobile-toggle"
type="button"
aria-expanded="false"
aria-controls="post-menu-panel"
aria-label="目录"
>
<span
class="post-menu-mobile-preview"
data-post-menu-preview
data-default-text={tocHeadings[0]?.text ?? "本文目录"}
>
{tocHeadings[0]?.text ?? "本文目录"}
</span>
<span class="post-menu-mobile-icon" aria-hidden="true">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 6.5H19M5 12H19M5 17.5H19"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
<circle cx="3.5" cy="6.5" r="1" fill="currentColor" />
<circle cx="3.5" cy="12" r="1" fill="currentColor" />
<circle cx="3.5" cy="17.5" r="1" fill="currentColor" />
</svg>
</span>
</button>
<aside class="post-menu" id="post-menu-panel">
<nav aria-label="Table of contents">
<p class="post-menu-title">目录</p>
<ul class="post-menu-list">
{tocHeadings.map((heading) => (
<li class={`post-menu-item depth-${heading.depth}`}>
<a
href={`#${heading.slug}`}
data-heading-link
data-heading-text={heading.text}
>
{heading.text}
</a>
</li>
))}
</ul>
</nav>
</aside>
</>
)
}
<style is:global>
:where(h1, h2, h3, h4, h5, h6) {
scroll-margin-top: 4rem;
}
</style>
<style>
.post-menu {
box-sizing: border-box;
}
.post-menu-title {
margin: 0 0 0.7rem;
padding-top: 0.65rem;
background-image: linear-gradient(
to right,
rgba(65, 65, 65, 0.8) 0,
rgba(65, 65, 65, 0.8) 8px,
transparent 8px,
transparent 14px
);
background-repeat: repeat-x;
background-size: 14px 2px;
background-position: top left;
font-size: 1.1rem;
font-weight: 600;
opacity: 0.72;
}
.post-menu-list {
position: relative;
list-style: none;
margin: 0;
padding: 0 0 0 0.9rem;
font-size: 1rem;
}
.post-menu-list::before {
content: "";
position: absolute;
left: 0.15rem;
top: 0.25rem;
bottom: 0.25rem;
width: 1px;
background: rgba(128, 128, 128, 0.55);
}
.post-menu-item {
position: relative;
margin: 0.42rem 0;
line-height: 1.45;
}
.post-menu-item.depth-3 {
padding-left: 0.85rem;
opacity: 0.82;
font-size: 0.94em;
}
.post-menu a {
color: inherit;
text-decoration: none;
}
.post-menu a:hover {
text-decoration: underline;
}
.post-menu-mobile-toggle {
display: none;
}
@media (min-width: 1200px) {
.post-menu {
--content-width: 90ch;
--menu-width: clamp(
140px,
calc((100vw - 1200px) * 0.42 + 140px),
260px
);
--menu-gap: 0rem;
position: fixed;
top: 8.5rem;
width: var(--menu-width);
left: calc(
50vw - var(--content-width) / 2 - var(--menu-gap) -
var(--menu-width)
);
max-height: calc(100vh - 10rem);
overflow: auto;
padding: 0.2rem 0.2rem 0.2rem 0;
opacity: 0.9;
}
}
@media (max-width: 1200px) {
.post-menu-mobile-toggle {
display: inline-flex;
align-items: center;
gap: 0.25rem;
position: fixed;
top: 0.75rem;
right: 0.8rem;
z-index: 30;
max-width: min(78vw, 22rem);
padding: 0.6rem 0.75rem;
border: 1.5px solid var(--deep-blue);
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(6px);
color: inherit;
font-size: 0.85rem;
cursor: pointer;
opacity: 0;
transform: translateY(calc(-100% - 0.6rem));
pointer-events: none;
transition:
transform 0.28s ease,
opacity 0.22s ease;
}
.post-menu-mobile-toggle.is-visible {
opacity: 1;
transform: translateY(calc(env(safe-area-inset-top) + 0.4rem));
pointer-events: auto;
}
.post-menu-mobile-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
opacity: 0.78;
}
.post-menu-mobile-preview {
display: inline-block;
width: 10rem;
/* height: 1rem; */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.post-menu {
display: none;
position: fixed;
top: calc(env(safe-area-inset-top) + 4.2rem);
right: 0.8rem;
z-index: 29;
width: min(88vw, 24rem);
max-height: min(65vh, 32rem);
overflow: auto;
padding: 0.9rem 1rem;
border: 1.5px solid var(--deep-blue);
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(8px);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
}
.post-menu-title {
padding-top: 0;
background-image: none;
}
.post-menu.is-open {
display: block;
}
:global(html.dark) .post-menu-mobile-toggle,
:global(html.dark) .post-menu {
background: var(--background-color-dark);
color: var(--text-color-dark);
}
}
</style>
<script is:inline>
(() => {
const setupPostMenu = () => {
const btn = document.querySelector(".post-menu-mobile-toggle");
const panel = document.querySelector("#post-menu-panel");
const preview = document.querySelector("[data-post-menu-preview]");
const headingLinks = Array.from(
document.querySelectorAll("[data-heading-link]"),
);
if (!btn || !panel || !preview || headingLinks.length === 0) return;
if (btn.dataset.bound === "true") return;
btn.dataset.bound = "true";
const defaultText =
preview.getAttribute("data-default-text") || "本文目录";
const getHeadingElements = () =>
headingLinks
.map((link) => {
const href = link.getAttribute("href");
if (!href || !href.startsWith("#")) return null;
const el = document.getElementById(href.slice(1));
if (!el) return null;
return {
link,
el,
text:
link.getAttribute("data-heading-text") ||
el.textContent ||
defaultText,
};
})
.filter(Boolean);
let headingItems = getHeadingElements();
const closeMenu = () => {
panel.classList.remove("is-open");
btn.setAttribute("aria-expanded", "false");
};
document.addEventListener("click", (e) => {
const target = e.target;
if (!(target instanceof Node)) return;
const clickedInsidePanel = panel.contains(target);
const clickedButton = btn.contains(target);
if (!clickedInsidePanel && !clickedButton) {
closeMenu();
}
});
const updateVisibility = () => {
if (window.innerWidth >= 1200) {
btn.classList.remove("is-visible");
closeMenu();
return;
}
const shouldShow = window.scrollY > 80;
btn.classList.toggle("is-visible", shouldShow);
if (!shouldShow) {
closeMenu();
}
};
const updateCurrentHeading = () => {
if (window.innerWidth >= 1200) return;
headingItems = getHeadingElements();
let current = null;
const triggerLine = 140;
for (const item of headingItems) {
const rect = item.el.getBoundingClientRect();
if (rect.top <= triggerLine) {
current = item;
} else {
break;
}
}
preview.textContent = current ? current.text : defaultText;
headingLinks.forEach((link) =>
link.classList.remove("is-current"),
);
if (current) current.link.classList.add("is-current");
};
const toggleMenu = () => {
const expanded = btn.getAttribute("aria-expanded") === "true";
btn.setAttribute("aria-expanded", String(!expanded));
panel.classList.toggle("is-open", !expanded);
};
btn.addEventListener("click", toggleMenu);
panel.addEventListener("click", (e) => {
const target = e.target;
if (!(target instanceof Element)) return;
const link = target.closest("a[data-heading-link]");
if (!link) return;
const href = link.getAttribute("href");
if (!href || !href.startsWith("#")) return;
const el = document.getElementById(href.slice(1));
if (!el) return;
e.preventDefault();
el.scrollIntoView({
behavior: "smooth",
block: "start",
});
history.replaceState(null, "", href);
closeMenu();
});
const onScroll = () => {
updateVisibility();
updateCurrentHeading();
};
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
updateVisibility();
updateCurrentHeading();
};
setupPostMenu();
document.addEventListener("astro:page-load", setupPostMenu);
})();
</script>

View file

@ -1,130 +0,0 @@
---
import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
import type { Lang } from "@/i18n";
interface Props {
post: CollectionEntry<"blog">;
lang: Lang;
}
const { post, lang } = Astro.props;
const allPosts = await getCollection("blog");
// 只保留当前语言
const sameLangPosts = allPosts.filter((p) => {
const [postLang] = p.id.split("/");
return postLang === lang;
});
// 按日期倒序:越新越靠前
const sortedPosts = [...sameLangPosts].sort(
(a, b) =>
new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime(),
);
// 找当前文章位置
const currentIndex = sortedPosts.findIndex((p) => p.id === post.id);
// 更旧的一篇
const prevPost =
currentIndex < sortedPosts.length - 1
? sortedPosts[currentIndex + 1]
: null;
// 更新的一篇
const nextPost = currentIndex > 0 ? sortedPosts[currentIndex - 1] : null;
const getSlugFromId = (id: string) => id.split("/").slice(1).join("/");
const prevSlug = prevPost ? getSlugFromId(prevPost.id) : null;
const nextSlug = nextPost ? getSlugFromId(nextPost.id) : null;
---
<nav class="post-nav" aria-label="Post navigation">
<div class="post-nav-item post-nav-prev">
{
nextPost && nextSlug && (
<a href={`/${lang}/posts/${nextSlug}/`}>
<span class="post-nav-label">
{lang === "zh" ? "上一篇" : "Previous"}
</span>
<span class="post-nav-title">{nextPost.data.title}</span>
</a>
)
}
</div>
<div class="post-nav-item post-nav-next">
{
prevPost && prevSlug && (
<a href={`/${lang}/posts/${prevSlug}/`}>
<span class="post-nav-label">
{lang === "zh" ? "下一篇" : "Next"}
</span>
<span class="post-nav-title">{prevPost.data.title}</span>
</a>
)
}
</div>
</nav>
<style>
.post-nav {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-top: 3rem;
padding-top: 1.5rem;
padding-bottom: 1.5rem;
border-top: 1px dashed rgba(128, 128, 128, 0.5);
border-bottom: 1px dashed rgba(128, 128, 128, 0.5);
}
.post-nav-item {
min-width: 0;
}
.post-nav-next {
text-align: right;
}
.post-nav-title {
display: inline-block;
transition: transform 0.18s ease;
}
.post-nav a:hover .post-nav-title {
transform: scale(1.04);
}
.post-nav a {
display: block;
text-decoration: none;
/* color: inherit; */
}
.post-nav-label {
display: block;
font-size: 0.9rem;
opacity: 0.65;
margin-bottom: 0.35rem;
color: black;
}
.post-nav-title {
display: block;
font-size: 1rem;
line-height: 1.5;
}
@media (max-width: 768px) {
.post-nav {
grid-template-columns: 1fr;
}
.post-nav-next {
text-align: right;
}
}
</style>

View file

@ -51,11 +51,9 @@ const groupedPosts = filteredPosts.reduce((acc: any[], post: any) => {
<a <a
href={`/${lang}/posts/${slug}/`} href={`/${lang}/posts/${slug}/`}
class="timeline-card" class="timeline-card"
data-astro-reload
> >
<h2 <h2 class="post-title">
class="post-title"
transition:name={`post-title-${post.id}`}
>
{post.data.title} {post.data.title}
</h2> </h2>
<p class="post-description"> <p class="post-description">
@ -147,8 +145,7 @@ const groupedPosts = filteredPosts.reduce((acc: any[], post: any) => {
margin: 0; margin: 0;
font-size: 0.88rem; font-size: 0.88rem;
color: #555; color: #555;
font-weight: 400; font-style: italic;
/* font-style: italic; */
line-height: 1.5; line-height: 1.5;
overflow-wrap: anywhere; overflow-wrap: anywhere;
word-break: break-word; word-break: break-word;

View file

@ -1,66 +0,0 @@
---
const { url } = Astro.props;
const host = import.meta.env.PUBLIC_REMARK42_HOST;
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
---
<span
class="remark42__counter"
data-url={url}
data-host={host}
data-site-id={siteId}></span>
<script is:inline data-astro-rerun>
function getTheme() {
return document.documentElement.classList.contains("dark")
? "dark"
: "light";
}
function mountRemark42Counters() {
const counters = Array.from(
document.querySelectorAll(".remark42__counter[data-url]"),
);
if (!counters.length) return;
const first = counters[0];
const host = first.dataset.host;
const siteId = first.dataset.siteId;
if (!host || !siteId) return;
window.remark_config = {
host,
site_id: siteId,
components: ["counter"],
show_rss_subscription: false,
theme: getTheme(),
};
for (const el of counters) {
el.textContent = "";
}
document.getElementById("remark42-counter-loader")?.remove();
const script = document.createElement("script");
script.id = "remark42-counter-loader";
script.defer = true;
if ("noModule" in script) {
script.type = "module";
script.src = `${host}/web/counter.mjs?ts=${Date.now()}`;
} else {
script.async = true;
script.src = `${host}/web/counter.js?ts=${Date.now()}`;
}
document.head.appendChild(script);
}
clearTimeout(window.__remark42CounterMountTimer);
window.__remark42CounterMountTimer = setTimeout(() => {
mountRemark42Counters();
}, 0);
</script>

View file

@ -1,108 +0,0 @@
---
const { slug } = Astro.props;
const pagePath = `/posts/${slug}/`;
const host = import.meta.env.PUBLIC_REMARK42_HOST;
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
---
<div id="remark42"></div>
<script define:vars={{ pagePath, host, siteId }} is:inline data-astro-rerun>
function getTheme() {
return document.documentElement.classList.contains("dark")
? "dark"
: "light";
}
function setRemarkConfig() {
window.remark_config = {
host,
site_id: siteId,
url: pagePath,
page_id: pagePath,
theme: getTheme(),
components: ["embed"],
show_rss_subscription: false,
};
}
function ensureScript() {
return new Promise((resolve, reject) => {
const existing = document.getElementById("remark42-loader");
if (existing) {
if (window.REMARK42) {
resolve(true);
return;
}
existing.addEventListener("load", () => resolve(true), {
once: true,
});
existing.addEventListener("error", reject, { once: true });
return;
}
const script = document.createElement("script");
script.id = "remark42-loader";
script.defer = true;
if ("noModule" in script) {
script.type = "module";
script.src = `${host}/web/embed.mjs`;
} else {
script.async = true;
script.src = `${host}/web/embed.js`;
}
script.onload = () => resolve(true);
script.onerror = reject;
document.head.appendChild(script);
});
}
async function mountRemark42() {
const node = document.getElementById("remark42");
if (!node) return;
setRemarkConfig();
await ensureScript();
if (!window.REMARK42) return;
if (typeof window.REMARK42.destroy === "function") {
window.REMARK42.destroy();
}
if (typeof window.REMARK42.createInstance === "function") {
node.innerHTML = "";
window.REMARK42.createInstance(window.remark_config);
}
}
function setupRemark42ThemeObserver() {
if (window.__remark42ThemeObserver) {
window.__remark42ThemeObserver.disconnect();
}
let lastTheme = getTheme();
const observer = new MutationObserver(() => {
const currentTheme = getTheme();
if (currentTheme === lastTheme) return;
lastTheme = currentTheme;
mountRemark42();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
window.__remark42ThemeObserver = observer;
}
mountRemark42();
setupRemark42ThemeObserver();
</script>

View file

@ -1,91 +0,0 @@
<div id="page-loading-indicator" aria-hidden="true" transition:persist>
<span class="page-loading-spinner"></span>
</div>
<script is:inline>
(() => {
let timer;
const show = () => {
clearTimeout(timer);
timer = setTimeout(() => {
const el = document.getElementById("page-loading-indicator");
if (el) el.classList.add("is-active");
}, 120);
};
const hide = () => {
clearTimeout(timer);
const el = document.getElementById("page-loading-indicator");
if (el) el.classList.remove("is-active");
};
document.addEventListener("astro:before-preparation", show);
document.addEventListener("astro:page-load", hide);
hide();
})();
</script>
<style is:global>
#page-loading-indicator {
position: fixed;
top: 1rem;
right: 1rem;
width: 2rem;
height: 2rem;
display: grid;
place-items: center;
transform: scale(1.3) translateY(-70px);
pointer-events: none;
transition: transform 160ms linear;
z-index: 9999;
}
#page-loading-indicator.is-active {
transform: scale(1) translateY(0px);
}
.page-loading-spinner {
width: 1.4rem;
height: 1.4rem;
box-sizing: border-box;
border: 1.5px solid #7da2ff;
border-top-color: white;
border-radius: 50%;
background: transparent;
box-shadow:
0 0 0 1px #606060,
inset 0 0 0 1px #606060;
animation: page-loading-spin-rhythm 2.1s infinite;
transform-origin: center;
}
@keyframes page-loading-spin-rhythm {
0% {
transform: rotate(0deg);
animation-timing-function: cubic-bezier(0.25, 0.75, 0.35, 1);
}
52% {
transform: rotate(720deg); /* 前两圈,稍微慢一点 */
animation-timing-function: cubic-bezier(0.55, 0.08, 0.78, 0.22);
}
100% {
transform: rotate(1080deg); /* 最后一圈更慢 */
}
}
.dark .page-loading-spinner,
html.dark .page-loading-spinner {
border-color: #d8c7a1;
border-top-color: transparent;
}
@keyframes page-loading-spin {
to {
transform: rotate(360deg);
}
}
</style>

View file

@ -34,7 +34,7 @@ const switchHref = "/" + segments.join("/");
</svg> </svg>
</button> </button>
<a class="lang-switch" href={switchHref}> <a class="lang-switch" href={switchHref} data-astro-reload>
{switchLabel} {switchLabel}
</a> </a>
</div> </div>
@ -70,15 +70,15 @@ const switchHref = "/" + segments.join("/");
} }
.lang-switch { .lang-switch {
color: black; color: black; !important
font-weight: 700; font-weight: 700; !important
font-size: 0.95rem; font-size: 0.95rem; !important
text-decoration: none; text-decoration: none; !important
line-height: 1; line-height: 1; !important
} }
.lang-switch:hover { .lang-switch:hover {
text-decoration: underline; text-decoration: underline; !important
} }
.sun { .sun {
@ -104,37 +104,37 @@ const switchHref = "/" + segments.join("/");
<script is:inline> <script is:inline>
function applyTheme() { function applyTheme() {
const localStorageTheme = localStorage?.getItem("theme") ?? ""; const localStorageTheme = localStorage?.getItem("theme") ?? "";
let theme = "light"; let theme = "light";
if (["dark", "light"].includes(localStorageTheme)) { if (["dark", "light"].includes(localStorageTheme)) {
theme = localStorageTheme; theme = localStorageTheme;
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
theme = "dark"; theme = "dark";
}
document.documentElement.classList.toggle("dark", theme === "dark");
window.localStorage.setItem("theme", theme);
} }
function bindThemeToggle() { document.documentElement.classList.toggle("dark", theme === "dark");
const button = document.getElementById("themeToggle"); window.localStorage.setItem("theme", theme);
if (!button) return; }
button.onclick = () => { function bindThemeToggle() {
const element = document.documentElement; const button = document.getElementById("themeToggle");
element.classList.toggle("dark"); if (!button) return;
const isDark = element.classList.contains("dark"); button.onclick = () => {
localStorage.setItem("theme", isDark ? "dark" : "light"); const element = document.documentElement;
}; element.classList.toggle("dark");
}
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
};
}
applyTheme();
bindThemeToggle();
document.addEventListener("astro:after-swap", () => {
applyTheme(); applyTheme();
bindThemeToggle(); bindThemeToggle();
});
document.addEventListener("astro:after-swap", () => {
applyTheme();
bindThemeToggle();
});
</script> </script>

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>

View file

@ -15,10 +15,6 @@ export default {
writtenBy: "Written by", writtenBy: "Written by",
comments: "Comments", comments: "Comments",
publishedOn: "Published on", publishedOn: "Published on",
commentsDesc: [
"Feel free to leave your thoughts here. 💭💡",
"After signing in, you can click the “Subscribe by Email” button at the bottom right of the text box to receive notifications of new interactions via email."
]
}, },
theme: { theme: {
toggle: "Toggle theme", toggle: "Toggle theme",
@ -29,8 +25,7 @@ export default {
content: [ content: [
"These few lines speak what the heart would say; Ink and paper end, but thoughts still stay", "These few lines speak what the heart would say; Ink and paper end, but thoughts still stay",
"Welcome to Cloverta's blog.", "Welcome to Cloverta's blog.",
], ]
latestTitle: "Latest Posts"
}, },
about: { about: {
title: "About Me, and This Blog", title: "About Me, and This Blog",
@ -46,10 +41,9 @@ export default {
"The design of this blog was inspired by:", "The design of this blog was inspired by:",
'· <a href="https://ex-tasty.com/">極限風味</a>', '· <a href="https://ex-tasty.com/">極限風味</a>',
'· <a href="https://blog.cloudti.de/">Parsifal\'s Blog</a>', '· <a href="https://blog.cloudti.de/">Parsifal\'s Blog</a>',
'· <a href="https://www.kokosa.icu/">Kokosa\'s Notebook</a>',
"· And some other websites whose names I have unfortunately forgotten", "· And some other websites whose names I have unfortunately forgotten",
"Thank you for your ideas and passion!", "Thank you for your ideas and passion!",
"In addition, this blog is open source under MIT License. You can find its source code through the link in the footer.", "In addition, this blog is fully open source. You can find its source code through the link in the footer.",
] ]
}, },
tags: { tags: {
@ -59,15 +53,5 @@ export default {
footer: { footer: {
githubIntro: 'See more on <a href="https://github.com/ClovertaTheTrilobita">Github</a>!', githubIntro: 'See more on <a href="https://github.com/ClovertaTheTrilobita">Github</a>!',
repoIntro: 'This blog is fully open source at <a href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog">ClovertaTheTrilobita/SanYeCao-blog</a>' repoIntro: 'This blog is fully open source at <a href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog">ClovertaTheTrilobita/SanYeCao-blog</a>'
},
friends: {
title: "Friends",
content: [
"Thank you for making it this far.",
"These are my friends, and you're very welcome to exchange links with me too!",
'You can send your link information to <a href="mailto:cloverta@petalmail.com">my email</a>.',
"The format is as follows:"
],
imgDesc: "Our everyday lives may, in fact, be a series of miracles."
} }
}; };

View file

@ -15,10 +15,6 @@ export default {
writtenBy: "作者", writtenBy: "作者",
comments: "评论", comments: "评论",
publishedOn: "发布于", publishedOn: "发布于",
commentsDesc: [
"欢迎在这里留下你的想法。💭💡",
"你可以在登录评论区后点击文本框右下角的「Subscribe by Email」以通过邮件接收最新的互动通知。"
]
}, },
theme: { theme: {
toggle: "切换主题", toggle: "切换主题",
@ -29,8 +25,7 @@ export default {
content: [ content: [
"见字如晤,展信舒颜。楮墨有限,不尽欲言。", "见字如晤,展信舒颜。楮墨有限,不尽欲言。",
"欢迎来到三叶的博客。", "欢迎来到三叶的博客。",
], ]
latestTitle: "最新文章"
}, },
about: { about: {
title: "关于我,和这个博客", title: "关于我,和这个博客",
@ -46,10 +41,9 @@ export default {
"这个博客在设计理念上参考了:", "这个博客在设计理念上参考了:",
'· <a href="https://ex-tasty.com/">極限風味</a>', '· <a href="https://ex-tasty.com/">極限風味</a>',
'· <a href="https://blog.cloudti.de/">Parsifal\'s Blog</a>', '· <a href="https://blog.cloudti.de/">Parsifal\'s Blog</a>',
'· <a href="https://www.kokosa.icu/">Kokosa\'s Notebook</a>',
"· 还有一些已经忘记名字的网站", "· 还有一些已经忘记名字的网站",
"谢谢你们的想法和热情!", "谢谢你们的想法和热情!",
"此外,这个博客的源码使用 MIT 协议开源,你可以从页脚的链接处获取它的源代码。", "此外,这个博客完全开源,你可以从页脚的链接处获取它的源代码。",
] ]
}, },
tags: { tags: {
@ -59,15 +53,5 @@ export default {
footer: { footer: {
githubIntro: '在 <a href="https://github.com/ClovertaTheTrilobita">Github</a> 查看更多!', githubIntro: '在 <a href="https://github.com/ClovertaTheTrilobita">Github</a> 查看更多!',
repoIntro: '这个博客完全开源于 <a href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog">ClovertaTheTrilobita/SanYeCao-blog</a>' repoIntro: '这个博客完全开源于 <a href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog">ClovertaTheTrilobita/SanYeCao-blog</a>'
},
friends: {
title: "友链",
content: [
"感谢你能看到这里。",
"这里是我的朋友们,也非常欢迎你来一起交换友链!",
'你可以把友链信息发送到<a href="mailto:cloverta@petalmail.com">我的邮箱</a>。',
"友链的格式如下:"
],
imgDesc: "——我们日复一日度过的日常,也许就是接连发生的奇迹"
} }
}; };

View file

@ -2,15 +2,7 @@
import Footer from "@/components/Footer.astro"; import Footer from "@/components/Footer.astro";
import Header from "@/components/Header.astro"; import Header from "@/components/Header.astro";
import { ClientRouter } from "astro:transitions"; import { ClientRouter } from "astro:transitions";
import { fade } from "astro:transitions";
import { getLangFromUrl } from "@/i18n";
import "katex/dist/katex.min.css";
const lang = getLangFromUrl(Astro.url);
const htmlLang = lang === "zh" ? "zh-CN" : "en-US";
import SEO from "@/components/SEO.astro"; import SEO from "@/components/SEO.astro";
import FloatingActions from "@/components/FloatingActions.astro";
import Spinner from "@/components/Spinner.astro";
const { const {
pageTitle, pageTitle,
@ -20,36 +12,61 @@ const {
--- ---
<!doctype html> <!doctype html>
<html lang={htmlLang}> <html lang="en">
<head> <head>
<ClientRouter />
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<SEO title={pageTitle} description={description} image={image} /> <SEO title={pageTitle} description={description} image={image} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<title>{pageTitle}</title> <title>{pageTitle}</title>
</head> </head>
<FloatingActions /> <div class="floating-actions">
<button
id="theme-toggle-fab"
aria-label="Toggle theme"
title="Toggle theme"
>
<svg
class="theme-toggle-icon"
viewBox="0 0 24 24"
width="20"
height="20"
aria-hidden="true"
>
<path
class="sun"
fill-rule="evenodd"
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
></path>
<path
class="moon"
fill-rule="evenodd"
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
></path>
</svg>
</button>
<button id="back-to-top" aria-label="Back to top" title="Back to top">
<svg
viewBox="0 0 24 24"
width="20"
height="20"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M18 15l-6-6-6 6"></path>
</svg>
</button>
</div>
<body> <body>
<Header /> <Header />
<Spinner /> <main class="page-content">
<main
class="page-content"
transition:animate={fade({ duration: "0.05s" })}
>
<slot /> <slot />
</main> </main>
<Footer /> <Footer />
@ -63,4 +80,184 @@ const {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.floating-actions {
position: fixed;
right: 1.25rem;
bottom: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
z-index: 1000;
}
#theme-toggle-fab,
#back-to-top {
width: 3rem;
height: 3rem;
border: #a7a7a7 1.5px solid;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
color: #222;
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(6px);
}
#theme-toggle-fab {
position: relative;
}
#theme-toggle-fab {
position: relative;
}
.theme-toggle-icon {
width: 1.2rem;
height: 1.2rem;
fill: currentColor;
}
.theme-toggle-icon .sun,
.theme-toggle-icon .moon {
transform-origin: center;
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.theme-toggle-icon .sun {
opacity: 1;
transform: scale(1) rotate(0deg);
}
.theme-toggle-icon .moon {
opacity: 0;
transform: scale(0.75) rotate(-20deg);
}
:global(.dark) .theme-toggle-icon .sun {
opacity: 0;
transform: scale(0.75) rotate(20deg);
}
:global(.dark) .theme-toggle-icon .moon {
opacity: 1;
transform: scale(1) rotate(0deg);
}
#theme-toggle-fab,
#back-to-top {
/* opacity: 0; */
visibility: hidden;
transform: translateY(150px);
transition:
opacity 0.2s ease,
transform 0.27s ease,
visibility 0.2s ease;
}
#theme-toggle-fab.show,
#back-to-top.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
#theme-toggle-fab.show:hover,
#back-to-top.show:hover {
transform: translateY(-2px);
}
:global(.dark) #theme-toggle-fab,
:global(.dark) #back-to-top {
background: rgba(34, 34, 34, 0.92);
color: #f5f5f5;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
border: #515151 2px solid;
}
@media (max-width: 640px) {
.floating-actions {
right: 1rem;
bottom: 1rem;
gap: 0.65rem;
}
#theme-toggle-fab,
#back-to-top {
width: 2.75rem;
height: 2.75rem;
}
}
</style> </style>
<script>
const initFloatingActions = () => {
const backToTop = document.getElementById("back-to-top");
const themeToggle = document.getElementById("theme-toggle-fab");
if (backToTop && backToTop.dataset.bound !== "true") {
const toggleBackToTop = () => {
if (window.scrollY > 300) {
backToTop.classList.add("show");
} else {
backToTop.classList.remove("show");
}
};
backToTop.addEventListener("click", () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
});
window.addEventListener("scroll", toggleBackToTop, {
passive: true,
});
toggleBackToTop();
backToTop.dataset.bound = "true";
}
if (themeToggle && themeToggle.dataset.bound !== "true") {
const togglethemeToggle = () => {
if (window.scrollY > 300) {
themeToggle.classList.add("show");
} else {
themeToggle.classList.remove("show");
}
};
const root = document.documentElement;
const applyTheme = (theme: string) => {
if (theme === "dark") {
root.classList.add("dark");
localStorage.setItem("color-theme", "dark");
} else {
root.classList.remove("dark");
localStorage.setItem("color-theme", "light");
}
};
themeToggle.addEventListener("click", () => {
const isDark = root.classList.contains("dark");
applyTheme(isDark ? "light" : "dark");
});
window.addEventListener("scroll", togglethemeToggle, {
passive: true,
});
togglethemeToggle();
themeToggle.dataset.bound = "true";
}
};
initFloatingActions();
document.addEventListener("astro:page-load", initFloatingActions);
</script>

View file

@ -1,14 +1,10 @@
--- ---
import BaseLayout from "./BaseLayout.astro"; import BaseLayout from "./BaseLayout.astro";
import Remark42Embed from "@/components/Remark42Embed.astro"; import Remark42Embed from "@/components/remark42-embed.svelte";
import PostNav from "@/components/Posts/PostNav.astro"; import { getLangFromUrl, getTranslations } from "@/i18n";
import PostMenu from "@/components/Posts/PostMenu.astro";
import Dialog from "@/components/Posts/Dialog.astro";
import { fade } from "astro:transitions";
import { getTranslations } from "@/i18n";
import "@/styles/global.css"; import "@/styles/global.css";
const { post, frontmatter, lang, slug, postId, headings } = Astro.props; const { frontmatter, lang, postId } = Astro.props;
const comments = lang === "zh" ? "评论区" : "comments"; const comments = lang === "zh" ? "评论区" : "comments";
const t = getTranslations(lang); const t = getTranslations(lang);
--- ---
@ -18,7 +14,7 @@ const t = getTranslations(lang);
description={frontmatter.description} description={frontmatter.description}
image={frontmatter.image?.url} image={frontmatter.image?.url}
> >
<div id="reading-progress" aria-hidden="true"></div> <div id="reading-progress" aria-hidden="true"></div>
<a <a
href="/" href="/"
class="back-button" class="back-button"
@ -40,24 +36,17 @@ const t = getTranslations(lang);
<path d="M15 18l-6-6 6-6"></path> <path d="M15 18l-6-6 6-6"></path>
</svg> </svg>
</a> </a>
<PostMenu headings={headings} />
<article class="post-article"> <article class="post-article">
<div class="post-header"> <div class="post-header">
<div class="post-meta"> <div class="post-meta">
<h1 class="post-title" transition:name={`post-title-${postId}`}> <h1 class="post-title">{frontmatter.title}</h1>
{frontmatter.title} <p class="description"><em>{frontmatter.description}</em></p>
</h1>
<p class="description">
<em>{frontmatter.description}</em>
</p>
<p class="meta-line"> <p class="meta-line">
{t.post.publishedOn}: {frontmatter.pubDate.toLocaleDateString()} {t.post.publishedOn}: {frontmatter.pubDate.toLocaleDateString()}
</p> </p>
<p class="meta-line">{t.post.writtenBy}: {frontmatter.author}</p> <p class="meta-line">{t.post.writtenBy}: {frontmatter.author}</p>
<div class="tags" transition:name={`post-tags-${postId}`}> <div class="tags">
{ {
frontmatter.tags.map((tag: string) => ( frontmatter.tags.map((tag: string) => (
<p class="tag"> <p class="tag">
@ -72,23 +61,17 @@ const t = getTranslations(lang);
src={frontmatter.image.url} src={frontmatter.image.url}
alt={frontmatter.image.alt} alt={frontmatter.image.alt}
class="post-cover" class="post-cover"
transition:name={`post-image-${postId}`}
/> />
</div> </div>
<div class="post-divider"></div> <div class="post-divider"></div>
<div class="post-content" transition:animate={fade({ duration: "0.2s" })}> <div class="post-content">
<slot /> <slot />
</div> </div>
<Dialog />
<PostNav post={post} lang={lang} />
<h2>{comments}</h2> <h2>{comments}</h2>
{t.post.commentsDesc.map((line: string) => <p set:html={line} />)} <Remark42Embed slug={postId} client:load />
<Remark42Embed slug={slug} />
</article> </article>
</BaseLayout> </BaseLayout>
@ -148,7 +131,7 @@ const t = getTranslations(lang);
backButton.dataset.bound = "true"; backButton.dataset.bound = "true";
} }
}; };
const updateProgress = () => { const updateProgress = () => {
const doc = document.documentElement; const doc = document.documentElement;
const scrollTop = window.scrollY || doc.scrollTop; const scrollTop = window.scrollY || doc.scrollTop;
const scrollHeight = doc.scrollHeight - window.innerHeight; const scrollHeight = doc.scrollHeight - window.innerHeight;
@ -169,7 +152,7 @@ const t = getTranslations(lang);
</script> </script>
<style> <style>
#reading-progress { #reading-progress {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -192,7 +175,7 @@ const t = getTranslations(lang);
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-decoration: none; text-decoration: none;
border: #536493 1.5px solid; border: #285ee9 1.5px solid;
background: rgba(255, 255, 255, 0.92); background: rgba(255, 255, 255, 0.92);
color: #222; color: #222;
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */ /* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
@ -203,8 +186,8 @@ const t = getTranslations(lang);
/* opacity: 0; */ /* opacity: 0; */
transition: transition:
opacity 0.2s ease, opacity 0.2s ease,
transform 0.27s ease, transform 0.27s ease,
visibility 0.2s ease, visibility 0.2s ease;
box-shadow 0.2s ease, box-shadow 0.2s ease,
background 0.2s ease, background 0.2s ease,
color 0.2s ease; color 0.2s ease;
@ -251,7 +234,6 @@ const t = getTranslations(lang);
"Noto Sans CJK SC", "Noto Sans CJK SC",
"Source Han Sans SC", "Source Han Sans SC",
sans-serif; sans-serif;
font-weight: 400;
} }
.post-content :global(code), .post-content :global(code),
@ -302,11 +284,10 @@ const t = getTranslations(lang);
.post-cover { .post-cover {
width: 300px; width: 300px;
height: auto; height: auto;
aspect-ratio: 16 / 10; aspect-ratio: 16 / 9;
object-fit: cover; object-fit: cover;
object-position: center; object-position: center;
/* border-radius: 0.4rem; */ border-radius: 0.4rem;
border: 1.5px #94a0ab dashed;
display: block; display: block;
} }
@ -365,91 +346,6 @@ const t = getTranslations(lang);
.post-cover { .post-cover {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
aspect-ratio: 16 / 9;
} }
} }
details {
margin: 1.5rem 0;
overflow: hidden;
border-top: 2px solid #1e3a5f;
border-radius: 0 0 0.5rem 0.5rem;
background: rgba(128, 128, 128, 0.14);
}
details summary {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.8rem 0.9rem;
cursor: pointer;
font-weight: 600;
user-select: none;
list-style: none;
}
details summary::-webkit-details-marker {
display: none;
}
details summary::before {
content: "";
display: inline-block;
font-size: 1.25rem;
line-height: 1;
transition: transform 0.2s ease;
}
details[open] summary::before {
transform: rotate(90deg);
}
/* 展开内容 */
details > :not(summary) {
box-sizing: border-box;
margin-top: 0;
margin-bottom: 0;
padding-right: 1rem;
}
/* 为列表序号预留空间 */
details > ol,
details > ul {
padding: 0.6rem 1rem 1rem 3rem;
}
details li {
margin: 0.8rem 0;
}
blockquote {
margin: 1.25rem 0;
padding: 0.75rem 1rem;
border-left: 4px solid var(--deep-red);
background: rgba(128, 128, 128, 0.1);
border-radius: 0 0.4rem 0.4rem 0;
}
blockquote p {
margin: 0;
}
blockquote p + p {
margin-top: 0.75rem;
}
/* 块级公式:超出屏幕时只滚动公式,不撑宽整篇文章 */
.katex-display {
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
padding: 0.25rem 0;
-webkit-overflow-scrolling: touch;
}
/* 避免 KaTeX 内部元素强行把父容器撑宽 */
.katex-display > .katex {
min-width: max-content;
}
</style> </style>

View file

@ -1,7 +1,6 @@
--- ---
import BaseLayout from "@/layouts/BaseLayout.astro"; import BaseLayout from "@/layouts/BaseLayout.astro";
import FriendlyLinkList from "@/components/FriendlyLinks/FriendlyLinkList.astro"; import FriendlyLinkList from "@/components/FriendlyLinks/FriendlyLinkList.astro";
import FriendsDescription from "@/components/FriendlyLinks/FriendsDescription.astro";
import { getLangFromUrl, getTranslations } from "@/i18n"; import { getLangFromUrl, getTranslations } from "@/i18n";
import "@/styles/global.css"; import "@/styles/global.css";
@ -15,16 +14,5 @@ const headerTitle = lang === "zh" ? "友情链接" : "Friends";
--- ---
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.title}`> <BaseLayout pageTitle=`${headerTitle} - ${t.banner.title}`>
<FriendsDescription />
<div class="section-divider"></div>
<FriendlyLinkList /> <FriendlyLinkList />
</BaseLayout> </BaseLayout>
<style>
.section-divider {
height: 1px;
background: #cdd2d8;
margin: 1.5rem 0;
opacity: 0.8;
}
</style>

View file

@ -2,7 +2,6 @@
import BaseLayout from "@/layouts/BaseLayout.astro"; import BaseLayout from "@/layouts/BaseLayout.astro";
import PostList from "@/components/Posts/PostList.astro"; import PostList from "@/components/Posts/PostList.astro";
import Remark42LatestComments from "@/components/remark42-latest-comments.svelte"; import Remark42LatestComments from "@/components/remark42-latest-comments.svelte";
import LatestComments from "@/components/LatestComments.astro";
import { getLangFromUrl, getTranslations } from "@/i18n"; import { getLangFromUrl, getTranslations } from "@/i18n";
import "@/styles/global.css"; import "@/styles/global.css";
@ -14,14 +13,13 @@ const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang); const t = getTranslations(lang);
const headerTitle = lang === "zh" ? "Cloverta的博客" : "Cloverta's blog"; const headerTitle = lang === "zh" ? "Cloverta的博客" : "Cloverta's blog";
const pageTitle = t.home.title; const pageTitle = t.home.title;
const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
--- ---
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.subtitle}`> <BaseLayout pageTitle=`${headerTitle} - ${t.banner.subtitle}`>
<h1 class="page-title"> <h1 class="page-title">
<span>{pageTitle}</span> <span>{pageTitle}</span>
<a <a
href={rssLink} href={`/rss.xml`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="RSS Feed" aria-label="RSS Feed"
@ -34,7 +32,7 @@ const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
width="33" width="33"
height="33" height="33"
aria-hidden="true" aria-hidden="true"
style="margin-left: 0px; margin-bottom: -5px; color:#3f50e5" style="margin-left: -15px; margin-bottom: -5px; color:#3f50e5"
> >
<path <path
d="M6.18 17.82a1.64 1.64 0 1 1 0 3.28 1.64 1.64 0 0 1 0-3.28ZM3 10.44v2.25c4.56 0 8.27 3.71 8.27 8.27h2.25C13.52 15.16 8.84 10.44 3 10.44Zm0-4.54v2.25c7.06 0 12.81 5.75 12.81 12.81h2.25C18.06 12.66 11.3 5.9 3 5.9Z" d="M6.18 17.82a1.64 1.64 0 1 1 0 3.28 1.64 1.64 0 0 1 0-3.28ZM3 10.44v2.25c4.56 0 8.27 3.71 8.27 8.27h2.25C13.52 15.16 8.84 10.44 3 10.44Zm0-4.54v2.25c7.06 0 12.81 5.75 12.81 12.81h2.25C18.06 12.66 11.3 5.9 3 5.9Z"
@ -48,14 +46,11 @@ const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
<div class="section-divider"></div> <div class="section-divider"></div>
<h2>{t.home.latestTitle}</h2>
<PostList /> <PostList />
<div class="section-divider"></div> <div class="section-divider"></div>
<!-- <Remark42LatestComments lang={lang} client:load /> --> <Remark42LatestComments lang={lang} client:load />
<LatestComments />
</BaseLayout> </BaseLayout>
<style> <style>
@ -69,8 +64,4 @@ const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
:global(.dark) .section-divider { :global(.dark) .section-divider {
background: #7f8b97; background: #7f8b97;
} }
.page-title {
font-weight: 700;
}
</style> </style>

View file

@ -22,19 +22,12 @@ export async function getStaticPaths() {
} }
const { post, lang } = Astro.props; const { post, lang } = Astro.props;
const { Content, headings } = await render(post); const { Content } = await render(post);
const [postLang, ...slugParts] = post.id.split("/"); const [postLang, ...slugParts] = post.id.split("/");
const slug = slugParts.join("/"); const slug = slugParts.join("/");
--- ---
<MarkdownPostLayout <MarkdownPostLayout frontmatter={post.data} lang={lang} postId={slug}>
post={post}
frontmatter={post.data}
lang={lang}
slug={slug}
postId={post.id}
headings={headings}
>
<Content /> <Content />
</MarkdownPostLayout> </MarkdownPostLayout>

View file

@ -34,7 +34,7 @@ const t = getTranslations(lang);
<BaseLayout pageTitle={String(tag)}> <BaseLayout pageTitle={String(tag)}>
<p class="tag-heading"> <p class="tag-heading">
{lang === "zh" ? "带有标签" : "Posts tagged with"}{" "} {lang === "zh" ? "带有标签" : "Posts tagged with"}{" "}
<span class="tag-chip" transition:name={`post-tags-${tag}`}>{tag}</span> <span class="tag-chip">{tag}</span>
{lang === "zh" ? "的文章" : ""} {lang === "zh" ? "的文章" : ""}
</p> </p>
<ul> <ul>
@ -53,8 +53,6 @@ const t = getTranslations(lang);
title={post.data.title} title={post.data.title}
date={formattedDate} date={formattedDate}
img={post.data.image.url} img={post.data.image.url}
tags={post.data.tags}
postId={post.id}
/> />
); );
}) })

View file

@ -15,14 +15,14 @@ const tags = [...new Set(allPosts.map((post: any) => post.data.tags).flat())];
const pageTitle = lang === "zh" ? "标签索引" : "Tag Index"; const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
--- ---
<BaseLayout pageTitle=`${pageTitle} - ${t.banner.title}`> <BaseLayout pageTitle=`${pageTitle} - ${t.banner.title}` `>
<h1>{t.tags.title}</h1> <h1>{t.tags.title}</h1>
<p>{t.tags.description}</p> <p>{t.tags.description}</p>
<div class="tags"> <div class="tags">
{ {
tags.map((tag) => ( tags.map((tag) => (
<p class="tag" transition:name={`post-tags-${tag}`}> <p class="tag">
<a href={`/${lang}/tags/${tag}`}>{tag}</a> <a href={`/${lang}/tags/${tag}`}>{tag}</a>
</p> </p>
)) ))
@ -39,19 +39,6 @@ const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
} }
.tag { .tag {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
margin: 0; margin: 0;
padding: 0.22em 0.65em; padding: 0.22em 0.65em;
font-size: 1rem; font-size: 1rem;
@ -64,7 +51,6 @@ const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
.tag a { .tag a {
color: #6f4e37; color: #6f4e37;
text-decoration: none; text-decoration: none;
font-weight: 500;
} }
.tag a:hover { .tag a:hover {

View file

@ -1,35 +0,0 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
const enPosts = posts
.filter((post) => post.id.startsWith('en/'))
.sort(
(a, b) =>
new Date(b.data.pubDate).getTime() -
new Date(a.data.pubDate).getTime()
);
return rss({
title: "Cloverta's Blog",
description:
"Discover more here. Welcome to Cloverta's blog 🥳",
site: context.site,
items: enPosts.map((post) => {
const [, ...slugParts] = post.id.split('/');
const slug = slugParts.join('/');
return {
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/en/posts/${slug}/`,
};
}),
customData: '<language>en-US</language>',
});
}

View file

@ -1,34 +1,26 @@
import rss from '@astrojs/rss'; import rss from '@astrojs/rss';
import { pagesGlobToRssItems } from '@astrojs/rss';
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
export async function GET(context) { export async function GET(context) {
const posts = await getCollection('blog'); const posts = await getCollection("blog");
const zhPosts = posts
.filter((post) => post.id.startsWith('zh/'))
.sort(
(a, b) =>
new Date(b.data.pubDate).getTime() -
new Date(a.data.pubDate).getTime()
);
return rss({ return rss({
title: 'Cloverta的博客', title: 'Cloverta的博客',
description: '在这里,发现更多(雾)欢迎来到三叶的博客🥳', description: '在这里,发现更多(雾)欢迎来到三叶的博客🥳',
site: context.site, site: context.site,
items: await pagesGlobToRssItems(import.meta.glob('./**/*.md')),
items: posts.map((post) => {
const [postLang, ...slugParts] = post.id.split("/");
const slug = slugParts.join("/");
items: zhPosts.map((post) => { return ({
const [, ...slugParts] = post.id.split('/');
const slug = slugParts.join('/');
return {
title: post.data.title, title: post.data.title,
pubDate: post.data.pubDate, pubDate: post.data.pubDate,
description: post.data.description, description: post.data.description,
link: `/zh/posts/${slug}/`, link: `/${postLang}/posts/${slug}/`,
}; })
}), }),
customData: `<language>en-us</language>`,
customData: '<language>zh-CN</language>', })
});
} }

View file

@ -1,95 +0,0 @@
.image-preview-dialog {
padding: 0;
border: none;
background: transparent;
max-width: min(96vw, 1200px);
max-height: 96vh;
}
.image-preview-dialog::backdrop {
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(6px);
}
.image-preview-dialog img {
display: block;
max-width: 96vw;
max-height: 90vh;
object-fit: contain;
box-shadow: 0 20px 80px rgba(0, 0, 0, 0.45);
}
.image-preview-close:focus {
outline: none;
}
.image-preview-close:focus-visible {
outline: 2px solid rgba(125, 162, 255, 0.9);
outline-offset: 4px;
}
.image-preview-close {
position: fixed;
top: 1.25rem;
right: 1.25rem;
z-index: 1;
width: 2.4rem;
height: 2.4rem;
border: none;
border-radius: 999px;
background: transparent;
color: var(--text-color-dark);
font-size: 2.6rem;
line-height: 1;
cursor: pointer;
}
/* .image-preview-close:hover {
background: rgba(0, 0, 0, 0.75);
} */
/* 给文章图片一个可点击提示 */
.prose img,
.markdown-body img,
article img {
cursor: zoom-in;
}
.image-preview-download {
position: fixed;
left: 50%;
bottom: max(1.5rem, env(safe-area-inset-bottom));
transform: translateX(-50%);
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
color: white;
background: transparent;
border: none;
padding: 0.4rem;
cursor: pointer;
text-decoration: none;
opacity: 0.88;
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.image-preview-download:hover {
opacity: 1;
transform: translateX(-50%) translateY(-2px);
}
.image-preview-download:focus {
outline: none;
}
.image-preview-download:focus-visible {
outline: 2px solid rgba(125, 162, 255, 0.9);
outline-offset: 4px;
border-radius: 8px;
}

View file

@ -1,9 +1,30 @@
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/400.css"); @import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/400.css");
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/400-italic.css"); @import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/400-italic.css");
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/700.css"); @import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/700.css");
@import "./latest-comments.css";
@import "./variables.css"; @font-face {
@import "./dialog.css"; font-family: "Maple Mono CN";
src: url("/fonts/subset/MapleMono-CN-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Maple Mono CN";
src: url("/fonts/subset/MapleMono-CN-Italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Maple Mono CN";
src: url("/fonts/subset/MapleMono-CN-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
pre { pre {
padding: 1rem; padding: 1rem;
@ -28,32 +49,10 @@ td code {
background: rgba(127, 127, 127, 0.12); background: rgba(127, 127, 127, 0.12);
} }
article svg.flowchart,
article svg[class*="flowchart"],
article svg[id^="mermaid-"] {
display: block;
margin: 1.5rem auto;
}
html { html {
/* font-family: "Maple Mono", "Maple Mono CN", monospace; */ font-family: "Maple Mono", "Maple Mono CN", monospace;
font-family: background-color: #ffffff;
"Noto Serif SC", color: #1f2328;
"Source Han Serif SC",
"Songti SC",
"STSong",
"SimSun",
serif;
font-weight: 500;
/* background-color: #ffffff; */
background-color: var(--background-color);
color: var(--text-color);
}
html {
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: #828282 transparent;
} }
body { body {
@ -74,14 +73,13 @@ body::after {
/* 按需调整大小 */ /* 按需调整大小 */
height: 400px; height: 400px;
/* 按需调整大小 */ /* 按需调整大小 */
background-image: url("https://files.seeusercontent.com/2026/04/16/1sXb/touhou___kirisame_marisa__2__by_.webp"); background-image: url("https://files.seeusercontent.com/2026/03/30/4Xfr/bc7e804dc2c8ecaf407c9d665414ff72.webp");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right bottom; background-position: right bottom;
background-size: contain; background-size: contain;
opacity: 0.35; opacity: 0.35;
pointer-events: none; pointer-events: none;
z-index: -1; z-index: -1;
} }
html.dark body::after { html.dark body::after {
@ -90,7 +88,7 @@ html.dark body::after {
@media (max-width: 900px) { @media (max-width: 900px) {
body::after { body::after {
opacity: 0; opacity: 0.1;
} }
} }
@ -102,6 +100,10 @@ html.dark body::after {
body { body {
font-size: 1rem; font-size: 1rem;
} }
body::after {
opacity: 0;
}
} }
@ -117,7 +119,7 @@ h1 {
a { a {
color: #3D74B6; color: #416bd6;
font-weight: 700; font-weight: 700;
text-decoration: none; text-decoration: none;
} }
@ -162,111 +164,3 @@ img {
height: auto; height: auto;
display: block; display: block;
} }
/* 避免 Markdown 内容撑宽正文 */
.post-article,
.post-content {
min-width: 0;
max-width: 100%;
}
/* 块级公式过长时独立横向滚动 */
.post-content .katex-display {
display: block;
width: 100%;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
box-sizing: border-box;
padding: 0.25rem 0;
-webkit-overflow-scrolling: touch;
}
.post-content .katex-display > .katex {
display: inline-block;
min-width: max-content;
white-space: nowrap;
}
/* =========================
Markdown details 下拉栏
========================= */
.post-content details {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
margin: 1.5rem 0;
overflow: hidden;
border-top: 2px solid #1e3a5f;
border-radius: 0 0 0.5rem 0.5rem;
background: rgba(128, 128, 128, 0.14);
}
.post-content details summary {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
padding: 0.8rem 0.9rem;
cursor: pointer;
font-weight: 600;
user-select: none;
list-style: none;
}
.post-content details summary::-webkit-details-marker {
display: none;
}
.post-content details summary::before {
content: "";
display: inline-block;
flex: 0 0 auto;
font-size: 1.25rem;
line-height: 1;
transition: transform 0.2s ease;
}
.post-content details[open] summary::before {
transform: rotate(90deg);
}
.post-content details > :not(summary) {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
margin-top: 0;
margin-bottom: 0;
padding-right: 1rem;
}
.post-content details > ol,
.post-content details > ul {
padding: 0.6rem 1rem 1rem 3rem;
}
.post-content details li {
margin: 0.8rem 0;
}
blockquote {
margin: 1.25rem 0;
padding: 0.75rem 1rem;
border-left: 4px solid var(--deep-red);
background: rgba(128, 128, 128, 0.1);
border-radius: 0 0.4rem 0.4rem 0;
}
blockquote p {
margin: 0;
}
blockquote p + p {
margin-top: 0.75rem;
}

View file

@ -1,263 +0,0 @@
.latest-comments {
margin-top: 1.5rem;
}
.latest-comments h2 {
margin-bottom: 0.8rem;
}
.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-list {
display: block;
}
.comment-card {
display: block;
margin: 0 0 14px 0;
padding: 0.6rem 1rem;
border: 2px dashed #aeb8c2;
background-color: #fbf5f2;
}
.comment-card:last-child {
margin-bottom: 0;
}
.comment-card-body {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
grid-template-areas:
"info title"
"text title";
gap: 0.2rem 1rem;
align-items: start;
}
.comment-card-info {
grid-area: info;
display: flex;
align-items: center;
gap: 0.7rem;
min-width: 0;
}
.comment-avatar {
width: 35px;
height: 35px;
flex-shrink: 0;
}
.comment-avatar-img,
.comment-avatar-fallback {
width: 35px;
height: 35px;
display: block;
border-radius: 50%;
object-fit: cover;
}
.comment-avatar-img {
border: 1px dashed #aeb8c2;
background: #f3f5f7;
}
.comment-avatar-fallback {
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #aeb8c2;
background: rgba(160, 175, 190, 0.12);
color: #5e6b77;
font-weight: 700;
font-size: 1rem;
user-select: none;
}
.comment-meta {
min-width: 0;
flex: 1;
}
.comment-author-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.6rem;
min-width: 0;
line-height: 1.3;
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;
}
.comment-author {
font-weight: 400;
font-size: 0.8rem;
color: gray;
overflow-wrap: anywhere;
word-break: break-word;
}
.comment-time {
color: #7a7a7a;
font-size: 0.7rem;
}
.comment-card-title {
grid-area: title;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
text-align: right;
min-width: 0;
}
.comment-title-link {
color: #6f8090;
text-decoration: none;
font-size: 1.1rem;
font-weight: 700;
line-height: 1.45;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.comment-title-link:hover {
text-decoration: underline;
}
.comment-card-text {
grid-area: text;
margin: 0;
color: #555;
line-height: 1.5;
font-size: 0.93rem;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
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;
}
.comment-card-text :global(p),
.comment-card-text p {
margin: 0;
}
.comment-card-text :global(img),
.comment-card-text img {
max-width: 100%;
height: auto;
}
.latest-comments-empty {
color: #777;
font-style: italic;
}
.comment-reply-sep {
font-size: 0.8rem;
opacity: 0.7;
}
.latest-comments .comment-reply-to {
font-weight: 400;
font-size: 0.8rem;
color: gray;
opacity: 0.9;
overflow-wrap: anywhere;
word-break: break-word;
}
html.dark .latest-comments .comment-card,
.dark .latest-comments .comment-card {
border-color: #7f8c97;
background-color: #252525;
}
html.dark .comment-title-link {
color: #c8d2dc;
}
html.dark .comment-card-text {
color: #d3d7db;
}
html.dark .comment-time {
color: #a8b0b7;
}
html.dark .latest-comments .comment-avatar-fallback,
.dark .latest-comments .comment-avatar-fallback {
border-color: #7f8c97;
background-color: #3a444d;
color: #dbe3ea;
}
:global(.dark) .comment-avatar-fallback {
background: rgba(180, 190, 200, 0.12);
color: #dbe3ea;
}
@media (max-width: 640px) {
.comment-card-body {
grid-template-columns: 1fr;
grid-template-areas:
"title"
"info"
"text";
}
.comment-card-title {
text-align: left;
margin-bottom: 0.2rem;
}
.comment-title-link {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}

View file

@ -1,9 +0,0 @@
:root {
--deep-blue: #536493;
--deep-red: #ef5a6f;
--background-color: #f9f2ed;
--text-color: #0E2F56;
--background-color-dark: #1e1e1e;
--text-color-dark: #e6e6e6;
}