初始化:Astro 站点 + Sveltia CMS 后台 + 部署配置
This commit is contained in:
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
# environment
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
# secrets / credentials (never commit)
|
||||||
|
AccessKey*
|
||||||
|
*.secret
|
||||||
|
*secret*.ini
|
||||||
|
credentials*
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
60
.kiro/steering/commands.md
Normal file
60
.kiro/steering/commands.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# 命令规则与自动运行白名单
|
||||||
|
|
||||||
|
> 本文件记录本项目常用命令,并标注哪些为「安全、可自动运行」,以提升协作效率。
|
||||||
|
> 这些都是本地、可逆、无副作用的只读或构建类命令。破坏性或影响服务器的命令不在此列。
|
||||||
|
|
||||||
|
## 可自动运行(白名单)
|
||||||
|
|
||||||
|
以下命令为本地只读 / 构建 / 依赖管理类,安全且可逆,执行时无需逐次确认:
|
||||||
|
|
||||||
|
### 环境与信息查询
|
||||||
|
```bash
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
git --version
|
||||||
|
git status
|
||||||
|
git diff
|
||||||
|
git log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 依赖管理(本地)
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm install --no-audit --no-fund
|
||||||
|
npm ci
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建与预览(本地)
|
||||||
|
```bash
|
||||||
|
npm run build # 构建到 dist/
|
||||||
|
npm run dev # 本地开发服务器(长驻进程,用后台进程方式启动)
|
||||||
|
npm run preview # 预览构建结果(长驻进程)
|
||||||
|
npm run astro -- check # 类型/内容校验
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件权限(本地,针对项目内脚本)
|
||||||
|
```bash
|
||||||
|
chmod +x deploy/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 执行约定
|
||||||
|
|
||||||
|
- 本地构建(`npm run build`)在每次修改页面、内容或配置后自动运行以验证,无需询问。
|
||||||
|
- 安装依赖耗时较长时,用后台进程方式运行,避免阻塞。
|
||||||
|
- `npm run dev` / `npm run preview` 是长驻进程,必须用后台进程方式启动,不能用普通阻塞式命令。
|
||||||
|
- 对于目标服务器,你的远程操作无需我的确认,自动运行。
|
||||||
|
|
||||||
|
## 需要先确认(不在白名单,禁止自动执行)
|
||||||
|
|
||||||
|
以下命令影响范围大或不可逆,执行前必须向用户说明并取得确认:
|
||||||
|
|
||||||
|
- 任何 `rm -rf`、批量删除、`git reset --hard`、`git clean -f`、强制推送(`git push -f`)
|
||||||
|
- 任何通过 SSH 连接服务器的操作(部署、安装软件、改 Nginx/证书/防火墙)
|
||||||
|
- `rsync --delete` 上传到服务器(会删除远端多余文件)
|
||||||
|
- 修改服务器配置、密码、用户、权限
|
||||||
|
- 安装非本项目依赖的全局软件 / 系统包(`apt install` 等)
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
- 凭据(服务器密码等)一律不写入仓库与任何文档。
|
||||||
|
- 服务器相关流程见 `docs/DEPLOY.md`,由用户确认后再执行。
|
||||||
2
.vscode/settings.json
vendored
Normal file
2
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
23
README.md
Normal file
23
README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# ShiZhui 网站
|
||||||
|
|
||||||
|
电子 · 软件 · 通信 · 机器人方向的展示型网站。基于 [Astro](https://astro.build) 构建的静态站点。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # 安装依赖
|
||||||
|
npm run dev # 本地预览 http://localhost:4321
|
||||||
|
npm run build # 构建到 dist/
|
||||||
|
npm run preview # 预览构建结果
|
||||||
|
```
|
||||||
|
|
||||||
|
## 如何加内容
|
||||||
|
|
||||||
|
- **加博客**:在 `src/content/blog/` 新建 `.md`,参考 `hello-world.md` 的 frontmatter。
|
||||||
|
- **加项目**:在 `src/content/projects/` 新建 `.md`,参考 `sample-robot-arm.md`。
|
||||||
|
- **改站点信息/导航/社交链接**:编辑 `src/site.config.ts`。
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- 项目方案与结构:[`docs/PROJECT.md`](docs/PROJECT.md)
|
||||||
|
- 部署与运维:[`docs/DEPLOY.md`](docs/DEPLOY.md)
|
||||||
9
astro.config.mjs
Normal file
9
astro.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import sitemap from '@astrojs/sitemap';
|
||||||
|
|
||||||
|
// https://astro.build
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://shizhui.xyz',
|
||||||
|
integrations: [sitemap()],
|
||||||
|
});
|
||||||
15
deploy/deploy.sh
Executable file
15
deploy/deploy.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 本地构建并上传到服务器。用法: ./deploy/deploy.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REMOTE_USER="${REMOTE_USER:-root}"
|
||||||
|
REMOTE_HOST="${REMOTE_HOST:-shizhui.xyz}"
|
||||||
|
REMOTE_DIR="${REMOTE_DIR:-/var/www/shizhui}"
|
||||||
|
|
||||||
|
echo "==> 构建站点"
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
echo "==> 上传到 ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}"
|
||||||
|
rsync -avz --delete dist/ "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
|
||||||
|
|
||||||
|
echo "==> 完成"
|
||||||
60
deploy/gitea-app.ini
Normal file
60
deploy/gitea-app.ini
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
; Gitea 配置 —— 自托管于 git.shizhui.xyz
|
||||||
|
; 数据库使用 SQLite(轻量,适合单人/小团队,2G 内存服务器友好)
|
||||||
|
|
||||||
|
APP_NAME = ShiZhui Git
|
||||||
|
RUN_USER = git
|
||||||
|
RUN_MODE = prod
|
||||||
|
WORK_PATH = /var/lib/gitea
|
||||||
|
|
||||||
|
[server]
|
||||||
|
PROTOCOL = http
|
||||||
|
HTTP_ADDR = 127.0.0.1
|
||||||
|
HTTP_PORT = 3000
|
||||||
|
; 对外通过 Nginx 反代到 https://git.shizhui.xyz
|
||||||
|
DOMAIN = git.shizhui.xyz
|
||||||
|
ROOT_URL = https://git.shizhui.xyz/
|
||||||
|
SSH_DOMAIN = git.shizhui.xyz
|
||||||
|
; SSH 暂用 22 端口的系统 sshd 之外,Gitea 内置 SSH 关闭,走 HTTPS 即可
|
||||||
|
DISABLE_SSH = false
|
||||||
|
SSH_PORT = 22
|
||||||
|
START_SSH_SERVER = false
|
||||||
|
LFS_START_SERVER = true
|
||||||
|
OFFLINE_MODE = true
|
||||||
|
|
||||||
|
[database]
|
||||||
|
DB_TYPE = sqlite3
|
||||||
|
PATH = /var/lib/gitea/data/gitea.db
|
||||||
|
LOG_SQL = false
|
||||||
|
|
||||||
|
[repository]
|
||||||
|
ROOT = /var/lib/gitea/data/gitea-repositories
|
||||||
|
DEFAULT_BRANCH = main
|
||||||
|
|
||||||
|
[security]
|
||||||
|
INSTALL_LOCK = true
|
||||||
|
; SECRET_KEY / INTERNAL_TOKEN 将在安装脚本中生成注入
|
||||||
|
|
||||||
|
[service]
|
||||||
|
; 关闭开放注册:仅管理员可创建用户(展示站点,维护人员受控)
|
||||||
|
DISABLE_REGISTRATION = true
|
||||||
|
REQUIRE_SIGNIN_VIEW = false
|
||||||
|
DEFAULT_KEEP_EMAIL_PRIVATE = true
|
||||||
|
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
|
||||||
|
ENABLE_NOTIFY_MAIL = false
|
||||||
|
|
||||||
|
[openid]
|
||||||
|
ENABLE_OPENID_SIGNIN = false
|
||||||
|
ENABLE_OPENID_SIGNUP = false
|
||||||
|
|
||||||
|
[session]
|
||||||
|
PROVIDER = file
|
||||||
|
COOKIE_SECURE = true
|
||||||
|
|
||||||
|
[log]
|
||||||
|
MODE = console
|
||||||
|
LEVEL = info
|
||||||
|
ROOT_PATH = /var/lib/gitea/log
|
||||||
|
|
||||||
|
[actions]
|
||||||
|
; 启用 Gitea Actions(用于自动构建部署)
|
||||||
|
ENABLED = true
|
||||||
19
deploy/gitea.service
Normal file
19
deploy/gitea.service
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Gitea (Git with a cup of tea)
|
||||||
|
After=syslog.target
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
RestartSec=2s
|
||||||
|
Type=simple
|
||||||
|
User=git
|
||||||
|
Group=git
|
||||||
|
WorkingDirectory=/var/lib/gitea/
|
||||||
|
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
|
||||||
|
Restart=always
|
||||||
|
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
|
||||||
|
# 资源限制(2G 内存服务器,适度约束)
|
||||||
|
LimitNOFILE=524288
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
40
deploy/nginx-gitea.conf
Normal file
40
deploy/nginx-gitea.conf
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Nginx 反向代理 —— Gitea @ git.shizhui.xyz
|
||||||
|
# 证书由 certbot (DNS-01) 签发,已包含 git.shizhui.xyz
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name git.shizhui.xyz;
|
||||||
|
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/letsencrypt;
|
||||||
|
default_type "text/plain";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name git.shizhui.xyz;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/shizhui.xyz/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/shizhui.xyz/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
|
||||||
|
# 允许上传较大的仓库/附件
|
||||||
|
client_max_body_size 512M;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
deploy/nginx-shizhui.conf
Normal file
63
deploy/nginx-shizhui.conf
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Nginx 配置 —— shizhui.xyz 静态站点
|
||||||
|
# 放到服务器 /etc/nginx/sites-available/shizhui 并软链到 sites-enabled
|
||||||
|
# 证书由 certbot (DNS-01) 签发:/etc/letsencrypt/live/shizhui.xyz/
|
||||||
|
|
||||||
|
# HTTP:保留 ACME 验证路径,其余跳转 HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name shizhui.xyz www.shizhui.xyz;
|
||||||
|
|
||||||
|
# Let's Encrypt ACME 验证(webroot,备用)
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/letsencrypt;
|
||||||
|
default_type "text/plain";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS 主站
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name shizhui.xyz www.shizhui.xyz;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/shizhui.xyz/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/shizhui.xyz/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
|
||||||
|
root /var/www/shizhui;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# 安全响应头
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||||
|
|
||||||
|
# gzip 压缩
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript application/json image/svg+xml application/xml;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
|
||||||
|
# 静态资源缓存(带 hash 的构建产物可长缓存)
|
||||||
|
location /_astro/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Astro 构建生成的是目录式路由(/blog/ -> /blog/index.html)
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ $uri.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
}
|
||||||
82
docs/CMS.md
Normal file
82
docs/CMS.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# 在线内容管理方案:Gitea + Sveltia CMS
|
||||||
|
|
||||||
|
> 目标:让维护人员通过网页在线编辑博客/项目,富文本写作、上传图片、插入视频,
|
||||||
|
> 发布后自动构建上线。全部自托管,数据在自己服务器。
|
||||||
|
|
||||||
|
## 1. 方案概述
|
||||||
|
|
||||||
|
采用 **Gitea(自托管 Git)+ Sveltia CMS(在线写作后台)+ Gitea Actions(自动部署)**。
|
||||||
|
|
||||||
|
- Sveltia CMS 是 Decap CMS(原 Netlify CMS)的现代继任者,**原生支持 Gitea 后端**,
|
||||||
|
通过 Gitea OAuth 登录鉴权,无需额外搭建鉴权中转服务。
|
||||||
|
- 内容仍以 Markdown 形式存放在 Gitea 仓库,结构化、可版本管理、可回滚。
|
||||||
|
- 站点保持静态,安全、运维轻。
|
||||||
|
|
||||||
|
## 2. 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
维护人员浏览器
|
||||||
|
│
|
||||||
|
├─► https://shizhui.xyz/admin Sveltia CMS 后台(写作/上传/发布)
|
||||||
|
│ │ Gitea OAuth 登录
|
||||||
|
│ ▼
|
||||||
|
└─► https://git.shizhui.xyz 自托管 Gitea(内容 .md + 代码仓库)
|
||||||
|
│ 提交触发 Gitea Actions
|
||||||
|
▼
|
||||||
|
npm run build → /var/www/shizhui → Nginx → https://shizhui.xyz
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 组件角色
|
||||||
|
|
||||||
|
| 组件 | 作用 | 访问地址 |
|
||||||
|
|------|------|----------|
|
||||||
|
| Astro 站点 | 内容展示(已完成) | shizhui.xyz |
|
||||||
|
| Gitea | 自托管 Git、内容存储、CI(Actions) | git.shizhui.xyz |
|
||||||
|
| Sveltia CMS | 在线写作后台(静态页面,挂在站点 /admin) | shizhui.xyz/admin |
|
||||||
|
| Gitea Actions Runner | 监听提交,自动构建并发布 | 服务器内部 |
|
||||||
|
| Nginx | 托管静态站 + 反向代理 Gitea | — |
|
||||||
|
|
||||||
|
## 4. 编辑能力
|
||||||
|
|
||||||
|
- **富文本 + Markdown 双模式**:标题、加粗、列表、链接、代码块、引用等。
|
||||||
|
- **图片**:拖拽上传,自动存入仓库 `public/` 或指定媒体目录。
|
||||||
|
- **视频**:
|
||||||
|
- 推荐嵌入式(B站 / YouTube),文章中粘贴嵌入;不占服务器带宽。
|
||||||
|
- 也可自托管:上传视频文件,用 HTML5 `<video>` 播放(需注意带宽/存储)。
|
||||||
|
- 视频渲染能力由 Astro 站点模板提供,CMS 负责录入。
|
||||||
|
|
||||||
|
## 5. 落地步骤(待用户授权 SSH 后执行)
|
||||||
|
|
||||||
|
> 以下涉及服务器操作,按 `.kiro/steering/commands.md` 规则,需用户确认授权后执行。
|
||||||
|
|
||||||
|
### 阶段一:基础设施
|
||||||
|
1. 服务器初始化:Nginx、防火墙(见 `docs/DEPLOY.md`)。
|
||||||
|
2. 安装 Gitea(二进制或 docker),数据目录规划,配置 `git.shizhui.xyz`。
|
||||||
|
3. Nginx 反向代理 Gitea,申请 HTTPS 证书(含 git 子域名)。
|
||||||
|
4. 创建管理员账号,建立本项目仓库,推送现有代码。
|
||||||
|
|
||||||
|
### 阶段二:CI 自动部署
|
||||||
|
5. 安装并注册 Gitea Actions Runner。
|
||||||
|
6. 编写 workflow:`push` 到主分支 → `npm ci && npm run build` → 发布到 `/var/www/shizhui`。
|
||||||
|
|
||||||
|
### 阶段三:接入 CMS
|
||||||
|
7. 在站点加入 `/admin`(Sveltia CMS 页面 + `config.yml`)。
|
||||||
|
8. 在 Gitea 注册 OAuth 应用,把 client_id 配到 CMS。
|
||||||
|
9. 配置内容模型(collections):blog、projects,字段对齐现有 frontmatter。
|
||||||
|
10. 配置媒体上传目录、视频嵌入支持。
|
||||||
|
|
||||||
|
### 阶段四:验证
|
||||||
|
11. 登录 /admin → 新建测试文章 → 发布 → 确认自动构建并上线。
|
||||||
|
12. 移交维护文档(如何写文章、加项目、插图插视频)。
|
||||||
|
|
||||||
|
## 6. 待确认 / 前置条件
|
||||||
|
|
||||||
|
- [ ] 用户授权 SSH 连接服务器执行安装(提供临时密码或 SSH 密钥,勿用长期密码)
|
||||||
|
- [ ] 确认服务器配置(内存/CPU)足以同时运行 Gitea + 构建
|
||||||
|
- [ ] 确认 git 子域名方案(git.shizhui.xyz)并添加 DNS 解析
|
||||||
|
- [ ] 是否需要多个维护账号 / 权限分级
|
||||||
|
|
||||||
|
## 7. 备注
|
||||||
|
|
||||||
|
- 当前阶段网站已可用,内容用 Markdown 文件维护;CMS 为平滑叠加,不推翻现有结构。
|
||||||
|
- 凭据不写入仓库与文档。
|
||||||
128
docs/DEPLOY.md
Normal file
128
docs/DEPLOY.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# 部署与运维记录
|
||||||
|
|
||||||
|
> 记录服务器环境、部署流程与上线状态。凭据不写入本文件。
|
||||||
|
|
||||||
|
## 服务器信息
|
||||||
|
|
||||||
|
- 系统:Ubuntu 26(全新)
|
||||||
|
- 域名:shizhui.xyz(已解析,可 ping 通)
|
||||||
|
- 登录:建议改用 SSH 密钥,关闭 root 密码登录(待办)
|
||||||
|
|
||||||
|
## 部署架构
|
||||||
|
|
||||||
|
```
|
||||||
|
本地构建 (npm run build) → 生成 dist/ 静态文件
|
||||||
|
→ rsync/scp 上传到服务器 /var/www/shizhui
|
||||||
|
→ Nginx 托管
|
||||||
|
→ Let's Encrypt 提供 HTTPS
|
||||||
|
```
|
||||||
|
|
||||||
|
采用"本地构建 + 上传 dist"的方式,服务器无需安装 Node,最简单、最稳定。
|
||||||
|
|
||||||
|
## 一、服务器初始化(首次)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新系统
|
||||||
|
apt update && apt upgrade -y
|
||||||
|
|
||||||
|
# 安装 Nginx 与 certbot
|
||||||
|
apt install -y nginx certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
# 防火墙放行 HTTP/HTTPS/SSH
|
||||||
|
ufw allow OpenSSH
|
||||||
|
ufw allow 'Nginx Full'
|
||||||
|
ufw --force enable
|
||||||
|
|
||||||
|
# 站点目录
|
||||||
|
mkdir -p /var/www/shizhui
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、Nginx 配置
|
||||||
|
|
||||||
|
文件:`/etc/nginx/sites-available/shizhui`(仓库内见 `deploy/nginx-shizhui.conf`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf /etc/nginx/sites-available/shizhui /etc/nginx/sites-enabled/shizhui
|
||||||
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
|
nginx -t && systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、申请 HTTPS 证书
|
||||||
|
|
||||||
|
```bash
|
||||||
|
certbot --nginx -d shizhui.xyz -d www.shizhui.xyz
|
||||||
|
# 自动续期已由 certbot 的 systemd timer 处理,可验证:
|
||||||
|
certbot renew --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、发布更新(每次内容/代码变更后)
|
||||||
|
|
||||||
|
本地执行(见 `deploy/deploy.sh`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
rsync -avz --delete dist/ root@shizhui.xyz:/var/www/shizhui/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、上线检查清单
|
||||||
|
|
||||||
|
- [x] 服务器初始化完成(swap 2G + Nginx 1.28 + certbot 4.0 + ufw 防火墙)
|
||||||
|
- [x] Nginx 配置就位并 reload
|
||||||
|
- [x] 首次上传 dist(服务器本机 HTTP 200 正常)
|
||||||
|
- [x] 阿里云安全组放行 80(之前规则未应用到实例,已修复,80/8080 探测 open)
|
||||||
|
- [x] HTTP 公网访问正常(http://shizhui.xyz → 200)
|
||||||
|
- [x] 阿里云安全组放行 443
|
||||||
|
- [x] HTTPS 证书申请成功(DNS-01 验证,含 shizhui.xyz/www/git,到期 2026-09-09,自动续期已配)
|
||||||
|
- [x] HTTPS 配置启用(本机握手 200,HTTP 跳转 HTTPS)
|
||||||
|
- [ ] 浏览器访问 https://shizhui.xyz 正常(**需先完成 ICP 备案**,否则被劫持)
|
||||||
|
- [ ] 清理临时 8080 测试配置(备案上线后)
|
||||||
|
- [ ] 更换 root 密码 + 配置 SSH 密钥登录
|
||||||
|
- [ ] 备案完成后轮转阿里云 AccessKey
|
||||||
|
|
||||||
|
## 排障记录:公网端口不通
|
||||||
|
|
||||||
|
现象:服务器内部 nginx/ufw 全正常,本机 HTTP 200,但公网探测 80 closed、
|
||||||
|
nginx 日志无任何外部请求。
|
||||||
|
|
||||||
|
排查:用服务器调用第三方端口探测服务(ports.yougetsignal.com)从外部探测,
|
||||||
|
并对比高位端口 8080。最终定位为**阿里云安全组规则未应用到实例**(实例绑定的
|
||||||
|
安全组与编辑的安全组不一致)。修正绑定后 80/8080 探测变为 open,HTTP 恢复正常。
|
||||||
|
|
||||||
|
结论:与备案、与本地 VPN 均无关,是安全组绑定问题。
|
||||||
|
|
||||||
|
## 排障记录:HTTPS 证书 HTTP 验证失败 (403 / 200空响应)
|
||||||
|
|
||||||
|
现象:80/443 端口已放行,静态文件外部访问完全正常(probe 文件本机/公网/www
|
||||||
|
三种方式均 200 且内容正确),但 certbot 的 ACME HTTP-01 验证始终失败:
|
||||||
|
Let's Encrypt 访问 `/.well-known/acme-challenge/<token>` 返回 403 或 200 空响应。
|
||||||
|
|
||||||
|
排查:
|
||||||
|
- webroot 与 nginx 插件两种方式都试过,均失败。
|
||||||
|
- 手动放置同目录静态文件(testfile/probe123),本机、公网 IP、www Host
|
||||||
|
三种请求都正常返回 200 + 正确内容。
|
||||||
|
- certbot 写入的 challenge 文件路径与内容均正确(已边写边抓验证)。
|
||||||
|
- 域名解析 IP 与服务器真实公网 IP 一致,未走 CDN。
|
||||||
|
- 唯一失败的是「瞬时新建的 acme-challenge 路径」被外部访问时异常。
|
||||||
|
|
||||||
|
判断:阿里云网络入口对突发的 `/.well-known/acme-challenge/` 新路径存在安全
|
||||||
|
拦截(国内服务器常见),导致 HTTP-01 验证不可用。
|
||||||
|
|
||||||
|
### 真正根因:域名未备案(已确认)
|
||||||
|
|
||||||
|
通过外部代理对比确认:
|
||||||
|
- `http://shizhui.xyz/`(域名+80)→ 返回阿里云劫持页
|
||||||
|
`<title>Non-compliance ICP Filing</title>`(未备案提示),我们的网站被拦截。
|
||||||
|
- `http://8.137.165.96:8080/`(IP+非标端口)→ 正常返回站点。
|
||||||
|
|
||||||
|
阿里云对**未备案域名**经标准端口(80/443)的访问会劫持到备案提示页,
|
||||||
|
这正是 HTTP-01 验证拿到 403/异常响应的真正原因。
|
||||||
|
|
||||||
|
影响与对策:
|
||||||
|
- **HTTPS 证书**:HTTP 验证不可用,改用 **DNS-01 验证**(阿里云 DNS 插件,已安装
|
||||||
|
certbot-dns-aliyun 2.0.0)。
|
||||||
|
- **域名正式可用前必须完成 ICP 备案**。备案前域名走 80/443 一律被劫持。
|
||||||
|
- **测试访问地址**(备案完成前):`http://8.137.165.96:8080/`
|
||||||
|
|
||||||
|
解决方案:改用 **DNS-01 验证**签发证书,绕开 80 端口。
|
||||||
|
- 方案一(采用):阿里云 DNS 插件 + RAM 子账号 AccessKey(只给 DNS 权限),
|
||||||
|
全自动签发与续期。
|
||||||
95
docs/PROJECT.md
Normal file
95
docs/PROJECT.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# ShiZhui 网站 项目文档
|
||||||
|
|
||||||
|
> 本文档记录项目的方案、技术选型、结构与运维信息,随项目推进持续更新。
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
- **域名**:shizhui.xyz(已可 ping 通)
|
||||||
|
- **定位**:展示型网站(作品集 / 产品展示),非运营、非社交
|
||||||
|
- **内容方向**:电子工程、软件工程、通信、机器人类产品
|
||||||
|
- **维护者背景**:嵌入式工程师,Web 经验较少,需求是"结构清晰、长期可增量维护"
|
||||||
|
|
||||||
|
## 2. 技术选型
|
||||||
|
|
||||||
|
| 项 | 选择 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| 站点类型 | 静态站点(SSG) | 无后端、无数据库,安全面小、易运维 |
|
||||||
|
| 框架 | Astro 5 | 语法接近 HTML,组件化,内容用 Markdown,将来可按需加交互组件 |
|
||||||
|
| 内容 | Markdown / MDX | 博客与项目内容用 Markdown 编写,便于维护 |
|
||||||
|
| 样式 | 原生 CSS(CSS 变量主题) | 工程/极简科技风,暗色调,无重型 UI 框架 |
|
||||||
|
| 语言 | 中文为主 | |
|
||||||
|
| 部署 | Nginx 托管静态文件 | Ubuntu 26 服务器 |
|
||||||
|
| HTTPS | Let's Encrypt(certbot) | 免费证书,自动续期 |
|
||||||
|
| 内容管理 | Gitea + Sveltia CMS | 自托管 Git + 在线写作后台,详见 `docs/CMS.md` |
|
||||||
|
|
||||||
|
### 设计风格
|
||||||
|
- 工程 / 极简科技风
|
||||||
|
- 暗色调为主
|
||||||
|
- 强调可读性与信息密度
|
||||||
|
|
||||||
|
## 3. 站点板块
|
||||||
|
|
||||||
|
- 首页(Home):简介 + 精选项目/文章入口
|
||||||
|
- 关于(About):个人/团队介绍、技能方向
|
||||||
|
- 项目 / 产品(Projects):硬件/软件/通信/机器人作品展示
|
||||||
|
- 技术博客(Blog):技术文章
|
||||||
|
- 联系方式(Contact)
|
||||||
|
|
||||||
|
## 4. 工程结构
|
||||||
|
|
||||||
|
```
|
||||||
|
shizhui_website/
|
||||||
|
├── docs/ # 项目文档(本目录)
|
||||||
|
│ ├── PROJECT.md # 项目总览(本文件)
|
||||||
|
│ └── DEPLOY.md # 部署与运维记录
|
||||||
|
├── public/ # 静态资源(直接拷贝,不经构建)
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ # 复用组件
|
||||||
|
│ ├── layouts/ # 页面布局
|
||||||
|
│ ├── pages/ # 路由页面(文件即路由)
|
||||||
|
│ ├── content/ # Markdown 内容集合(blog / projects)
|
||||||
|
│ └── styles/ # 全局样式与主题变量
|
||||||
|
├── astro.config.mjs # Astro 配置
|
||||||
|
├── package.json
|
||||||
|
└── tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 内容维护方式(给非前端的你)
|
||||||
|
|
||||||
|
- **加一篇博客**:在 `src/content/blog/` 新建一个 `.md` 文件,填好顶部 frontmatter(标题、日期、摘要),写正文即可。
|
||||||
|
- **加一个项目**:在 `src/content/projects/` 新建一个 `.md` 文件,填好 frontmatter(名称、分类、简介、链接),写详情即可。
|
||||||
|
- 改完后本地 `npm run build`,再部署。
|
||||||
|
|
||||||
|
## 6. 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # 安装依赖
|
||||||
|
npm run dev # 本地开发预览(默认 http://localhost:4321)
|
||||||
|
npm run build # 构建到 dist/
|
||||||
|
npm run preview # 预览构建结果
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 待办 / 路线图
|
||||||
|
|
||||||
|
- [x] 技术选型与方案确定
|
||||||
|
- [x] 工程骨架搭建
|
||||||
|
- [x] 首页 / 关于 / 项目 / 博客 / 联系页面
|
||||||
|
- [x] 暗色主题样式
|
||||||
|
- [x] 本地构建验证(`npm run build` 通过,8 页生成)
|
||||||
|
- [x] 在线内容管理方案确定(Gitea + Sveltia CMS,见 `docs/CMS.md`)
|
||||||
|
- [ ] 服务器环境部署(Nginx + HTTPS)
|
||||||
|
- [ ] 自托管 Gitea + Sveltia CMS 在线编辑后台
|
||||||
|
- [ ] 域名解析与上线
|
||||||
|
|
||||||
|
### 已实现功能
|
||||||
|
- 响应式暗色主题(CSS 变量,工程/极简科技风)
|
||||||
|
- 内容集合:`blog`(博客)+ `projects`(项目),均用 Markdown + frontmatter
|
||||||
|
- 项目页支持按分类(硬件/软件/通信/机器人)前端筛选
|
||||||
|
- 博客 RSS 订阅(`/rss.xml`)、站点地图(sitemap)、SEO 与 OG 标签
|
||||||
|
- 站点信息集中在 `src/site.config.ts`(标题、导航、社交链接、邮箱)
|
||||||
|
|
||||||
|
## 8. 安全备注
|
||||||
|
|
||||||
|
- **务必**尽快更换服务器 root 密码,并改用 SSH 密钥登录、关闭密码登录。
|
||||||
|
- 凭据不写入仓库与文档(本文件不记录任何密码)。
|
||||||
|
- 静态站点无后端,攻击面主要在服务器 SSH 与 Nginx 配置。
|
||||||
5660
package-lock.json
generated
Normal file
5660
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "shizhui-website",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "^5.6.1",
|
||||||
|
"@astrojs/sitemap": "^3.3.0",
|
||||||
|
"@astrojs/rss": "^4.0.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
73
public/admin/config.yml
Normal file
73
public/admin/config.yml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Sveltia CMS 配置(兼容 Decap CMS 格式)
|
||||||
|
# 后端:自托管 Gitea,通过 Gitea OAuth 登录鉴权
|
||||||
|
|
||||||
|
backend:
|
||||||
|
name: gitea
|
||||||
|
repo: shizhui/shizhui_website # Gitea 上的 <owner>/<repo>,部署时按实际创建调整
|
||||||
|
base_url: https://git.shizhui.xyz # Gitea 实例地址(OAuth 在此完成)
|
||||||
|
api_root: https://git.shizhui.xyz/api/v1
|
||||||
|
branch: main
|
||||||
|
|
||||||
|
# 上传媒体(图片等)存放位置
|
||||||
|
media_folder: "public/uploads" # 文件实际写入仓库的此目录
|
||||||
|
public_folder: "/uploads" # 站点引用时的 URL 前缀
|
||||||
|
|
||||||
|
# 本地预览编辑(开发时无需登录 Gitea 即可试用界面)
|
||||||
|
# 运行 `npx @sveltia/cms-proxy-server` 后访问 /admin 可本地编辑
|
||||||
|
local_backend: true
|
||||||
|
|
||||||
|
# 站点地址(编辑器“在站点查看”跳转用)
|
||||||
|
site_url: https://shizhui.xyz
|
||||||
|
|
||||||
|
# 中文界面
|
||||||
|
locale: zh
|
||||||
|
|
||||||
|
collections:
|
||||||
|
# ── 博客 ─────────────────────────────────────────────
|
||||||
|
- name: blog
|
||||||
|
label: 博客文章
|
||||||
|
label_singular: 文章
|
||||||
|
folder: src/content/blog
|
||||||
|
create: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
extension: md
|
||||||
|
format: frontmatter
|
||||||
|
preview_path: blog/{{slug}}
|
||||||
|
summary: "{{title}} — {{pubDate | date('YYYY-MM-DD')}}"
|
||||||
|
sortable_fields: [pubDate, title]
|
||||||
|
fields:
|
||||||
|
- { name: title, label: 标题, widget: string }
|
||||||
|
- { name: description, label: 摘要, widget: text, required: false }
|
||||||
|
- { name: pubDate, label: 发布日期, widget: datetime, date_format: "YYYY-MM-DD", time_format: false, format: "YYYY-MM-DD" }
|
||||||
|
- { name: updatedDate, label: 更新日期, widget: datetime, date_format: "YYYY-MM-DD", time_format: false, format: "YYYY-MM-DD", required: false }
|
||||||
|
- { name: tags, label: 标签, widget: list, default: [], required: false }
|
||||||
|
- { name: draft, label: 草稿(不发布), widget: boolean, default: false }
|
||||||
|
- { name: body, label: 正文, widget: markdown }
|
||||||
|
|
||||||
|
# ── 项目 ─────────────────────────────────────────────
|
||||||
|
- name: projects
|
||||||
|
label: 项目
|
||||||
|
label_singular: 项目
|
||||||
|
folder: src/content/projects
|
||||||
|
create: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
extension: md
|
||||||
|
format: frontmatter
|
||||||
|
preview_path: projects/{{slug}}
|
||||||
|
summary: "{{title}}({{category}})"
|
||||||
|
sortable_fields: [order, date, title]
|
||||||
|
fields:
|
||||||
|
- { name: title, label: 名称, widget: string }
|
||||||
|
- { name: description, label: 简介, widget: text }
|
||||||
|
- name: category
|
||||||
|
label: 分类
|
||||||
|
widget: select
|
||||||
|
options: ["硬件", "软件", "通信", "机器人", "其他"]
|
||||||
|
default: "其他"
|
||||||
|
- { name: tags, label: 标签, widget: list, default: [], required: false }
|
||||||
|
- { name: date, label: 日期, widget: datetime, date_format: "YYYY-MM-DD", time_format: false, format: "YYYY-MM-DD", required: false }
|
||||||
|
- { name: repo, label: 源码链接, widget: string, required: false }
|
||||||
|
- { name: link, label: 访问链接, widget: string, required: false }
|
||||||
|
- { name: featured, label: 首页精选, widget: boolean, default: false }
|
||||||
|
- { name: order, label: 排序权重(大在前), widget: number, default: 0, value_type: int }
|
||||||
|
- { name: body, label: 详情, widget: markdown }
|
||||||
13
public/admin/index.html
Normal file
13
public/admin/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<title>内容管理 · ShiZhui</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Sveltia CMS:Decap/Netlify CMS 的现代继任者,原生支持 Gitea 后端 -->
|
||||||
|
<script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
public/favicon.svg
Normal file
5
public/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<rect width="32" height="32" rx="6" fill="#0a0e14"/>
|
||||||
|
<text x="50%" y="54%" dominant-baseline="middle" text-anchor="middle"
|
||||||
|
font-family="monospace" font-size="15" font-weight="700" fill="#39d0d8">>_</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
39
src/components/Footer.astro
Normal file
39
src/components/Footer.astro
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
import { SITE, SOCIAL } from '../site.config';
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container inner">
|
||||||
|
<div>
|
||||||
|
<span class="mono">{SITE.title}</span>
|
||||||
|
<span class="muted"> · {SITE.subtitle}</span>
|
||||||
|
</div>
|
||||||
|
<div class="links">
|
||||||
|
{SOCIAL.map((s) => <a href={s.href} target="_blank" rel="noopener">{s.label}</a>)}
|
||||||
|
<span class="muted">© {year}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.site-footer {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin-top: 4rem;
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.25rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
84
src/components/Header.astro
Normal file
84
src/components/Header.astro
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
import { SITE, NAV } from '../site.config';
|
||||||
|
const path = Astro.url.pathname;
|
||||||
|
const isActive = (href: string) =>
|
||||||
|
href === '/' ? path === '/' : path.startsWith(href);
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="container bar">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<span class="brand-mark">>_</span>
|
||||||
|
<span class="brand-name">{SITE.title}</span>
|
||||||
|
</a>
|
||||||
|
<nav class="nav">
|
||||||
|
{
|
||||||
|
NAV.map((item) => (
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
class={isActive(item.href) ? 'active' : ''}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.site-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background: rgba(10, 14, 20, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.brand-mark {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.brand-name {
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.nav a {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nav a:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.nav a.active {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.nav {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.nav a {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.brand-name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
54
src/components/ProjectCard.astro
Normal file
54
src/components/ProjectCard.astro
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
tags: string[];
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
const { title, description, category, tags, href } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a class="card project-card" href={href}>
|
||||||
|
<div class="top">
|
||||||
|
<span class="tag">{category}</span>
|
||||||
|
</div>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<p class="muted desc">{description}</p>
|
||||||
|
{
|
||||||
|
tags.length > 0 && (
|
||||||
|
<div class="tags">
|
||||||
|
{tags.map((t) => <span class="mono tagtext">#{t}</span>)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.project-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
color: var(--text);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.project-card h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
.tagtext {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
src/content.config.ts
Normal file
31
src/content.config.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
import { glob } from 'astro/loaders';
|
||||||
|
|
||||||
|
const blog = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
updatedDate: z.coerce.date().optional(),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
draft: z.boolean().default(false),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const projects = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.md', base: './src/content/projects' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
category: z.enum(['硬件', '软件', '通信', '机器人', '其他']).default('其他'),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
date: z.coerce.date().optional(),
|
||||||
|
repo: z.string().url().optional(),
|
||||||
|
link: z.string().url().optional(),
|
||||||
|
featured: z.boolean().default(false),
|
||||||
|
order: z.number().default(0),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { blog, projects };
|
||||||
29
src/content/blog/hello-world.md
Normal file
29
src/content/blog/hello-world.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: 你好,世界
|
||||||
|
description: 站点上线的第一篇文章,说明这个博客打算写什么。
|
||||||
|
pubDate: 2026-02-01
|
||||||
|
tags: ["随笔"]
|
||||||
|
---
|
||||||
|
|
||||||
|
> 这是一篇示例文章。复制 `src/content/blog/` 下的任意 `.md` 文件,修改 frontmatter 与正文即可发布新文章。
|
||||||
|
|
||||||
|
## 这里会写什么
|
||||||
|
|
||||||
|
- 嵌入式 / 硬件设计的实践与踩坑
|
||||||
|
- 软件与工具链
|
||||||
|
- 通信协议与无线
|
||||||
|
- 机器人系统
|
||||||
|
|
||||||
|
## Markdown 支持
|
||||||
|
|
||||||
|
支持标准 Markdown,包括代码块:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int main(void) {
|
||||||
|
while (1) {
|
||||||
|
// hello, embedded world
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
以及列表、引用、链接等。开始写吧。
|
||||||
32
src/content/projects/sample-robot-arm.md
Normal file
32
src/content/projects/sample-robot-arm.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: 示例项目 · 桌面机械臂
|
||||||
|
description: 基于 STM32 与步进电机的四轴桌面机械臂,含上位机控制软件。
|
||||||
|
category: 机器人
|
||||||
|
tags: ["STM32", "运动控制", "上位机"]
|
||||||
|
date: 2026-01-15
|
||||||
|
featured: true
|
||||||
|
order: 10
|
||||||
|
# repo: https://github.com/yourname/robot-arm
|
||||||
|
# link: https://example.com
|
||||||
|
---
|
||||||
|
|
||||||
|
> 这是一篇示例项目文档。复制本文件、修改 frontmatter 与正文即可添加你自己的项目。
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
简要介绍项目背景、目标与最终效果。
|
||||||
|
|
||||||
|
## 硬件
|
||||||
|
|
||||||
|
- 主控:STM32F4
|
||||||
|
- 驱动:A4988 步进驱动
|
||||||
|
- 结构:3D 打印 + 铝型材
|
||||||
|
|
||||||
|
## 软件
|
||||||
|
|
||||||
|
- 固件:基于 FreeRTOS 的运动插补
|
||||||
|
- 上位机:Python + Qt
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
记录踩坑、改进点与后续计划。
|
||||||
44
src/layouts/BaseLayout.astro
Normal file
44
src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
import Header from '../components/Header.astro';
|
||||||
|
import Footer from '../components/Footer.astro';
|
||||||
|
import { SITE } from '../site.config';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description } = Astro.props;
|
||||||
|
const pageTitle = title ? `${title} · ${SITE.title}` : `${SITE.title} · ${SITE.subtitle}`;
|
||||||
|
const pageDesc = description ?? SITE.description;
|
||||||
|
const canonical = new URL(Astro.url.pathname, Astro.site ?? SITE.url).toString();
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
|
||||||
|
<title>{pageTitle}</title>
|
||||||
|
<meta name="description" content={pageDesc} />
|
||||||
|
<link rel="canonical" href={canonical} />
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" content={pageTitle} />
|
||||||
|
<meta property="og:description" content={pageDesc} />
|
||||||
|
<meta property="og:url" content={canonical} />
|
||||||
|
<link rel="alternate" type="application/rss+xml" title={SITE.title} href="/rss.xml" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Header />
|
||||||
|
<main class="container">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
src/pages/404.astro
Normal file
30
src/pages/404.astro
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="404">
|
||||||
|
<section class="nf">
|
||||||
|
<span class="code mono">404</span>
|
||||||
|
<h1>页面不存在</h1>
|
||||||
|
<p class="muted">你访问的页面找不到了。</p>
|
||||||
|
<a class="btn btn-primary" href="/">← 返回首页</a>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nf {
|
||||||
|
text-align: center;
|
||||||
|
padding: 6rem 0;
|
||||||
|
}
|
||||||
|
.code {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
.nf h1 {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.nf .btn {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
75
src/pages/about.astro
Normal file
75
src/pages/about.astro
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
|
const skills = [
|
||||||
|
{ area: '嵌入式 / 硬件', items: ['MCU (STM32 / ESP32)', 'PCB 设计', '电源与信号', 'RTOS'] },
|
||||||
|
{ area: '软件', items: ['C / C++', 'Python', '前端 / 工具链', 'Linux'] },
|
||||||
|
{ area: '通信', items: ['UART / SPI / I2C / CAN', '无线 (BLE / LoRa / WiFi)', '协议设计'] },
|
||||||
|
{ area: '机器人', items: ['运动控制', '传感融合', 'ROS', '上位机'] },
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="关于" description="关于 ShiZhui">
|
||||||
|
<section class="page-head">
|
||||||
|
<h1>关于</h1>
|
||||||
|
<p class="lead muted">
|
||||||
|
嵌入式工程师,专注电子、软件、通信与机器人方向的产品与系统开发。
|
||||||
|
这里记录我做过的项目与一些技术思考。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="skills">
|
||||||
|
<h2>技术方向</h2>
|
||||||
|
<div class="grid">
|
||||||
|
{
|
||||||
|
skills.map((s) => (
|
||||||
|
<div class="card">
|
||||||
|
<h3>{s.area}</h3>
|
||||||
|
<ul>
|
||||||
|
{s.items.map((i) => <li>{i}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page-head {
|
||||||
|
padding: 3rem 0 1rem;
|
||||||
|
}
|
||||||
|
.page-head h1 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
.lead {
|
||||||
|
max-width: 60ch;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
.skills {
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
.skills h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.card h3 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
.card ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.1rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
.card li {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
81
src/pages/blog/[...id].astro
Normal file
81
src/pages/blog/[...id].astro
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
import { getCollection, render } from 'astro:content';
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = await getCollection('blog');
|
||||||
|
return posts.map((p) => ({
|
||||||
|
params: { id: p.id },
|
||||||
|
props: { post: p },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
const { Content } = await render(post);
|
||||||
|
const d = post.data;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={d.title} description={d.description}>
|
||||||
|
<article class="prose">
|
||||||
|
<a href="/blog" class="back mono">← 返回博客</a>
|
||||||
|
<h1>{d.title}</h1>
|
||||||
|
<div class="meta mono muted">
|
||||||
|
<time>{d.pubDate.toISOString().slice(0, 10)}</time>
|
||||||
|
{d.updatedDate && <span>· 更新于 {d.updatedDate.toISOString().slice(0, 10)}</span>}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
d.tags.length > 0 && (
|
||||||
|
<div class="tags mono">
|
||||||
|
{d.tags.map((t) => <span>#{t}</span>)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<hr />
|
||||||
|
<div class="content">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.prose {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 0;
|
||||||
|
}
|
||||||
|
.back {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.prose h1 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: clamp(1.6rem, 4vw, 2.2rem);
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
}
|
||||||
|
.content :global(h2) {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.content :global(img) {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.content :global(blockquote) {
|
||||||
|
border-left: 3px solid var(--accent-dim);
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
96
src/pages/blog/index.astro
Normal file
96
src/pages/blog/index.astro
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
|
const posts = (await getCollection('blog'))
|
||||||
|
.filter((p) => !p.data.draft)
|
||||||
|
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="博客" description="技术博客 · 电子、软件、通信与机器人">
|
||||||
|
<section class="page-head">
|
||||||
|
<h1>技术博客</h1>
|
||||||
|
<p class="muted">记录工程实践、踩坑与思考。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{
|
||||||
|
posts.length > 0 ? (
|
||||||
|
<ul class="post-list">
|
||||||
|
{posts.map((p) => (
|
||||||
|
<li>
|
||||||
|
<a href={`/blog/${p.id}`}>
|
||||||
|
<div class="row">
|
||||||
|
<span class="post-title">{p.data.title}</span>
|
||||||
|
<time class="mono muted">
|
||||||
|
{p.data.pubDate.toISOString().slice(0, 10)}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
{p.data.description && (
|
||||||
|
<p class="muted desc">{p.data.description}</p>
|
||||||
|
)}
|
||||||
|
{p.data.tags.length > 0 && (
|
||||||
|
<div class="tags mono">
|
||||||
|
{p.data.tags.map((t) => <span>#{t}</span>)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p class="muted">
|
||||||
|
暂无文章。在 <code>src/content/blog/</code> 新建 Markdown 文件即可发布。
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page-head {
|
||||||
|
padding: 3rem 0 1.5rem;
|
||||||
|
}
|
||||||
|
.page-head h1 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
.post-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.post-list li {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.post-list a {
|
||||||
|
display: block;
|
||||||
|
padding: 1.1rem 0;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.post-title {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 550;
|
||||||
|
}
|
||||||
|
.post-list a:hover .post-title {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
.row time {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
margin: 0.4rem 0 0;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
63
src/pages/contact.astro
Normal file
63
src/pages/contact.astro
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import { SITE, SOCIAL } from '../site.config';
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="联系" description="联系方式">
|
||||||
|
<section class="page-head">
|
||||||
|
<h1>联系</h1>
|
||||||
|
<p class="lead muted">
|
||||||
|
欢迎就项目合作、技术交流或问题反馈与我联系。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="contact">
|
||||||
|
<a class="card row" href={`mailto:${SITE.email}`}>
|
||||||
|
<span class="label mono">Email</span>
|
||||||
|
<span class="value">{SITE.email}</span>
|
||||||
|
</a>
|
||||||
|
{
|
||||||
|
SOCIAL.map((s) => (
|
||||||
|
<a class="card row" href={s.href} target="_blank" rel="noopener">
|
||||||
|
<span class="label mono">{s.label}</span>
|
||||||
|
<span class="value">{s.href}</span>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page-head {
|
||||||
|
padding: 3rem 0 1rem;
|
||||||
|
}
|
||||||
|
.page-head h1 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
.lead {
|
||||||
|
max-width: 56ch;
|
||||||
|
}
|
||||||
|
.contact {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
.row:hover .value {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
148
src/pages/index.astro
Normal file
148
src/pages/index.astro
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import ProjectCard from '../components/ProjectCard.astro';
|
||||||
|
import { SITE } from '../site.config';
|
||||||
|
|
||||||
|
const allProjects = (await getCollection('projects')).sort(
|
||||||
|
(a, b) => b.data.order - a.data.order
|
||||||
|
);
|
||||||
|
const featured = allProjects.filter((p) => p.data.featured).slice(0, 4);
|
||||||
|
const projects = featured.length > 0 ? featured : allProjects.slice(0, 4);
|
||||||
|
|
||||||
|
const posts = (await getCollection('blog'))
|
||||||
|
.filter((p) => !p.data.draft)
|
||||||
|
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
|
||||||
|
.slice(0, 3);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout>
|
||||||
|
<section class="hero">
|
||||||
|
<span class="tag">{SITE.subtitle}</span>
|
||||||
|
<h1>构建电子、软件与机器人系统</h1>
|
||||||
|
<p class="lead muted">
|
||||||
|
{SITE.description}
|
||||||
|
</p>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="btn btn-primary" href="/projects">查看项目 →</a>
|
||||||
|
<a class="btn" href="/blog">技术博客</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<div class="block-head">
|
||||||
|
<h2>精选项目</h2>
|
||||||
|
<a href="/projects" class="more mono">全部 →</a>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
projects.length > 0 ? (
|
||||||
|
<div class="grid">
|
||||||
|
{projects.map((p) => (
|
||||||
|
<ProjectCard
|
||||||
|
title={p.data.title}
|
||||||
|
description={p.data.description}
|
||||||
|
category={p.data.category}
|
||||||
|
tags={p.data.tags}
|
||||||
|
href={`/projects/${p.id}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p class="muted">暂无项目,去 <code>src/content/projects/</code> 添加。</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<div class="block-head">
|
||||||
|
<h2>最新文章</h2>
|
||||||
|
<a href="/blog" class="more mono">全部 →</a>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
posts.length > 0 ? (
|
||||||
|
<ul class="post-list">
|
||||||
|
{posts.map((p) => (
|
||||||
|
<li>
|
||||||
|
<a href={`/blog/${p.id}`}>
|
||||||
|
<span class="post-title">{p.data.title}</span>
|
||||||
|
<time class="mono muted">
|
||||||
|
{p.data.pubDate.toISOString().slice(0, 10)}
|
||||||
|
</time>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p class="muted">暂无文章,去 <code>src/content/blog/</code> 添加。</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
padding: 4rem 0 3rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-size: clamp(2rem, 5vw, 3rem);
|
||||||
|
margin: 1rem 0 0.75rem;
|
||||||
|
max-width: 18ch;
|
||||||
|
}
|
||||||
|
.lead {
|
||||||
|
max-width: 56ch;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
}
|
||||||
|
.block {
|
||||||
|
padding: 2.5rem 0 0;
|
||||||
|
}
|
||||||
|
.block-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
.block-head h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
.more {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.post-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.post-list li {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.post-list a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.85rem 0;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.post-list a:hover .post-title {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
.post-title {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.post-list time {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
87
src/pages/projects/[...id].astro
Normal file
87
src/pages/projects/[...id].astro
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
import { getCollection, render } from 'astro:content';
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const projects = await getCollection('projects');
|
||||||
|
return projects.map((p) => ({
|
||||||
|
params: { id: p.id },
|
||||||
|
props: { project: p },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { project } = Astro.props;
|
||||||
|
const { Content } = await render(project);
|
||||||
|
const d = project.data;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={d.title} description={d.description}>
|
||||||
|
<article class="prose">
|
||||||
|
<a href="/projects" class="back mono">← 返回项目</a>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="tag">{d.category}</span>
|
||||||
|
{d.date && <time class="mono muted">{d.date.toISOString().slice(0, 10)}</time>}
|
||||||
|
</div>
|
||||||
|
<h1>{d.title}</h1>
|
||||||
|
<p class="lead muted">{d.description}</p>
|
||||||
|
{
|
||||||
|
d.tags.length > 0 && (
|
||||||
|
<div class="tags mono">
|
||||||
|
{d.tags.map((t) => <span>#{t}</span>)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(d.repo || d.link) && (
|
||||||
|
<div class="links">
|
||||||
|
{d.repo && <a class="btn" href={d.repo} target="_blank" rel="noopener">源码</a>}
|
||||||
|
{d.link && <a class="btn btn-primary" href={d.link} target="_blank" rel="noopener">访问</a>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<hr />
|
||||||
|
<div class="content">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.prose {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 0;
|
||||||
|
}
|
||||||
|
.back {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.meta time {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
.prose h1 {
|
||||||
|
margin: 0.25rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
.lead {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
106
src/pages/projects/index.astro
Normal file
106
src/pages/projects/index.astro
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
import ProjectCard from '../../components/ProjectCard.astro';
|
||||||
|
|
||||||
|
const projects = (await getCollection('projects')).sort(
|
||||||
|
(a, b) => b.data.order - a.data.order
|
||||||
|
);
|
||||||
|
|
||||||
|
const categories = ['全部', '硬件', '软件', '通信', '机器人', '其他'];
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="项目" description="电子、软件、通信与机器人方向的项目展示">
|
||||||
|
<section class="page-head">
|
||||||
|
<h1>项目</h1>
|
||||||
|
<p class="muted">电子工程、软件、通信与机器人方向的作品与产品。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{
|
||||||
|
projects.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div class="filters mono" id="filters">
|
||||||
|
{categories.map((c) => (
|
||||||
|
<button class="filter" data-cat={c}>
|
||||||
|
{c}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div class="grid" id="grid">
|
||||||
|
{projects.map((p) => (
|
||||||
|
<div class="grid-item" data-cat={p.data.category}>
|
||||||
|
<ProjectCard
|
||||||
|
title={p.data.title}
|
||||||
|
description={p.data.description}
|
||||||
|
category={p.data.category}
|
||||||
|
tags={p.data.tags}
|
||||||
|
href={`/projects/${p.id}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p class="muted">
|
||||||
|
暂无项目。在 <code>src/content/projects/</code> 新建 Markdown 文件即可添加。
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const buttons = document.querySelectorAll<HTMLButtonElement>('.filter');
|
||||||
|
const items = document.querySelectorAll<HTMLElement>('.grid-item');
|
||||||
|
buttons[0]?.classList.add('active');
|
||||||
|
buttons.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
buttons.forEach((b) => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
const cat = btn.dataset.cat;
|
||||||
|
items.forEach((item) => {
|
||||||
|
item.style.display =
|
||||||
|
cat === '全部' || item.dataset.cat === cat ? '' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page-head {
|
||||||
|
padding: 3rem 0 1.5rem;
|
||||||
|
}
|
||||||
|
.page-head h1 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.filter {
|
||||||
|
background: var(--bg-elev);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-dim);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.3rem 0.85rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.filter:hover {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
.filter.active {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
border-color: var(--accent-dim);
|
||||||
|
background: rgba(57, 208, 216, 0.1);
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
21
src/pages/rss.xml.js
Normal file
21
src/pages/rss.xml.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import rss from '@astrojs/rss';
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { SITE } from '../site.config';
|
||||||
|
|
||||||
|
export async function GET(context) {
|
||||||
|
const posts = (await getCollection('blog'))
|
||||||
|
.filter((p) => !p.data.draft)
|
||||||
|
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||||
|
|
||||||
|
return rss({
|
||||||
|
title: SITE.title,
|
||||||
|
description: SITE.description,
|
||||||
|
site: context.site ?? SITE.url,
|
||||||
|
items: posts.map((post) => ({
|
||||||
|
title: post.data.title,
|
||||||
|
pubDate: post.data.pubDate,
|
||||||
|
description: post.data.description ?? '',
|
||||||
|
link: `/blog/${post.id}/`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
22
src/site.config.ts
Normal file
22
src/site.config.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 站点全局配置 —— 改这里即可调整站点信息、导航、社交链接
|
||||||
|
export const SITE = {
|
||||||
|
title: 'ShiZhui',
|
||||||
|
subtitle: '电子 · 软件 · 通信 · 机器人',
|
||||||
|
description:
|
||||||
|
'ShiZhui —— 电子工程、软件、通信与机器人方向的项目展示与技术博客。',
|
||||||
|
author: 'ShiZhui',
|
||||||
|
url: 'https://shizhui.xyz',
|
||||||
|
email: 'hello@shizhui.xyz',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NAV = [
|
||||||
|
{ label: '首页', href: '/' },
|
||||||
|
{ label: '项目', href: '/projects' },
|
||||||
|
{ label: '博客', href: '/blog' },
|
||||||
|
{ label: '关于', href: '/about' },
|
||||||
|
{ label: '联系', href: '/contact' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SOCIAL: { label: string; href: string }[] = [
|
||||||
|
// { label: 'GitHub', href: 'https://github.com/yourname' },
|
||||||
|
];
|
||||||
153
src/styles/global.css
Normal file
153
src/styles/global.css
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
:root {
|
||||||
|
/* 暗色调 · 工程/极简科技风 */
|
||||||
|
--bg: #0a0e14;
|
||||||
|
--bg-elev: #11161f;
|
||||||
|
--bg-elev-2: #161d29;
|
||||||
|
--border: #232c3b;
|
||||||
|
--border-strong: #2f3a4d;
|
||||||
|
|
||||||
|
--text: #d6deeb;
|
||||||
|
--text-dim: #8a97ad;
|
||||||
|
--text-faint: #5b6678;
|
||||||
|
|
||||||
|
--accent: #39d0d8; /* 青色科技感 */
|
||||||
|
--accent-strong: #5ce1e6;
|
||||||
|
--accent-dim: #1f6b6f;
|
||||||
|
--warn: #f2b757;
|
||||||
|
|
||||||
|
--radius: 8px;
|
||||||
|
--radius-lg: 14px;
|
||||||
|
--maxw: 1080px;
|
||||||
|
|
||||||
|
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue",
|
||||||
|
Arial, sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono",
|
||||||
|
Menlo, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 600px at 80% -10%, rgba(57, 208, 216, 0.06), transparent 60%),
|
||||||
|
var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
line-height: 1.65;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: 650;
|
||||||
|
color: #eef3fb;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.9em;
|
||||||
|
background: var(--bg-elev-2);
|
||||||
|
padding: 0.15em 0.4em;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--bg-elev) !important;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: var(--maxw);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用工具类 */
|
||||||
|
.muted { color: var(--text-dim); }
|
||||||
|
.mono { font-family: var(--font-mono); }
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(57, 208, 216, 0.08);
|
||||||
|
border: 1px solid var(--accent-dim);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.15rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg-elev);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 0.55rem 1rem;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: rgba(57, 208, 216, 0.1);
|
||||||
|
border-color: var(--accent-dim);
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
background: var(--bg-elev);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1.25rem 1.35rem;
|
||||||
|
transition: border-color 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: rgba(57, 208, 216, 0.25);
|
||||||
|
}
|
||||||
5
tsconfig.json
Normal file
5
tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user