Compare commits

...

30 Commits

Author SHA1 Message Date
6c525bcf98 解决构建问题
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-07 13:34:10 +08:00
7a3059f486 添加 RSS 侧渲染和抓取缓存
Some checks failed
continuous-integration/drone/push Build is failing
2026-04-07 13:19:01 +08:00
1b8bd19370 添加博客渲染缓存 2026-04-07 12:57:47 +08:00
ca1cf10aa8 添加错误重试机制
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-07 12:19:13 +08:00
880afe34ab 微调排版
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-07 03:35:00 +08:00
c3973e779a 添加标题上边距 2026-04-07 03:32:17 +08:00
d605131bda 添加格式化相关
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-07 03:27:22 +08:00
2a71d714e8 添加 code 元素样式
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-05 21:24:50 +08:00
a47a537a64 修复链接
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-05 21:21:48 +08:00
e36492a692 优化排版
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-05 21:17:40 +08:00
9c019351a3 添加博客头图 2026-04-05 20:59:31 +08:00
5e53a213e6 补充 Artalk 和博客文章页
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-05 20:34:57 +08:00
29b3c89c40 补充图片 densities
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-05 19:21:21 +08:00
43a85ac056 完成博客索引页
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-05 19:05:31 +08:00
3d0eeb7996 修复手机视图 dock 问题
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-05 16:58:29 +08:00
e466006f81 添加友链
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-04 12:40:57 +08:00
12e66a644e 友链部分的层级设计使用更加 token 化的设计
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 01:03:38 +08:00
404c315657 no log
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 00:30:31 +08:00
ac984b320f
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 23:59:56 +08:00
c5c25b6617 wakatime
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 23:47:47 +08:00
d8fb7fd63f 语言添补
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 23:41:51 +08:00
428be3170e 修复锯齿问题
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 23:34:26 +08:00
b833065baf notation 下对齐修正
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 23:18:18 +08:00
00218a17ba 头像更换
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 23:16:54 +08:00
fc73828155 内封头像图片
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 23:13:54 +08:00
d06e525385 使用 lvh 而非 dvh 增强移动端体验
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 23:10:39 +08:00
b1e1f05886 修复 Chrome 意外的滚动条
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 23:05:35 +08:00
14c30a2a30 调整关于页面
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 22:57:24 +08:00
8fa4ed1b0d 完成首页 2026-04-01 22:54:47 +08:00
4c60081298 导航栏与首页动画优化 2026-04-01 20:20:07 +08:00
31 changed files with 2066 additions and 197 deletions

1
.husky/pre-commit Normal file
View File

@ -0,0 +1 @@
npx lint-staged

View File

