博客开发日记

2026年8月29日星期六·#开发笔记博客·5821 字 29 分钟·
-浏览量
·
简介

从零搭建 Astro 博客的完整记录:技术选型、后台管理系统、友链页面设计、站内自助申请表单、构建系统防卡死优化、域名绑定与 nginx 静态托管、CSRF 移除决策,以及 debug 才发现的坑。

很早之前就想做个博客,但是一直没费功夫 一直在考虑是wordpress,typecho还是别的。但是最后还是选了astro。本站大部分部件是照搬了明崽大佬的魔改firefly模板,同时增加了一些自己的灵感 再次感谢。()

选 Astro 的理由很简单:Islands Architecture(岛屿架构) 默认零 JS,只在需要交互的组件注水,对博客这种内容为主的站点特别友好。再加上原生支持 Svelte/Vue/React 组件,挺灵活的。

这篇笔记把从零搭站过程中踩过的坑、做过的设计决策都记下来,既是对自己折腾过程的复盘,也希望能给同样想入坑 Astro 的朋友一点参考。(同时本篇写作手法通过了豆包同志 因为懒得做md内容排班格式)

涉及的技术栈:Astro + Svelte 5 + TypeScript 的前台,Fastify + SQLite 的后台管理服务。

一、后台管理系统:双 SQLite 驱动降维打击 🛠️#

博客前台是静态的,但后台管理总得有个服务端。我选了 Fastify 起一个轻量 API 服务,数据库用 SQLite 单文件,部署简单。

问题来了——better-sqlite3 这个原生模块,在不同环境下各有各的坑:

  • Windows 本地开发:装完 Visual Studio Build Tools,依然可能缺 MSVC VC++ 工具集,better-sqlite3 编译直接失败
  • Linux 生产服务器:GLIBC 版本太老(比如找不到 GLIBC_2.29),prebuild-install 直接跪了

双驱动降级策略#

解决方案是在数据库访问层做了一层抽象,优先用原生驱动吃性能,挂了就回退到内置模块保可用

let driver: 'better-sqlite3' | 'node:sqlite' = 'better-sqlite3';
try {
const BetterSqlite = require('better-sqlite3');
const probe = new BetterSqlite(':memory:');
probe.close();
// 探测成功,使用 better-sqlite3(性能更好)
} catch {
driver = 'node:sqlite';
// 回退到 Node 22+ 内置的 node:sqlite
}

Node 22+ 已经内置了 node:sqlite(实验性),作为开发环境的兜底完全够用。这样一来,本地 Windows 开发不用装编译工具链,服务器 GLIBC 老也不怕,两个环境都能跑起来。

Fastify 插件超时问题#

部署时还遇到一个隐蔽的坑:Fastify 默认插件超时是 10 秒,而 tsx 实时转译 TypeScript 在首次加载时比较慢,直接触发 AVV_ERR_PLUGIN_EXEC_TIMEOUT

解决方案是在 Fastify 构造里关掉超时:

const app = Fastify({
pluginTimeout: 0, // tsx 实时转译较慢,关闭避免误杀
// ...
});

同时把所有同步风格的路由注册器用 asPlugin 包成 async 插件,避免 Fastify 把同步函数误判成回调风格导致 Promise 永不 resolve。这个坑真的很阴间,表现是服务挂起但不出错,debug 半天才定位到。

静默退出问题#

最诡异的是服务偶尔会以 exit code 0 静默退出,没有任何错误日志。最后的保命组合拳是:

process.on('uncaughtException', (e) => console.error('[boot] uncaughtException:', e));
process.on('unhandledRejection', (e) => console.error('[boot] unhandledRejection:', e));
const keepAlive = setInterval(() => {}, 1000); // 防止事件循环空转退出

setInterval(() => {}, 1000) 看起来很蠢,但它确实能让事件循环保持活跃,避免 Node 判断「没有待处理的任务」后主动退出。


二、一键启动脚本:Win & Linux 双平台 🚀#

为了让部署更省心,写了两套一键启动脚本:

脚本平台用途
start.ps1Windows PowerShell本地开发 + 生产部署
start.shLinux Bash服务器部署

核心功能#

  • deploy 子命令:环境检查 → 依赖安装 → 构建 → 启动,一条龙
  • prod 模式:自动检测构建产物是否存在,缺失则先构建
  • 前端 astro preview 默认只绑 localhost必须加 --host 0.0.0.0 才能外部访问,这是个老坑了

