Compare commits

..

19 Commits

Author SHA1 Message Date
joyzhao
21539772ef feat(blog): 添加博客工具函数以处理文章数据和分类标签 2026-01-12 14:43:05 +08:00
joyzhao
b8a8cc474b feat(comments): 添加 Waline 自定义样式文件以适配网站主题
fix(blog): 删除不再使用的博客工具函数文件
2026-01-12 14:36:11 +08:00
joyzhao
78c468a7d2 feat(comments): 添加页面浏览和评论功能支持 2026-01-12 11:04:30 +08:00
joyzhao
ca9e083399 feat(comments): 添加 Waline 评论组件,替换 Giscus 评论组件 2026-01-12 09:46:29 +08:00
joyzhao
1952f4de64 fix(i18n): 更新翻译文本,将“查看项目”修改为“关于我” 2026-01-09 17:39:35 +08:00
joyzhao
bdf9fad89f fix(blog): 更新注释内容,修正密码登录相关说明 2026-01-09 16:43:55 +08:00
joyzhao
c97d31afe4 feat(blog): 添加 1Panel 自动化部署笔记,提供轻量级部署方案和 SSH 免密登录配置 2026-01-09 16:41:14 +08:00
joyzhao
7675b6c299 feat(blog): 添加 1Panel 自动化部署笔记,简化部署流程 2026-01-09 16:12:59 +08:00
joyzhao
9f05776781 feat(blog): 更新博客文章元数据,添加阅读时间和分类信息
Some checks failed
Deploy docs for Site / deploy (20.x) (push) Has been cancelled
2026-01-09 12:10:37 +08:00
joyzhao
63c97c9b00 feat(blog): 更新博客文章,分享全栈开发者的技术选型反思与经验 2026-01-09 12:00:02 +08:00
joyzhao
3d0a37a751 feat(blog): 优化博客文章列表链接样式,提升用户体验 2026-01-09 10:01:11 +08:00
joyzhao
0e6f611455 feat: 添加“返回顶部”组件,提升用户导航体验
Some checks failed
Deploy docs for Site / deploy (20.x) (push) Has been cancelled
2026-01-09 09:55:52 +08:00
joyzhao
99c41d537f feat(blog): 添加“返回博客列表”链接,支持多语言切换 2026-01-09 09:45:58 +08:00
joyzhao
551877780f fix: 修正博客导航中上一篇和下一篇文章的索引逻辑 2026-01-09 09:39:20 +08:00
joyzhao
51eac1c2ef feat(blog): add multiple new articles on browser rendering, JavaScript traps, Git commit conventions, Tailwind CSS integration, and Gitea actions automation
Some checks failed
Deploy docs for Site / deploy (20.x) (push) Has been cancelled
2026-01-09 09:31:20 +08:00
joyzhao
1c9f154652 feat: 添加 GiscusComments 组件,支持文章评论功能 2026-01-09 09:07:41 +08:00
joyzhao
4bf7dd83f9 feat: 添加 @giscus/react 依赖,支持评论功能 2026-01-09 08:30:03 +08:00
joyzhao
23b5fac1da refactor: 更新语言类型导入路径,统一使用 '@/types/i18n' 2026-01-09 08:19:48 +08:00
joyzhao
e7e15ab69f feat: 更新全栈开发者选型忏悔录文章,添加元数据和描述信息
Some checks failed
Deploy docs for Site / deploy (20.x) (push) Has been cancelled
2026-01-08 22:18:52 +08:00
32 changed files with 1208 additions and 220 deletions

View File

@@ -1,59 +0,0 @@
name: Deploy docs for Site
run-name: ${{ gitea.actor }} is building out Gitea Actions 🚀
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- name: Checkout repository code
uses: https://gitea.com/actions/checkout@v4
with:
ref: 'main'
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
# Skip pnpm cache to reduce overhead
- name: Install pnpm
run: |
if ! command -v pnpm &> /dev/null; then
npm install -g pnpm@9
else
echo "pnpm is already installed"
fi
- name: Check Node.js and pnpm versions
run: |
echo "pnpm version ===>" && pnpm -v
echo "node version ===>" && node -v
# Simplify node_modules handling without caching
- name: Install dependencies
run: |
if [ ! -d "node_modules" ] || [ "$(find package.json -newer node_modules)" ]; then
echo "Dependencies are outdated or missing, installing..."
pnpm install
else
echo "Dependencies are up-to-date, skipping installation."
fi
- name: Build site
run: pnpm build
- name: ssh deploy
uses: easingthemes/ssh-deploy@v5.1.0
with:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.DEPLOY_HOST }}
REMOTE_USER: ${{ secrets.DEPLOY_USER }}
SOURCE: "/dist/"
TARGET: "/opt/1panel/apps/openresty/openresty/www/sites/zhaoguiyang.com/index/dist"
EXCLUDE: "/node_modules/"

3
.gitignore vendored
View File

@@ -24,3 +24,6 @@ pnpm-debug.log*
.idea/ .idea/
.trae/ .trae/
# deploy script
deploy.sh

View File

@@ -19,6 +19,7 @@
"@astrojs/mdx": "^4.3.0", "@astrojs/mdx": "^4.3.0",
"@astrojs/react": "^4.2.1", "@astrojs/react": "^4.2.1",
"@astrojs/sitemap": "^3.4.1", "@astrojs/sitemap": "^3.4.1",
"@giscus/react": "^3.1.0",
"@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@tailwindcss/vite": "^4.0.14", "@tailwindcss/vite": "^4.0.14",
@@ -38,6 +39,7 @@
"tw-animate-css": "^1.2.4" "tw-animate-css": "^1.2.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.16" "@tailwindcss/typography": "^0.5.16",
"@waline/client": "^3.8.0"
} }
} }

335
pnpm-lock.yaml generated
View File

