Astro 博客搭建:多主题设计系统与移动端适配
基于 Astro v6 搭建静态博客,实现 Dark/Light/System 三模式主题切换,并构建名为"The Terminal"的设计系统
为什么选 Astro
静态站点生成器(SSG)对内容型博客是最优解。Astro v6 的核心优势:
| 特性 | Astro v6 | Next.js (SSG) | Hugo |
|---|---|---|---|
| 默认零客户端 JS | ✅ | ❌ | ✅ |
| 构建速度 | ~550ms | ~3s | ~800ms |
| 内容集合类型安全 | ✅ (Zod) | ❌ | ❌ |
| 需要学习模板语法 | 轻量 JSX | React 全家桶 | Go Template |
关键数字:整站构建耗时 ~550ms(Docker node:22-alpine 环境,20 篇文章,零依赖的 Islands),输出纯静态 HTML 到 dist/。无客户端 JS 意味着 Lighthouse Performance 稳定在 98-100 分。
整体架构
部署链路:
Mac mini (Docker Build)
↓ npm run build → dist/
Caddy (port 8089, 静态文件服务)
↓ frp 内网穿透
DMIT VPS (frps)
↓
Caddy (HTTPS, 自动证书)
↓
用户浏览器
Mac mini 作为构建机,Caddy 直接 serve 静态文件,frp 把内网服务暴露到公网,VPS 上的 Caddy 负责终止 TLS。整条链路无 CDN,首屏 TTFB 约 180ms(亚洲节点)。
构建容器命令:
docker run --rm \
-v ~/projects/blog:/app \
-w /app \
node:22-alpine \
sh -c 'npm run build'
设计系统 “The Terminal”
美学定位
Brutalist + Editorial Tech。灵感来自终端界面与技术杂志的排版:粗边框、等宽字体、克制的装饰、大量留白。不是”科技感蓝紫渐变”,而是”你能打印出来的网页”。
色彩系统
Dark 模式主色调是深海军蓝配琥珀金,Light 模式是暖纸色配深琥珀。双模式色板通过 CSS 变量统一管理:
[data-theme="dark"] {
--bg-primary: #0a0e17;
--bg-secondary: #111827;
--text-primary: #e5e7eb;
--text-secondary: #9ca3af;
--accent: #f0b429;
--accent-hover: #fbbf24;
--border-color: #1f2937;
--code-bg: #1e293b;
}
[data-theme="light"] {
--bg-primary: #faf9f6;
--bg-secondary: #f5f5f4;
--text-primary: #1c1917;
--text-secondary: #57534e;
--accent: #b45309;
--accent-hover: #d97706;
--border-color: #d6d3d1;
--code-bg: #1e293b; /* 代码块反转为深色 */
}
设计决策:Light 模式的代码块保持深色背景。浅色代码 + 浅色背景的对比度极差(实测 WCAG AA 不通过),反转为深色后可读性显著提升。
字体
| 角色 | 字体 | 理由 |
|---|---|---|
| 英文正文 | IBM Plex Mono | 技术感、等宽、可读性好 |
| 中文正文 | Noto Serif SC | 衬线体增加阅读舒适度 |
| 代码 | IBM Plex Mono | 与正文统一,降低字体加载量 |
Serif + Mono 的对比是关键设计选择。中文衬线 + 英文等宽,在视觉上形成”正文/代码”的自然分界。
纹理与动效
Landing page 使用 SVG filter 实现胶片颗粒纹理叠加:
<svg class="hidden">
<filter id="grain">
<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/>
<feColorMatrix type="saturate" values="0"/>
</filter>
</svg>
<div class="grain-overlay"></div>
.grain-overlay {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9999;
opacity: 0.03;
filter: url(#grain);
}
网格背景用 CSS linear-gradient 实现,配合 animation 做缓慢移动:
.grid-bg {
background-image:
linear-gradient(rgba(240,180,41,0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(240,180,41,0.06) 1px, transparent 1px);
background-size: 60px 60px;
animation: gridScroll 20s linear infinite;
}
多主题实现
三种模式:dark、light、system。通过 <html data-theme="..."> 属性切换,全部 CSS 变量在属性选择器下定义。
切换逻辑(~40 行)
核心逻辑放在 <head> 中的 is:inline 脚本里,确保在页面渲染前执行,避免闪烁(FOUC):
<script is:inline>
(function() {
const STORAGE_KEY = 'theme';
const THEME_VALUES = ['dark', 'light', 'system'];
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function getStoredTheme() {
try { return localStorage.getItem(STORAGE_KEY); } catch(e) { return null; }
}
function applyTheme(preference) {
const effective = preference === 'system' ? getSystemTheme() : preference;
document.documentElement.setAttribute('data-theme', effective);
}
// 初始化:读取存储值,默认 system
const stored = getStoredTheme();
const theme = THEME_VALUES.includes(stored) ? stored : 'system';
applyTheme(theme);
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
const current = getStoredTheme();
if (current === 'system') applyTheme('system');
});
// 暴露切换函数给主题切换按钮
window.__setTheme = function(newTheme) {
localStorage.setItem(STORAGE_KEY, newTheme);
applyTheme(newTheme);
};
})();
</script>
关键点:
- FOUC 防护:
is:inline脚本在<head>中同步执行,页面渲染前就设置好data-theme - System 模式:监听
prefers-color-scheme媒体查询变化,实时跟随 OS 切换 - 持久化:
localStorage存储用户选择,刷新后保持
Light 模式的特殊处理
[data-theme="light"] :where(pre, code) {
background: var(--code-bg);
color: #e2e8f0;
}
[data-theme="light"] :not(pre) > code {
background: transparent;
color: var(--accent); /* 深琥珀色,不用背景色 */
}
代码块(<pre>)反转为深色背景,行内代码(<code>)用深琥珀色文字标识,不使用背景色块以保持行内阅读流畅。
响应式适配
四个断点,覆盖从桌面到小屏手机的完整范围:
| 断点 | 目标设备 | 调整内容 |
|---|---|---|
| ≤1024px | 平板 | 侧边栏收起,单栏布局 |
| ≤768px | 大屏手机 | 汉堡菜单,字号 17px→15px |
| ≤640px | 手机 | 全宽 CTA,表格横向滚动 |
| ≤380px | 小屏手机 | 字号 15px→14px,间距压缩 |
汉堡菜单实现:
@media (max-width: 768px) {
.nav-links { display: none; }
.nav-links.active {
display: flex;
flex-direction: column;
position: absolute;
top: 100%;
right: 0;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
}
表格滚动容器:
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
踩坑记录
1. Astro v6 Content API 变更
// Astro v5 — 已废弃
const { Content } = await post.render();
// Astro v6 — 新 API
const { Content } = await render(post);
render() 从 astro:content 导入,不再挂载在 content entry 对象上。迁移时搜索全部 *.render() 调用即可定位。
2. Heredoc over SSH 吞掉 frontmatter
# ❌ 这样写,--- 会被 shell 解释为 heredoc 分隔符
ssh host "cat > file.md << 'EOF'
---
title: xxx
EOF"
# ✅ 用 base64 传输
cat file.md | base64 | ssh host "base64 -d > file.md"
实际踩了这个坑。Heredoc 中的 --- 在某些 shell 环境下会被提前截断,导致 Astro 无法解析 frontmatter。base64 编码绕过了所有 shell 解析问题。
3. Google Fonts 加载优化
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;700&display=swap" rel="stylesheet">
实测 @import url() 方式延迟约 200ms,加上 preconnect 后降至 ~80ms。两个字体文件合计约 1.2MB,首次加载后浏览器缓存。
4. 阅读进度条脚本
<script is:inline>
// ❌ module 脚本在 Astro 中执行顺序不可控
// <script> — 可能延迟执行
// ✅ is:inline 确保同步执行
window.addEventListener('scroll', () => {
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = (scrollTop / scrollHeight) * 100;
document.querySelector('.reading-progress').style.width = progress + '%';
});
</script>
Astro 的 module script 会被打包和延迟加载,阅读进度条这类需要在页面渲染后立即响应的脚本必须用 is:inline。
总结
整套方案的核心数据:
- 构建时间:~550ms(Docker node:22-alpine)
- 零客户端 JS:Lighthouse Performance 98-100
- 主题切换:三模式,~40 行 JS,localStorage 持久化
- 设计系统:12+ CSS 变量,双色调色板,serif+mono 字体组合
- 响应式:4 断点,380px 起步
Astro v6 + 自定义设计系统 + CSS 变量驱动的多主题,这套组合对个人博客来说开发效率和运行性能都达到了很好的平衡。没有运行时框架的开销,没有主题切换的闪烁,没有移动端的布局崩坏。静态文件的简单性意味着部署和运维成本几乎为零。