部署到服务器后,一条命令就能把前台、后台、管理界面全拉起来,不用再手敲三串命令。


三、友链页面设计:卡片网格 + 实时筛选 🎨#

友链页面是最早动手做的功能页之一。设计目标很明确:简洁、好用、移动端友好

布局选择#

最终选了卡片网格布局,用 CSS Grid 的 auto-fill 自适应列数:

.grid-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: 1rem;
}

每张卡片最小 16rem,空间够了就自动多排一列,省去媒体查询的麻烦。

交互细节#

  • 搜索框:实时过滤站点名、描述、标签,输入即筛选
  • 标签筛选:点击标签快速过滤,激活态用反色按钮
  • 悬停效果:卡片上浮 + 头像从灰度变彩色
.friend-card:hover {
border-color: var(--grid-ink);
transform: translateY(-4px);
box-shadow: 0 8px 24px color-mix(in oklab, var(--grid-ink) 12%, transparent);
}

头像兜底#

友链头像经常失效(毕竟别人的图床你管不了),所以做了 fallback:

<img src={friend.imgurl} onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none';
(e.currentTarget.nextElementSibling as HTMLElement).style.display = 'flex';
}} />
<span class="avatar-fallback">{initialOf(friend.title)}</span>

图片挂了就显示首字符占位,不会出现裂图。这种细节虽然不起眼,但用户体感差别很大。

响应式#

移动端自动切换为 2 列,超小屏(<400px)切 1 列。筛选标签横向滚动,避免换行导致布局错乱。


四、站内自助友链申请表单 📝#

友链申请这个功能花的时间最多。一开始想的是跳转 GitHub Issue 模板提交,但这对不会用 GitHub 的访客太不友好了。最终方案是前台直接提交表单 → 后台审核队列 → 一键通过自动写入配置

后端架构#

后台 API 基于 Fastify,分了两组路由:

  • 公开路由组:只有 POST /friends/applications(提交申请),无需登录
  • 受保护路由组:友链 CRUD、审核通过/拒绝等,需要 JWT 鉴权

这里有个设计要点:公开接口必须从受保护路由组里单独拎出来注册,否则会被鉴权中间件拦掉。同时公开接口要加入 CSRF 豁免白名单:

const CSRF_EXEMPT = new Set<string>([
`${API_PREFIX}/auth/login`,
`${API_PREFIX}/auth/refresh`,
`${API_PREFIX}/friends/applications`, // 公开提交接口
]);

全局限流 200 次/分钟依然生效,防止有人拿脚本刷申请。

前端表单组件#

用 Svelte 5 的 runes 语法写了表单组件,字段包括:

  • 站点名称(必填)
  • 站点链接(必填,自动校验 URL)
  • 头像链接(选填,留空自动抓取 favicon)
  • 站点描述
  • 分类标签
  • 联系邮箱(选填,校验格式)

几个细节优化:

  • URL 失焦自动抓取 favicon:用 Google 的 favicon 服务 https://www.google.com/s2/favicons?sz=64&domain=xxx
  • 三态反馈idle / success / error,提交结果即时可见
  • 懒加载:用 client:visible 而不是 client:load,滚到才加载 JS

工作流程#

访客填写表单 → POST 到后台公开接口
→ 写入 SQLite friend_applications 表(status='pending')
→ 管理员登录后台「友链申请」页面
→ 点击「通过」→ 自动写入 friendsConfig.ts

审核通过后,友链会以默认权重 5、启用状态自动追加到 friendsConfig.ts 的数组里,下次构建即可见。这样访客提交完直接走人,站长后台一键通过,整个流程闭环。


五、细节调优:那些容易被忽略的小事 🔧#

1. 关闭 Astro Dev Toolbar#

开发时页面右下角那个黑色的 Astro logo 按钮虽然功能丰富,但有时候确实碍眼。一行配置搞定:

astro.config.mjs
export default defineConfig({
devToolbar: {
enabled: false,
},
// ...
});

底部只保留「隐私政策」和「用户协议」两个入口,RSS 按钮去掉了。友链申请入口直接做成页面下方的表单,比塞在 Footer 里更合理。

3. 友链申请指南弹窗#

做了一个 <dialog> 元素的申请指南弹窗,展示本站信息(可一键复制)、申请流程和注意事项。之前还在底部放了「自助申请友链」和「前往评论区」两个跳转按钮,后来有了站内表单,这两个跳转就多余了,直接删掉,UI 更干净。


