Compare commits
@@ -1,53 +0,0 @@
|
|||||||
name: 自动部署
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- published
|
|
||||||
workflow_dispatch:
|
|
||||||
env:
|
|
||||||
TZ: Asia/Shanghai
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: 检出分支
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: master
|
|
||||||
- name: 缓存项目 npm 包
|
|
||||||
id: cache-node-modules
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: node_modules
|
|
||||||
key: ${{ runner.os }}-nodeModules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('package.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-nodeModules-
|
|
||||||
- name: 安装 Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22.x"
|
|
||||||
- name: 安装依赖
|
|
||||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
npm install
|
|
||||||
- name: 同步 Astro 类型
|
|
||||||
run: |
|
|
||||||
npm run clean
|
|
||||||
- name: 生成静态文件
|
|
||||||
run: |
|
|
||||||
npm run build
|
|
||||||
- name: 部署到 page 分支
|
|
||||||
run: |
|
|
||||||
cd ./dist
|
|
||||||
git init
|
|
||||||
git config user.name "${{ gitea.actor }}"
|
|
||||||
git config user.email "${{ gitea.actor }}@noreply.gitea.io"
|
|
||||||
git add .
|
|
||||||
git commit -m "${{ gitea.event.head_commit.message }} -- [$(date +"%Z %Y-%m-%d %A %H:%M:%S")]"
|
|
||||||
git push --force --quiet "https://${{ gitea.actor }}:${{ secrets.DEPLOY_TOKEN }}@git.biss.click/biss/blog.git" master:page
|
|
||||||
- name: 触发服务器部署
|
|
||||||
run: |
|
|
||||||
curl -k -X POST "https://panel.biss.click/hook?access_key=1XJG8IvYTSZVvD5dpm86GYIpQxgxBcucULnX1MFskZSKayXU"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
db.json
|
|
||||||
*.log
|
|
||||||
node_modules/
|
|
||||||
.astro/
|
|
||||||
dist/
|
|
||||||
.deploy*/
|
|
||||||
_multiconfig.yml
|
|
||||||
.hintrc
|
|
||||||
genkey.js
|
|
||||||
push2typesense.js
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[{"title":"网站迁移日志","href":"/posts/8f3b9d21/"},{"title":"用 Umami 分享链接给网站信息卡提供统计数据","href":"/posts/4c91a8f2/"},{"title":"把普通的一天过成自己的节奏","href":"/posts/efe1e805/"},{"title":"GPT Image2 初试","href":"/posts/3f8de22/"},{"title":"电子价签玩法","href":"/posts/8712baaa/"},{"title":"OpenGL- Cohen-Sutherland 编码裁剪算法","href":"/posts/9b2ef00a/"},{"title":"OpenGL-直线的扫描转换","href":"/posts/7207243b/"},{"title":"OpenGL-基础程序","href":"/posts/437a5198/"},{"title":"配置OpenGL环境","href":"/posts/c4477b0c/"},{"title":"opencv应用-算术运算","href":"/posts/b559997d/"},{"title":"opencv应用-基础操作","href":"/posts/e8f95ead/"},{"title":"配置opencv","href":"/posts/6e3332b/"},{"title":"自建bitwarden服务","href":"/posts/7baa41fc/"},{"title":"自建renovate-bot","href":"/posts/56f57c0b/"},{"title":"新年快乐!","href":"/posts/5785bd01/"},{"title":"添加网站左上角菜单","href":"/posts/7e921903/"},{"title":"将博客仓库转移到gitea","href":"/posts/d2c8521/"},{"title":"安装gitea","href":"/posts/34725d47/"},{"title":"添加typesense搜索","href":"/posts/f287c563/"},{"title":"自定义字体","href":"/posts/b5601a7e/"},{"title":"在侧边栏添加日历和倒计时","href":"/posts/5ed2f1e6/"},{"title":"和朋友们的故事","href":"/posts/a31d95d9/"},{"title":"Adguard和Openclash共存","href":"/posts/66e66374/"},{"title":"Adguard规则分享","href":"/posts/69b16001/"},{"title":"在Openwrt上安装AdguardHome","href":"/posts/b57500e9/"},{"title":"在Openwrt中安装OpenClash","href":"/posts/2b2fb1a7/"},{"title":"Cudy TR3000刷入Openwrt系统","href":"/posts/8802e397/"},{"title":"搭建Owncloud并集成Onlyoffice","href":"/posts/59c8572a/"},{"title":"为博客添加CMS系统","href":"/posts/3a26a97a/"},{"title":"为1Panel添加自己想要的应用","href":"/posts/bb18d851/"},{"title":"使用GitHub推送Hexo到服务器","href":"/posts/ce1ec3fe/"},{"title":"C# 基本语法","href":"/posts/e9a8e898/"},{"title":"C# 入门","href":"/posts/bc56f789/"},{"title":"自定义页脚(新)","href":"/posts/c6143ad3/"},{"title":"自定义分类条","href":"/posts/33249733/"},{"title":"自定义导航栏","href":"/posts/3e61a389/"},{"title":"利用 SiteMap 随机访问站内页面","href":"/posts/b5866b9e/"},{"title":"博客添加AI总结","href":"/posts/41b9aff7/"},{"title":"利用插件自定义页脚菜单","href":"/posts/a6ab0925/"},{"title":"英语文章分享-去奋力生活吧,就好像死亡近在咫尺","href":"/posts/f0a1a1f4/"},{"title":"自定义右键菜单","href":"/posts/8bdb35fb/"},{"title":"在侧边栏中添加欢迎信息","href":"/posts/da17d00/"},{"title":"配置说说页面","href":"/posts/ad244066/"},{"title":"部署历程","href":"/posts/6e5f5039/"}]
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
|
||||||
import sitemap from '@astrojs/sitemap';
|
|
||||||
import { unified } from '@astrojs/markdown-remark';
|
|
||||||
import rehypeMathjax from 'rehype-mathjax';
|
|
||||||
import remarkMath from 'remark-math';
|
|
||||||
import remarkButterflyTags from './src/lib/remarkButterflyTags.mjs';
|
|
||||||
import { siteConfig } from './site.config.mjs';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
site: siteConfig.site.url,
|
|
||||||
output: 'static',
|
|
||||||
integrations: [sitemap()],
|
|
||||||
markdown: {
|
|
||||||
processor: unified({
|
|
||||||
remarkPlugins: [remarkMath, remarkButterflyTags],
|
|
||||||
rehypePlugins: [rehypeMathjax],
|
|
||||||
}),
|
|
||||||
shikiConfig: {
|
|
||||||
theme: 'github-dark',
|
|
||||||
wrap: true,
|
|
||||||
langAlias: {
|
|
||||||
BASH: 'bash',
|
|
||||||
YAML: 'yaml',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 457 KiB After Width: | Height: | Size: 457 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Bi's Blog",
|
|
||||||
"version": "3.1.1",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"prebuild": "node scripts/generate-abbrlinks.mjs",
|
|
||||||
"build": "astro build",
|
|
||||||
"clean": "astro sync",
|
|
||||||
"dev": "astro dev",
|
|
||||||
"preview": "astro preview",
|
|
||||||
"server": "astro dev"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/rss": "^4.0.18",
|
|
||||||
"@astrojs/sitemap": "^3.7.3",
|
|
||||||
"astro": "^6.4.8",
|
|
||||||
"rehype-mathjax": "^7.1.0",
|
|
||||||
"remark-math": "^6.0.0",
|
|
||||||
"typesense": "^3.0.6"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:recommended"
|
|
||||||
],
|
|
||||||
"timezone": "Asia/Shanghai",
|
|
||||||
"enabledManagers": [
|
|
||||||
"npm"
|
|
||||||
],
|
|
||||||
"ignorePaths": [
|
|
||||||
"themes/**",
|
|
||||||
".gitea/**"
|
|
||||||
],
|
|
||||||
"rangeStrategy": "bump",
|
|
||||||
"prHourlyLimit": 0,
|
|
||||||
"prConcurrentLimit": 0,
|
|
||||||
"dependencyDashboard": false
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
||||||
import { basename, extname, join, relative } from 'node:path';
|
|
||||||
|
|
||||||
const POSTS_DIR = join(process.cwd(), 'src', 'content', 'posts');
|
|
||||||
|
|
||||||
const CRC32_TABLE = new Uint32Array(256);
|
|
||||||
|
|
||||||
for (let i = 0; i < 256; i += 1) {
|
|
||||||
let value = i;
|
|
||||||
for (let bit = 0; bit < 8; bit += 1) {
|
|
||||||
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
||||||
}
|
|
||||||
CRC32_TABLE[i] = value >>> 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function crc32(input) {
|
|
||||||
const bytes = new TextEncoder().encode(input);
|
|
||||||
let crc = 0xffffffff;
|
|
||||||
|
|
||||||
for (const byte of bytes) {
|
|
||||||
crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ((crc ^ 0xffffffff) >>> 0).toString(16);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listMarkdownFiles(dir) {
|
|
||||||
const entries = await readdir(dir, { withFileTypes: true });
|
|
||||||
const files = await Promise.all(
|
|
||||||
entries.map(async (entry) => {
|
|
||||||
const path = join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) return listMarkdownFiles(path);
|
|
||||||
if (entry.isFile() && extname(entry.name).toLowerCase() === '.md') return [path];
|
|
||||||
return [];
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return files.flat();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFrontmatterBounds(content) {
|
|
||||||
if (!content.startsWith('---')) return null;
|
|
||||||
|
|
||||||
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
||||||
const lines = content.split(newline);
|
|
||||||
const closeIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
||||||
|
|
||||||
if (closeIndex === -1) return null;
|
|
||||||
return { closeIndex, lines, newline };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getScalar(frontmatterLines, key) {
|
|
||||||
const pattern = new RegExp(`^${key}\\s*:\\s*(.*)$`);
|
|
||||||
const match = frontmatterLines.map((line) => line.match(pattern)).find(Boolean);
|
|
||||||
if (!match) return '';
|
|
||||||
|
|
||||||
return match[1]
|
|
||||||
.trim()
|
|
||||||
.replace(/^['"]|['"]$/g, '')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeSeed(file, frontmatterLines) {
|
|
||||||
const title = getScalar(frontmatterLines, 'title');
|
|
||||||
if (title) return title;
|
|
||||||
|
|
||||||
const filename = getScalar(frontmatterLines, 'filename');
|
|
||||||
if (filename) return filename;
|
|
||||||
|
|
||||||
return basename(file, '.md');
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeUniqueAbbrlink(seed, usedAbbrlinks) {
|
|
||||||
let attempt = 0;
|
|
||||||
let abbrlink = crc32(seed);
|
|
||||||
|
|
||||||
while (usedAbbrlinks.has(abbrlink)) {
|
|
||||||
attempt += 1;
|
|
||||||
abbrlink = crc32(`${seed}:${attempt}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
usedAbbrlinks.add(abbrlink);
|
|
||||||
return abbrlink;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const files = await listMarkdownFiles(POSTS_DIR);
|
|
||||||
const usedAbbrlinks = new Set();
|
|
||||||
const pending = [];
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const content = await readFile(file, 'utf8');
|
|
||||||
const bounds = getFrontmatterBounds(content);
|
|
||||||
if (!bounds) continue;
|
|
||||||
|
|
||||||
const frontmatterLines = bounds.lines.slice(1, bounds.closeIndex);
|
|
||||||
const existing = getScalar(frontmatterLines, 'abbrlink');
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
usedAbbrlinks.add(existing);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending.push({ file, content, bounds, frontmatterLines });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of pending) {
|
|
||||||
const seed = makeSeed(item.file, item.frontmatterLines);
|
|
||||||
const abbrlink = makeUniqueAbbrlink(seed, usedAbbrlinks);
|
|
||||||
const insertAt = item.bounds.closeIndex;
|
|
||||||
|
|
||||||
item.bounds.lines.splice(insertAt, 0, `abbrlink: ${abbrlink}`);
|
|
||||||
await writeFile(item.file, item.bounds.lines.join(item.bounds.newline), 'utf8');
|
|
||||||
|
|
||||||
console.log(`Added abbrlink ${abbrlink} to ${relative(process.cwd(), item.file)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pending.length === 0) {
|
|
||||||
console.log('All posts already have abbrlink.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exitCode = 1;
|
|
||||||
});
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
export const siteConfig = {
|
|
||||||
site: {
|
|
||||||
url: 'https://blog.biss.click',
|
|
||||||
name: "Bi's Blog",
|
|
||||||
description: 'Personal blog by biss.',
|
|
||||||
language: 'zh-CN',
|
|
||||||
domain: 'blog.biss.click',
|
|
||||||
slogan: '好奇心转化为代码的空间。',
|
|
||||||
startTime: '2025-01-01T00:00:00+08:00',
|
|
||||||
copyrightStartYear: 2025,
|
|
||||||
license: '内容采用 CC BY-NC-SA 4.0 协议',
|
|
||||||
},
|
|
||||||
author: {
|
|
||||||
name: 'biss',
|
|
||||||
email: 'bishsh2006@gmail.com',
|
|
||||||
github: 'https://github.com/bishshi',
|
|
||||||
avatar: 'https://free.picui.cn/free/2025/08/10/689845496a283.png',
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
icon: '/images/Bi.ico',
|
|
||||||
background: '/images/background.png',
|
|
||||||
},
|
|
||||||
nav: [
|
|
||||||
{ label: '归档', href: '/archives/', icon: 'fa-solid fa-box-archive' },
|
|
||||||
{ label: '分类', href: '/categories/', icon: 'fa-solid fa-folder-open' },
|
|
||||||
{ label: '标签', href: '/tags/', icon: 'fa-solid fa-tags' },
|
|
||||||
{ label: '说说', href: '/shuoshuo/', icon: 'fa-regular fa-comments' },
|
|
||||||
{ label: '关于', href: '/about/', icon: 'fa-solid fa-heart' },
|
|
||||||
{ label: '友链', href: '/link/', icon: 'fa-solid fa-link' },
|
|
||||||
],
|
|
||||||
footer: {
|
|
||||||
contactLabel: '联系方式',
|
|
||||||
links: [
|
|
||||||
{ label: '发展历程', href: '/archives/' },
|
|
||||||
{ label: '隐私政策', href: '/privacy/' },
|
|
||||||
],
|
|
||||||
poweredBy: [
|
|
||||||
{ label: 'Fastly', href: 'https://fastly.com/' },
|
|
||||||
{ label: 'ASTRO', href: 'https://astro.build/' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
social: [
|
|
||||||
{ label: '发送邮件', href: 'mailto:bishsh2006@gmail.com', icon: 'fa-solid fa-envelope' },
|
|
||||||
{ label: '订阅 RSS', href: '/rss.xml', icon: 'fa-solid fa-rss' },
|
|
||||||
{ label: 'GitHub', href: 'https://github.com/bishshi', icon: 'fa-brands fa-github', external: true },
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
postsPerPage: 16,
|
|
||||||
},
|
|
||||||
series: {
|
|
||||||
labels: {
|
|
||||||
webcustom: '博客定制手记',
|
|
||||||
opencv: 'OpenCV 学习笔记',
|
|
||||||
OpenGL: 'OpenGL 学习笔记',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
comments: {
|
|
||||||
envId: 'https://comment.biss.click',
|
|
||||||
region: '',
|
|
||||||
script: 'https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.min.js',
|
|
||||||
},
|
|
||||||
analytics: {
|
|
||||||
umami: {
|
|
||||||
script: 'https://umami.biss.click/script.js',
|
|
||||||
websiteId: '3d97b310-b241-4fc6-bc9c-68e317fc7d42',
|
|
||||||
statsEndpoint: 'https://blogumami.biss.click',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
externalLinks: {
|
|
||||||
waitSeconds: 10,
|
|
||||||
whitelist: [
|
|
||||||
'blog.biss.click',
|
|
||||||
'biss.click',
|
|
||||||
'github.com',
|
|
||||||
'localhost',
|
|
||||||
'127.0.0.1',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
talks: {
|
|
||||||
apiUrl: 'https://mm.biss.click/api/echo/page',
|
|
||||||
pageSize: 30,
|
|
||||||
homeLimit: 6,
|
|
||||||
cacheKey: 'talksCache',
|
|
||||||
cacheTimeKey: 'talksCacheTime',
|
|
||||||
cacheDurationMs: 30 * 60 * 1000,
|
|
||||||
fallbackAvatar: 'https://free.picui.cn/free/2025/08/10/689845496a283.png',
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
apiKey: '2uxQzk7eD2GBCljdSmIiuU3ause7UK9n',
|
|
||||||
server: {
|
|
||||||
host: 'typesense.biss.click',
|
|
||||||
port: '443',
|
|
||||||
protocol: 'https',
|
|
||||||
},
|
|
||||||
indexName: 'blogs',
|
|
||||||
searchParams: {
|
|
||||||
query_by: 'title,content',
|
|
||||||
highlight_full_fields: 'title,content',
|
|
||||||
per_page: 8,
|
|
||||||
num_typos: 1,
|
|
||||||
typo_tokens_threshold: 1,
|
|
||||||
prefix: true,
|
|
||||||
},
|
|
||||||
ui: {
|
|
||||||
maxRetries: 10,
|
|
||||||
retryDelay: 100,
|
|
||||||
animationDuration: 300,
|
|
||||||
maxInitRetries: 30,
|
|
||||||
placeholder: '输入关键词寻找故事...',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cdn: {
|
|
||||||
instantsearchCss: 'https://cdn.jsdmirror.com/npm/instantsearch.css/themes/reset-min.css',
|
|
||||||
fancyboxCss: 'https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css',
|
|
||||||
fontawesomeCss: 'https://cdn.jsdmirror.com/npm/@fortawesome/fontawesome-free@6.7.2/css/all.min.css',
|
|
||||||
instantsearchJs: 'https://cdn.jsdmirror.com/npm/instantsearch.js@4.56.0',
|
|
||||||
typesenseAdapterJs: 'https://cdn.jsdmirror.com/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js',
|
|
||||||
fancyboxJs: 'https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js',
|
|
||||||
jqueryJs: 'https://code.jquery.com/jquery-4.0.0.min.js',
|
|
||||||
welcomeMessageJs: 'https://cdn.jsdmirror.com/gh/bishshi/welcomemessage/txmap.js',
|
|
||||||
},
|
|
||||||
friends: [
|
|
||||||
{
|
|
||||||
name: 'GitHub',
|
|
||||||
url: 'https://github.com/bishshi',
|
|
||||||
avatar: '/images/Bi.ico',
|
|
||||||
description: '站长的 GitHub 主页',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const publicSiteConfig = {
|
|
||||||
site: siteConfig.site,
|
|
||||||
author: siteConfig.author,
|
|
||||||
comments: siteConfig.comments,
|
|
||||||
analytics: siteConfig.analytics,
|
|
||||||
talks: siteConfig.talks,
|
|
||||||
search: siteConfig.search,
|
|
||||||
externalLinks: siteConfig.externalLinks,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default siteConfig;
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><sitemap><loc>https://blog.biss.click/sitemap-0.xml</loc></sitemap></sitemapindex>
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
---
|
|
||||||
import type { BlogPost } from '@/lib/posts';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
posts: BlogPost[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArchiveMonth = {
|
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
posts: BlogPost[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ArchiveYear = {
|
|
||||||
year: number;
|
|
||||||
count: number;
|
|
||||||
months: ArchiveMonth[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const { posts } = Astro.props;
|
|
||||||
const monthFormatter = new Intl.DateTimeFormat('zh-CN', { month: 'long' });
|
|
||||||
const dayFormatter = new Intl.DateTimeFormat('zh-CN', { day: '2-digit' });
|
|
||||||
const archiveYears = posts.reduce<ArchiveYear[]>((years, post) => {
|
|
||||||
const year = post.date.getFullYear();
|
|
||||||
const monthKey = `${year}-${String(post.date.getMonth() + 1).padStart(2, '0')}`;
|
|
||||||
let yearGroup = years.find((item) => item.year === year);
|
|
||||||
|
|
||||||
if (!yearGroup) {
|
|
||||||
yearGroup = { year, count: 0, months: [] };
|
|
||||||
years.push(yearGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
let monthGroup = yearGroup.months.find((item) => item.key === monthKey);
|
|
||||||
|
|
||||||
if (!monthGroup) {
|
|
||||||
monthGroup = {
|
|
||||||
key: monthKey,
|
|
||||||
label: monthFormatter.format(post.date),
|
|
||||||
posts: [],
|
|
||||||
};
|
|
||||||
yearGroup.months.push(monthGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
yearGroup.count += 1;
|
|
||||||
monthGroup.posts.push(post);
|
|
||||||
|
|
||||||
return years;
|
|
||||||
}, []);
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="archive-timeline">
|
|
||||||
{archiveYears.map((yearGroup) => (
|
|
||||||
<section class="archive-year" aria-labelledby={`archive-year-${yearGroup.year}`}>
|
|
||||||
<div class="archive-year-marker" aria-hidden="true"></div>
|
|
||||||
<header class="archive-year-head">
|
|
||||||
<h2 id={`archive-year-${yearGroup.year}`}>{yearGroup.year}</h2>
|
|
||||||
<span>{yearGroup.count} 篇</span>
|
|
||||||
</header>
|
|
||||||
<div class="archive-months">
|
|
||||||
{yearGroup.months.map((monthGroup) => (
|
|
||||||
<section class="archive-month" aria-label={`${yearGroup.year} ${monthGroup.label}`}>
|
|
||||||
<div class="archive-month-label">
|
|
||||||
<span>{monthGroup.label}</span>
|
|
||||||
<small>{monthGroup.posts.length} 篇</small>
|
|
||||||
</div>
|
|
||||||
<div class="archive-list">
|
|
||||||
{monthGroup.posts.map((post) => (
|
|
||||||
<a href={post.href}>
|
|
||||||
<time datetime={post.date.toISOString()}>{dayFormatter.format(post.date)}</time>
|
|
||||||
<span>{post.title}</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
---
|
|
||||||
import { getCategoryHref, type BlogPost } from '@/lib/posts';
|
|
||||||
import SiteInfoCard from '@/components/SiteInfoCard.astro';
|
|
||||||
import { siteConfig } from '../../site.config.mjs';
|
|
||||||
|
|
||||||
type TocItem = {
|
|
||||||
depth: number;
|
|
||||||
text: string;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TocSection = {
|
|
||||||
heading: TocItem;
|
|
||||||
children: TocItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
post: BlogPost;
|
|
||||||
toc: TocItem[];
|
|
||||||
relatedPosts: BlogPost[];
|
|
||||||
posts: BlogPost[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { post, toc, relatedPosts, posts } = Astro.props;
|
|
||||||
const avatar = siteConfig.author.avatar;
|
|
||||||
const topDepth = toc.length > 0 ? Math.min(...toc.map((item) => item.depth)) : 1;
|
|
||||||
const tocSections = toc.reduce<TocSection[]>((sections, item) => {
|
|
||||||
if (item.depth <= topDepth || sections.length === 0) {
|
|
||||||
sections.push({ heading: item, children: [] });
|
|
||||||
} else {
|
|
||||||
sections[sections.length - 1].children.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections;
|
|
||||||
}, []);
|
|
||||||
---
|
|
||||||
|
|
||||||
<aside class="article-sidebar" aria-label="文章侧边栏">
|
|
||||||
<section class="sidebar-card article-author-card">
|
|
||||||
<img class="profile-avatar" src={avatar} alt={siteConfig.author.name} loading="lazy" />
|
|
||||||
<div class="profile-name">{siteConfig.author.name}</div>
|
|
||||||
<div class="article-author-meta">
|
|
||||||
<span>{post.dateText}</span>
|
|
||||||
{post.categories[0] && <a href={getCategoryHref(post.categories[0])}>{post.categories[0]}</a>}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{post.tags.length > 0 && (
|
|
||||||
<section class="sidebar-card article-tags-card">
|
|
||||||
<h2>
|
|
||||||
<span aria-hidden="true">#</span>
|
|
||||||
文章标签
|
|
||||||
<em>{post.tags.length}</em>
|
|
||||||
</h2>
|
|
||||||
<div class="article-tag-list" aria-label="文章标签">
|
|
||||||
{post.tags.map((tag) => (
|
|
||||||
<a href={`/tags/${encodeURIComponent(tag)}/`}>#{tag}</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="article-sticky-sidebar">
|
|
||||||
{toc.length > 0 && (
|
|
||||||
<section class="sidebar-card toc-card">
|
|
||||||
<h2>
|
|
||||||
<span aria-hidden="true">☰</span>
|
|
||||||
文章目录
|
|
||||||
<em>{toc.length}</em>
|
|
||||||
</h2>
|
|
||||||
<nav class="toc-list toc-tree" aria-label="文章目录" data-toc-tree>
|
|
||||||
{tocSections.map((section, index) => (
|
|
||||||
<div class:list={['toc-group', index === 0 && 'is-open']} data-toc-group data-heading-id={section.heading.id}>
|
|
||||||
<a class="toc-link toc-link-main" href={`#${section.heading.id}`} data-toc-link data-toc-top>
|
|
||||||
<span class="toc-marker" aria-hidden="true"></span>
|
|
||||||
<span>{section.heading.text}</span>
|
|
||||||
</a>
|
|
||||||
{section.children.length > 0 && (
|
|
||||||
<div class="toc-children" data-toc-children>
|
|
||||||
{section.children.map((item) => (
|
|
||||||
<a class={`toc-link toc-link-child toc-depth-${item.depth}`} href={`#${item.id}`} data-toc-link>
|
|
||||||
<span>{item.text}</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<section class="sidebar-card same-category-card">
|
|
||||||
<h2>
|
|
||||||
<span aria-hidden="true">▣</span>
|
|
||||||
同类文章
|
|
||||||
<em>{relatedPosts.length}</em>
|
|
||||||
</h2>
|
|
||||||
{relatedPosts.length > 0 ? (
|
|
||||||
<div class="same-category-list">
|
|
||||||
{relatedPosts.map((item) => (
|
|
||||||
<a href={item.href}>
|
|
||||||
<strong>{item.title}</strong>
|
|
||||||
<time datetime={item.date.toISOString()}>{item.dateText}</time>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p class="sidebar-empty">这个分类下暂时没有其他文章。</p>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<SiteInfoCard posts={posts} />
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||