2288 字
11 分钟
从 Fuwari 到 Miike's Blog:一次 Astro 博客的开发、构建与部署记录

这几天,我把 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;
}

修复后重新执行:

Terminal window
pnpm build

Astro 完成静态页面生成,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:USERPROFILEGet-Content 都属于 PowerShell:

Terminal window
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 生成密钥,把公钥追加到服务器的授权文件中。

服务器端执行:

Terminal window
mkdir -p /root/.ssh
chmod 700 /root/.ssh
printf '%s\n' '这里替换为公钥内容' >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys

随后还发现服务器 SSH 配置中关闭了公钥认证:

PubkeyAuthentication no

将其改为下面的配置,并先检查语法再重载服务:

PubkeyAuthentication yes
PermitRootLogin yes
Terminal window
sshd -t
systemctl reload ssh || systemctl reload sshd

另一个现象是,部分客户端可以连接,而自动化环境中的新连接会在密钥交换阶段被服务器主动关闭。因为连接尚未进入身份认证阶段,所以此时反复更换密码或私钥没有意义。更有效的排查方向是 SSH 服务日志、连接频率限制、防火墙和安全组:

Terminal window
journalctl -u ssh -u sshd --since "10 minutes ago"
ss -lntp | grep ':22'

这次最终选择通过已经可用的终端手动部署。工具并不是目的,能够判断故障发生在网络、密钥交换还是身份认证阶段,才是排障的关键。

在服务器上构建 Astro#

项目指定使用 pnpm,因此服务器安装 Node.js 后,还要通过 Corepack 启用对应版本:

Terminal window
corepack enable
corepack prepare pnpm@9.14.4 --activate

然后从 GitHub 拉取源码并构建:

Terminal window
mkdir -p /var/www
cd /var/www
git clone --branch main \
https://github.com/cut3y1/Miikes-blogs.git miike-blog
cd /var/www/miike-blog
pnpm install --frozen-lockfile
SITE_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";
}
}

启用配置前先测试:

Terminal window
ln -sf /etc/nginx/sites-available/miike-blog \
/etc/nginx/sites-enabled/miike-blog
nginx -t
systemctl enable nginx
systemctl reload nginx

最后同时从服务器内部和外部浏览器检查站点。服务器内部可以使用:

Terminal window
curl -I http://127.0.0.1/

只有返回正常状态码、页面资源能够加载、文章路由可以访问,部署才算真正完成。

以后如何更新#

现在 GitHub 仓库是源码的唯一来源。以后发布文章或修改样式,只需要先把改动推送到 main,再在服务器执行:

Terminal window
cd /var/www/miike-blog
git pull --ff-only origin main
pnpm install --frozen-lockfile
SITE_URL="http://154.222.21.240/" pnpm build
nginx -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 模板开发,使用 AstroTailwind CSS 构建。感谢原作者和开源社区提供的优秀工具。