六、后台一键构建 + 文章日期序列化踩坑 🔄#

这是开发过程中花时间最多的一个功能,也是踩坑踩得最狠的。背景很简单:Astro 是静态站点生成器(SSG),写完文章必须重新 pnpm build 才能在前台看到。本地开发还好,SSH 上服务器敲命令也能忍,但既然有后台管理系统了,为什么不直接在后台点一下就构建呢?

需求拆解#

  • 手动触发:Dashboard 上放个「重新构建」按钮,点一下就跑 pnpm build
  • 自动触发:写/改/删文章后自动构建,省得忘
  • 实时日志:构建过程中把 stdout/stderr 实时展示出来,方便排查

构建管理器设计#

核心是一个单例的 BuildManager,挂在 Fastify 进程里:

// 同一时刻只允许一个构建任务,避免 dist 写入冲突
if (state.status === 'running') {
pushLog('[skip] 已有构建任务在运行,本次触发已跳过');
return;
}

child_process.spawnpnpm run build,stdout/stderr 都 pipe 进来收集日志,保留最近 500 行供前端轮询。构建完成时把结果(触发方式、状态、耗时、退出码)持久化到 SQLite 的 build_history 表,方便后续做统计。

去抖机制很关键——后台编辑文章时经常会连续保存(写一段 Ctrl+S 一下),如果每次保存都触发构建,磁盘和 CPU 都扛不住。所以自动触发加了 3 秒去抖:

export function triggerBuildDebounced(delayMs = 3000): void {
if (state.status === 'running') return; // 运行中不叠加
if (pendingTimer) clearTimeout(pendingTimer);
pendingTimer = setTimeout(() => {
pendingTimer = null;
runBuild('auto');
}, delayMs);
}

Dashboard 集成#

前端在 Dashboard 底部加了个「前端构建」卡片,包含:

  • 状态标签(空闲 / 构建中 / 成功 / 失败)
  • 「重新构建」按钮(构建中自动 loading + 禁用)
  • 上次构建用时和完成时间
  • 构建日志框(每 5 秒轮询一次,实时刷新)

这里踩了个小坑:一开始用 pnpm --dir <path> run build 传项目路径,结果 Windows 下路径含空格(C:\Users\William Chih\...)被截断成 C:\Users\William,构建直接报 ENOENT。解决方案是不用 --dir 参数,直接用 spawncwd 选项

const child = spawn('pnpm', ['run', 'build'], {
cwd: config.blogRoot, // 工作目录交给 cwd,不用命令行参数
shell: process.platform === 'win32', // Windows 需要 shell 解析 pnpm.cmd
});

Date 序列化:最阴间的坑#

功能写完测试时发现一个诡异问题:后台保存文章后,自动构建成功了,但前台首页死活不显示新文章。去 dev server 里一看,整个 Content Collections 直接崩了,所有文章(旧的新的)全不见了。

排查发现是新文章的 frontmatter 里 published 字段被序列化成了带引号的字符串:

# 实际生成的(错误)
published: '2026-08-12T17:30:02'
# Astro schema 期望的(正确)
published: 2026-08-12T17:30:02

Astro 7 的 Content Collections schema 把 published 定义为 z.date(),YAML 里加引号会被解析成 string 而非 date,schema 校验失败 → 整个 Collections 初始化崩掉 → 所有文章不显示。

根因是后台保存文章时,published 字段存的是字符串(new Date().toISOString()),gray-matter 库在 stringify 时把它当成普通字符串序列化,YAML 自动加了引号。

修复方案是把字符串转成 Date 对象再交给 gray-matter,YAML 序列化 Date 时会输出原生日期格式(不带引号):

// 修改前:存的是字符串,会被加引号
published: data.published ?? new Date().toISOString()
// 修改后:存 Date 对象,YAML 原生日期格式
published: new Date(data.published ?? Date.now())

创建和编辑两个接口都要改,凡是涉及 publishedupdated 的赋值都包一层 new Date()。这个坑真的很隐蔽——本地 dev 模式下文章能正常显示(dev server 对类型校验相对宽松),但 astro build 时会严格执行 schema,构建失败。如果没开自动构建,可能要等到部署时才发现。

💡 教训:Content Collections 的 schema 类型不是摆设。YAML 的类型系统和 TypeScript 的类型系统是两套,gray-matter.stringify 不会自动把字符串转成 YAML 的 date 类型,必须传入 Date 对象。凡是 schema 里定义为 z.date() 的字段,写入时一律用 new Date() 包一层