@@ -17,6 +17,9 @@ importers:
'@astrojs/sitemap': '@astrojs/sitemap':
specifier: ^3.4.1 specifier: ^3.4.1
version: 3.4.1 version: 3.4.1
'@giscus/react':
specifier: ^3.1.0
version: 3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-scroll-area': '@radix-ui/react-scroll-area':
specifier: ^1.2.9 specifier: ^1.2.9
version: 1.2.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.2.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -72,6 +75,9 @@ importers:
'@tailwindcss/typography': '@tailwindcss/typography':
specifier: ^0.5.16 specifier: ^0.5.16
version: 0.5.16(tailwindcss@4.1.10) version: 0.5.16(tailwindcss@4.1.10)
'@waline/client':
specifier: ^3.8.0
version: 3.8.0(typescript@5.8.3)
packages: packages:
@@ -156,6 +162,10 @@ packages:
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.27.1': '@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -169,6 +179,11 @@ packages:
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
hasBin: true hasBin: true
'@babel/parser@7.28.5':
resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-transform-react-jsx-self@7.27.1': '@babel/plugin-transform-react-jsx-self@7.27.1':
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -197,6 +212,10 @@ packages:
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/types@7.28.5':
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
'@capsizecss/unpack@2.4.0': '@capsizecss/unpack@2.4.0':
resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==} resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==}
@@ -365,6 +384,12 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@giscus/react@3.1.0':
resolution: {integrity: sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg==}
peerDependencies:
react: ^16 || ^17 || ^18 || ^19
react-dom: ^16 || ^17 || ^18 || ^19
'@img/sharp-darwin-arm64@0.33.5': '@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -391,67 +416,79 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5': '@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4': '@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4': '@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4': '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4': '@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.33.5': '@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.33.5': '@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.33.5': '@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.33.5': '@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5': '@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5': '@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.33.5': '@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@@ -489,9 +526,18 @@ packages:
'@jridgewell/sourcemap-codec@1.5.0': '@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@lit-labs/ssr-dom-shim@1.5.1':
resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==}
'@lit/reactive-element@2.1.2':
resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==}
'@mdx-js/mdx@3.1.0': '@mdx-js/mdx@3.1.0':
resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==}
@@ -651,56 +697,67 @@ packages:
resolution: {integrity: sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==} resolution: {integrity: sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.43.0': '@rollup/rollup-linux-arm-musleabihf@4.43.0':
resolution: {integrity: sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==} resolution: {integrity: sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.43.0': '@rollup/rollup-linux-arm64-gnu@4.43.0':
resolution: {integrity: sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==} resolution: {integrity: sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.43.0': '@rollup/rollup-linux-arm64-musl@4.43.0':
resolution: {integrity: sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==} resolution: {integrity: sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.43.0': '@rollup/rollup-linux-loongarch64-gnu@4.43.0':
resolution: {integrity: sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==} resolution: {integrity: sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.43.0': '@rollup/rollup-linux-powerpc64le-gnu@4.43.0':
resolution: {integrity: sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==} resolution: {integrity: sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.43.0': '@rollup/rollup-linux-riscv64-gnu@4.43.0':
resolution: {integrity: sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==} resolution: {integrity: sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.43.0': '@rollup/rollup-linux-riscv64-musl@4.43.0':
resolution: {integrity: sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==} resolution: {integrity: sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.43.0': '@rollup/rollup-linux-s390x-gnu@4.43.0':
resolution: {integrity: sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==} resolution: {integrity: sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.43.0': '@rollup/rollup-linux-x64-gnu@4.43.0':
resolution: {integrity: sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==} resolution: {integrity: sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.43.0': '@rollup/rollup-linux-x64-musl@4.43.0':
resolution: {integrity: sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==} resolution: {integrity: sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.43.0': '@rollup/rollup-win32-arm64-msvc@4.43.0':
resolution: {integrity: sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==} resolution: {integrity: sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==}
@@ -779,24 +836,28 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.10': '@tailwindcss/oxide-linux-arm64-musl@4.1.10':
resolution: {integrity: sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==} resolution: {integrity: sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.10': '@tailwindcss/oxide-linux-x64-gnu@4.1.10':
resolution: {integrity: sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==} resolution: {integrity: sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.10': '@tailwindcss/oxide-linux-x64-musl@4.1.10':
resolution: {integrity: sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==} resolution: {integrity: sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.10': '@tailwindcss/oxide-wasm32-wasi@4.1.10':
resolution: {integrity: sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==} resolution: {integrity: sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==}
@@ -895,12 +956,18 @@ packages:
'@types/sax@1.2.7': '@types/sax@1.2.7':
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/unist@2.0.11': '@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
'@types/unist@3.0.3': '@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -910,6 +977,56 @@ packages:
peerDependencies: peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
'@vue/compiler-core@3.5.26':
resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==}
'@vue/compiler-dom@3.5.26':
resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==}
'@vue/compiler-sfc@3.5.26':
resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==}
'@vue/compiler-ssr@3.5.26':
resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==}
'@vue/reactivity@3.5.26':
resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==}
'@vue/runtime-core@3.5.26':
resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==}
'@vue/runtime-dom@3.5.26':
resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==}
'@vue/server-renderer@3.5.26':
resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==}
peerDependencies:
vue: 3.5.26
'@vue/shared@3.5.26':
resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==}
'@vueuse/core@14.1.0':
resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
peerDependencies:
vue: ^3.5.0
'@vueuse/metadata@14.1.0':
resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
'@vueuse/shared@14.1.0':
resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
peerDependencies:
vue: ^3.5.0
'@waline/api@1.0.0':
resolution: {integrity: sha512-o0lWjt+/oZH1/4q3DJxTf5kdkgNbSmoLRXIiGznW225A6hq9/9IkOO1DiAijIsxGYJS6psFC+58+IzkD1EerBA==}
engines: {node: '>=18'}
'@waline/client@3.8.0':
resolution: {integrity: sha512-UAbqLmqy2Pns/CXFlrLYEDG4F1peRPo+wp2FmpfFL9BO0ceQO6KvvAJXw3EhX3wWcMiKlX29G0gJVtG4UlCzGw==}
engines: {node: '>=18'}
acorn-jsx@5.3.2: acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies: peerDependencies:
@@ -970,6 +1087,9 @@ packages:
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
hasBin: true hasBin: true
autosize@6.0.1:
resolution: {integrity: sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==}
axobject-query@4.1.0: axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1108,6 +1228,9 @@ packages:
csstype@3.1.3: csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
debug@4.4.1: debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@@ -1179,6 +1302,10 @@ packages:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
entities@7.0.0:
resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==}
engines: {node: '>=0.12'}
es-module-lexer@1.7.0: es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
@@ -1404,6 +1531,9 @@ packages:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
giscus@1.6.0:
resolution: {integrity: sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ==}
github-slugger@2.0.0: github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
@@ -1582,24 +1712,28 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.1: lightningcss-linux-arm64-musl@1.30.1:
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.1: lightningcss-linux-x64-gnu@1.30.1:
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.1: lightningcss-linux-x64-musl@1.30.1:
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.1: lightningcss-win32-arm64-msvc@1.30.1:
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
@@ -1617,6 +1751,15 @@ packages:
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
lit-element@4.2.2:
resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==}
lit-html@3.3.2:
resolution: {integrity: sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==}
lit@3.3.2:
resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==}
locale-emoji@0.3.0: locale-emoji@0.3.0:
resolution: {integrity: sha512-JGm8+naU49CBDnH1jksS3LecPdfWQLxFgkLN6ZhYONKa850pJ0Xt8DPGJnYK0ZuJI8jTuiDDPCDtSL3nyacXwg==} resolution: {integrity: sha512-JGm8+naU49CBDnH1jksS3LecPdfWQLxFgkLN6ZhYONKa850pJ0Xt8DPGJnYK0ZuJI8jTuiDDPCDtSL3nyacXwg==}
@@ -1652,6 +1795,9 @@ packages:
magic-string@0.30.17: magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
magicast@0.3.5: magicast@0.3.5:
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
@@ -1662,6 +1808,16 @@ packages:
markdown-table@3.0.4: markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
marked-highlight@2.2.3:
resolution: {integrity: sha512-FCfZRxW/msZAiasCML4isYpxyQWKEEx44vOgdn5Kloae+Qc3q4XR7WjpKKf8oMLk7JP9ZCRd2vhtclJFdwxlWQ==}
peerDependencies:
marked: '>=4 <18'
marked@16.4.2:
resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==}
engines: {node: '>= 20'}
hasBin: true
mdast-util-definitions@6.0.0: mdast-util-definitions@6.0.0:
resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
@@ -1942,6 +2098,10 @@ packages:
resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==} resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
prismjs@1.30.0: prismjs@1.30.0:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1976,6 +2136,9 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'} engines: {node: '>= 14.18.0'}
recaptcha-v3@1.11.3:
resolution: {integrity: sha512-sEE6J0RzUkS+sKEBpgCD/AqCU0ffrAVOADGjvAx9vcttN+VLK42SWMkj/J/I6vHu3Kew+xcfbBqDVb65N0QGDw==}
recma-build-jsx@1.0.0: recma-build-jsx@1.0.0:
resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
@@ -2369,6 +2532,14 @@ packages:
vite: vite:
optional: true optional: true
vue@3.5.26:
resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
web-namespaces@2.0.1: web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
@@ -2600,6 +2771,8 @@ snapshots:
'@babel/helper-validator-identifier@7.27.1': {} '@babel/helper-validator-identifier@7.27.1': {}
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-option@7.27.1': {} '@babel/helper-validator-option@7.27.1': {}
'@babel/helpers@7.27.6': '@babel/helpers@7.27.6':
@@ -2611,6 +2784,10 @@ snapshots:
dependencies: dependencies:
'@babel/types': 7.27.6 '@babel/types': 7.27.6
'@babel/parser@7.28.5':
dependencies:
'@babel/types': 7.28.5
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)': '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)':
dependencies: dependencies:
'@babel/core': 7.27.4 '@babel/core': 7.27.4
@@ -2646,6 +2823,11 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1 '@babel/helper-validator-identifier': 7.27.1
'@babel/types@7.28.5':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@capsizecss/unpack@2.4.0': '@capsizecss/unpack@2.4.0':
dependencies: dependencies:
blob-to-buffer: 1.2.9 blob-to-buffer: 1.2.9
@@ -2740,6 +2922,12 @@ snapshots:
'@esbuild/win32-x64@0.25.5': '@esbuild/win32-x64@0.25.5':
optional: true optional: true
'@giscus/react@3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
giscus: 1.6.0
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@img/sharp-darwin-arm64@0.33.5': '@img/sharp-darwin-arm64@0.33.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4 '@img/sharp-libvips-darwin-arm64': 1.0.4
@@ -2831,11 +3019,19 @@ snapshots:
'@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/trace-mapping@0.3.25':
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
'@lit-labs/ssr-dom-shim@1.5.1': {}
'@lit/reactive-element@2.1.2':
dependencies:
'@lit-labs/ssr-dom-shim': 1.5.1
'@mdx-js/mdx@3.1.0(acorn@8.15.0)': '@mdx-js/mdx@3.1.0(acorn@8.15.0)':
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@@ -3212,10 +3408,14 @@ snapshots:
dependencies: dependencies:
'@types/node': 24.0.1 '@types/node': 24.0.1
'@types/trusted-types@2.0.7': {}
'@types/unist@2.0.11': {} '@types/unist@2.0.11': {}
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}
'@types/web-bluetooth@0.0.21': {}
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1))': '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1))':
@@ -3230,6 +3430,87 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vue/compiler-core@3.5.26':
dependencies:
'@babel/parser': 7.28.5
'@vue/shared': 3.5.26
entities: 7.0.0
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.26':
dependencies:
'@vue/compiler-core': 3.5.26
'@vue/shared': 3.5.26
'@vue/compiler-sfc@3.5.26':
dependencies:
'@babel/parser': 7.28.5
'@vue/compiler-core': 3.5.26
'@vue/compiler-dom': 3.5.26
'@vue/compiler-ssr': 3.5.26
'@vue/shared': 3.5.26
estree-walker: 2.0.2
magic-string: 0.30.21
postcss: 8.5.6
source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.26':
dependencies:
'@vue/compiler-dom': 3.5.26
'@vue/shared': 3.5.26
'@vue/reactivity@3.5.26':
dependencies:
'@vue/shared': 3.5.26
'@vue/runtime-core@3.5.26':
dependencies:
'@vue/reactivity': 3.5.26
'@vue/shared': 3.5.26
'@vue/runtime-dom@3.5.26':
dependencies:
'@vue/reactivity': 3.5.26
'@vue/runtime-core': 3.5.26
'@vue/shared': 3.5.26
csstype: 3.2.3
'@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.8.3))':
dependencies:
'@vue/compiler-ssr': 3.5.26
'@vue/shared': 3.5.26
vue: 3.5.26(typescript@5.8.3)
'@vue/shared@3.5.26': {}
'@vueuse/core@14.1.0(vue@3.5.26(typescript@5.8.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.1.0
'@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.8.3))
vue: 3.5.26(typescript@5.8.3)
'@vueuse/metadata@14.1.0': {}
'@vueuse/shared@14.1.0(vue@3.5.26(typescript@5.8.3))':
dependencies:
vue: 3.5.26(typescript@5.8.3)
'@waline/api@1.0.0': {}
'@waline/client@3.8.0(typescript@5.8.3)':
dependencies:
'@vueuse/core': 14.1.0(vue@3.5.26(typescript@5.8.3))
'@waline/api': 1.0.0
autosize: 6.0.1
marked: 16.4.2
marked-highlight: 2.2.3(marked@16.4.2)
recaptcha-v3: 1.11.3
vue: 3.5.26(typescript@5.8.3)
transitivePeerDependencies:
- typescript
acorn-jsx@5.3.2(acorn@8.15.0): acorn-jsx@5.3.2(acorn@8.15.0):
dependencies: dependencies:
acorn: 8.15.0 acorn: 8.15.0
@@ -3382,6 +3663,8 @@ snapshots:
- uploadthing - uploadthing
- yaml - yaml
autosize@6.0.1: {}
axobject-query@4.1.0: {} axobject-query@4.1.0: {}
bail@2.0.2: {} bail@2.0.2: {}
@@ -3507,6 +3790,8 @@ snapshots:
csstype@3.1.3: {} csstype@3.1.3: {}
csstype@3.2.3: {}
debug@4.4.1: debug@4.4.1:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@@ -3556,6 +3841,8 @@ snapshots:
entities@6.0.1: {} entities@6.0.1: {}
entities@7.0.0: {}
es-module-lexer@1.7.0: {} es-module-lexer@1.7.0: {}
esast-util-from-estree@2.0.0: esast-util-from-estree@2.0.0:
@@ -3769,6 +4056,10 @@ snapshots:
get-east-asian-width@1.3.0: {} get-east-asian-width@1.3.0: {}
giscus@1.6.0:
dependencies:
lit: 3.3.2
github-slugger@2.0.0: {} github-slugger@2.0.0: {}
globals@11.12.0: {} globals@11.12.0: {}
@@ -4034,6 +4325,22 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-arm64-msvc: 1.30.1
lightningcss-win32-x64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1
lit-element@4.2.2:
dependencies:
'@lit-labs/ssr-dom-shim': 1.5.1
'@lit/reactive-element': 2.1.2
lit-html: 3.3.2
lit-html@3.3.2:
dependencies:
'@types/trusted-types': 2.0.7
lit@3.3.2:
dependencies:
'@lit/reactive-element': 2.1.2
lit-element: 4.2.2
lit-html: 3.3.2
locale-emoji@0.3.0: {} locale-emoji@0.3.0: {}
lodash.castarray@4.4.0: {} lodash.castarray@4.4.0: {}
@@ -4062,6 +4369,10 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magicast@0.3.5: magicast@0.3.5:
dependencies: dependencies:
'@babel/parser': 7.27.5 '@babel/parser': 7.27.5
@@ -4072,6 +4383,12 @@ snapshots:
markdown-table@3.0.4: {} markdown-table@3.0.4: {}
marked-highlight@2.2.3(marked@16.4.2):
dependencies:
marked: 16.4.2
marked@16.4.2: {}
mdast-util-definitions@6.0.0: mdast-util-definitions@6.0.0:
dependencies: dependencies:
'@types/mdast': 4.0.4 '@types/mdast': 4.0.4
@@ -4618,6 +4935,12 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
prismjs@1.30.0: {} prismjs@1.30.0: {}
prompts@2.4.2: prompts@2.4.2:
@@ -4642,6 +4965,8 @@ snapshots:
readdirp@4.1.2: {} readdirp@4.1.2: {}
recaptcha-v3@1.11.3: {}
recma-build-jsx@1.0.0: recma-build-jsx@1.0.0:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@@ -5096,6 +5421,16 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1) vite: 6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)
vue@3.5.26(typescript@5.8.3):
dependencies:
'@vue/compiler-dom': 3.5.26
'@vue/compiler-sfc': 3.5.26
'@vue/runtime-dom': 3.5.26
'@vue/server-renderer': 3.5.26(vue@3.5.26(typescript@5.8.3))
'@vue/shared': 3.5.26
optionalDependencies:
typescript: 5.8.3
web-namespaces@2.0.1: {} web-namespaces@2.0.1: {}
webidl-conversions@3.0.1: {} webidl-conversions@3.0.1: {}

