Astro 博客搭建:多主题设计系统与移动端适配

基于 Astro v6 搭建静态博客,实现 Dark/Light/System 三模式主题切换,并构建名为"The Terminal"的设计系统

为什么选 Astro

静态站点生成器(SSG)对内容型博客是最优解。Astro v6 的核心优势:

特性Astro v6Next.js (SSG)Hugo
默认零客户端 JS
构建速度~550ms~3s~800ms
内容集合类型安全✅ (Zod)
需要学习模板语法轻量 JSXReact 全家桶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;
}

多主题实现

三种模式:darklightsystem。通过 <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 变量驱动的多主题,这套组合对个人博客来说开发效率和运行性能都达到了很好的平衡。没有运行时框架的开销,没有主题切换的闪烁,没有移动端的布局崩坏。静态文件的简单性意味着部署和运维成本几乎为零。