slug 自动生成#

顺手解决了一个 UX 问题:之前后台新建文章时 slug 字段是必填的,对不熟悉技术的人来说很懵——“slug 是啥?“。改成留空自动生成:

let slug = (data.slug ?? '').trim();
if (!slug) {
slug = `post-${Date.now().toString(36)}`; // base36 时间戳,短且唯一
let suffix = 1;
while (existsSync(slugToPath(slug))) {
slug = `post-${Date.now().toString(36)}-${suffix++}`;
}
}

生成的 slug 形如 post-msqd7cgi,够短、够唯一、够语义化。当然用户想自定义也完全可以,字段只是从必填改成了可选。


七、错误总结 💡#

问题根因解决方案
better-sqlite3 编译失败缺 MSVC 工具集 / GLIBC 版本低双驱动降级到 node:sqlite
Fastify 插件超时tsx 转译慢 + 默认 10s 超时pluginTimeout: 0
进程静默退出事件循环空转 / 未捕获异常全局错误处理 + keepAlive
Astro preview 外部访问失败默认绑 localhost--host 0.0.0.0
src/api.tssrc/api/ 命名冲突Vite/Rollup 模块解析重命名为 src/http.ts
图片加载失败出现裂图友链头像失效onerror 回退到首字符占位
构建命令路径被截断pnpm --dir 参数含空格改用 spawncwd 选项
新文章导致全站文章消失published 字段被序列化成带引号字符串new Date() 包一层,输出 YAML 原生日期

💡 关于 src/api.ts 那个坑:Astro 项目里文件名和目录名同名会导致 Vite/Rollup 模块解析出错,报错信息还特别误导。养成习惯:文件名和同级目录名不要重复

💡 关于 Date 序列化那个坑:这个 bug 在本地 dev 模式下可能完全不暴露,只有 astro build 时才会触发。建议开发时定期跑一次完整构建,别等部署时才发现问题。


八、技术选型回顾 📐#

回过头看整个技术选型,大概是这样的决策链:

场景选择理由
前台框架Astro零 JS by default,内容站点性能拉满
交互组件Svelte 5编译产物小,runes 语法写起来舒服
样式Tailwind CSS 4原子化 + CSS 变量主题切换两不误
后台框架Fastify比 Express 快,插件系统清晰
数据库SQLite单文件部署,博客场景够用
包管理npm兼容性好,服务器构建不出幺蛾子

九、CSRF 校验移除 + 用户名修改功能 🔓#

CSRF 校验:从有到无#

后台一开始做了完整的 CSRF 防护——登录时签发 csrfToken 写入 Cookie,前端 Axios 拦截器自动读取并附加 X-CSRF-Token 请求头,后端全局 preHandler 校验所有非 GET 请求。设计上很标准,但实际用起来:

后台操作老显示「CSRF 校验失败」——Cookie 的 Secure 标记在 HTTP 环境下导致浏览器拒绝存储 csrfToken,前端拿不到 token,所有写操作全被 403。虽然之前修过一次(按 request.protocol 判断是否加 Secure),但反代环境下协议判断又有边界 case。

纠结了一圈,最终决定彻底移除 CSRF 校验——后台管理系统是内网性质(复杂路径 + JWT 鉴权 + 密码二次确认),CSRF 带来的收益不如它造成的问题多:

// 之前:全局 preHandler 里校验 CSRF
app.addHook('preHandler', async (request, reply) => {
if (!isCsrfExempt(request) && !await verifyCsrf(request)) {
return fail(reply, 'CSRF 校验失败', 403);
}
});
// 现在:整段删掉

移除涉及 7 个文件——后端删 requireCsrf 函数、删 CSRF_EXEMPT 白名单、删全局钩子;前端删 Axios 拦截器、删 csrfToken 状态、删类型定义。删干净比加进去还费劲。

修改用户名功能#

后台本来只有改密码,没有改用户名。用户名想改只能 SSH 上服务器敲 SQL。既然「账号与访问」页面已经有了密码修改和端口配置,加个用户名修改 Tab 顺理成章:

// POST /system/username
// 参数校验:3-20 位,字母/数字/下划线
const changeUsernameSchema = z.object({
password: z.string().min(1).max(128),
newUsername: z.string().min(3).max(20)
.regex(/^[a-zA-Z0-9_]+$/),
});

