Compare commits
@@ -0,0 +1,53 @@
|
||||
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"
|
||||
@@ -0,0 +1,12 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
db.json
|
||||
*.log
|
||||
node_modules/
|
||||
.astro/
|
||||
dist/
|
||||
.deploy*/
|
||||
_multiconfig.yml
|
||||
.hintrc
|
||||
genkey.js
|
||||
push2typesense.js
|
||||
@@ -1 +0,0 @@
|
||||
[{"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/"}]
|
||||
@@ -0,0 +1,27 @@
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
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 |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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;
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
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;
|
||||
@@ -1 +0,0 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
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>
|
||||