@ -8,4 +8,3 @@
npm i
npm run dev
```

View File

@ -12,6 +12,7 @@ const ICON2_URL =
// https://astro.build/config
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone',
}),
@ -21,4 +22,11 @@ export default defineConfig({
redirects: {
'/assets/icon-qq-BThBBmjV.jpg': ICON2_URL,
},
vite: {
build: {
rollupOptions: {
external: ['node:crypto', 'buffer', 'keyv'],
},
},
},
})

43
eslint.config.mjs Normal file
View File

@ -0,0 +1,43 @@
import eslintPluginAstro from 'eslint-plugin-astro'
import tseslint from '@typescript-eslint/eslint-plugin'
import tsparser from '@typescript-eslint/parser'
import svelteParser from 'svelte-eslint-parser'
export default [
{
ignores: ['dist/**', 'node_modules/**', '.astro/**'],
plugins: {
astro: eslintPluginAstro,
'@typescript-eslint': tseslint,
},
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
},
rules: {
...eslintPluginAstro.configs.recommended.rules,
...tseslint.configs.recommended.rules,
'no-console': 'warn',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
},
},
{
files: ['**/*.svelte'],
languageOptions: {
parser: svelteParser,
parserOptions: {
parser: tsparser,
ecmaVersion: 'latest',
sourceType: 'module',
},
},
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
]

802
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,20 @@
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
"astro": "astro",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"prepare": "husky"
},
"lint-staged": {
"*.{js,ts,jsx,tsx,svelte,vue}": [
"eslint --fix",
"prettier --write"
],
"*.{astro,css,md,mdx,json}": [
"prettier --write"
]
},
"dependencies": {
"@astrojs/node": "^10.0.4",
@ -17,16 +30,19 @@
"@astrojs/rss": "^4.0.18",
"@astrojs/svelte": "^8.0.4",
"@astrojs/vue": "^6.0.1",
"@iconify-json/material-symbols": "^1.2.65",
"@iconify-json/mdi": "^1.2.3",
"@markdoc/markdoc": "^0.5.7",
"@traptitech/markdown-it-katex": "^3.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@unocss/reset": "^66.6.7",
"artalk": "^2.9.1",
"astro": "^6.1.0",
"astro-icon": "^1.1.5",
"axios": "^1.13.6",
"katex": "^0.16.43",
"keyv": "^5.6.0",
"markdown-it": "^14.1.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@ -37,10 +53,15 @@
},
"devDependencies": {
"@types/markdown-it": "^14.1.2",
"@typescript-eslint/eslint-plugin": "^8.58.0",
"@typescript-eslint/parser": "^8.57.2",
"eslint": "^10.1.0",
"eslint-plugin-astro": "^1.6.0",
"eslint-plugin-vue": "^10.8.0",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1"
"prettier-plugin-astro": "^0.14.1",
"svelte-eslint-parser": "^1.6.0"
}
}

View File

@ -17,6 +17,12 @@ const config = {
parser: 'astro',
},
},
{
files: '*.css',
options: {
tabWidth: 4,
},
},
],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -1,6 +1,7 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Bold.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Bold.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@ -8,7 +9,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-BoldItalic.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-BoldItalic.woff2')
format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
@ -16,7 +18,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraBold.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraBold.woff2')
format('woff2');
font-weight: 800;
font-style: normal;
font-display: swap;
@ -24,7 +27,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraBoldItalic.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraBoldItalic.woff2')
format('woff2');
font-weight: 800;
font-style: italic;
font-display: swap;
@ -32,7 +36,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraLight.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraLight.woff2')
format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
@ -40,7 +45,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraLightItalic.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraLightItalic.woff2')
format('woff2');
font-weight: 200;
font-style: italic;
font-display: swap;
@ -48,7 +54,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Italic.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Italic.woff2')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
@ -56,7 +63,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Light.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Light.woff2')
format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
@ -64,7 +72,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-LightItalic.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-LightItalic.woff2')
format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap;
@ -72,7 +81,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Medium.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Medium.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@ -80,7 +90,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-MediumItalic.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-MediumItalic.woff2')
format('woff2');
font-weight: 500;
font-style: italic;
font-display: swap;
@ -88,7 +99,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Regular.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Regular.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@ -96,7 +108,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-SemiBold.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-SemiBold.woff2')
format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@ -104,7 +117,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-SemiBoldItalic.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-SemiBoldItalic.woff2')
format('woff2');
font-weight: 600;
font-style: italic;
font-display: swap;
@ -112,7 +126,8 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Thin.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Thin.woff2')
format('woff2');
font-weight: 100;
font-style: normal;
font-display: swap;
@ -120,19 +135,23 @@
@font-face {
font-family: 'Maple Mono';
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ThinItalic.woff2') format('woff2');
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ThinItalic.woff2')
format('woff2');
font-weight: 100;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "ZLabsRoundPix 16px M CN";
src: url('https://cdn.passthem.top/fonts/ZLabsRoundPix_16px_M_CN.ttf.woff2') format('woff2');
font-family: 'ZLabsRoundPix 16px M CN';
src: url('https://cdn.passthem.top/fonts/ZLabsRoundPix_16px_M_CN.ttf.woff2')
format('woff2');
font-display: swap;
}
:root {
--font-mono: 'Maple Mono NF CN', 'Maple Mono', monospace, var(--font-sans);
--font-sans: 'HarmonyOS Sans SC', 'Source Han Sans SC', 'Noto Sans CJK SC', sans-serif;
--font-sans:
'HarmonyOS Sans SC', 'Source Han Sans SC', 'Noto Sans CJK SC',
sans-serif;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

94
src/assets/prose.css Normal file
View File

@ -0,0 +1,94 @@
.prose {
& p,
& h1,
& h2,
& h3,
& h4,
& h5,
& h6 {
line-height: 1.6em;
}
& p {
margin-block: 0.6em;
}
& h1,
& h2,
& h3,
& h4,
& h5,
& h6 {
font-weight: 700;
margin-block: 1em;
text-align: center;
}
& h1 {
font-size: 2rem;
}
& h2 {
font-size: 1.8rem;
}
& h3 {
font-size: 1.6rem;
}
& h4 {
font-size: 1.4rem;
}
& h5 {
font-size: 1.2rem;
}
& h6 {
font-size: 1.1rem;
}
& blockquote {
background-color: var(--color-bg-1);
color: var(--color-fg-1);
border-radius: 0.5rem;
padding-block: 0.5rem;
padding-inline: 1.25rem;
margin-block: 1.25rem;
border-inline-start: solid 0.25rem;
}
& pre.shiki {
border-radius: 0.5rem;
padding-block: 1rem;
padding-inline: 1.25rem;
margin-block: 1.25rem;
overflow-x: scroll;
}
& a {
color: var(--color-blue);
&:hover {
text-decoration: underline;
}
}
& img {
border-radius: 0.5rem;
margin-block: 1.25rem;
}
& p > code {
background-color: var(--color-bg-1);
padding: 0.25rem;
border-radius: 0.25rem;
}
& pre,
& code {
font-family: var(--font-mono);
}
}

View File

@ -7,14 +7,23 @@
:root {
color-scheme: light dark;
--color-bg-n: light-dark(oklch(100% 0 0), oklch(35% 0.02 270));
--color-bg-0: light-dark(oklch(95% 0 0), oklch(30% 0.02 270));
--color-bg-n: light-dark(oklch(100% 0 0), oklch(30% 0.025 270));
--color-bg-0: light-dark(oklch(95% 0 0), oklch(25% 0.02 270));
--color-bg-1: light-dark(oklch(90% 0 0), oklch(20% 0.02 270));
--color-bg-2: light-dark(oklch(85% 0 0), oklch(15% 0.02 270));
--color-fg-0: light-dark(oklch(25% 0.02 270), oklch(90% 0.02 270));
--color-bg-1: light-dark(oklch(90% 0 0), oklch(25% 0.02 270));
--color-fg-1: light-dark(oklch(35% 0.02 270), oklch(80% 0.02 270));
--color-blue: light-dark(oklch(40% 0.2 270), oklch(80% 0.2 270));
--color-green: light-dark(oklch(50% 0.1 150), oklch(80% 0.15 150));
--color-link: var(--color-blue);
--size-nav: 3rem;
@media (width < 768px) {
--size-nav: 4rem;
}
}
/* == 页面设置 == */
@ -31,12 +40,14 @@ body {
color: var(--color-fg-0);
font-family: var(--font-sans);
/* writing-mode: vertical-rl; */
}
/* == 配置默认字体 == */
code {
font-family: var(--font-mono);
font-size: .9rem;
font-size: 0.9rem;
}
/* == 支持 Shiki 的高亮 == */

View File

@ -0,0 +1,30 @@
<script lang="ts">
// https://artalk.js.org/zh/develop/import-framework
import Artalk from 'artalk'
import { onMount, onDestroy } from 'svelte'
import 'artalk/Artalk.css'
let el: HTMLElement
let artalk: Artalk
onMount(() => {
artalk = Artalk.init({
el: el,
pageKey: location.pathname,
pageTitle: document.title,
server: 'https://artalk.sv.passthem.top',
site: '小帕的小窝',
darkMode: 'auto',
})
onDestroy(() => {
if (artalk) {
artalk.destroy()
}
})
})
</script>
<div class="comment" bind:this={el}></div>

View File

@ -0,0 +1,165 @@
---
import { Image } from 'astro:assets'
import { Icon } from 'astro-icon/components'
interface Props {
link: string
title: string
featureImage?: string
createdAt?: Date
updatedAt?: Date
author?: {
name: string
avatar?: string
}
}
const { link, title, featureImage, author, createdAt, updatedAt } = Astro.props
const formatDate = (date: Date) =>
`${date.getFullYear()} 年 ${date.getMonth() + 1} 月 ${date.getDate()} 日`
---
<a class="card" href={link}>
{
featureImage && (
<Image
class="image"
src={featureImage}
width={640}
height={360}
densities={[1.5, 2]}
alt="博文的头图"
/>
)
}
<div class="info">
<h1 class="title">{title}</h1>
{
author && (
<div class="author">
<Image
class="image"
src={author.avatar ?? ''}
alt={`用户 ${author.name} 的头像`}
width={24}
height={24}
densities={[1.5, 2]}
/>
<span class="content">{author.name}</span>
</div>
)
}
{
createdAt && (
<div class="iconinfo">
<Icon class="icon" name="material-symbols:calendar-add-on-rounded" />
<span class="content">{formatDate(createdAt)}</span>
</div>
)
}
{
updatedAt &&
(!createdAt || formatDate(createdAt) != formatDate(updatedAt)) && (
<div class="iconinfo">
<Icon class="icon" name="material-symbols:calendar-check-rounded" />
<span class="content">{formatDate(updatedAt)}</span>
</div>
)
}
</div>
</a>
<style>
.card {
--icon-size: 24px;
background-color: var(--color-bg-n);
margin: 1rem;
padding-inline: 1rem;
padding-block: 1.25rem;
display: flex;
flex-direction: column;
/* gap: 0.75rem; */
border-radius: 2rem;
transition: all cubic-bezier(0.4, 1, 0.4, 1) 0.3s;
@media (width >= 768px) {
flex-direction: row;
gap: 0.5rem;
}
outline: solid 0px transparent;
&:hover {
outline-offset: 4px;
outline: solid 4px var(--color-blue);
}
&:has(> .image) {
padding-block-start: 1rem;
@media (width >= 768px) {
padding-block-end: 1rem;
}
& > .image {
border-radius: 1rem;
aspect-ratio: 16 / 9;
object-fit: cover;
margin-block-end: 0.75rem;
@media (width >= 768px) {
width: 300px;
margin-block-end: 0;
}
}
}
& > .info {
& > h1 {
margin-inline: 0.5rem;
font-size: larger;
font-weight: 600;
margin-block-start: 0.25rem;
margin-block-end: 0.75rem;
@media (width < 768px) {
margin-inline: 0.75rem;
}
}
& > .author,
& > .iconinfo {
margin-inline: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-block: 0.1rem;
& > .image {
aspect-ratio: 1;
object-fit: cover;
width: var(--icon-size);
border-radius: 50%;
}
& > .icon {
width: var(--icon-size);
height: var(--icon-size);
padding: 2px;
opacity: 0.5;
}
& > .content {
opacity: 0.5;
font-size: 0.9rem;
}
}
& > .author {
margin-block: 0.25rem;
}
}
}
</style>

View File

@ -3,6 +3,7 @@
import { Image } from 'astro:assets'
import IceAvatar from '../assets/mainpage_avatars/ice.png'
import MnkAvatar from '../assets/mainpage_avatars/mnk.jpg'
---
<div class="comments">
@ -46,7 +47,7 @@ import IceAvatar from '../assets/mainpage_avatars/ice.png'
<p>帕,移动</p>,
],
[
'https://legacy.passthem.top/assets/mono-BkJO6fUP.webp',
MnkAvatar,
'モノクロ子',
'全て変えるのなら黒にする。',
<>

View File

@ -81,6 +81,13 @@
'钻研手工开发',
],
},
{
stages: [
{ text: '', editing: ['b', 'bw', "bw'v", "bw'vj"] },
{ text: '备战', editing: ['k', 'kc', "kc'y", "kc'yj"] },
'备战考研',
],
},
]
// 状态机定义

View File

@ -28,7 +28,7 @@ export const MarkdocTreeRender: React.FC<{
tree: RenderableTreeNode
}> = ({ tree }) => {
return (
<div className="markdoc-container">
<div className="markdoc-container prose">
{Markdoc.renderers.react(tree, React, { components: { CodeBlock } })}
</div>
)

View File

@ -1,6 +1,7 @@
---
import '../assets/style.css'
import '../assets/fonts.css'
import '../assets/prose.css'
import 'katex/dist/katex.min.css'
import '@unocss/reset/tailwind-v4.css'
import { ClientRouter } from 'astro:transitions'

View File

@ -35,6 +35,10 @@ const { title = '小帕的小窝' } = Astro.props
margin-block: 1rem;
}
& :global(p) {
margin-block: 0.25rem;
}
& :global(a) {
color: var(--color-link);
text-decoration: none;

View File

@ -4,9 +4,10 @@ import { Icon } from 'astro-icon/components'
interface Props {
title?: string
withGap?: boolean
}
const { title = '小帕的小窝' } = Astro.props
const { title = '小帕的小窝', withGap = false } = Astro.props
---
<BaseLayout title={title}>
@ -28,11 +29,6 @@ const { title = '小帕的小窝' } = Astro.props
<li class="website-logo">
<a href="/">小帕的小窝</a>
</li>
<li>
<a href="https://legacy.passthem.top"
><Icon name="mdi:history" /> 旧博客系统*</a
>
</li>
<li>
<a href="/blogs"><Icon name="mdi:invoice-text-outline" />文章</a>
</li>
@ -42,7 +38,18 @@ const { title = '小帕的小窝' } = Astro.props
>
</li>
</ul>
<ul class="right">Hello!</ul>
<ul class="right">
<li>
<a href="https://legacy.passthem.top"
><Icon name="mdi:history" /> 旧博客系统*</a
>
</li>
<li>
<a href="/rss.xml">
<Icon name="material-symbols:rss-feed" /> 博客 RSS
</a>
</li>
</ul>
</nav>
</div>
<div
@ -53,6 +60,7 @@ const { title = '小帕的小窝' } = Astro.props
>
</div>
<main>
{withGap && <div aria-hidden="true" class="layout-gap" />}
<slot />
</main>
</BaseLayout>
@ -63,6 +71,11 @@ const { title = '小帕的小窝' } = Astro.props
navMain?.classList.add('nav-container-hidden')
}
const navOpen = () => {
const navMain = document.getElementById('nav-main')
navMain?.classList.remove('nav-container-hidden')
}
document.getElementById('nav-main')?.addEventListener('click', (e) => {
const target = e.target as HTMLElement
if (target.id == 'nav-main') {
@ -70,6 +83,18 @@ const { title = '小帕的小窝' } = Astro.props
}
})
document.getElementById('nav-main')?.addEventListener('focusin', navOpen)
document.getElementById('nav-main')?.addEventListener('focusout', (e) => {
if (
e.relatedTarget &&
!document
.getElementById('nav-main')
?.contains(e.relatedTarget as HTMLElement)
) {
navClose()
}
})
document.addEventListener('astro:before-preparation', () => {
document.getElementById('loading-indicator')?.classList.remove('hidden')
})
@ -78,19 +103,36 @@ const { title = '小帕的小窝' } = Astro.props
requestAnimationFrame(navClose)
document.getElementById('loading-indicator')?.classList.add('hidden')
})
document.addEventListener('astro:page-load', () => {
document.querySelectorAll('#nav-main li:has(a)').forEach((li) => {
const child = li.querySelector('a')
if (!child) {
li.classList.add('url-here')
return
}
if (
window.location.href.replace(/\/$/, '') == child.href.replace(/\/$/, '')
) {
li.classList.add('url-here')
} else {
li.classList.remove('url-here')
}
})
})
</script>
<style>
.nav-container {
width: 100dvw;
height: 3rem;
inline-size: 100dvi;
block-size: 3rem;
position: fixed;
z-index: 10000;
backdrop-filter: blur(4px);
background-color: rgb(from var(--color-bg-n) r g b / 0.5);
@media (max-width: 767px) {
height: 100dvh;
@media (width < 768px) {
block-size: 100dvh;
background-color: #00000033;
transition:
background-color ease 0.2s,
@ -98,7 +140,7 @@ const { title = '小帕的小窝' } = Astro.props
}
&.nav-container-hidden {
@media (max-width: 767px) {
@media (width < 768px) {
backdrop-filter: blur(0px);
background-color: transparent;
pointer-events: none;
@ -106,25 +148,25 @@ const { title = '小帕的小窝' } = Astro.props
}
& > nav {
width: 100%;
height: 100%;
inline-size: 100%;
block-size: 100%;
padding-inline: 40px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
max-width: 1400px;
max-inline-size: 1400px;
margin-inline: auto;
font-weight: 400;
@media (max-width: 767px) {
@media (width < 768px) {
flex-direction: column;
padding-block: 5rem;
padding-inline: 2rem;
align-items: start;
width: 70%;
inline-size: 70%;
background-color: var(--color-bg-n);
margin-inline-start: 0;
font-size: larger;
@ -132,8 +174,8 @@ const { title = '小帕的小窝' } = Astro.props
}
.nav-container-hidden & {
@media (max-width: 767px) {
transform: translateX(-80dvw);
@media (width < 768px) {
transform: translateX(-80dvi);
}
}
@ -141,29 +183,34 @@ const { title = '小帕的小窝' } = Astro.props
display: flex;
flex-direction: row;
@media (max-width: 767px) {
@media (width < 768px) {
flex-direction: column;
gap: 0.25rem;
width: 100%;
inline-size: 100%;
}
}
& li {
height: 2rem;
@media (max-width: 767px) {
@media (width < 768px) {
height: 3rem;
width: 100%;
inline-size: 100%;
}
&:hover {
background-color: rgb(from var(--color-fg-0) r g b / 0.1);
}
&.url-here {
color: var(--color-blue);
background-color: rgb(from var(--color-blue) r g b / 0.1);
}
& > a {
padding-inline: 0.75rem;
width: 100%;
height: 100%;
inline-size: 100%;
block-size: 100%;
display: flex;
align-items: center;
gap: 0.25rem;
@ -190,8 +237,8 @@ const { title = '小帕的小窝' } = Astro.props
}
.nav-mobile {
width: 100dvw;
height: 4rem;
inline-size: 100dvw;
block-size: 4rem;
position: fixed;
z-index: 100;
backdrop-filter: blur(4px);
@ -205,12 +252,13 @@ const { title = '小帕的小窝' } = Astro.props
gap: 0.5rem;
& > button {
width: 3rem;
inline-size: 3rem;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
cursor: pointer;
&:active,
&:hover {
@ -227,7 +275,7 @@ const { title = '小帕的小窝' } = Astro.props
}
}
@media (min-width: 768px) {
@media (width >= 768px) {
display: none;
}
}
@ -273,4 +321,8 @@ const { title = '小帕的小窝' } = Astro.props
background-position: var(--loading-x-size) 0;
}
}
.layout-gap {
height: var(--size-nav);
}
</style>

View File

@ -5,10 +5,10 @@ export type ListBlogItemType = {
title: string
/** ISO8601 */
created_at: string
created_at: Date
/** ISO8601 */
updated_at: string
updated_at: Date
featured_image: null | {
image_url: string
@ -18,9 +18,16 @@ export type ListBlogItemType = {
id: number
username: string
nickname: string
avatar: {
image_url: string
}
}
}
export type BlogContentType = ListBlogItemType & {
content: string
}
export const listBlogs = async ({
page = 1,
limit = 20,
@ -29,22 +36,56 @@ export const listBlogs = async ({
limit?: number
}) => {
const resp = await legacyClient.post('/v1/blog/list', { page, limit })
return resp.data.data as ListBlogItemType[]
return resp.data.data.map((blog: any) => {
let _blog = blog
if (blog.featured_image) {
_blog = {
...blog,
featured_image: {
image_url:
'https://legacy.passthem.top' + blog.featured_image.image_url,
},
}
} else {
_blog = blog
}
_blog.author.avatar = {
image_url: 'https://legacy.passthem.top' + _blog.author.avatar.image_url,
}
_blog.created_at = new Date(_blog.created_at)
_blog.updated_at = new Date(_blog.updated_at)
return _blog
}) as ListBlogItemType[]
}
export const getBlog: (
blog_id: number,
) => Promise<null | { title: string; content: string }> = async (
blog_id: number,
) => {
) => Promise<null | BlogContentType> = async (blog_id: number) => {
const resp = await legacyClient.get(`/v1/blog/${blog_id}`, {
validateStatus: (status) => status == 200 || status == 404,
})
if (resp.status == 404) {
return null
}
return resp.data as {
title: string
content: string
let _blog = resp.data
if (_blog.featured_image) {
_blog = {
..._blog,
featured_image: {
image_url:
'https://legacy.passthem.top' + _blog.featured_image.image_url,
},
}
}
_blog.author.avatar = {
image_url: 'https://legacy.passthem.top' + _blog.author.avatar.image_url,
}
_blog.created_at = new Date(_blog.created_at)
_blog.updated_at = new Date(_blog.updated_at)
return _blog
}

16
src/lib/markdoc-cache.ts Normal file
View File

@ -0,0 +1,16 @@
import type { RenderableTreeNode } from '@markdoc/markdoc'
import KeyV from 'keyv'
import { createHash } from 'node:crypto'
const markdocVersion = '1'
export const markdocCache = new KeyV<RenderableTreeNode>({
namespace: `markdoc-cache-${markdocVersion}`,
ttl: 1000 * 60 * 60 * 24 * 7,
})
export const generateCacheKey = (content: string) => {
return createHash('sha256')
.update(content)
.update(markdocVersion)
.digest('hex')
}

View File

@ -12,6 +12,15 @@ export const markdocConfig: Config = {
highlightedHtml: { type: String },
},
transform(node, config) {
/*
* 注意力!!!!!!!!!!!!!!
* 注意!!!!!!!!!!!!
*
* 如果改了这里的逻辑,记得更改 `markdoc-cache.ts` 里的版本号
* 以让对应的缓存能够被更新
*
**/
const attributes = node.transformAttributes(config)
const highlightedHtml = shikiRender(
@ -29,8 +38,28 @@ export const markdocConfig: Config = {
},
}
export const toMarkdocTree = async (content: string) => {
const _toMarkdocTree = async (content: string) => {
const ast = Markdoc.parse(content)
await ensureShikiEngine()
return Markdoc.transform(ast, markdocConfig)
const tree = Markdoc.transform(ast, markdocConfig)
return tree
}
export const toMarkdocTree = async (content: string) => {
if (!import.meta.env.SSR) {
return await _toMarkdocTree(content)
}
const cacheLib = await import('./markdoc-cache.ts')
const key = cacheLib.generateCacheKey(content)
const cached = await cacheLib.markdocCache.get(key)
if (cached) {
return cached
}
const tree = await _toMarkdocTree(content)
cacheLib.markdocCache.set(key, tree)
return tree
}

View File

@ -32,6 +32,17 @@ const allowed_langs: BundledLanguage[] = [
'sh',
'mermaid',
'cpp',
'fish',
'zsh',
'docker',
'dockerfile',
'yml',
'system-verilog',
'sql',
'glsl',
'mediawiki',
'systemd',
'ini',
]
export async function ensureShikiEngine() {

View File

@ -1,36 +1,13 @@
---
import BoringLayout from '../layout/BoringLayout.astro'
const friends = [
['https://omega98.top', '核子的博客'],
['https://tnot.top', 'TNOT 的博客'],
['https://ruusuge.top', 'ルース毛的博客'],
['https://akarin.dev/', '宸佬的博客'],
['https://wzq02.top', 'wzq02 的博客'],
['https://r1kka.one/', "Rikka's Blog"],
]
---
<BoringLayout title="关于">
<h1>关于<a href="/">这里</a></h1>
<p>这里是 passthem 和他朋友们的博客</p>
<p>主要由我也就是 passthem 运维这个网站,但是我会接收朋友们的投稿</p>
<h1>怎么感觉有点未完成</h1>
<p>之前,我心血来潮,写了一个自己的博客系统,但是我把它的源代码搞丢了!</p>
<p>正好想重新设计,所以,我打算重构了。但是,重构它很漫长</p>
<p>这里的占位的东西,会慢慢填充的!</p>
<h1>友链</h1>
<p>
{
friends
.map((friend) => {
return <a href={friend[0]}>{friend[1]}</a>
})
.map((block, i) => {
if (i == friends.length - 1) {
return block
} else {
return [block, <span>・</span>]
}
})
.flat()
}
</p>
</BoringLayout>

View File

@ -1,30 +1,63 @@
---
import FullLayoutV1 from '../layout/FullLayoutV1.astro'
import { listBlogs } from '../lib/apis/legacy/blog'
import { listBlogs, type ListBlogItemType } from '../lib/apis/legacy/blog'
import BlogCard from '../components/BlogCard.astro'
import BlogHeaderImage from '../assets/blogs-header.webp'
import { Image } from 'astro:assets'
export const prerender = false
const _page = parseInt(Astro.url.searchParams.get('page') || '1')
const page = isNaN(_page) ? 1 : Math.max(1, _page)
const blogs = await listBlogs({ page })
let blogs: ListBlogItemType[] = []
try {
blogs = await listBlogs({ page, limit: 100 })
} catch {
return Astro.redirect('/500')
}
---
<FullLayoutV1>
<div class="__dev__caution">
<FullLayoutV1 withGap title="博客列表 - 小帕的小窝">
<!-- <div class="__dev__caution">
<h1>仍在开发中,界面会崩坏</h1>
</div> -->
<div class="container">
<section class="blogs-header">
<Image
class="image"
src={BlogHeaderImage}
alt="博客列表的头图,一个悬挂在空中的 LCD1602上面写着一些示例文本"
width={960}
height={540}
densities={[1.5, 2]}
/>
<div class="description">
<h1>博客列表</h1>
<h2>这里是 passthem 和他朋友们的文章</h2>
</div>
</section>
<main>
<ul>
{
blogs.map((blog) => (
<BlogCard
title={blog.title}
link={`/blogs/${blog.id}`}
featureImage={blog.featured_image?.image_url}
author={{
name: blog.author.nickname,
avatar: blog.author.avatar.image_url,
}}
createdAt={blog.created_at}
updatedAt={blog.updated_at}
/>
))
}
</ul>
</main>
</div>
<section>
<ul>
{
blogs.map((blog) => (
<li>
<a href={`/blogs/${blog.id}`}>{blog.title}</a>
</li>
))
}
</ul>
</section>
</FullLayoutV1>
<style>
@ -39,4 +72,72 @@ const blogs = await listBlogs({ page })
font-weight: 800;
}
}
.container {
max-inline-size: 960px;
margin-inline: auto;
}
.blogs-header {
margin-block-end: 3rem;
position: relative;
aspect-ratio: 16 / 9;
@media (width < 768px) and (width >= 540px) {
aspect-ratio: 2.5 / 1;
}
@media (width >= 768px) {
aspect-ratio: 4 / 1;
}
& > .image {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
filter: saturate(0.6);
object-fit: cover;
user-select: none;
}
& > .description {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
gap: 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #00000099;
color: white;
& > h1 {
font-size: 2.5rem;
font-weight: 600;
}
& > h2 {
font-size: 1rem;
font-weight: 300;
}
& > h1,
& > h2 {
text-shadow: 0 0 10px black;
}
}
}
@media (width >= 768px) {
main {
margin-inline: auto;
max-width: 840px;
}
}
</style>

View File

@ -3,6 +3,10 @@ import { getBlog } from '../../lib/apis/legacy/blog'
import FullLayoutV1 from '../../layout/FullLayoutV1.astro'
import { MarkdocTreeRender } from '../../components/MarkdocRenderer.tsx'
import { toMarkdocTree } from '../../lib/markdoc'
import { Image } from 'astro:assets'
import { Icon } from 'astro-icon/components'
import Artalk from '../../components/Artalk.svelte'
export const prerender = false
@ -13,19 +17,70 @@ if (isNaN(blog_id_num) || blog_id_num < 0) {
return Astro.redirect('/404')
}
const blogData = await getBlog(blog_id_num)
let blogData = null
try {
blogData = await getBlog(blog_id_num)
} catch (e) {
return Astro.redirect('/500')
}
if (blogData === null) {
return Astro.redirect('/404')
}
const tree = await toMarkdocTree(blogData.content)
const formatDate = (date: Date) =>
`${date.getFullYear()} 年 ${date.getMonth() + 1} 月 ${date.getDate()} 日`
---
<FullLayoutV1 title={blogData.title}>
<div class="__dev__caution">
<h1>仍在开发中,界面会崩坏</h1>
</div>
<MarkdocTreeRender tree={tree} client:load />
<FullLayoutV1 title={blogData.title} withGap>
{
blogData.featured_image && (
<div class="featured-image">
<Image
class="image"
src={blogData.featured_image.image_url}
width={960}
height={540}
alt="博客的头图"
/>
</div>
)
}
<main>
<h1>{blogData.title}</h1>
{
blogData.author && (
<div class="blog-info">
<a
class="author"
href={`https://legacy.passthem.top/user/${blogData.author.username}`}
>
<div class="avatar">
<Image
class="image"
src={blogData.author.avatar.image_url}
width={32}
height={32}
alt={`用户 ${blogData.author.nickname} 的头像`}
/>
</div>
<div class="username">{blogData.author.nickname}</div>
</a>
<span class="split">・</span>
<span>{formatDate(blogData.updated_at)} 更新</span>
</div>
)
}
<article>
<MarkdocTreeRender tree={tree} client:load />
</article>
<h1 class="comment-title">
<Icon name="material-symbols:chat-rounded" /> 评论区
</h1>
<Artalk client:idle />
</main>
</FullLayoutV1>
<style>
@ -40,4 +95,86 @@ const tree = await toMarkdocTree(blogData.content)
font-weight: 800;
}
}
.featured-image {
margin-inline: auto;
width: 100%;
max-width: 960px;
aspect-ratio: 16 / 9;
margin-block-end: 2rem;
position: relative;
& > .image {
object-fit: cover;
height: 100%;
width: 100%;
}
&::after {
content: '';
display: block;
position: absolute;
left: 0;
bottom: 0;
height: 50px;
width: 100%;
background: linear-gradient(to bottom, transparent, #00000044);
}
@media (width < 768px) and (width >= 540px) {
aspect-ratio: 2.5 / 1;
}
@media (width >= 768px) {
aspect-ratio: 4 / 1;
margin-block-end: 3rem;
}
}
.blog-info {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
margin-bottom: 1rem;
color: var(--color-fg-1);
& > .author {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.25rem;
& > .avatar {
& > .image {
width: 32px;
height: 32px;
object-fit: cover;
border-radius: 16px;
}
}
}
}
main {
max-inline-size: 840px;
margin-inline: auto;
padding-block-end: 4rem;
padding-inline: 20px;
& > h1 {
font-size: 2rem;
font-weight: 900;
margin-block-end: 1.2rem;
margin-block-start: 2rem;
display: flex;
flex-direction: row;
align-items: center;
gap: 0.25rem;
&.comment-title {
margin-block-start: 3rem;
}
}
}
</style>

View File

@ -8,13 +8,10 @@ import MainpageComments from '../components/MainpageComments.astro'
import MainpageTypewriter from '../components/MainpageTypewriter.svelte'
import { Image } from 'astro:assets'
import { Icon } from 'astro-icon/components'
import PassthemAvatar from '../assets/mainpage_avatars/passthem.png'
import { mainpageGetClick } from '../lib/apis/legacy/mainpage'
// 常量定义
const AVATAR_URL =
'https://cdn.passthem.top/sharex/2026/03/%E6%93%A6%E6%B1%97%E5%90%8E_%E6%96%B9%E5%BD%A2.png.upscale.png'
// 无用按钮的数字预注入
let initialClicks = 0
@ -33,11 +30,12 @@ try {
<div class="self-introduction">
<figure class="avatar">
<Image
src={AVATAR_URL}
src={PassthemAvatar}
alt="passthem 的头像,一个戴眼镜的男孩"
loading="eager"
loading="lazy"
width={240}
height={240}
densities={[1.5, 2]}
/>
</figure>
<div class="descriptions">
@ -97,7 +95,49 @@ try {
<div class="slide-title-design2" aria-hidden="true">
<Icon name="mdi:email" />
</div>
<h1>当前页面还在开发中......</h1>
<div class="contact-container">
<h1>
<div class="text">联系我<div class="animation-before"></div></div><div
class="animation"
>
</div>
</h1>
<table>
<tbody>
<tr>
<th>Github</th>
<th><a href="https://github.com/passthem-desu">passthem-desu</a></th
>
</tr>
<tr>
<th>Wakatime</th>
<th><a href="https://wakatime.com/@passthem">@passthem</a></th>
</tr>
<tr>
<th>Youtube</th>
<th
><a href="https://www.youtube.com/@Passthem183">@Passthem183</a
></th
>
</tr>
<tr>
<th>OtoSite</th>
<th><a href="https://otomad.site/@passthem">@passthem</a></th>
</tr>
<tr>
<th>Email</th>
<th
><a href="mailto:passthem183@gmail.com">passthem183@gmail.com</a
></th
>
</tr>
<tr>
<th>Bilibili</th>
<th><a href="https://space.bilibili.com/92852604">passthem</a></th>
</tr>
</tbody>
</table>
</div>
</section>
<section class="slide">
@ -110,7 +150,71 @@ try {
<div class="slide-title-design2" aria-hidden="true">
<Icon name="mdi:link-variant" />
</div>
<h1>当前页面还在开发中......</h1>
<h1>友情链接</h1>
<div class="friends">
{
[
[
'https://omega98.top/images/blog_avatar.jpg',
'https://omega98.top',
'核子的博客',
],
['https://www.tnot.top/logo.png', 'https://tnot.top', 'TNOT 的博客'],
[
'https://ruusuge.top/img/logo_hu_cc283ac364b1bc0b.png',
'https://ruusuge.top',
'ルース毛的博客',
],
[
'https://cdn.jsdelivr.net/gh/TransparentLC/transparentlc.github.io/img/avatar.jpg',
'https://akarin.dev',
'存在感消失的地方',
],
[
'https://wzq02.top/assets/icons/favicon-96x96.png',
'https://wzq02.top',
'wzq02 的博客',
],
[
'https://pics.r1kka.one/file/1738764637932_-305500c1acccdf39.jpg',
'https://r1kka.one/',
"Rikka's Blog",
],
[
'https://shimizukaede.top/vite.svg',
'https://shimizukaede.top',
'Kaede 的博客',
],
[
'https://nonerd.tech/assets/apple-touch-icon.png',
'https://nonerd.tech/',
'废柴铁克诺',
],
[
'https://legacy.passthem.top/assets/ydt-DIeb2Djx.png',
'https://qm.qq.com/q/nDnHUy9KQo',
'有顶天变电站 (QQ)',
],
[
'https://legacy.passthem.top/assets/lfxdxy-BogfTZvz.png',
'https://qm.qq.com/q/QOpCVZcvyS',
'六方相的新月 (QQ)',
],
].map((b) => (
<a href={b[1]} class="friend">
<div class="avatar">
<Image
src={b[0]}
width={60}
height={60}
alt={`网站「${b[2]}」的图标`}
/>
</div>
<div class="description">{b[2]}</div>
</a>
))
}
</div>
</section>
</FullLayoutV1>
@ -131,6 +235,11 @@ try {
document.querySelectorAll('section.slide').forEach((ele) => {
observer.observe(ele)
;(ele as HTMLElement).addEventListener('focusin', () => {
ele.scrollIntoView({
block: 'start',
})
})
})
}
@ -145,7 +254,8 @@ try {
.slide {
scroll-snap-align: start;
height: 100dvh;
scroll-snap-stop: always;
height: 100lvh;
display: flex;
align-items: center;
justify-content: center;
@ -163,7 +273,7 @@ try {
padding-inline-start: 2rem;
padding-block-end: 2rem;
inset-inline-start: 0;
inset-block-end: 0;
inset-block-end: calc(100lvh - 100dvh);
font-size: smaller;
color: var(--color-fg-0);
opacity: 0.3;
@ -201,6 +311,24 @@ try {
}
}
@property --avatar-scalar {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
@property --avatar-hover-scalar {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
@property --avatar-rotation {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
/* 自我介绍部分 */
.self-introduction {
display: flex;
@ -217,16 +345,32 @@ try {
& > .avatar {
margin: 0;
border-radius: 50%;
--final-width: clamp(160px, 30vw, 240px);
transition: all cubic-bezier(0.4, 1, 0.4, 1) 1.2s;
transform: rotate(10deg);
--avatar-scalar: 0;
--avatar-hover-scalar: 1;
--avatar-rotation: 20deg;
transition:
--avatar-rotation cubic-bezier(0.2, 1, 0.4, 1) 1.8s,
--avatar-scalar cubic-bezier(0.2, 2, 0.3, 0.8) 1.2s,
--avatar-hover-scalar cubic-bezier(0.2, 3, 0.4, 0.6) 0.6s,
width cubic-bezier(0.4, 1, 0.4, 1) 0.9s;
transform: rotate(var(--avatar-rotation)) scale(var(--avatar-scalar))
scale(var(--avatar-hover-scalar));
aspect-ratio: 1 / 1;
overflow: hidden;
width: 0;
section.slide.slide-visible & {
--avatar-scalar: 1;
--avatar-rotation: 0deg;
width: var(--final-width);
transform: rotate(0);
}
&:hover,
&:focus {
--avatar-hover-scalar: 1.05;
}
}
@ -288,7 +432,8 @@ try {
}
}
section:has(.thsx:hover) .motto > p {
section:has(.thsx:hover) .motto > p,
section:has(.thsx:focus) .motto > p {
&:nth-child(1)::before {
transition-delay: 0;
}
@ -401,4 +546,232 @@ try {
}
}
}
/* 联络页 */
@keyframes bg-contact-roll {
0% {
background-position: 0 0;
}
100% {
background-position: var(--bg-width) 0;
}
}
.contact-container {
& > h1 {
--height: 1.5em;
--skew-angle: 15deg;
--col1: var(--color-fg-0);
--bg-width: 2rem;
--unit: calc(tan(var(--skew-angle)) * var(--height) * 0.5);
height: var(--height);
line-height: var(--height);
display: grid;
grid-template-columns: auto 1fr;
color: var(--color-bg-0);
margin-block-end: 0.5em;
position: relative;
& > .text {
background-color: var(--color-fg-0);
position: relative;
}
& .animation-before {
width: calc(var(--unit) * 2);
position: absolute;
overflow: hidden;
height: 100%;
inset-inline-end: calc(var(--unit) * -2);
inset-block-start: 0;
&::after {
content: '';
display: block;
background-color: var(--col1);
width: 100%;
height: 100%;
transform: skewX(var(--skew-angle)) translateX(calc(-1 * var(--unit)));
}
}
& > .animation {
content: '';
display: block;
background-image: linear-gradient(
90deg,
transparent 0,
transparent 25%,
var(--col1) 25%,
var(--col1) 75%,
transparent 75%,
transparent 100%
);
background-repeat: repeat;
background-size: var(--bg-width) 100%;
animation: bg-contact-roll infinite linear 2s;
transform: skewX(var(--skew-angle)) translateX(calc(var(--unit) - 1px));
}
}
& > table {
font-family: var(--font-mono);
& caption {
text-align: center;
}
& tr {
text-align: left;
display: grid;
grid-template-columns: 6em 16em;
gap: 1em;
line-height: 2em;
@media (max-width: 767px) {
grid-template-columns: 18em;
gap: 0;
line-height: 1.8em;
margin-block: 1em;
}
& th:nth-child(1) {
display: flex;
justify-content: space-between;
@media (max-width: 767px) {
justify-content: left;
}
&::after {
content: ':';
}
}
& th:nth-child(2) {
display: flex;
justify-content: space-between;
@media (max-width: 767px) {
justify-content: end;
}
&::before {
content: '[';
}
&::after {
content: ']';
}
&:hover,
&:has(a:focus) {
background-color: var(--color-fg-0);
color: var(--color-bg-0);
}
& > a {
text-align: center;
margin-inline: 1em;
&:focus {
outline: none;
}
}
}
}
}
}
/* 友链页 */
.slide:has(> .friends) {
flex-direction: column;
gap: 1rem;
}
.friends {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 1rem;
max-width: 50rem;
max-height: 30rem;
padding-block: 3rem;
overflow-y: auto;
margin-inline: 1rem;
/* background-color: rgb(from var(--color-bg-0) r g b / .3); */
background-color: rgb(from var(--color-bg-2) r g b / 0.5);
border-radius: 2rem;
backdrop-filter: blur(5px);
& > .friend {
background-color: var(--color-bg-0);
display: grid;
align-items: center;
border-radius: 30px;
overflow: hidden;
width: 14rem;
height: 60px;
grid-template-columns: 60px 1fr;
position: relative;
isolation: isolate;
& > .avatar {
border-radius: 30px;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background-color: var(--color-bg-n);
/* box-shadow: 0 0 0 10px var(--color-bg-2); */
transition: all cubic-bezier(0.2, 1, 0.4, 1) 0.5s;
}
& > .description {
text-align: center;
}
&::before {
content: '';
display: block;
border-radius: 30px;
background-color: var(--color-bg-n);
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
inset-inline-start: calc(-100% + 30px);
box-shadow: 0 0 0 10px var(--color-blue);
transition: inset-inline-start cubic-bezier(0.2, 1, 0.4, 1) 0.5s;
}
outline: solid 0px transparent;
outline-offset: 0px;
transition: all cubic-bezier(0.2, 1, 0.4, 1) 0.5s;
&:focus {
outline: solid 3px var(--color-blue);
outline-offset: 3px;
}
&:hover,
&:focus {
transform: scale(1.1);
}
&:hover::before,
&:focus::before {
inset-inline-start: 0;
}
&:hover .avatar,
&:focus .avatar {
/* transform: rotate(360deg); */
}
}
}
</style>

View File

@ -8,9 +8,45 @@ import {
import { ensureShikiEngine } from '../lib/markdown'
// import { toMarkdocTree } from '../lib/markdoc'
import Markdoc from '@markdoc/markdoc'
import KeyV from 'keyv'
import { createHash } from 'node:crypto'
export const prerender = false
const renderCache = new KeyV<string>({
namespace: 'markdoc-rss',
})
const _render = async (content: string) => {
const key = 'html:' + createHash('sha256').update(content).digest('hex')
const cached = await renderCache.get(key)
if (cached) {
return cached
}
const blogTree = Markdoc.transform(Markdoc.parse(content))
const html = Markdoc.renderers.html(blogTree)
await renderCache.set(key, html, 1000 * 60 * 60 * 24)
return html
}
const _getBlog = async (blogId: number) => {
const key = `raw:blog-${blogId}`
const cached = await renderCache.get(key)
if (cached) {
return cached
}
const content = (await getBlog(blogId))?.content
if (content) {
await renderCache.set(key, content, 1000 * 60 * 60 * 12)
}
return content
}
export const GET = (async (context) => {
let blogs: ListBlogItemType[] = []
let pid = 0
@ -34,18 +70,13 @@ export const GET = (async (context) => {
site,
items: await Promise.all(
blogs.map(async (blog) => {
const blogContent =
(await getBlog(blog.id))?.content || '博客内容暂不可用'
// const blogTree = await toMarkdocTree(blogContent)
const blogTree = Markdoc.transform(Markdoc.parse(blogContent))
const html = Markdoc.renderers.html(blogTree)
const blogContent = (await _getBlog(blog.id)) || '博客内容暂不可用'
const rssItem: RSSFeedItem = {
title: blog.title,
description: `一篇由 ${blog.author.nickname} 写的博客`,
link: `${site}/blogs/${blog.id}`,
pubDate: new Date(blog.created_at),
content: html,
content: await _render(blogContent),
author: blog.author.nickname,
enclosure: blog.featured_image
? {

View File

@ -1,14 +1,9 @@
{
"extends": "astro/tsconfigs/strict",
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
],
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
}