安全措施和改密码一致:密码二次确认 → 查重(不能和已有用户名重复)→ 改完吊销所有令牌 + 清 cookie(JWT 里含旧用户名,必须重新登录)→ 操作日志记录。

前端在「账号与访问」页面加了第二个 Tab,显示当前用户名、新用户名输入框、密码确认框。提交后 1.5 秒自动跳转登录页,用新用户名 + 旧密码登录。

💡 教训:CSRF 防护在「内网管理系统 + HTTPS 反代」场景下,收益不如成本高。如果你的后台已经有多层鉴权(JWT + 复杂路径 + 密码确认),可以考虑移除 CSRF,省掉一堆 Cookie/Secure/SameSite 的边界问题。


十、构建系统全面优化:2 核 2G 服务器防卡死 ⚡#

这是投入精力最多的一轮优化。服务器配置是 2 核 2G,Astro 全量构建吃内存很凶,频繁出现「构建卡死」——后台点「重新构建」后进度条转一辈子,自动构建(保存文章后触发)也经常无声中断。

卡死的三大根因#

根因表现发生条件
OOM 杀进程子进程被内核静默杀掉,无 stderr 输出2G 内存无 swap,构建峰值超 1.5G
Swap 抖动进程活着但极慢,几乎无输出有 swap 但太少,疯狂换页
超时太晚卡了 20 分钟才被超时发现BUILD_TIMEOUT_MS = 20min 太长

修复 1:Swap 自动创建(根本手段)#

start.sh 新增 ensure_swap() 函数,构建前自动检测:

Terminal window
ensure_swap() {
# 读取 SwapTotal 和 MemTotal
# 总内存 <= 2G 且无 swap → 自动创建 2G swap 文件
fallocate -l 2G /swapfile # 秒建(回退 dd)
chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
# 写入 /etc/fstab 实现重启后自动挂载
echo "/swapfile none swap defaults 0 0" >> /etc/fstab
}

有了 2G swap,即使物理内存只有 2G,构建峰值 1.5G 也不会被 OOM 杀掉——最差也就是慢一点(走 swap),但能跑完。

修复 2:停滞检测(120 秒无输出 = 卡死)#

之前只有 20 分钟总超时,太被动。新增停滞检测——每 30 秒检查一次 lastOutputAt,超过 120 秒没有任何 stdout/stderr 输出就判定卡死,立即强杀:

let lastOutputAt: number | null = null;
const STALL_TIMEOUT_MS = 120 * 1000;
// 子进程每次输出都更新 lastOutputAt
const handleLine = (data: Buffer) => {
lastOutputAt = Date.now();
// ...
};
// 每 30 秒轮询
stallTimer = setInterval(() => {
if (lastOutputAt && Date.now() - lastOutputAt > STALL_TIMEOUT_MS) {
pushLog(`[stall] 构建已 ${stallSec} 秒无输出,判定为卡死`);
killBuildTree(); // 强杀整个进程树
}
}, 30_000);

这个机制能捕获两种之前无法检测的场景:

  1. OOM 杀进程后子进程静默退出——没有 close 事件触发(有时),但肯定没有输出了
  2. Swap 抖动假死——进程活着但慢到几乎不输出,120 秒够判断了

修复 3:堆内存上限适配低配服务器#

之前堆上限是 [512, 2048] MB,对 2G 服务器太激进。改为按总内存分级:

总内存堆上限范围比例给系统留
≤ 2G[384, 768] MB50%~1.2G
> 2G[512, 1024] MB55%充裕
function computeHeapCapMb(): number {
const totalMb = Math.floor(os.totalmem() / 1024 / 1024);
const ratio = totalMb <= 2048 ? 0.50 : 0.55;
const maxCap = totalMb <= 2048 ? 768 : 1024;
// ...
}

修复 4:构建工具从 pnpm 统一为 npm#

构建命令原来走 pnpm run build,但服务器上 pnpm 的依赖完整性检查经常出问题(optional native binding 安装失败、build scripts 被 ignore)。统一改为 npm run build

builder.ts
function getRunner() {
return { cmd: 'npm', args: ['run', 'build'] };
}

同时 package.json 的 build 脚本改为直接调 node,绕过 .bin/ 软链(服务器 npm install 不完整时软链可能缺失):

{
"build": "node scripts/generate-icons.js && node node_modules/astro/bin/astro.mjs build && node node_modules/pagefind/lib/runner/bin.cjs --site dist"
}

