这几天,我把 Fuwari 改造成了现在的 Miike’s Blog。
它以 Astro 为框架,用 Tailwind CSS 负责样式,最后通过 GitHub、SSH 和 Nginx 部署到自己的云主机。整个过程看上去只是“改主题、构建、上传”,真正做起来却遇到了不少值得记录的问题。
这篇文章既是本次开发的复盘,也是一份留给未来自己的部署手册。
从模板开始,但不止于模板
Fuwari 已经提供了文章系统、归档、搜索、亮暗模式、响应式布局等完整能力,所以这次开发不需要从零搭建博客。我的主要工作是把模板调整成自己的站点:
- 修改站点名称、作者信息与页面内容;
- 整理项目目录、README 和 Wiki;
- 将主题色选择改成四个固定色板;
- 修复生产构建中暴露的样式问题;
- 将代码推送到 GitHub;
- 在云主机上完成构建并通过 Nginx 提供访问。
使用成熟模板最大的好处是节省基础功能的开发时间,但模板中的配置、依赖和样式约定仍然要认真理解。否则,本地看似正常的修改,很容易在生产构建时暴露问题。
主题色:从滑块改成四个色板
原主题通过滑块自由选择色相。对个人博客而言,自由度太高反而会让界面缺少统一感。我最终保留了四个预设:淡粉色、粉白色、超浅紫色和奶蓝色,并把淡粉色设为默认值。
主题色仍然使用 OKLCH 的色相值,只把可选范围限制成固定数组:
export const themeColorOptions = [ { name: "淡粉色", hue: 350 }, { name: "粉白色", hue: 12 }, { name: "超浅紫色", hue: 285 }, { name: "奶蓝色", hue: 220 },] as const;显示设置中的滑块则被替换成四个按钮。界面上只显示颜色预览,不显示文字;名称保留在 aria-label 中,方便屏幕阅读器识别:
{#each themeColorOptions as option} <button type="button" aria-label={`切换到${option.name}`} aria-pressed={hue === option.hue} on:click={() => hue = option.hue} > <span class="w-6 h-6 rounded-full" style={`background: oklch(0.86 0.08 ${option.hue});`} ></span> </button>{/each}选中的色相会写入浏览器的 localStorage,刷新页面后仍然生效。同时,对读取出的值做白名单校验,避免旧数据或异常数据让主题进入未定义状态:
export function setHue(hue: number): void { const themeHue = isThemeHue(hue) ? hue : getDefaultHue(); localStorage.setItem("hue", String(themeHue)); document.documentElement.style.setProperty("--hue", String(themeHue));}这次修改让我再次意识到,设置项并非越自由越好。有限、经过挑选的选项,通常能带来更稳定的视觉体验。
本地正常,不代表生产构建一定正常
开发服务器能够运行后,我原以为构建只是最后走一遍流程,实际却在 Markdown 代码块的复制按钮样式上失败了。
问题来自 @apply 中引用的自定义组合类。Tailwind 在生产构建时无法确认这个类一定存在,因此直接中断构建。解决方法是把组合类展开为明确的原子样式:
.copy-btn { all: initial; @apply flex items-center justify-center bg-[oklch(0.45_0.01_var(--hue))] hover:bg-[oklch(0.50_0.01_var(--hue))] active:bg-[oklch(0.55_0.01_var(--hue))] dark:bg-[oklch(0.30_0.02_var(--hue))] opacity-0 absolute h-8 w-8 top-3 right-3 rounded-lg transition-all cursor-pointer;}修复后重新执行:
pnpm buildAstro 完成静态页面生成,Pagefind 随后为文章建立搜索索引。这个问题提醒我,pnpm dev 只能验证开发状态;提交或部署前,必须至少完成一次与生产环境一致的构建。
让仓库成为唯一可信来源
开发过程中会产生构建目录、压缩包、临时密钥和工具缓存。如果这些内容都留在项目根目录,仓库很快就会变得难以维护。
我重新整理了目录,并明确区分源码、文档和生成物:
Miike_Blog/├─ public/ # 静态资源├─ src/│ ├─ components/ # 页面组件│ ├─ content/posts/ # 博客文章│ ├─ layouts/ # 页面布局│ └─ styles/ # 全局与 Markdown 样式├─ docs/ # 项目文档├─ astro.config.mjs├─ package.json└─ pnpm-lock.yaml像 node_modules/、dist/、部署压缩包以及私钥,都不应该提交到 GitHub。源码推送完成后,README 和 Wiki 也同步重写,明确说明项目来源、技术栈、本地运行方法以及部署方式。
这样做的意义不只是让目录更好看。仓库应该能够回答三个问题:项目是什么、怎样运行、怎样部署。只要一台新机器能够仅凭仓库完成构建,项目才算真正可复现。
SSH 排障:命令必须在正确的机器上执行
部署阶段最有代表性的坑,是把 Windows PowerShell 命令粘贴到了 Linux 服务器终端。
例如下面的 $env:USERPROFILE 和 Get-Content 都属于 PowerShell:
ssh-keygen -t ed25519 ` -C "miike-blog-deploy" ` -f "$env:USERPROFILE\.ssh\miike_blog_deploy"
Get-Content "$env:USERPROFILE\.ssh\miike_blog_deploy.pub"如果在 Linux shell 中执行,变量不会按预期展开,甚至会生成名字奇怪的文件。正确流程是:在本地 Windows 生成密钥,把公钥追加到服务器的授权文件中。
服务器端执行:
mkdir -p /root/.sshchmod 700 /root/.ssh
printf '%s\n' '这里替换为公钥内容' >> /root/.ssh/authorized_keyschmod 600 /root/.ssh/authorized_keys随后还发现服务器 SSH 配置中关闭了公钥认证:
PubkeyAuthentication no将其改为下面的配置,并先检查语法再重载服务:
PubkeyAuthentication yesPermitRootLogin yessshd -tsystemctl reload ssh || systemctl reload sshd另一个现象是,部分客户端可以连接,而自动化环境中的新连接会在密钥交换阶段被服务器主动关闭。因为连接尚未进入身份认证阶段,所以此时反复更换密码或私钥没有意义。更有效的排查方向是 SSH 服务日志、连接频率限制、防火墙和安全组:
journalctl -u ssh -u sshd --since "10 minutes ago"ss -lntp | grep ':22'这次最终选择通过已经可用的终端手动部署。工具并不是目的,能够判断故障发生在网络、密钥交换还是身份认证阶段,才是排障的关键。
在服务器上构建 Astro
项目指定使用 pnpm,因此服务器安装 Node.js 后,还要通过 Corepack 启用对应版本:
corepack enablecorepack prepare pnpm@9.14.4 --activate然后从 GitHub 拉取源码并构建:
mkdir -p /var/wwwcd /var/wwwgit clone --branch main \ https://github.com/cut3y1/Miikes-blogs.git miike-blog
cd /var/www/miike-blogpnpm install --frozen-lockfileSITE_URL="http://154.222.21.240/" pnpm build这里的 SITE_URL 很重要。Astro 会用它生成 Sitemap、RSS 等绝对地址。如果构建时仍然保留旧站点地址,页面虽然能够打开,订阅和站点地图中的链接却可能指向错误的位置。
构建完成后,最终静态文件位于:
/var/www/miike-blog/dist用 Nginx 提供静态站点
Astro 的默认开发服务器适合本地调试,不适合作为正式的静态文件服务。生产环境使用 Nginx,配置保持简单即可:
server { listen 80; listen [::]:80;
server_name 154.222.21.240; root /var/www/miike-blog/dist; index index.html;
location / { try_files $uri $uri/ =404; }
error_page 404 /404.html;
location ~* \.(css|js|jpg|jpeg|png|gif|svg|webp|ico|woff|woff2)$ { expires 7d; add_header Cache-Control "public, no-transform"; }}启用配置前先测试:
ln -sf /etc/nginx/sites-available/miike-blog \ /etc/nginx/sites-enabled/miike-blog
nginx -tsystemctl enable nginxsystemctl reload nginx最后同时从服务器内部和外部浏览器检查站点。服务器内部可以使用:
curl -I http://127.0.0.1/只有返回正常状态码、页面资源能够加载、文章路由可以访问,部署才算真正完成。
以后如何更新
现在 GitHub 仓库是源码的唯一来源。以后发布文章或修改样式,只需要先把改动推送到 main,再在服务器执行:
cd /var/www/miike-bloggit pull --ff-only origin mainpnpm install --frozen-lockfileSITE_URL="http://154.222.21.240/" pnpm buildnginx -t && systemctl reload nginx当前部署仍然使用 IP 和 HTTP。后续如果绑定域名,还需要把 SITE_URL 和 Nginx 的 server_name 改成正式域名,并配置 HTTPS 证书。
这次开发留下的几条经验
第一,开发服务器能运行只是起点,生产构建必须单独验证。Astro、Tailwind 和内容索引工具在构建阶段会执行更严格的检查。
第二,部署问题要分层判断。连接在密钥交换阶段断开时,不应该把时间花在密码上;页面打不开时,也要分别检查构建产物、Nginx、服务器防火墙和云平台安全组。
第三,命令有明确的执行环境。PowerShell、Linux shell 和远程服务器终端看起来都能输入命令,但变量语法、路径格式和内置命令完全不同。
第四,部署流程应该可重复。固定 Node.js 与 pnpm 版本、保留锁文件、明确 SITE_URL、让服务器从 GitHub 构建,都能减少“只在某台电脑上可以运行”的情况。
最后,基于模板开发并不意味着只是换一个名字。真正属于自己的博客,来自对交互、色彩、内容结构和部署方式的一次次选择。Miike’s Blog 已经上线,而这篇文章正好成为它从源码走向服务器的第一份完整记录。
本站基于 Fuwari 模板开发,使用 Astro 与 Tailwind CSS 构建。感谢原作者和开源社区提供的优秀工具。