View File

@@ -65,10 +65,11 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
<nav className="hidden md:flex items-center space-x-6 text-sm font-medium"> <nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
{[ {[
{ key: 'nav.home', icon: '🏠 ', href: getLocalizedPath('/', lang) }, { key: 'nav.home', icon: '🏠 ', href: getLocalizedPath('/', lang) },
{ key: 'nav.about', icon: '👨‍💻 ', href: getLocalizedPath('/about', lang) },
{ key: 'nav.services', icon: '🛠️ ', href: getLocalizedPath('/services', lang) },
// { key: 'nav.projects', icon: '🚀 ', href: getLocalizedPath('/projects', lang) }, // { key: 'nav.projects', icon: '🚀 ', href: getLocalizedPath('/projects', lang) },
{ key: 'nav.blog', icon: '📝 ', href: getLocalizedPath('/blog', lang) }, { key: 'nav.blog', icon: '📝 ', href: getLocalizedPath('/blog', lang) },
{ key: 'nav.services', icon: '🛠️ ', href: getLocalizedPath('/services', lang) },
{ key: 'nav.about', icon: '👨‍💻 ', href: getLocalizedPath('/about', lang) },
].map((item, index) => ( ].map((item, index) => (
<motion.a <motion.a
key={item.key} key={item.key}

View File

@@ -0,0 +1,64 @@
import { useEffect, useRef } from 'react';
import type { WalineInstance, WalineInitOptions } from '@waline/client';
import { init } from '@waline/client';
import '@waline/client/style';
import '@/styles/waline-custom.css';
import type { Lang } from '@/types/i18n';
export type CommentsProps = Partial<Omit<WalineInitOptions, 'el' | 'serverURL'>> & { lang?: Lang };
export default function Comments({ lang = 'en', ...props }: CommentsProps) {
const walineInstanceRef = useRef<WalineInstance | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const walineLang = lang === 'zh' ? 'zh-CN' : 'en';
useEffect(() => {
if (!containerRef.current) return;
const getTheme = () => document.documentElement.classList.contains('dark') ? 'html.dark' : false;
const pathname = window.location.pathname;
const postsMatch = pathname.match(/\/posts\/([^/]+)/);
const path = postsMatch ? postsMatch[1] : pathname;
walineInstanceRef.current = init({
...props,
el: containerRef.current,
serverURL: import.meta.env.PUBLIC_WALINE_SERVER_URL,
lang: walineLang,
dark: getTheme(),
pageview: true,
comment: true,
path,
});
return () => walineInstanceRef.current?.destroy();
}, []);
useEffect(() => {
const getTheme = () => document.documentElement.classList.contains('dark') ? 'html.dark' : false;
walineInstanceRef.current?.update({
...props,
lang: walineLang,
dark: getTheme(),
});
const handleThemeChange = () => {
walineInstanceRef.current?.update({
...props,
lang: walineLang,
dark: getTheme(),
});
};
const observer = new MutationObserver(handleThemeChange);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
}, [lang, props, walineLang]);
return <div ref={containerRef} className="waline-container" />;
}

View File

@@ -64,8 +64,8 @@ const sortedPosts = posts
const currentIndex = sortedPosts.findIndex((post) => post.slug === currentSlug); const currentIndex = sortedPosts.findIndex((post) => post.slug === currentSlug);
const nextPost = currentIndex > 0 ? sortedPosts[currentIndex - 1] : null; const nextPost = currentIndex < sortedPosts.length - 1 ? sortedPosts[currentIndex + 1] : null;
const prevPost = currentIndex < sortedPosts.length - 1 ? sortedPosts[currentIndex + 1] : null; const prevPost = currentIndex > 0 ? sortedPosts[currentIndex - 1] : null;
const prevText = lang === 'zh' ? '上一篇' : 'Previous Post'; const prevText = lang === 'zh' ? '上一篇' : 'Previous Post';
const nextText = lang === 'zh' ? '下一篇' : 'Next Post'; const nextText = lang === 'zh' ? '下一篇' : 'Next Post';

View File

@@ -0,0 +1,46 @@
import { ArrowUp } from "lucide-react";
import { Button } from "./button";
import { useEffect, useState } from "react";
export default function BackToTop() {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const toggleVisibility = () => {
if (window.scrollY > 300) {
setIsVisible(true);
} else {
setIsVisible(false);
}
};
window.addEventListener("scroll", toggleVisibility);
return () => window.removeEventListener("scroll", toggleVisibility);
}, []);
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
return (
<div
className={`fixed bottom-8 right-8 z-50 transition-all duration-300 ${
isVisible
? "translate-y-0 opacity-100"
: "translate-y-4 opacity-0 pointer-events-none"
}`}
>
<Button
size="icon"
onClick={scrollToTop}
className="rounded-full shadow-lg bg-primary/90 backdrop-blur-sm border border-border/50 hover:bg-primary hover:shadow-xl hover:-translate-y-1 transition-all h-10 w-10"
>
<ArrowUp className="h-5 w-5 text-primary-foreground" />
<span className="sr-only">Back to top</span>
</Button>
</div>
);
}

View File

@@ -20,7 +20,7 @@ export const translations = {
}, },
hero: { hero: {
greeting: "Hello, I'm", greeting: "Hello, I'm",
viewProjects: 'View Projects', viewProjects: 'About Me',
contactMe: 'Contact Me', contactMe: 'Contact Me',
lookingForJob: 'Looking for a Frontend/TS Full-stack Engineer (Remote)? Contact me!', lookingForJob: 'Looking for a Frontend/TS Full-stack Engineer (Remote)? Contact me!',
digitalNomad: 'Exploring the freelance journey, striving to become a digital nomad', digitalNomad: 'Exploring the freelance journey, striving to become a digital nomad',
@@ -49,6 +49,7 @@ export const translations = {
}, },
blog: { blog: {
slogan: 'This is where innovative thinking meets complex problems.', slogan: 'This is where innovative thinking meets complex problems.',
backToList: 'Back to Blog',
}, },
services: { services: {
title: 'What I Do', title: 'What I Do',
@@ -76,7 +77,7 @@ export const translations = {
}, },
hero: { hero: {
greeting: '你好,我是', greeting: '你好,我是',
viewProjects: '查看项目', viewProjects: '关于我',
contactMe: '联系我', contactMe: '联系我',
lookingForJob: '正在寻找前端/TS 全栈工程师(远程)?联系我!', lookingForJob: '正在寻找前端/TS 全栈工程师(远程)?联系我!',
digitalNomad: '探索自由职业道路,努力成为数字游民', digitalNomad: '探索自由职业道路,努力成为数字游民',
@@ -105,6 +106,7 @@ export const translations = {
}, },
blog: { blog: {
slogan: '这里是创新思维与复杂问题相遇的地方。', slogan: '这里是创新思维与复杂问题相遇的地方。',
backToList: '返回博客列表',
}, },
services: { services: {
title: '我能做什么', title: '我能做什么',

View File

@@ -4,6 +4,7 @@ import type { MarkdownLayoutProps } from 'astro';
import { type Lang } from '@/types/i18n'; import { type Lang } from '@/types/i18n';
import { type FrontmatterProps } from '@/types'; import { type FrontmatterProps } from '@/types';
import { defaultLang } from '@/i18n/ui'; import { defaultLang } from '@/i18n/ui';
import { useTranslations } from '@/i18n/utils';
import GlassHeader from '@/components/GlassHeader'; import GlassHeader from '@/components/GlassHeader';
import Footer from '@/components/Footer'; import Footer from '@/components/Footer';
import AuthorCard from '@/components/AuthorCard'; import AuthorCard from '@/components/AuthorCard';
@@ -11,6 +12,7 @@ import TableOfContents from '@/components/layout/TableOfContents.astro';
import BlogNavigation from '@/components/layout/BlogNavigation.astro'; import BlogNavigation from '@/components/layout/BlogNavigation.astro';
import PostMeta from '@/components/blog/PostMeta.astro'; import PostMeta from '@/components/blog/PostMeta.astro';
import Container from '../components/ui/Container'; import Container from '../components/ui/Container';
import Comments from '@/components/blog/Comments';
// Use Astro's MarkdownLayoutProps for proper type safety // Use Astro's MarkdownLayoutProps for proper type safety
export type Props = MarkdownLayoutProps<FrontmatterProps>; export type Props = MarkdownLayoutProps<FrontmatterProps>;
@@ -30,6 +32,9 @@ const {
} = frontmatter; } = frontmatter;
const lang = Astro.currentLocale as Lang || defaultLang; const lang = Astro.currentLocale as Lang || defaultLang;
const t = useTranslations(lang);
const blogListUrl = lang === 'zh' ? '/zh/blog' : '/blog';
// Handle different field names for backward compatibility // Handle different field names for backward compatibility
const finalPublishDate = publishDate || date; const finalPublishDate = publishDate || date;
@@ -50,6 +55,23 @@ const finalReadingTime = readTime ? parseInt(readTime.replace(/\D/g, '')) : unde
<!-- Blog post header --> <!-- Blog post header -->
<header class="mb-10"> <header class="mb-10">
<a
href={blogListUrl}
class="inline-flex items-center gap-2 text-foreground hover:text-primary transition-colors duration-200 mb-6 group"
>
<svg
width="16"
height="16"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="group-hover:-translate-x-1 transition-transform duration-200"
>
<path d="M6.85355 3.85355C7.04882 3.65829 7.04882 3.34171 6.85355 3.14645C6.65829 2.95118 6.34171 2.95118 6.14645 3.14645L2.14645 7.14645C1.95118 7.34171 1.95118 7.65829 2.14645 7.85355L6.14645 11.8536C6.34171 12.0488 6.65829 12.0488 6.85355 11.8536C7.04882 11.6583 7.04882 11.3417 6.85355 11.1464L3.20711 7.5L6.85355 3.85355Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
<span class="font-medium">{t('blog.backToList')}</span>
</a>
<PostMeta <PostMeta
lang={lang} lang={lang}
publishDate={finalPublishDate} publishDate={finalPublishDate}
@@ -83,6 +105,11 @@ const finalReadingTime = readTime ? parseInt(readTime.replace(/\D/g, '')) : unde
<BlogNavigation /> <BlogNavigation />
</div> </div>
<!-- Comments Section -->
<div class="mt-10 sm:mt-16 border-t border-border pt-10">
<Comments client:only="react" lang={lang} />
</div>
<!-- Author Card moved to bottom with enhanced styling --> <!-- Author Card moved to bottom with enhanced styling -->
<div class="mt-10 sm:mt-16 border-t border-border pt-10"> <div class="mt-10 sm:mt-16 border-t border-border pt-10">
<div class="bg-card/30 backdrop-blur-sm rounded-2xl border border-border p-6 shadow-sm hover:shadow-md transition-all duration-300"> <div class="bg-card/30 backdrop-blur-sm rounded-2xl border border-border p-6 shadow-sm hover:shadow-md transition-all duration-300">

View File

@@ -1,4 +1,5 @@
--- ---
import BackToTop from "@/components/ui/back-to-top";
import { useTranslations } from "@/i18n/utils"; import { useTranslations } from "@/i18n/utils";
import type { Lang } from "@/types/i18n"; import type { Lang } from "@/types/i18n";
import { defaultLang } from "@/i18n/ui"; import { defaultLang } from "@/i18n/ui";
@@ -44,6 +45,7 @@ const t = useTranslations(lang);
> >
</div> </div>
<slot /> <slot />
<BackToTop client:load />
</body> </body>
</html> </html>

View File

@@ -1,6 +1,6 @@
--- ---
import TaxonomyPageLayout from '../../../layouts/TaxonomyPageLayout.astro'; import TaxonomyPageLayout from '../../../layouts/TaxonomyPageLayout.astro';
import { type Lang } from '@/i18n/utils'; import { type Lang } from '@/types/i18n';
import { defaultLang } from '@/i18n/ui'; import { defaultLang } from '@/i18n/ui';
import { getTaxonomyPageData } from '@/utils/blog-utils'; import { getTaxonomyPageData } from '@/utils/blog-utils';

View File

@@ -5,7 +5,8 @@ import CategoryCard from '../../components/blog/CategoryCard.astro';
import TagCard from '../../components/blog/TagCard.astro'; import TagCard from '../../components/blog/TagCard.astro';
import Container from '../../components/ui/Container'; import Container from '../../components/ui/Container';
import { type BlogPost } from '@/types'; import { type BlogPost } from '@/types';
import { type Lang, useTranslations } from '@/i18n/utils'; import { useTranslations } from '@/i18n/utils';
import { type Lang } from '@/types/i18n';
import { defaultLang } from '@/i18n/ui'; import { defaultLang } from '@/i18n/ui';
import { sortPostsByDate } from '@/utils/blog-utils'; import { sortPostsByDate } from '@/utils/blog-utils';

View File

@@ -0,0 +1,127 @@
---
layout: "@/layouts/BlogPostLayout.astro"
title: "1Panel Automated Deployment Notes: Say Goodbye to Manual Uploads, One-Command Takeoff from Local"
description: "Learn how to build a lightweight deployment pipeline using 1Panel with SSH key authentication and ACL permissions. Complete guide for solo developers to automate server deployments."
date: "2024-06-08"
image: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1470&auto=format&fit=crop"
tags: ["1Panel", "DevOps", "Automation", "SSH", "Deployment"]
tagId: ["1panel", "devops", "automation", "ssh", "deployment"]
category: "DevOps"
categoryId: "devops"
readTime: "5 min read"
---
Updating code used to make me feel like a "human FTP client":
1. Package everything locally, sweat included
2. Open browser, log into 1Panel (forgot password again?)
3. Dig through file manager like an archaeologist
4. Manual upload, manual overwrite, plus manual cleanup of old files
This whole ritual took at least 5 minutes each time, and last week I actually brought the site down for 10 minutes because I forgot to upload a new config file. Enough is enough—I built this lightweight deployment pipeline. No heavy Jenkins setup needed, perfect for solo developers or small projects. Fast, reliable, and nobody else to blame.
## 1. Create a "Deployment Runner" Account
Running scripts as root? That's like performing surgery with a chainsaw. Let's create a dedicated account just for deployments.
```bash
# Create the deployment account
sudo adduser deploy_user
# Critical: disable password login, SSH keys only
# Even if someone guesses the password, they can't get in
# -l locks the password account (disabled password login)
# -u unlocks the password account (if you need enabled password login)
sudo passwd -l deploy_user
```
## 2. The Permission Puzzle: ACL to the Rescue
Here's the tricky part: 1Panel has its own permission system. If you `chown` everything to your deployment user, websites in the panel might start throwing 500 errors.
My solution: **ACL** (Access Control Lists). Think of it as giving your deployment user a "VIP backstage pass"—it can read/write files without messing with 1Panel's original file ownership.
```bash
# Install ACL tools first
sudo apt install acl -y
# Grant access to sites directory (make sure this is your web root)
# -R means recursive, but be careful if directory has other sites
sudo setfacl -R -m u:deploy_user:rwx /opt/1panel/www/sites
# Set default inheritance: new sites get access automatically
sudo setfacl -R -d -m u:deploy_user:rwx /opt/1panel/www/sites
```
## 3. SSH Key Access: No More Password Typing
Copy your local SSH public key to the server, and never type a password again.
```bash
# Create SSH directory for deployment user
sudo -u deploy_user mkdir -p /home/deploy_user/.ssh
# Append your public key (using >> not > to avoid overwriting)
echo "your-ssh-public-key-content" >> /home/deploy_user/.ssh/authorized_keys
# Permissions must be exact, or SSH will refuse
sudo chown -R deploy_user:deploy_user /home/deploy_user/.ssh
sudo chmod 700 /home/deploy_user/.ssh
sudo chmod 600 /home/deploy_user/.ssh/authorized_keys
```
**Even lazier method**: If you have `ssh-copy-id` locally, just run `ssh-copy-id deploy_user@your-server-ip`.
## 4. SSH Aliases (For the Truly Lazy)
Memorize IP addresses? Not in this decade. Add this to your local `~/.ssh/config`:
```text
Host your-server-alias # Nickname for your server
HostName your-server-ip-address # Your server IP
User deploy_user # Login user
IdentityFile ~/.ssh/id_rsa # Private key path
```
Now just type `ssh your-server-alias` and you're in. Magic.
## 5. The Grand Finale: One-Command Deployment
Create `deploy.sh` in your project root:
```bash
#!/bin/bash
# For frontend projects, build first
# echo "📦 Building..."
# npm run build
echo "🚀 Syncing to production..."
# rsync for incremental updates
# -a: archive mode (preserves everything)
# -v: verbose (show me what's happening)
# -z: compress during transfer
# --delete: ⚠️ Warning: removes files on target that don't exist locally!
# Remove this flag for first sync to be safe
rsync -avz --delete --progress ./dist/ your-server-alias:/opt/1panel/www/sites/your-project-directory/
echo "✅ Deployment complete!"
echo "⏱️ Next time just run: ./deploy.sh"
```
Make it executable: `chmod +x deploy.sh`. Now deploying is literally one command.
## 6. Pitfalls I Stepped In (So You Don't Have To)
1. **Skip `--delete` on first sync**: Do a dry run first. This flag can delete important files if you're not careful.
2. **Check ACL permissions**: If files still have permission issues, run `getfacl /opt/1panel/www/sites/your-project` to verify.
3. **Verify paths**: 1Panel's default path is `/opt/1panel/www/sites/`, but check if you customized it.
4. **Frontend projects**: Don't upload `node_modules`. Your server doesn't need that 200MB baggage.
## Wrapping Up
I've been using this setup for weeks, and it just works. What used to be a 5-minute chore is now a 30-second command. The mental load is gone—no more "did I forget that one file?" anxiety.
For small to medium projects that don't need full CI/CD complexity, this lightweight approach is perfect. A few minutes to set up, a lifetime of easy deployments.
**Final reminder**: Automation is powerful but dangerous. Test with a dummy directory first, especially before using `--delete`. Happy deploying, and may your uptime be forever!

View File

@@ -1,67 +1,87 @@
# Stop Being Held Hostage by "Best Practices": Confessions of a Full-Stack Developers Tech Stack Struggles ---
layout: "@/layouts/BlogPostLayout.astro"
title: "All This Tinkering... For What? Engineering Reflections from a Full-Stack Developer"
description: "An honest reflection on tech stack selection paralysis. One developer's journey from chasing 'best practices' to realizing that technology is a tool, not a totem—how to avoid over-engineering and find the right fit for your actual needs."
date: "2026-01-08"
image: "https://images.unsplash.com/photo-1518770660439-4636190af475?q=80&w=1470&auto=format&fit=crop"
tags: ["Reflections", "Full-Stack", "Engineering", "Personal Reflection"]
tagId: ["reflections", "fullstack", "engineering", "development-reflection"]
category: "Engineering"
categoryId: "engineering"
readTime: "8 min read"
---
> **Foreword:** > *A log of the soul-crushing details I encountered during a recent project setup. This is a summary of my own internal tug-of-war—lessons learned the hard way through endless trial and error. It might not be the "correct" way, but every word was paid for in lost time.*
> I am a developer who transitioned from Frontend to Node.js Full-Stack. This article is simply a summary of my recent experiences and reflections while developing a project. Given my limited knowledge and perspective, the views expressed here may not be universally "correct" or represent industry standards. This is just a personal debrief after stepping into countless pitfalls, shared in the hope of exchanging ideas with the community and providing a reference for those facing similar dilemmas. If there are any inaccuracies, please feel free to correct me in the comments.
Recently, I set out to build a small bookmark-style tool with only about 30 endpoints. I thought it would take two weeks; instead, I spent over a month just "wrestling" with the tech stack. I recently set out to build a simple bookmarking tool. It only needed about 30 endpoints, yet I spent nearly a month agonizing over the tech stack. I felt like I was wandering through a dark forest, sprinting toward every glimmer of light only to find a new pitfall waiting for me.
I felt like a hunter lost in a technical fog: wherever I saw a light (a new tool or an "expert" opinion), I rushed toward it, only to find a deeper pit hidden behind every glow. ### Step 1: Chasing the "Mainstream" and Feeling the Friction
## 1. Chasing Trends is the Start of Internal Friction It started because I wanted to build a side project. In my day job, I use Vue; its second nature to me. But everywhere I looked online, the consensus was: **"Next.js + React + shadcn-ui"** is the gold standard. Great ecosystem, endless components, future-proof.
I started by following the crowd and chose **Next.js + NestJS + shadcn-ui**. I thought, "Since everyone says this is the 'Full-Stack Gold Standard,' I can't go wrong." The reality, however, gave me a swift wake-up call. I told myself: *Dont get stuck in your comfort zone. Try something new.* So, I benched Vue and picked up React. I was immediately hit by **choice paralysis.**
In Next.js, I wasted a whole week just deciding on a data-fetching and state management solution (SWR vs. Zustand?). Once I finally started, I was overwhelmed by the complexity of Server Components (RSC) vs. Client Components—constantly defining `"use client"`, fixing mysterious Hydration errors, and manually managing state dependencies while optimizing endless callback functions. It wasn't just a choice between A and B; it was a choice between five different ways to do everything. I spent more time reading docs and comparisons than writing logic:
I kept thinking: **I just want to write some simple business logic. Why am I spending 80% of my energy dealing with the overhead of the framework?** * Do I fetch data with **SWR** or **TanStack Query**?
* Is global state better in **Zustand**, **Redux**, or **Jotai**?
* How do I bridge the gap between server and client data?
## 2. The Heavier the Framework, the Heavier the Cognitive Load Instead of building features, I was researching "how to build." To make matters worse, I kept hitting those cryptic **"Hydration Errors."** One refresh, one error, and an hour gone trying to find the mismatch.
Later, I switched the frontend to Nuxt, which was indeed smoother. But on the backend, I stuck with **NestJS**, chasing so-called "standardization" and "enterprise engineering." The most draining part was tagging everything with `"use client"`. I felt less like a developer and more like a clerk labeling boxes. I kept thinking: **"If most of this ends up running on the client anyway, why am I using a framework that splits the stack so aggressively and demands such high mental overhead?"**
But I only had 30 endpoints. The logic was incredibly simple. In NestJS, I was forced to write Controllers, Services, Modules, DTOs... the amount of code tripled. Even worse was the **ESM compatibility issue**. NestJS still clings to the CommonJS dream, leading to constant configuration errors when I tried to use modern ESM-only libraries. To run a simple TypeScript Worker thread, I had to spend hours researching ESM compilers. ### Step 2: Retreating to Comfort—and the Fear of Missing Out
The most frustrating part was **Swagger integration**. Most people prefer Zod for validation now, but Swagger is deeply coupled with the Class-Validator (Decorator) pattern. To get Swagger to recognize my Zod schemas and generate documentation, I had to manually write adapters and custom decorators. I went back to my familiar Vue ecosystem and fired up Nuxt. Suddenly, I could breathe. My productivity skyrocketed.
**I felt like I wasn't building a product; I was repairing a broken tractor with incompatible parts.** But the peace didn't last. Id see another post praising the Next.js ecosystem and start doubting myself: **"Am I choosing the wrong path? Am I falling behind?"** I gave Next.js another shot for a few days, and like clockwork, all those petty, nagging frustrations returned.
## 3. Monorepo: The "Tender Trap" for Indie Developers Thats when I realized: **Tech isn't about "better" or "worse"; its about fit.** Trust your "handfeel." That sense of flow (or lack thereof) doesn't lie.
To pursue "code reuse," I even set up a **Monorepo**. ### Step 3: The "Professionalism" Trap
I thought: *Front-end and back-end sharing types, enums, and error codes—how elegant!* The reality: trying to get a pure ESM frontend to share a package with a non-pure ESM backend plunged me into a bottomless pit of build configurations. Due to the NestJS environment, I had to compile and export the shared package every time I made a change, making frequent debugging and code modification an absolute nightmare. Once the frontend was settled, I moved to the backend. I chose **NestJS** because its billed as the most "Enterprise-grade" option. To make it even more "professional," I forced it into a **Monorepo**. I effectively built myself a prison:
Code that should have taken one minute to write took ten because I was busy dealing with cross-package debugging, TS type synchronization, and build logic. * **Immense Friction:** Changing a single shared enum or type meant re-building, waiting for workspace syncs, and restarting services. A 10-second change turned into a 1-minute wait.
* **Compatibility Hell:** NestJS is still clinging to CommonJS. Trying to use a pure ESM library or running a TS Worker thread resulted in config errors that made me question my career choices.
* **Ceremony Over Substance:** For 30 endpoints, I was writing endless Controllers, Services, Modules, and DTOs. The lines of code tripled, but the core logic stayed exactly the same.
* **The Swagger Tax:** I wanted to use Zod for validation, but Swagger only recognizes decorators (class-validator). I ended up maintaining two nearly identical data definitions just to have an API doc.
**I finally realized: Monorepos are built to solve "organizational collaboration." For an indie developer, they are often a productivity killer.** **I traded my immediate efficiency for the "advantages of a large team" that I don't actually have.** I wanted speed; the framework demanded "maintainability" for a scale I'll likely never hit.
## 4. Returning to Pragmatism: My "Two-Tier Strategy" ### Step 4: Stripping it Down and Losing My Way
At the end of all this exhaustion, I reflected: Is there a perfect framework? The answer is no; there is only the *suitable* one. Consequently, I have simplified my selection logic into two tiers: I ditched the heavy frameworks for **Fastify**, thinking Id keep it lean. But I quickly discovered that the problem wasn't just "weight"—it was **"the anxiety of absolute freedom."**
* **Tier A: Rapid Validation (MVP / Personal Projects)** Fastify is liberating, but for someone used to structure, freedom is a burden:
**Stack: Nuxt All-in-One.** Don't even separate the frontend and backend. Nuxts built-in Server API (Nitro) is more than enough for small to medium businesses. Types are naturally shared, and there are no CORS or build-sync headaches. At the validation stage, **"Speed" is a hundred times more important than "Elegance."**
* **Tier B: Complex Business (Large Projects / Team Collaboration)** * "Where should this file go?"
**Stack: Nuxt + NestJS (Decoupled) + Monorepo.** Only when the business is complex enough to require strict layering, Dependency Injection (DI) for decoupling, and multi-person collaboration will I endure the "ceremony" and management costs of these heavy frameworks. * "How do I organize this into a plugin?"
* Logging, queues, auth—everything NestJS gave me for free now required me to find, integrate, and debug a third-party library.
I hated the rules of heavy frameworks, yet I craved their structure. My mindset was completely torn.
### The Epiphany: Tech is a Tool, Not a Totem
## 5. A Side Note: A New Hope in AdonisJS One night, working late, I asked myself: **"Why am I doing this? I just wanted to build a simple tool."**
Just as I was summarizing these strategies, I stumbled upon a new framework—**AdonisJS**. Many developers describe it as the "Laravel of Node.js." Everything clicked. I was applying "Long-term Enterprise Maintenance" standards to a "Solo Rapid Prototype." It was like trying to build an eight-lane highway just to go to the grocery store around the corner.
I took a quick look at its philosophy, and it seems to precisely hit the pain points I mentioned: it supports ESM natively, has a powerful built-in ORM and Auth solution, and doesn't require jumping through hoops with custom adapters just to get automated Swagger documentation. I set a new, simple rule for myself:
This "Convention over Configuration" full-stack framework seems to balance development efficiency with engineering quality. I plan to use it in my next project and will share my findings once I have more experience. 1. **For Prototypes & Small Tools:** Use **Nuxt (or a monolithic framework) exclusively.** Frontend and backend in one repo, natural type synchronization, no CORS issues, no sync friction. The goal is to *ship*.
2. **For Complex Systems & Large Teams:** *Then* consider **Nuxt + NestJS + Monorepo.** The complexity is a fair trade for the architectural guardrails.
## 6. Conclusion: A Few Words of Advice ### A New Contender: AdonisJS
1. **There is no perfect framework, only the one that fits the moment.** Don't expect any "star" framework to solve all your problems; they all come with a cost. During this spiral, I stumbled upon **AdonisJS**. Its often called the "Laravel of Node.js." From a quick look, it seems to hit the sweet spot: it has the structure I crave (ORM and Auth built-in) but feels modern and supports ESM natively. I might give it a spin next time.
2. **Do not easily try a tech stack you aren't familiar with during indie development or tight deadlines.** Unless you truly have the time and energy to burn. You think you're learning new tech, but you're actually burning your product's lifespan.
3. **Be wary of "Big Tech Best Practices."** Many tools built to solve pain points in giant corporations (like Monorepos or extreme layering) only create pain points in personal projects.
4. **Familiarity > Modernity.** Even if a framework is called "old school," if it's intuitive to you, lets you finish work early, and helps you write clearer logic with AI assistance, it is your "silver bullet."
**The best tech stack is the one that allows you to forget the technology itself and focus on creating value.** ### Final Thoughts
Finally, the solutions I've summarized are only what fits my personal habits and current understanding; they may not work for everyone. Everyone's business scenarios and technical backgrounds are different. **If you have better ideas or different solutions, Id love to hear them in the comments so I can learn from you too.** If I've missed anything, please let me know. Thanks in advance! 1. **Dont blind-follow the hype:** What works for Vercel might not work for your weekend project.
2. **Trust your "handfeel":** If a tool makes you feel productive, that's worth more than any "State of JS" ranking.
3. **Ship first, optimize later:** For a solo project, an un-launched architecture is just expensive fan fiction.
4. **Watch out for "Config Friction":** If you spend more time in `.json` and `.config.js` files than in your logic, your tools are failing you.
This was my journey through the weeds. **My approach might be "wrong" or even a bit clumsy.** Id love to hear how you handle the "stack anxiety." **Do you have a go-to setup that just works, or are you still searching for the "perfect" balance?**

View File

@@ -1,6 +1,6 @@
--- ---
import TaxonomyPageLayout from '../../../layouts/TaxonomyPageLayout.astro'; import TaxonomyPageLayout from '../../../layouts/TaxonomyPageLayout.astro';
import { type Lang } from '@/i18n/utils'; import { type Lang } from '@/types/i18n';
import { defaultLang } from '@/i18n/ui'; import { defaultLang } from '@/i18n/ui';
import { getTaxonomyPageData } from '@/utils/blog-utils'; import { getTaxonomyPageData } from '@/utils/blog-utils';

View File

@@ -1,6 +1,6 @@
--- ---
import TaxonomyPageLayout from '../../../../layouts/TaxonomyPageLayout.astro'; import TaxonomyPageLayout from '../../../../layouts/TaxonomyPageLayout.astro';
import { type Lang } from '@/i18n/utils'; import { type Lang } from '@/types/i18n';
import { defaultLang } from '@/i18n/ui'; import { defaultLang } from '@/i18n/ui';
import { getTaxonomyPageData } from '@/utils/blog-utils'; import { getTaxonomyPageData } from '@/utils/blog-utils';

View File

@@ -55,7 +55,7 @@ const tags = extractTags(allPostsArray);
--- ---
<BlogLayout title="博客 - 赵桂阳" description="深入我对编程、技术趋势和开发者生活的思考。探索我的最新文章。"> <BlogLayout title="博客 - Joy Zhao" description="深入我对编程、技术趋势和开发者生活的思考。探索我的最新文章。">
<main class="min-h-screen"> <main class="min-h-screen">
<!-- 头部区域 --> <!-- 头部区域 -->
<Container client:load className="pt-24 pb-12"> <Container client:load className="pt-24 pb-12">

View File

@@ -0,0 +1,127 @@
---
layout: "@/layouts/BlogPostLayout.astro"
title: "1Panel 自动化部署笔记:告别\"手动操作\",本地一键起飞"
description: "学习如何基于 1Panel 构建轻量级部署流水线,配合 SSH 密钥认证和 ACL 权限管理。本文为单兵作战的开发者提供了完整的自动化部署方案。"
date: "2024-06-08"
image: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1470&auto=format&fit=crop"
tags: ["1Panel", "DevOps", "自动化", "SSH", "部署"]
tagId: ["1panel", "devops", "automation", "ssh", "deployment"]
category: "DevOps"
categoryId: "devops"
readTime: "5 min read"
---
以前每次更新代码,都感觉自己像个"人肉传输带"
1. 本地吭哧吭哧打包
2. 打开浏览器,输密码登录 1Panel 面板
3. 在文件管理器里翻来覆去找目录
4. 手动上传、手动覆盖,有时候还得手动删服务器上的旧文件,就怕哪个缓存文件没清干净
这一套流程下来,没个三五分钟搞不定,关键还特别容易漏文件。上周就因为我忘记上传一个新加的配置文件,线上挂了十分钟才反应过来。痛定思痛,我决定搞一套轻量级部署方案——不用整 Jenkins 那种大炮打蚊子,特别适合我这种单兵作战的小项目,要的就是快、稳、不求人。
## 1. 先整个"跑腿小弟"账号
用 root 跑脚本?那太野了,万一脚本写崩了,服务器直接原地升天。得建个专门干这活的"小弟账号"。
```bash
# 创建专门负责部署的账号
sudo adduser deploy_user
# 关键一步:禁用这个账号的密码登录,只认 SSH 密钥
# 这样就算有人猜到密码也进不来,安全第一
# -l 锁定账号密码(禁用密码登录)
# -u 解锁账号密码(如需恢复密码登录)
sudo passwd -l deploy_user
```
## 2. 权限难题的"优雅解法"ACL 授权
这里有个坑1Panel 管理的文件有自己的一套权限体系,如果你用 `chown` 强行改属主,面板里的网站可能就直接 500 错误了。
我的解决方案是 **ACL**(访问控制列表)。这玩意就像给文件加了张"访客通行证",让 `deploy_user` 这个账号能读写文件,但又不会破坏 1Panel 原本的文件归属关系。
```bash
# 先安装 ACL 工具
sudo apt install acl -y
# 给 sites 目录开绿灯(注意:确认这是你的网站根目录)
# -R 表示递归,但如果目录里已有其他站点,建议先备份或确认操作
sudo setfacl -R -m u:deploy_user:rwx /opt/1panel/www/sites
# 设置默认权限继承:以后在面板里新建的站点,自动给 deploy_user 权限
sudo setfacl -R -d -m u:deploy_user:rwx /opt/1panel/www/sites
```
## 3. SSH 免密登录:从此进出如风
把你本地电脑的 SSH 公钥塞到服务器上,以后就不用输密码了。
```bash
# 切换到部署账号的家目录
sudo -u deploy_user mkdir -p /home/deploy_user/.ssh
# 追加你的公钥(用 >> 而不是 >,避免覆盖别人的密钥)
echo "your-ssh-public-key-content" >> /home/deploy_user/.ssh/authorized_keys
# 权限必须严格设置,否则 SSH 会拒绝连接
sudo chown -R deploy_user:deploy_user /home/deploy_user/.ssh
sudo chmod 700 /home/deploy_user/.ssh
sudo chmod 600 /home/deploy_user/.ssh/authorized_keys
```
**更懒的方法**:如果你本地有 `ssh-copy-id` 命令,直接 `ssh-copy-id deploy_user@your-server-ip-address`
## 4. SSH 别名配置(懒癌患者的福音)
记 IP 地址?不存在的。在本地 `~/.ssh/config` 文件里加一段:
```text
Host your-server-alias # 给你服务器起的外号,随便起,好记就行
HostName your-server-ip-address # 你的服务器 IP
User deploy_user # 登录用户
IdentityFile ~/.ssh/id_rsa # 私钥路径
```
配置完,以后登录就直接 `ssh your-server-alias`,爽!
## 5. 终极一击:一键部署脚本
在项目根目录创建 `deploy.sh`
```bash
#!/bin/bash
# 如果是前端项目,先打包
# echo "📦 正在打包..."
# npm run build
echo "🚀 开始同步到生产环境..."
# 使用 rsync 增量同步
# -a: 归档模式,保持文件属性
# -v: 显示详细过程
# -z: 压缩传输
# --delete: ⚠️ 注意:这会删除目标端有而源端没有的文件!
# 首次同步建议先去掉这个参数,确认无误后再加上
rsync -avz --delete --progress ./dist/ your-server-alias:/opt/1panel/www/sites/your-project-directory/
echo "✅ 部署完成!"
echo "⏱️ 下次更新只需: ./deploy.sh"
```
给脚本执行权限:`chmod +x deploy.sh`,以后部署就是一行命令的事儿。
## 6. 我踩过的坑和注意事项
1. **首次同步别用 `--delete`**:先完整同步一次,确认文件都对,再加这个参数。不然可能把服务器上的重要文件删了。
2. **ACL 权限检查**:如果同步后还是没权限,可以用 `getfacl /opt/1panel/www/sites/your-project-directory` 看看 ACL 设置是否生效。
3. **文件路径要对**1Panel 的默认站点路径是 `/opt/1panel/www/sites/`,但如果你改过,记得调整。
4. **前端项目注意**:打包前清理 `node_modules`,这玩意别传到服务器。
## 总结
这套方案我从上周用到现在真香以前更新一次至少折腾5分钟现在30秒搞定。关键是心理负担小了——再也不用担心漏传文件`rsync` 会帮我搞定一切。
对于还没到需要 CI/CD 的中小型项目,这种轻量级方案简直完美。几分钟配置,一劳永逸。如果你也在用 1Panel又被手动上传困扰试试这个方案吧。
**最后提醒**:任何自动化操作都有风险,尤其是删除操作。第一次在生产环境用的时候,建议先找个测试目录跑一遍,确认无误再上。祝大家部署顺利,永不宕机!

View File

@@ -1,65 +1,89 @@
# 别再被“最佳实践”绑架了:一个全栈开发者的选型忏悔录 ---
layout: "@/layouts/BlogPostLayout.astro"
title: "折腾来折腾去,到底是为了什么? 一个全栈开发者的工程化反思"
description: "关于技术选型困境的真实反思。一个开发者在'最佳实践'诱惑下的挣扎与醒悟:技术是工具不是目的,如何避免过度工程化,找到真正适合自己的方案。"
date: "2026-01-08"
image: "https://images.unsplash.com/photo-1518770660439-4636190af475?q=80&w=1470&auto=format&fit=crop"
tags: ["全栈", "工程化", "开发感悟"]
tagId: ["fullstack", "engineering", "development-reflection"]
category: "工程化"
categoryId: "engineering"
readTime: "8 min read"
---
> **写在前面:** > 记录一下最近做项目选型时,那些真能把人搞崩溃的细节。是我自己跟自己较劲,来回折腾后的一点总结。不一定对,但每句话都是花时间踩坑换来的。
> 本人是一名从前端转 Node.js 全栈的开发者。这篇文章只是基于我近期开发项目时的一些真实经历和感悟。由于个人知识储备和认知水平有限,文中的观点不一定正确,更不代表行业标准。这仅仅是我在踩了无数坑后的一点自我总结,发出来是希望能和大家交流,也给有类似纠结的朋友提供一个参考。如有谬误,欢迎评论区指正。
最近我为了做一个只有 30 个接口的书签类小工具,把自己折腾疯了 最近想做个书签小工具,一共也就 30 个接口,结果在“用什么技术”这个问题上,我纠结了快一个月。感觉就像在技术的林子里瞎转,看见哪儿有光就往哪儿跑,最后发现每个亮光后面都是个坑
起初,我像个在技术丛林里乱撞的猎人:哪里有亮光(新工具/大牛言论),我就往哪冲。结果发现,每一个亮光后面都藏着一个更深的坑。 ## 第一步:跟着“技术主流”走,结果浑身难受
## 1. 追逐流行,是内耗的开始 事情是这样的,我想自己做点小产品。工作上我一直用 Vue很顺手。但网上看了一圈大家都在说 **“Next.js + React + shadcn-ui”** 才是现在做东西的正道。生态好、组件多、未来可期。
最开始随大流选了 **Next.js + NestJS + shadcn-ui**。我想着,既然大家都说这是“全栈天花板”,那选它准没错。结果现实反手就给了我一巴掌。 一想,不能老用自己熟悉的,换个口味吧。于是放下 Vue开始用 React。结果一上来就懵了**选择太多了,不知道该用哪个。**
在 Next.js 里为了选一个数据处理和状态管理方案SWR 还是 Zustand我硬生生磨掉了一周。好不容易开工了又被服务端渲染SSR带来的复杂度搞得头大——频繁定义 `"use client"` 指令、处理莫名其妙的“状态水合Hydration”报错、还要手动管理状态依赖并优化一堆回调函数。 根本不是简单的二选一,而是每个地方都有好几个选择,我大部分时间都在搜资料、看对比、做选择:
我当时就在想:**我只是想写个简单的业务逻辑,为什么要花 80% 的精力去处理框架带来的麻烦?** * 数据请求是用 **SWR** 还是 **TanStack Query**
* 全局状态是用 **Zustand** 还是继续用 **Redux**、**Jotai**
* 服务端和客户端数据怎么同步?在这个边界上我反复拉扯。
## 2. 框架重一点,心智负担就大一点 感觉我的主要工作不是写代码,而是在研究“该用什么写代码”。更烦的是那些莫名其妙的 **“水合错误Hydration Error”**,写个页面一刷新就报错,然后就得花时间找哪里对不上。
后来前端切到了 Nuxt 确实顺手了些,但后端我依然守着 **NestJS**,追求所谓的“大而全”。 最心累的是到处都要加 `“use client”`。我感觉自己不是在写功能,是在给代码打标签。每加一行我就在想:**“如果这么多东西最终都要在客户端跑,那我为啥要选这个把前后端拆这么碎、心智负担这么重的框架?”**
但我一共才 30 个接口,业务逻辑极其简单。但在 NestJS 里,我不得不写 Controller、Service、Module、DTO……代码量翻了几倍。更崩溃的是 **ESM 的兼容问题**NestJS 依然守着 CommonJS 的旧梦,导致我想用一些最新的 ESM 库时,各种配置文件报错。为了跑通一个简单的 TypeScript Worker 线程,我还得自己去研究 ESM 编译器。 ## 第二步:用回顺手的,又怕自己落伍了
最心累的是 **Swagger 的集成**。现在大家都爱用 Zod 做验证,但 Swagger 深度绑定类装饰器模式class-validator。为了让 Swagger 识别 Zod Schema 并生成文档,我得自己手搓适配器和装饰器 我回到熟悉的 Vue 生态,用了 Nuxt。果然舒服多了开发效率起飞
**我感觉自己不是在开发产品,我是在修一辆零件互不兼容的破拖拉机。** 但安静没两天,看网上还是到处在夸 Next.js 生态好。心里又开始想:**“我是不是选错了?会不会错过什么?”** 不信邪,又回头去试了几天 Next.js。结果那些熟悉的、琐碎的麻烦全回来了。
## 3. Monorepo独立开发者的“温柔陷阱” 这回我彻底认了:**技术没有绝对的好坏,只有合不合适。我相信自己用着舒服的感觉,这种“手感”骗不了人。**
为了追求代码复用,我还折腾了 **Monorepo** ## 第三步:总想“搞得专业点”,结果给自己挖坑
我想着,前后端共享类型、枚举、错误码,多优雅!但现实是:为了让纯 ESM 的前端和非纯血 ESM 的后端共享一个包,我陷入了无穷无尽的编译配置中。由于 NestJS 的环境问题,我必须为共享包进行编译导出才能使用,导致频繁修改调试代码时异常麻烦。 前端定了,该写后端了。我选了大家说最工程化的 **NestJS**。为了显得更专业,又强行上了 **Monorepo**。这下彻底麻烦了,我亲手给自己搞了个大工程:
原本一分钟写完的代码因为要处理跨包调试、TS 类型同步和打包逻辑,硬生生变成了十分钟。 * **开发摩擦力极大:** 在 Monorepo 里改个公共的枚举值或类型,要重新构建、等 Workspace 同步、重启服务。原本 10 秒钟的事,现在要等 1 分钟。
* **兼容性地狱:** NestJS 守着 CommonJS 的旧梦,我想用个纯 ESM 的库或者跑个 TypeScript Worker 线程,配置文件的报错能让你怀疑人生,得花大把时间研究编译器路径。
* **仪式感的内耗:** 就 30 个接口,要写一堆 Controller、Service、Module、DTO。代码量翻了几倍但核心逻辑一点没变。
* **Swagger 的折磨:** 我想用 Zod 做验证Swagger 却只认装饰器class-validator。为了那张 API 文档,我得被迫维护两份几乎一样的数据定义,手动同步两份 DTO 的痛苦只有写过的人才懂。
**我悟了Monorepo 是为了解决“组织协作”的,对于独立开发者,它往往是效率杀手。** **我用一个可能永远用不上的“大团队优势”,换来了眼前的“低效率”。** 我想要的是快点做出来,但重框架让我优先考虑怎么让代码“更好维护”,这完全本末倒置了。
## 4. 回归务实:我的“两套方案” ## 第四步:换个轻量的,反而更不会写了
在折腾的终点,我反思:有没有完美的框架?结论是没有,只有适合的。于是,我把我的选型逻辑简化成了两套: 我决定不用这些重框架了,选了 **Fastify**,想着以后不行还能换回去。但我发现,问题不只是“重”,而是 **“自由带来的恐慌”**。
* **方案 A快速验证MVP / 个人项目)** Fastify 很自由,但对我这种习惯了某种结构的人来说,自由反而成了压力:
**技术栈Nuxt 一把梭。** 别分前后端了Nuxt 的内置 Server APINitro足够处理中小业务。类型天然共享没有跨域烦恼。在验证阶段**“快”比“优雅”重要一百倍。**
* **方案 B复杂业务大型项目 / 团队协作)** * “代码该放哪?”
**技术栈Nuxt + NestJS 分离 + Monorepo。** 当业务复杂到需要严格的分层、需要依赖注入DI解耦、需要多人协作时才去忍受这种“重”框架带来的仪式感和管理成本。 * “这个功能怎么组织成插件?”
* 日志、队列、鉴权这些在 NestJS 里现成的,现在都要自己找库、集成、调试。
## 5. 题外话:意外发现的新希望 AdonisJS 我嫌重框架规矩多,但真给自由了,我又不知道该怎么用了。我从复杂的流程里出来,又掉进了“什么都要自己找”的坑。
就在我总结完上述方案后,我最近无意间发现了一个新的框架——**AdonisJS**。它被很多开发者评价为 Node.js 界的 Laravel。 **我终于明白:我选的那些框架,都基于我自己的知识惯性。我嫌重框架规矩多,但内心又想要固定结构,这种心态本身就是撕裂的。**
我简单看了一下它的设计理念,感觉它似乎精准地击中了我上述遇到的很多痛点:它原生支持 ESM、内置了强大的 ORM 和 Auth 方案、不需要像 NestJS 那样去折腾各种复杂的适配器来搞定自动化的 Swagger 文档生成。 ## 终于想明白:技术是拿来用的,不是拿来供着的
这种“约定优于配置”的全栈框架,似乎能平衡开发效率与工程质量。我准备在接下来的项目中实际试用一下,如果确实好用,有了心得后再专门写篇文章分享给大家。 又一个加班到很晚的深夜,我对着屏幕问自己:**我折腾这么久,到底图啥?不就是想快点做出这个小工具吗?**
## 6. 总结:给开发者的一点建议 一下子通透了。我之前一直在用“大团队长期维护”的标准,来要求我这个“个人快速项目”。这就像为了去隔壁超市买菜,非要先修一条八车道的高速公路。
1. **没有完美的框架,只有最适合当下的。** 不要指望任何一个明星框架能解决所有问题,它们都有代价。 我给自己定了个简单的规矩:
2. **不要在独立开发或工作中轻易尝试自己不熟悉的技术栈。** 除非你真的有大把的时间和精力去踩坑。你以为在学新技术,其实你是在浪费产品的寿命。
3. **警惕“大厂最佳实践”。** 很多在大公司里解决痛点的工具(如 Monorepo、过度分层在个人项目里往往只会制造痛点。
4. **熟悉度大于先进性。** 哪怕一个框架被说成是“老古董”,只要你用得顺手、能让你早点下班,它就是你的银弹。
**最好的技术栈,是那个能让你忘记技术本身,而专注于创造价值的工具。** * **做原型、小工具、简单应用**:直接用 **Nuxt 一把梭**。前后端在一个包里写,类型天然同步,没有跨域,没有同步内耗。目标就是快,能跑起来比什么都强。
* **复杂系统、多人协作、长期维护**:再考虑 **Nuxt + NestJS + Monorepo**。这时候复杂点能接受,因为确实需要架构支撑。
最后想说,我总结的这些方案也仅仅是适合我个人的开发习惯和目前的认知,并不一定适合所有人。每个人面对的业务场景和技术背景都不同,**大家如果有更好的方案或想法,非常欢迎在评论区讨论,让我也有机会学习一下。** 不对的地方也希望各位大佬多多指教,先行谢过! ## 顺便提一句:看到个新选择
折腾的时候注意到了 **AdonisJS**,有人说是 Node.js 里的 Laravel。简单看了看它好像正好平衡了我的纠结有我要的固定结构ORM、Auth 都有),又是现代的风格,原生支持 ESM。下次可以试试。
## 最后说几句
1. **别盲目追技术热点**:适合别人的不一定适合你。
2. **相信自己用着顺手的感觉**:这比流行趋势重要得多。
3. **先做出来再优化**:对于个人项目,跑不起来的架构就是一堆废纸。
4. **警惕“配置内耗”**:如果配置时间比写代码还长,就该换工具了。
这些就是我折腾一圈的想法。**我的做法可能不对,甚至挺笨的。** 所以想听听你们怎么看。**你们有没有类似的问题?最后怎么解决的?** 欢迎在评论区聊聊,让我也学习一下。

View File

@@ -1,76 +0,0 @@
# 从追逐流行到回归工程:我技术选型的“降噪”思考
过去几年里,我尝试过很多 Node.js 框架和前端技术栈:**Express、Koa、Fastify、Hono.js**,以及前端的 **Next.js**。这些框架和工具都有各自的优点:轻量、灵活、性能好,社区里也有不少人推荐。然而,随着项目的深入,我逐渐发现了一个事实:**简单、快速实现、自己熟悉、生态完善、可扩展性好**,才是最重要的,而不是盲目追逐“最流行”或“性能最高”的框架。
## 为什么不是 Next.js
我也写过 React 和 Next.js但总感觉生态太杂。尤其是 **状态管理**,从 Redux、MobX 到 Zustand、Jotai再到 React Query、TanStack Query每一种都有各自的优缺点但选择过多反而让我心智负担加重。很多问题并不是没有解决方案而是解决方案太多、没有统一的“最佳实践”。
相比之下,**NuxtJS 在 Vue 生态下要更工程化**。比如:
* 内置路由、布局、服务端渲染等功能,不需要额外选型。
* Vue 的响应式和 Pinia 状态管理简单好用,不需要反复对比。
* 配合 Nuxt UI、TailwindCSS可以快速构建出一致的前端体验。
对我来说NuxtJS 的“约定优于配置”正好能让我聚焦在业务上,而不是被生态碎片化牵扯精力。
## 为什么选择 NestJS
很多人说 **NestJS 太重**,但我的实际体验是:**它恰到好处**。
* NestJS 自带 CLI可以快速生成模块、控制器、服务大大提升开发效率。
* 内置依赖注入、装饰器模式,让代码组织更清晰、可维护。
* 和 TypeScript 深度结合,不仅提高了开发体验,也减少了运行时错误。
* 对数据库、认证、缓存、消息队列等都有完善的解决方案,遇到问题时社区文档和生态资源都很丰富。
更重要的是,在 **AI 辅助开发** 的今天,像 NestJS 和 NuxtJS 这种约定清晰、工程化程度高的框架,更容易被 AI 理解和提供正确的解决方案。相比一些过于灵活的轻量框架AI 在这类框架上的“知识盲区”更少。
## 两套技术方案
结合我的经验和实际需求,我总结出了两套适合的技术栈:
### 1. 轻量级方案(业务简单)
* **NuxtJS**
* **Drizzle ORM**
* **Nuxt UI**
* **TailwindCSS**
适合快速原型、简单应用、验证市场。
### 2. 完整方案(业务复杂)
* **NestJS**
* **NuxtJS**
* **Zod**
* **Drizzle ORM**
* **Redis**
* **Postgres**
* **Nuxt UI**
* **TailwindCSS**
适合需要后端独立、扩展性强的项目,可以通过微服务、缓存、消息队列等方式支撑更复杂的业务。
## 性能并不是唯一的答案
很多人推崇 **Hono.js**、**Elysia.js** 等轻量框架,说它们“简单、快、性能好”。确实,在 Hello World 或简单 API 的场景下,它们的性能数据很漂亮。但在真实的生产环境里,**性能瓶颈往往不在框架**,而在:
* 数据库查询优化
* 缓存策略
* 多实例部署
* 微服务架构
* 负载均衡与扩展能力
这时候,单一框架的“性能优势”几乎可以忽略。真正决定系统表现的,是整体架构设计和资源利用。
## 结论
我的体会是:**技术选型没有绝对的好坏,关键在于适合自己和项目需求**。
对我来说NestJS + NuxtJS 这套组合:
* 工程化、生态完善,能快速实现功能。
* 熟悉度高,减少了心智负担。
* 易于扩展,能应对简单和复杂的业务场景。
* 在 AI 辅助的今天,更容易获得准确支持。
因此,哪怕有人说它们“重”或“不如某某框架”,我依然会坚持:**适合自己的,才是最好的技术栈**。

View File

@@ -0,0 +1,342 @@
/**
* Waline Custom Styles
* 适配网站主题风格 - 紫色渐变主题
*/
/* ========== 基础变量覆盖 ========== */
:root {
/* 主题色 - 使用网站的紫色渐变accent */
--waline-theme-color: #8B5CF6;
--waline-active-color: #EC4899;
/* 背景色 */
--waline-bg-color: oklch(1 0 0);
--waline-bg-color-light: oklch(0.97 0 0);
--waline-bg-color-hover: oklch(0.93 0 0);
/* 文字颜色 */
--waline-color: oklch(0.4 0 0);
--waline-light-grey: oklch(0.65 0 0);
--waline-dark-grey: oklch(0.3 0 0);
/* 边框颜色 */
--waline-border-color: oklch(0.87 0 0);
/* 其他颜色 */
--waline-badge-color: #8B5CF6;
--waline-info-bg-color: oklch(0.97 0 0);
--waline-info-color: oklch(0.55 0 0);
--waline-bq-color: oklch(0.93 0 0);
/* 代码块背景 */
--waline-code-bg-color: oklch(0.25 0 0);
/* 圆角 - 与网站一致 */
--waline-border-radius: var(--radius, 0.75rem);
/* 阴影 */
--waline-box-shadow: var(--shadow-md, 0 4px 12px rgba(0, 0, 0, 0.08));
/* 头像圆角 - 使用网站风格 */
--waline-avatar-radius: var(--radius-md, 0.5rem);
/* 按钮样式 */
--waline-btn-radius: var(--radius, 0.75rem);
}
/* ========== 暗黑模式 ========== */
.dark {
--waline-bg-color: oklch(0.205 0 0);
--waline-bg-color-light: oklch(0.269 0 0);
--waline-bg-color-hover: oklch(0.32 0 0);
--waline-color: oklch(0.9 0 0);
--waline-light-grey: oklch(0.6 0 0);
--waline-dark-grey: oklch(0.75 0 0);
--waline-border-color: oklch(1 0 0 / 15%);
--waline-info-bg-color: oklch(0.269 0 0);
--waline-info-color: oklch(0.6 0 0);
--waline-bq-color: oklch(0.269 0 0);
--waline-code-bg-color: oklch(0.12 0 0);
--waline-box-shadow: var(--shadow-md, 0 4px 12px rgba(0, 0, 0, 0.25));
}
/* ========== 评论面板样式 ========== */
.wl-panel {
border-radius: var(--waline-border-radius) !important;
border: 1px solid var(--waline-border-color) !important;
box-shadow: var(--waline-box-shadow) !important;
background: var(--waline-bg-color) !important;
}
/* ========== 输入框样式 ========== */
.wl-header {
border-bottom-color: var(--waline-border-color) !important;
}
.wl-header label {
color: var(--waline-color) !important;
}
.wl-header input {
background: transparent !important;
}
.wl-editor {
background: var(--waline-bg-color-light) !important;
border-radius: var(--waline-btn-radius) !important;
}
.wl-editor:focus {
background: var(--waline-bg-color-hover) !important;
}
/* ========== 按钮样式 ========== */
.wl-btn {
border-radius: var(--waline-btn-radius) !important;
font-size: 0.875em !important;
transition: all 0.2s ease !important;
}
.wl-btn:hover {
border-color: var(--waline-theme-color) !important;
color: var(--waline-theme-color) !important;
background: var(--waline-bg-color-light) !important;
}
.wl-btn.primary {
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important;
border: none !important;
color: white !important;
font-weight: 500 !important;
}
.wl-btn.primary:hover {
background: linear-gradient(135deg, #7C3AED, #DB2777) !important;
border: none !important;
color: white !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
/* ========== 头部标签页 ========== */
.wl-header label {
transition: all 0.2s ease !important;
}
.wl-header label:hover {
background: var(--waline-bg-color-light) !important;
}
/* ========== 评论卡片样式 ========== */
.wl-card {
border-bottom-color: var(--waline-border-color) !important;
}
.wl-card:first-child {
margin-inline-start: 0 !important;
}
.wl-nick {
color: var(--waline-color) !important;
}
.wl-time {
color: var(--waline-info-color) !important;
}
.wl-content {
color: var(--waline-color) !important;
line-height: 1.8 !important;
}
/* ========== 徽章样式 ========== */
.wl-badge {
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important;
border: none !important;
color: white !important;
font-size: 0.7em !important;
padding: 2px 8px !important;
border-radius: 9999px !important;
}
/* ========== 操作按钮 ========== */
.wl-action {
color: var(--waline-light-grey) !important;
transition: all 0.2s ease !important;
}
.wl-action:hover {
color: var(--waline-theme-color) !important;
transform: scale(1.1);
}
.wl-action.active {
color: var(--waline-active-color) !important;
}
/* ========== 评论操作 ========== */
.wl-delete:hover,
.wl-like:hover,
.wl-reply:hover,
.wl-edit:hover {
color: var(--waline-theme-color) !important;
}
.wl-delete.active,
.wl-like.active,
.wl-reply.active,
.wl-edit.active {
color: var(--waline-active-color) !important;
}
/* ========== 排序按钮 ========== */
.wl-sort li {
color: var(--waline-info-color) !important;
transition: all 0.2s ease !important;
}
.wl-sort li:hover {
color: var(--waline-theme-color) !important;
}
.wl-sort li.active {
color: var(--waline-theme-color) !important;
font-weight: 600;
}
/* ========== 表情弹窗 ========== */
.wl-emoji-popup {
border-radius: var(--waline-border-radius) !important;
box-shadow: var(--waline-box-shadow) !important;
background: var(--waline-bg-color) !important;
border: 1px solid var(--waline-border-color) !important;
}
.wl-emoji-popup .wl-tab.active {
background: var(--waline-bg-color-light) !important;
}
.wl-emoji-popup button:hover {
background: var(--waline-bg-color-hover) !important;
}
/* ========== GIF 弹窗 ========== */
.wl-gif-popup {
border-radius: var(--waline-border-radius) !important;
box-shadow: var(--waline-box-shadow) !important;
background: var(--waline-bg-color) !important;
border: 1px solid var(--waline-border-color) !important;
}
/* ========== 评论统计 ========== */
.wl-count {
color: var(--waline-theme-color) !important;
font-size: 1.5em !important;
font-weight: 700 !important;
}
/* ========== 空状态 ========== */
.wl-empty {
color: var(--waline-info-color) !important;
}
/* ========== 头像样式 ========== */
.wl-avatar {
border-radius: var(--waline-avatar-radius) !important;
border: 2px solid var(--waline-border-color) !important;
box-shadow: var(--waline-box-shadow) !important;
}
/* ========== 回复引用框 ========== */
.wl-quote {
border-left-color: var(--waline-border-color) !important;
}
/* ========== 预览区域 ========== */
.wl-preview .wl-content {
background: var(--waline-bg-color-light) !important;
border-radius: var(--waline-btn-radius) !important;
}
/* ========== 反应区域 ========== */
.wl-reaction-title {
color: var(--waline-color) !important;
}
.wl-reaction-item.active .wl-reaction-text {
color: var(--waline-theme-color) !important;
}
.wl-reaction-votes {
border-color: var(--waline-theme-color) !important;
color: var(--waline-theme-color) !important;
}
.wl-reaction-item.active .wl-reaction-votes {
background: var(--waline-theme-color) !important;
color: white !important;
}
/* ========== 加载动画 ========== */
.wl-loading svg {
color: var(--waline-theme-color) !important;
}
/* ========== 滚动条样式 ========== */
[data-waline] ::-webkit-scrollbar {
width: 6px;
height: 6px;
}
[data-waline] ::-webkit-scrollbar-track-piece {
background: var(--waline-bg-color-light);
border-radius: 3px;
}
[data-waline] ::-webkit-scrollbar-thumb {
background: var(--waline-theme-color);
border-radius: 3px;
}
[data-waline] ::-webkit-scrollbar-thumb:hover {
background: var(--waline-active-color);
}
/* ========== 链接样式 ========== */
[data-waline] a {
color: var(--waline-theme-color) !important;
text-decoration: none !important;
transition: all 0.2s ease !important;
}
[data-waline] a:hover {
color: var(--waline-active-color) !important;
}
/* ========== 代码块样式 ========== */
.wl-content pre,
.wl-content pre[class*="language-"] {
background: var(--waline-code-bg-color) !important;
border-radius: var(--waline-btn-radius) !important;
}
/* ========== 移动端适配 ========== */
@media (max-width: 640px) {
.wl-panel {
border-radius: var(--radius-md, 0.5rem) !important;
}
.wl-btn {
font-size: 0.8em !important;
padding: 0.4em 0.8em !important;
}
.wl-editor {
min-height: 6em !important;
}
}