修复 5:总超时降低#

参数旧版新版
总超时20 分钟15 分钟
停滞超时120 秒(新增)

有了停滞检测兜底,总超时可以短一些。15 分钟对于个人博客的全量构建已经非常充裕了。

效果#

优化前:构建 10 次卡死 7 次,只能 SSH 手动重启服务。 优化后:swap 自动创建后不再 OOM,偶发卡死 2 分钟内被停滞检测捕获并强杀,状态从 running 落为 failed,后续构建自动恢复,不再永久卡死。

💡 教训:2G 服务器跑 Astro 构建必须有 swap,这不是可选项是必选项。没有 swap 时 OOM-killer 会杀进程且不一定触发 close 事件,导致构建状态永久卡在 running。停滞检测(无输出超时)比单纯的总超时更有效——它能区分「正常构建中偶尔沉默」和「已经死了只是进程没退出」。


十一、域名绑定与静态托管 🌐#

从 preview 到 nginx 静态托管#

之前前端跑 astro preview(Node 进程),通过 IP:4321 访问。绑定域名 qiguai.net 后遇到一堆问题:

  1. Vite allowedHosts 拦截Blocked request. This host ("qiguai.net") is not allowed.——Vite 默认只允许 localhost
  2. 端口暴露:访问 IP:4321 不够优雅,也不安全
  3. preview 进程占内存:2G 服务器多跑一个 Node 进程很奢侈

最终方案是彻底去掉 preview,改用 nginx 静态托管

宝塔建静态站点 qiguai.net → 网站目录指向 /www/wwwroot/my-blog-master/dist

Astro 是 output: "static"dist/ 目录就是纯 HTML/CSS/JS,nginx 直接发静态文件,不需要 Node 中间层。start.shprod 模式从启动 preview 改为只启动后台 + 提示「nginx → dist/ 静态托管」。

好处:省一个 Node 进程、更快(nginx 发静态文件比 preview 快)、没有 allowedHosts / 端口 / Host 头问题、内存占用少。更新文章 → npm run build → nginx 自动 serve 新 dist,前端连重启都不用。

后台反代#

后台(Fastify 8787 端口)通过子域名 xxxx.qiguai.net 反代:

宝塔建站点 xxxx.qiguai.net → 反向代理 → http://127.0.0.1:8787

踩坑:反代目标一开始写的 http://xxxxx:8787(公网 IP),阿里云服务器访问自己的公网 IP 会被安全组拦。改为 http://127.0.0.1:8787 就好了——请求不走出服务器,不经安全组,速度快。

端口修改功能的角色变化#

绑定域名后,「改端口」功能基本没用了:

功能之前nginx 反代后
前端端口preview 监听端口无用——nginx 直托管 dist/,不涉及端口
后台端口后端换端口重启改了反而坏事——nginx 指向旧端口,反代断链
后台路径SPA 重建 + 重启仍有用——路径是 URL 路径,nginx 透传
改密码仍有用

端口对外已经隐身(走域名),没必要改了。路径和密码继续用。

Vite allowedHosts#

虽然改成了 nginx 静态托管(不再跑 preview),但 astro.config.mjs 里还是加了 allowedHosts,以备本地 dev 时用域名访问:

vite: {
preview: {
allowedHosts: ['qiguai.net', 'www.qiguai.net'],
},
},

💡 教训:静态站点(output: "static")最自然的部署方式就是 nginx 直托管,不需要 Node 进程。astro preview 适合本地预览,不适合生产——多一个 Node 进程、有 Host 检查问题、占内存。反代目标永远用 127.0.0.1,不要用公网 IP——云服务器访问自己的公网 IP 会被安全组拦截。


写在最后 🎯#

  1. 架构分层要清晰,前台静态、后台动态、数据库单文件,各司其职
  2. 能自动化的流程坚决不手动,从部署到友链审核

Astro 的 Islands Architecture 对博客这种内容站点真的特别合适——默认零 JS,只在需要交互的组件注水,Lighthouse 跑分看着就舒服。再加上 Svelte 5 的 runes 语法、Tailwind 4 的 CSS 变量主题,开发体验整体很丝滑。

谢谢大家

评论区

看板娘
公告
欢迎关于我的介绍

欢迎来到奇怪的博客,我是一名热爱JAVA,C++,AI等技术的偏执学家

查看详情
音乐
封面

音乐

暂未播放

