Compare commits
12 Commits
d605131bda
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
8c3aa00e15
|
|||
|
bb347dab3b
|
|||
|
5e5f13a949
|
|||
|
cd689aba98
|
|||
|
2de3841f7d
|
|||
|
ee3585586d
|
|||
|
6c525bcf98
|
|||
|
7a3059f486
|
|||
|
1b8bd19370
|
|||
|
ca1cf10aa8
|
|||
|
880afe34ab
|
|||
|
c3973e779a
|
@ -1,5 +1,5 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config'
|
||||
import { defineConfig, envField } from 'astro/config'
|
||||
|
||||
import node from '@astrojs/node'
|
||||
import svelte from '@astrojs/svelte'
|
||||
@ -12,6 +12,7 @@ const ICON2_URL =
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: node({
|
||||
mode: 'standalone',
|
||||
}),
|
||||
@ -21,4 +22,27 @@ export default defineConfig({
|
||||
redirects: {
|
||||
'/assets/icon-qq-BThBBmjV.jpg': ICON2_URL,
|
||||
},
|
||||
vite: {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['node:crypto', 'buffer', 'keyv'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 环境变量管理
|
||||
env: {
|
||||
schema: {
|
||||
LEGACY_BACKEND_URL_SSR: envField.string({
|
||||
context: 'server',
|
||||
access: 'secret',
|
||||
default: 'https://legacy.passthem.top/api',
|
||||
}),
|
||||
REDIS_URL: envField.string({
|
||||
context: 'server',
|
||||
access: 'secret',
|
||||
default: '',
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
97
package-lock.json
generated
97
package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"@astrojs/vue": "^6.0.1",
|
||||
"@iconify-json/material-symbols": "^1.2.65",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@keyv/redis": "^5.1.6",
|
||||
"@markdoc/markdoc": "^0.5.7",
|
||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
@ -24,6 +25,7 @@
|
||||
"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",
|
||||
@ -43,6 +45,7 @@
|
||||
"lint-staged": "^16.4.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"svelte-eslint-parser": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -1975,6 +1978,29 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@keyv/redis": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-5.1.6.tgz",
|
||||
"integrity": "sha512-eKvW6pspvVaU5dxigaIDZr635/Uw6urTXL3gNbY9WTR8d3QigZQT+r8gxYSEOsw4+1cCBsC4s7T2ptR0WC9LfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@redis/client": "^5.10.0",
|
||||
"cluster-key-slot": "^1.1.2",
|
||||
"hookified": "^1.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"keyv": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@keyv/serialize": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
|
||||
"integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@markdoc/markdoc": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@markdoc/markdoc/-/markdoc-0.5.7.tgz",
|
||||
@ -2081,6 +2107,26 @@
|
||||
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@redis/client": {
|
||||
"version": "5.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.11.0.tgz",
|
||||
"integrity": "sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@node-rs/xxhash": "^1.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@node-rs/xxhash": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
|
||||
@ -3931,6 +3977,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||
@ -5142,6 +5197,16 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache/node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
@ -5624,6 +5689,12 @@
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hookified": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
|
||||
"integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||||
@ -5974,13 +6045,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
|
||||
"integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
"@keyv/serialize": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kolorist": {
|
||||
@ -7752,6 +7822,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier-plugin-svelte": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz",
|
||||
"integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.0.0",
|
||||
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
@ -9305,9 +9386,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
"@astrojs/vue": "^6.0.1",
|
||||
"@iconify-json/material-symbols": "^1.2.65",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@keyv/redis": "^5.1.6",
|
||||
"@markdoc/markdoc": "^0.5.7",
|
||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
@ -42,6 +43,7 @@
|
||||
"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",
|
||||
@ -61,6 +63,7 @@
|
||||
"lint-staged": "^16.4.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"svelte-eslint-parser": "^1.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ const config = {
|
||||
tabWidth: 2,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
plugins: ['prettier-plugin-astro'],
|
||||
plugins: ['prettier-plugin-astro', 'prettier-plugin-svelte'],
|
||||
overrides: [
|
||||
{
|
||||
files: '*.astro',
|
||||
|
||||
47
scripts/generate_contact.py
Normal file
47
scripts/generate_contact.py
Normal file
@ -0,0 +1,47 @@
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
raw_contact: list[dict[str, str]] = [
|
||||
{
|
||||
"platform": "Github",
|
||||
"href": "https://github.com/passthem-desu",
|
||||
"name": "passthem-desu",
|
||||
},
|
||||
{
|
||||
"platform": "Wakatime",
|
||||
"href": "https://wakatime.com/@passthem",
|
||||
"name": "@passthem",
|
||||
},
|
||||
{
|
||||
"platform": "Youtube",
|
||||
"href": "https://www.youtube.com/@Passthem183",
|
||||
"name": "@Passthem183",
|
||||
},
|
||||
{
|
||||
"platform": "OtoSite",
|
||||
"href": "https://otomad.site/@passthem",
|
||||
"name": "@passthem",
|
||||
},
|
||||
{
|
||||
"platform": "Email",
|
||||
"href": "mailto:passthem183@gmail.com",
|
||||
"name": "passthem183@gmail.com",
|
||||
},
|
||||
{
|
||||
"platform": "Bilibili",
|
||||
"href": "https://space.bilibili.com/92852604",
|
||||
"name": "passthem",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def hack(raw: str) -> str:
|
||||
return base64.b64encode(raw[::-1].encode()).decode()
|
||||
|
||||
|
||||
hacked_contact = [{k: hack(v) for k, v in c.items()} for c in raw_contact]
|
||||
(Path(__file__).parent / "../src/lib/data").mkdir(exist_ok=True)
|
||||
_ = (Path(__file__).parent / "../src/lib/data/contact.json").write_text(
|
||||
json.dumps(hacked_contact)
|
||||
)
|
||||
BIN
src/assets/mainpage_avatars/igbt.png
Executable file
BIN
src/assets/mainpage_avatars/igbt.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
@ -1,5 +1,13 @@
|
||||
.prose {
|
||||
line-height: 1.6em;
|
||||
& p,
|
||||
& h1,
|
||||
& h2,
|
||||
& h3,
|
||||
& h4,
|
||||
& h5,
|
||||
& h6 {
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin-block: 0.6em;
|
||||
@ -78,4 +86,9 @@
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
& pre,
|
||||
& code {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
}
|
||||
|
||||
98
src/components/MainpageContactMe.svelte
Normal file
98
src/components/MainpageContactMe.svelte
Normal file
@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import _data from '../lib/data/contact.json'
|
||||
|
||||
/**
|
||||
* 为什么要一个独立的 Contact 组件呢...?这就不得不提到,现在可是有网络爬虫的
|
||||
* 时代!我们得给我们的个人信息做反爬呀!
|
||||
*
|
||||
* 另见:https://spencermortensen.com/articles/email-obfuscation/
|
||||
*/
|
||||
|
||||
let data = $state<{ platform: string; name: string; href: string }[]>([])
|
||||
|
||||
function unhack(raw: string) {
|
||||
return atob(raw).split('').reverse().join('')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
data = _data
|
||||
})
|
||||
</script>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
{#each data as item}
|
||||
<tr>
|
||||
<td>{unhack(item.platform)}</td>
|
||||
<td><a href={unhack(item.href)}>{unhack(item.name)}</a></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
font-family: var(--font-mono);
|
||||
|
||||
& 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;
|
||||
}
|
||||
|
||||
& td:nth-child(1) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: ':';
|
||||
}
|
||||
}
|
||||
|
||||
& td: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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc'
|
||||
import { toMarkdocTree } from '../lib/markdoc'
|
||||
import { renderMarkdocTree } from '../lib/markdoc'
|
||||
import { CodeBlock } from './markdoc/CodeBlock.tsx'
|
||||
|
||||
export const MarkdocContent: React.FC<{
|
||||
@ -11,7 +11,7 @@ export const MarkdocContent: React.FC<{
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
toMarkdocTree(content).then((result) => {
|
||||
renderMarkdocTree(content).then((result) => {
|
||||
if (active) setTree(result)
|
||||
})
|
||||
return () => {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export const legacyClient = axios.create({
|
||||
// baseURL: import.meta.env.SSR
|
||||
// ? 'https://legacy.passthem.top/api'
|
||||
// : '/api/legacy',
|
||||
baseURL: 'https://legacy.passthem.top/api',
|
||||
baseURL: import.meta.env.SSR
|
||||
? ((await import('astro:env/server')) as any)['LEGACY_BACKEND_URL_SSR']
|
||||
: 'https://legacy.passthem.top/api',
|
||||
// baseURL: 'https://legacy.passthem.top/api',
|
||||
timeout: 6000,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
|
||||
32
src/lib/data/contact.json
Normal file
32
src/lib/data/contact.json
Normal file
@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"platform": "YnVodGlH",
|
||||
"href": "dXNlZC1tZWh0c3NhcC9tb2MuYnVodGlnLy86c3B0dGg=",
|
||||
"name": "dXNlZC1tZWh0c3NhcA=="
|
||||
},
|
||||
{
|
||||
"platform": "ZW1pdGFrYVc=",
|
||||
"href": "bWVodHNzYXBAL21vYy5lbWl0YWthdy8vOnNwdHRo",
|
||||
"name": "bWVodHNzYXBA"
|
||||
},
|
||||
{
|
||||
"platform": "ZWJ1dHVvWQ==",
|
||||
"href": "MzgxbWVodHNzYVBAL21vYy5lYnV0dW95Lnd3dy8vOnNwdHRo",
|
||||
"name": "MzgxbWVodHNzYVBA"
|
||||
},
|
||||
{
|
||||
"platform": "ZXRpU290Tw==",
|
||||
"href": "bWVodHNzYXBAL2V0aXMuZGFtb3RvLy86c3B0dGg=",
|
||||
"name": "bWVodHNzYXBA"
|
||||
},
|
||||
{
|
||||
"platform": "bGlhbUU=",
|
||||
"href": "bW9jLmxpYW1nQDM4MW1laHRzc2FwOm90bGlhbQ==",
|
||||
"name": "bW9jLmxpYW1nQDM4MW1laHRzc2Fw"
|
||||
},
|
||||
{
|
||||
"platform": "aWxpYmlsaUI=",
|
||||
"href": "NDA2MjU4MjkvbW9jLmlsaWJpbGliLmVjYXBzLy86c3B0dGg=",
|
||||
"name": "bWVodHNzYXA="
|
||||
}
|
||||
]
|
||||
4
src/lib/keyv-cache.server.ts
Normal file
4
src/lib/keyv-cache.server.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { REDIS_URL } from 'astro:env/server'
|
||||
import KeyvRedis from '@keyv/redis'
|
||||
|
||||
export const storage = REDIS_URL ? new KeyvRedis(REDIS_URL) : new Map()
|
||||
20
src/lib/markdoc-cache.server.ts
Normal file
20
src/lib/markdoc-cache.server.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import './server-only.ts'
|
||||
|
||||
import type { RenderableTreeNode } from '@markdoc/markdoc'
|
||||
import KeyV from 'keyv'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { storage } from './keyv-cache.server.ts'
|
||||
|
||||
const markdocVersion = '1'
|
||||
|
||||
export const markdocCache = new KeyV<RenderableTreeNode>({
|
||||
store: storage,
|
||||
namespace: `markdoc-cache-${markdocVersion}`,
|
||||
ttl: 1000 * 60 * 60 * 24 * 7,
|
||||
})
|
||||
export const generateCacheKey = (content: string) => {
|
||||
return createHash('sha256')
|
||||
.update(content)
|
||||
.update(markdocVersion)
|
||||
.digest('hex')
|
||||
}
|
||||
21
src/lib/markdoc.server.ts
Normal file
21
src/lib/markdoc.server.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import './server-only.ts'
|
||||
|
||||
import { renderMarkdocTree } from './markdoc.ts'
|
||||
import { generateCacheKey, markdocCache } from './markdoc-cache.server.ts'
|
||||
|
||||
export const getCachedMarkdocTree = async (content: string) => {
|
||||
if (!import.meta.env.SSR) {
|
||||
return await renderMarkdocTree(content)
|
||||
}
|
||||
|
||||
const key = generateCacheKey(content)
|
||||
const cached = await markdocCache.get(key)
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const tree = await renderMarkdocTree(content)
|
||||
markdocCache.set(key, tree)
|
||||
return tree
|
||||
}
|
||||
@ -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,10 @@ export const markdocConfig: Config = {
|
||||
},
|
||||
}
|
||||
|
||||
export const toMarkdocTree = async (content: string) => {
|
||||
export const renderMarkdocTree = async (content: string) => {
|
||||
const ast = Markdoc.parse(content)
|
||||
await ensureShikiEngine()
|
||||
return Markdoc.transform(ast, markdocConfig)
|
||||
const tree = Markdoc.transform(ast, markdocConfig)
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
3
src/lib/server-only.ts
Normal file
3
src/lib/server-only.ts
Normal file
@ -0,0 +1,3 @@
|
||||
if (!import.meta.env.SSR) {
|
||||
throw Error('这段代码不可以在客户端执行!')
|
||||
}
|
||||
10
src/pages/404.astro
Normal file
10
src/pages/404.astro
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
import BoringLayout from '../layout/BoringLayout.astro'
|
||||
---
|
||||
|
||||
<BoringLayout title="404 - 小帕的小窝">
|
||||
<h1>网页走丢了!!</h1>
|
||||
<p>不知道为什么就到了这里。。</p>
|
||||
<p><a href="javascript:history.back()">点击这里</a>回到上一页</p>
|
||||
<p>或者<a href="/">点击这里</a>回到主页</p>
|
||||
</BoringLayout>
|
||||
10
src/pages/500.astro
Normal file
10
src/pages/500.astro
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
import BoringLayout from '../layout/BoringLayout.astro'
|
||||
---
|
||||
|
||||
<BoringLayout title="500 - 小帕的小窝">
|
||||
<h1>服务器内部错误</h1>
|
||||
<p>哦不肯定是小帕搞错了什么东西。。</p>
|
||||
<p><a href="javascript:history.back()">点击这里</a>回到上一页</p>
|
||||
<p>或者<a href="/">点击这里</a>回到主页</p>
|
||||
</BoringLayout>
|
||||
5
src/pages/[...slug].astro
Normal file
5
src/pages/[...slug].astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
export const prerender = false
|
||||
|
||||
return Astro.redirect('/404')
|
||||
---
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
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'
|
||||
@ -10,7 +10,14 @@ 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, limit: 100 })
|
||||
let blogs: ListBlogItemType[] = []
|
||||
|
||||
try {
|
||||
blogs = await listBlogs({ page, limit: 100 })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return Astro.redirect('/500')
|
||||
}
|
||||
---
|
||||
|
||||
<FullLayoutV1 withGap title="博客列表 - 小帕的小窝">
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
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 { getCachedMarkdocTree } from '../../lib/markdoc.server.ts'
|
||||
import { Image } from 'astro:assets'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
|
||||
@ -17,12 +17,20 @@ 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) {
|
||||
console.error(e)
|
||||
return Astro.redirect('/500')
|
||||
}
|
||||
|
||||
if (blogData === null) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
const tree = await toMarkdocTree(blogData.content)
|
||||
const tree = await getCachedMarkdocTree(blogData.content)
|
||||
const formatDate = (date: Date) =>
|
||||
`${date.getFullYear()} 年 ${date.getMonth() + 1} 月 ${date.getDate()} 日`
|
||||
---
|
||||
@ -158,7 +166,8 @@ const formatDate = (date: Date) =>
|
||||
& > h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 900;
|
||||
margin-block: 1.2rem;
|
||||
margin-block-end: 1.2rem;
|
||||
margin-block-start: 2rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
---
|
||||
import BoringLayout from '../layout/BoringLayout.astro'
|
||||
---
|
||||
|
||||
<BoringLayout title="联络我">
|
||||
<h1>联络我</h1>
|
||||
<p>邮箱:<a href="mailto:passthem183@gmail.com">passthem183@gmail.com</a></p>
|
||||
<p>点击 <a href="/">这里</a> 返回主页</p>
|
||||
</BoringLayout>
|
||||
@ -1,14 +1,18 @@
|
||||
---
|
||||
export const prerender = false
|
||||
|
||||
// import BoringLayout from '../layout/BoringLayout.astro'
|
||||
import FullLayoutV1 from '../layout/FullLayoutV1.astro'
|
||||
|
||||
import MainpageButton from '../components/MainpageButton.svelte'
|
||||
import MainpageComments from '../components/MainpageComments.astro'
|
||||
import MainpageTypewriter from '../components/MainpageTypewriter.svelte'
|
||||
import MainpageContactMe from '../components/MainpageContactMe.svelte'
|
||||
|
||||
import { Image } from 'astro:assets'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
|
||||
import PassthemAvatar from '../assets/mainpage_avatars/passthem.png'
|
||||
import IgbtAvatar from '../assets/mainpage_avatars/igbt.png'
|
||||
|
||||
import { mainpageGetClick } from '../lib/apis/legacy/mainpage'
|
||||
|
||||
@ -102,41 +106,7 @@ try {
|
||||
>
|
||||
</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>
|
||||
<MainpageContactMe client:idle />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -154,63 +124,75 @@ try {
|
||||
<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)',
|
||||
],
|
||||
{
|
||||
avatar: 'https://omega98.top/images/blog_avatar.jpg',
|
||||
href: 'https://omega98.top',
|
||||
name: '核子的博客',
|
||||
},
|
||||
{
|
||||
avatar: 'https://www.tnot.top/logo.png',
|
||||
href: 'https://tnot.top',
|
||||
name: 'TNOT 的博客',
|
||||
},
|
||||
{
|
||||
avatar: 'https://ruusuge.top/img/logo_hu_cc283ac364b1bc0b.png',
|
||||
href: 'https://ruusuge.top',
|
||||
name: 'ルース毛的博客',
|
||||
},
|
||||
{
|
||||
avatar: IgbtAvatar,
|
||||
href: 'https://zetsuengate.org',
|
||||
name: 'IGBT :: Home',
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
'https://cdn.jsdelivr.net/gh/TransparentLC/transparentlc.github.io/img/avatar.jpg',
|
||||
href: 'https://akarin.dev',
|
||||
name: '存在感消失的地方',
|
||||
},
|
||||
{
|
||||
avatar: 'https://wzq02.top/assets/icons/favicon-96x96.png',
|
||||
href: 'https://wzq02.top',
|
||||
name: 'wzq02 的博客',
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
'https://pics.r1kka.one/file/1738764637932_-305500c1acccdf39.jpg',
|
||||
href: 'https://r1kka.one/',
|
||||
name: "Rikka's Blog",
|
||||
},
|
||||
{
|
||||
avatar: 'https://shimizukaede.top/vite.svg',
|
||||
href: 'https://shimizukaede.top',
|
||||
name: 'Kaede 的博客',
|
||||
},
|
||||
{
|
||||
avatar: 'https://nonerd.tech/assets/apple-touch-icon.png',
|
||||
href: 'https://nonerd.tech/',
|
||||
name: '废柴铁克诺',
|
||||
},
|
||||
{
|
||||
avatar: 'https://legacy.passthem.top/assets/ydt-DIeb2Djx.png',
|
||||
href: 'https://qm.qq.com/q/nDnHUy9KQo',
|
||||
name: '有顶天变电站 (QQ)',
|
||||
},
|
||||
{
|
||||
avatar: 'https://legacy.passthem.top/assets/lfxdxy-BogfTZvz.png',
|
||||
href: 'https://qm.qq.com/q/QOpCVZcvyS',
|
||||
name: '六方相的新月 (QQ)',
|
||||
},
|
||||
].map((b) => (
|
||||
<a href={b[1]} class="friend">
|
||||
<a href={b.href} class="friend">
|
||||
<div class="avatar">
|
||||
<Image
|
||||
src={b[0]}
|
||||
src={b.avatar as string}
|
||||
width={60}
|
||||
height={60}
|
||||
alt={`网站「${b[2]}」的图标`}
|
||||
alt={`网站「${b.name}」的图标`}
|
||||
densities={[1.5, 2]}
|
||||
/>
|
||||
</div>
|
||||
<div class="description">{b[2]}</div>
|
||||
<div class="description">{b.name}</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
@ -614,74 +596,6 @@ try {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 友链页 */
|
||||
|
||||
@ -8,9 +8,47 @@ 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'
|
||||
import { storage } from '../lib/keyv-cache.server'
|
||||
|
||||
export const prerender = false
|
||||
|
||||
const renderCache = new KeyV<string>({
|
||||
store: storage,
|
||||
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 +72,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
|
||||
? {
|
||||
|
||||
Reference in New Issue
Block a user