0:00
0:00
暂无歌词
标签
# 博客1
目录
工具

隐私政策

更新日期: 2026 年 7 月 15 日
生效日期: 2026 年 7 月 15 日

适用范围#

本政策适用于 Qiguai的博客(以下简称“本站”)。本站是个人博客,用于发布和分享内容;不提供账户注册、支付、定位或广告投放服务。访问本站、发表文章评论或在留言板留言前,请阅读本政策。

信息收集与使用#

本站只在提供内容、评论和留言功能,以及维护站点安全所需的范围内处理信息。

  • 访问与统计信息:访问页面时,统计服务可能处理访问时间、页面地址、来源页、浏览器和设备相关信息,用于了解内容访问情况、排查故障和改进站点。
  • 评论信息:使用文章评论功能时,Waline 可能处理您主动提交的昵称、邮箱、站点链接和评论内容;还可能处理 IP 地址等必要信息,用于防止垃圾评论、滥用和维护服务安全。评论内容、昵称和站点链接(如填写)可能公开展示在文章下方;邮箱不会公开展示。
  • 留言信息:留言板使用 Waline /guestbook/ 频道。Waline 可能处理您主动提交的昵称、可选邮箱、站点链接、留言内容和图片,以及浏览器、操作系统、IP 地址等必要的反滥用信息。默认情况下,留言图片以内嵌数据随留言提交;如站点维护者配置了远程图片上传接口,图片会先发送至该接口并在留言中保存返回的图片地址。留言内容、昵称、图片和站点链接(如填写)可能公开展示;邮箱和 IP 地址不会在留言板公开展示。
  • AI 对话信息:使用 AI 搜索时,本站会处理您提交的问题以及最近 6 条对话历史,用于检索博客内容并生成回答。问题和对话历史会发送至 ModelScope,或在第三方接口不可用时由 Cloudflare Workers AI 处理。请勿在 AI 对话中提交密码、Token、身份证件、联系方式或其他敏感信息。

请不要在评论或留言中提交身份证件、银行卡、住址、密码或其他不必要的敏感个人信息。

第三方服务#

为实现本站功能,以下第三方会在各自服务范围内处理相关数据:

  • Umami:用于匿名化的网站访问统计和出站链接点击统计,帮助我了解本站的使用情况。
  • Waline:用于文章评论、留言板及访问量统计。服务会按照其自身规则处理您在评论或留言时提交的信息及必要的反滥用信息。
  • Cloudflare:为本站提供静态资源分发、AI Worker、Vectorize 和相关基础设施。留言板不使用项目 Worker 或 KV 存储。
  • ModelScope:为 AI 搜索提供文本向量和对话模型服务,会处理您提交的问题及发送给模型的最近对话历史。
  • Cloudflare Workers AI:在未配置第三方 AI 接口时提供文本向量和对话模型服务,并处理相同的 AI 请求数据。
  • unpkg:用于加载 Waline 的前端脚本、样式和表情资源;请求这些资源时,您的浏览器会与该服务建立连接。

第三方服务可能有独立的隐私政策和数据保存规则。请在使用相关功能前查阅其规则;本站无法控制其独立的数据处理活动。

本站主要使用浏览器本地存储(Local Storage 或 Session Storage)保存使用偏好,例如主题颜色、明暗模式、文章列表视图和音乐播放设置。留言板会在本地保存匿名资料、未发送草稿和登录状态,以便恢复输入与会话;管理员登录状态仅保存在当前会话。AI 搜索的会话标题和完整对话也会保存在当前浏览器的 Local Storage 中,最长保存 7 天;您可以在 AI 面板中使用“清空全部会话”立即删除这些数据。

本站不主动设置用于广告定向的第一方 Cookie。评论、统计或资源服务可能按照其自身规则使用 Cookie 或类似技术。您可以通过浏览器设置查看、删除或限制 Cookie 和本地存储;清除后,部分偏好或互动状态可能会恢复为默认值,评论功能也可能受到影响。

信息公开、保存与安全#

评论和留言属于公开互动内容,提交后可能被搜索引擎收录、被他人引用或在缓存中短暂保留。请谨慎决定发布内容。除非您提出删除请求、内容违反规则或法律法规另有要求,公开内容会持续保留以维持讨论上下文。

本站会采取合理措施保护数据安全,包括使用 HTTPS、输入校验、内容转义和访问频率限制。但互联网传输和第三方服务均无法保证绝对安全,请理解并自行承担公开发布信息的相应风险。

你的权利#

你可以通过 3530002633@qq.com 联系我,申请查询、更正或删除由本站直接保存的评论、留言或相关公开内容。为保护他人权益,请在请求中提供足以定位内容的信息,并说明你与该内容的关系;必要时可能需要进行合理核验。

对于由 Waline、Umami、Cloudflare 或 unpkg 独立处理的数据,你也可以直接向对应服务提供方行使相关权利。删除公开评论或留言后,第三方缓存、搜索引擎索引或他人转载的副本可能无法立即同步删除。

未成年人条款#

未满 14 周岁的未成年人应在监护人同意和指导下使用本站的评论、留言等互动功能。监护人如发现未成年人未经同意提交了个人信息,可通过上述联系方式与我联系,我会在合理范围内协助处理。

政策更新与联系#

我可能因本站功能或适用规则变化更新本政策,更新后的版本将在本站公布并标明日期。继续使用相关功能即表示你已阅读并理解更新后的政策。

如对本政策或数据处理有疑问,请联系 3530002633@qq.com

用户协议

更新日期: 2026 年 5 月 19 日
生效日期: 2026 年 5 月 19 日

适用范围#

本协议适用于你访问 Qiguai的博客,以及使用文章评论、留言板等互动功能的行为。继续浏览本站或提交评论、留言,即表示你已阅读、理解并同意遵守本协议及本站的隐私政策。

评论及留言规则#

请在交流中保持友善、理性和尊重。你不得利用本站发布、传播或实施以下行为:

  • 发布任何违反中华人民共和国法律法规的内容。
  • 发布任何侵犯他人合法权益的内容,包括但不限于隐私、名誉、肖像、著作权、商标权和其他知识产权。
  • 恶意攻击、辱骂、骚扰、威胁、歧视其他用户或任何第三方。
  • 发布垃圾广告、恶意推广、刷屏、灌水,或与讨论主题明显无关的重复内容。
  • 利用本站进行网络诈骗、钓鱼、传播恶意软件,或发布可能危害网络和信息安全的内容。
  • 绕越或试图绕越本站的审核、限流、封禁等管理措施。
  • 冒充他人、伪造身份,或收集、公开他人的个人信息。

内容与访问管理#

你应对自己发布的评论和留言负责,并保证拥有发布该等内容所需的合法权利。论坛管理员有权在不另行通知的情况下删除违规内容、限制或封禁违规账号,或限制其继续使用本站互动功能。

如发现涉嫌违法犯罪、严重侵权或危及本站安全的内容,本站可保留相关记录,并在法律法规要求或必要时向有关部门提供协助。对管理措施有疑问时,可通过文末联系方式说明情况;本站会结合实际情况处理,但不承诺恢复已删除内容或访问权限。

知识产权与内容授权#

本站原创文章、页面设计和其他受保护内容的权利归作者或权利人所有。未经授权,请勿复制、转载、镜像或用于商业用途;法律法规允许的合理使用除外。

你发布评论或留言时,授予本站为展示、存储、备份、审核、删除和维护互动功能所必需的非独占、免费的使用许可。该许可不改变你对原创内容依法享有的权利。

免责声明#

本站内容仅用于个人记录、学习交流和一般信息参考,不构成任何专业意见、承诺或担保。你应结合自身情况独立判断,并对据此采取的行动负责。

评论、留言和外部链接中的内容由其发布者或运营者负责,不代表本站立场。本站会在合理范围内处理明显违规内容,但不保证所有内容均及时发现,也不对第三方网站的可用性、内容、安全性或隐私实践承担责任。

因网络故障、不可抗力、第三方服务异常、维护升级或超出合理控制范围的原因导致本站暂时无法访问、内容延迟或数据丢失的,本站会尽力恢复,但不承担由此产生的间接损失。

未成年人条款#

未满 14 周岁的未成年人应在监护人同意和指导下使用评论、留言等互动功能。监护人应协助未成年人理解本协议,并对其使用行为进行必要的引导。

其他条款#

我可以根据本站功能、管理需要或法律法规变化更新本协议,更新后的版本将在本站公布并标明日期。继续使用本站即视为接受更新后的协议。

本协议的订立、执行和解释适用中华人民共和国法律。因本协议或使用本站产生争议时,双方应先友好协商;协商不成的,依法向有管辖权的人民法院解决。

如对本协议或内容管理有疑问,请联系 3530002633@qq.com