用 Astro 搭建个人博客:从零到上线的完整实践

为什么选择 Astro

首先排除SPA模式,思考了以下几种:

  • Next.js ------ 太重,我只是写博客,不需要服务端渲染那套
  • Nuxt ------ Vue 技术栈,但我更想试试新东西
  • VitePress ------ 适合文档,博客功能不够灵活
  • Astro ------ 静态生成、按需加载、支持 Vue/React 组件

最后选了 Astro,原因很简单:

  1. 够轻 ------ 默认零 JavaScript,页面加载快,SEO 友好
  2. 够灵活 ------ 可以用 Vue 写组件
  3. 够新 ------ 2024 年之后火起来的,新技术意味着有探索空间,值得试试
  4. 部署简单 ------ 静态站点,扔哪都能跑

实际用下来,挺满意的。下面记录一下搭建过程。包括我踩过的每一个坑。希望你的博客之路能比我顺畅一些。

项目架构

最初只是普通的静态博客,后来加了"个人动态"功能,就需要后端了,考虑到项目并不复杂,没必要分多个仓,于是就搞了个 Monorepo。后续有其他想法也会加入其他动态功能。

我有两个选择:

  1. 纯前端方案 ------ 用第三方服务(例如文章的评论使用的 Giscus,后续有空会自己实现)
  2. 前后端分离 ------ 自己写个简单的 API 服务

我选了第二个。原因很简单:我想掌握全部控制权,随意改动。

于是我的博客变成了这样:

csharp 复制代码
blog/
├── apps/
│   ├── web/         # 前端 --- Astro 静态博客
│   └── server/      # 后端 --- Hono API 服务
├── package.json
├── pnpm-workspace.yaml
└── pnpm-lock.yaml

pnpm

pnpm 现代前端开发优选。它用硬链接的方式共享依赖,节省空间,速度快。对于 Monorepo 来说,pnpm 几乎是唯一选择。

技术栈

我的技术选型原则是:权衡利弊,刚刚好,不过度追求,不强行炫技

技术 用途 选择理由
Astro 5.x 静态站点框架 核心理由:零 JS 默认,快
Vue 3.x 交互组件(个人动态页) 日常使用,生态丰富
Swup 页面无刷新过渡动画 让网站有点 SPA 的感觉,提升体验
CSS Variables 主题系统,使用 hsl() 原生支持,不用额外库,暗色模式轻松搞

有尝试过 Tailwind CSS,但觉得太重了,有些地方需要去了解默认的各种样式,然后选择原生 CSS + Less 简单方便。

目录结构

根据开发过程逐渐需要新的结构并参考优秀项目目录,最后形成了自己的规范:

bash 复制代码
apps/web/
├── public/                  # 静态资源(图片、字体)--- 直接复制不构建
├── src/
│   ├── components/          # 组件 --- 按功能模块分组
│   │   ├── aside/           # 侧边栏(作者卡片、分类、标签)
│   │   ├── blog/            # 博客组件(文章卡片、详情)
│   │   ├── header/          # 页头(导航、搜索、主题切换)
│   │   └── moments/         # 个人动态(Vue 组件,因为交互多)
│   ├── content/             # 内容集合 --- Astro 的核心特性
│   │   └── blog/            # 博客文章(Markdown 文件都放这里)
│   ├── layouts/             # 页面布局 --- 统一 header/footer
│   ├── pages/               # 页面路由 --- 文件即路由
│   ├── plugins/             # Remark/Rehype 插件 --- Markdown 增强
│   └── styles/              # 全局样式 --- CSS Variables 定义
└── astro.config.mjs         # Astro 配置

几个设计原则:

  1. 按功能分组,而不是按类型 ------ 比如 components/blog/ 里放所有博客相关的组件,而不是把所有组件平铺
  2. Markdown 和代码分离 ------ content/blog/ 只放 Markdown 文件,组件和逻辑放 src/
  3. 路由即文件 ------ Astro 的 pages/ 目录结构直接对应 URL 路径,直观

核心功能实现

1. Markdown 内容管理

Astro 的 Content Collections 是我决定用它的重要原因之一。

我的文章 schema 定义:

typescript 复制代码
const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(), // 标题必填
    pubDate: z.coerce.date(), // 发布日期,自动解析
    updatedDate: z.coerce.date().optional(), // 可选
    description: z.string().optional(),
    tags: z.array(z.string()).default([]),
    category: z.string().optional(),
    draft: z.boolean().default(false), // 草稿模式
  }),
})

每篇文章的 Frontmatter 长这样:

markdown 复制代码
---
title: '文章标题'
pubDate: '2026-03-12'
description: '文章描述'
tags: ['Astro', '博客']
category: '前端'
draft: false
---

正文内容...

2. 阅读时间统计

我注意到很多博客都有"预计阅读时间"这个功能。

我的做法如下:核心依赖是两个插件

  • mdast-util-to-string ------ 提取 Markdown 的纯文本内容
  • reading-time ------ 计算阅读时间

插件代码:

typescript 复制代码
// src/plugins/remark-reading-time.ts
import { toString } from 'mdast-util-to-string'
import readingTime from 'reading-time'

export function remarkReadingTime() {
  return function (tree, file) {
    const textContent = toString(tree) // 提取纯文本
    const stats = readingTime(textContent) // 计算阅读时间

    // 把结果写入 Frontmatter
    file.data.astro.frontmatter.readingTime = {
      words: stats.words,
      minutes: Math.ceil(stats.minutes),
    }
  }
}

最后将 remarkReadingTime 插件添加到 Astro 配置中。

3. Markdown 样式:从 Tailwind 到自定义

Markdown 的渲染样式,我走过一段弯路。

最开始 ,我直接用了 @tailwindcss/typography 插件,几行配置就能让 Markdown 有不错的默认样式。但用了一段时间后发现:

  • Tailwind 的类名太多,调试起来麻烦
  • 有些细节样式不符合我的设计
  • 为了自定义一个颜色,得覆盖一堆类

后来,我干脆自己写了一套 Markdown 样式,用 Less 组织,按模块拆分:

bash 复制代码
styles/markdown/
├── markdown.css      # 基础排版(标题、段落、列表、表格)
└── markdown-code.css # 代码块样式(行号、复制按钮、滚动条)

代码块,自定义复制按钮:

  • 用 CSS 画图标(SVG mask),不用额外图片
  • 悬停时显示,点击后变勾选动画
  • 复制成功有视觉反馈

:::note提示 自定义样式虽然花时间,但完全可控,后期维护反而更轻松。 :::

Markdown 插件方面,我加了这些:

  • remark-gfm ------ GitHub Flavored Markdown,支持表格、删除线等
  • remark-github-admonitions-to-directives ------ GitHub 风格的提示块(例如上方的提示 ------ :::note、:::warning)
  • rehype-autolink-headings ------ 标题自动生成锚点,点击可跳转
  • rehype-code-copy ------ 代码块复制按钮(配合自定义样式)

4. Vue 组件集成:Astro 的 Island 架构

个人动态页(类似朋友圈)是我博客里动态数据的部分。有发布动态、图片上传、删除。

所以我用 Vue 写了这个页面。这也是 Astro "Island 架构"的典型应用------只有需要交互的部分才加载 JavaScript。

在 Astro 中使用 Vue 组件:

astro 复制代码
---
import MomentsPage from '../components/moments/MomentsPage.vue'
---

<MomentsPage client:load />

注意 client:load ------ 这是 Astro 的指令,告诉它这个组件需要在客户端加载和渲染。

Astro 的几种客户端指令:

  • client:load ------ 页面加载时立即加载(适合首屏交互)
  • client:idle ------ 浏览器空闲时加载(适合非关键交互)
  • client:visible ------ 滚动到可见区域时加载(适合长页面)
  • client:only ------ 只在客户端渲染,构建时跳过(适合依赖浏览器 API 的组件)

坑:

一开始我忘了加客户端指令,部署后直接报错:"window is not defined"。

原因是 Astro 默认是服务端渲染(SSR),在构建时执行。而 windowdocument 这些浏览器 API 在服务端不存在。

加上 client:load 后,Astro 就知道这个组件要在客户端跑,构建时会跳过它。

5. 页面无刷新过渡:Swup 让体验更流畅

点击一个链接,页面一下切换,没有白屏,没有闪烁,像 SPA 一样流畅,这就是 Swup 做的事。

配置非常简单:

javascript 复制代码
// astro.config.mjs
import swup from '@swup/astro'

export default defineConfig({
  integrations: [swup({ containers: ['#swup'] })],
})

然后在布局文件里给会变化的区域加个 id="swup"

astro 复制代码
<main id="swup" data-swup>
  <slot />
</main>

过渡动画用 CSS 写,我用的默认淡入淡出,也可以自定义更复杂的效果。

使用上可能说不清哪里不一样,但会觉得"这个网站很流畅"。

6. 主题切换:HSL 让调色变得简单

暗色模式现在是标配了。我用 CSS Variables 实现,而且用 HSL 定义颜色。

HSL(色相、饱和度、亮度)比 RGB 更直观。比如我想让主题色饱和度高一点,在 HSL 里只需要调饱和度(S),而在 RGB 里不够直观,需要多次尝试。

  • 想换主题色?只需要改 --hue 一个值,全站颜色自动更新
  • 调亮度?改 --lig 就行

亮暗模式切换逻辑:优先使用用户上次选择的主题,如果没有,就跟随系统偏好。

javascript 复制代码
const theme =
  localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')

document.documentElement.classList.toggle('dark', theme === 'dark')

注意: 我把这段代码放在 <head> 里,并使用 <script is:inline></script>,避免页面加载时闪烁(先亮后暗)。

构建优化

博客性能是我非常在意的。我用了几种优化手段:

1. 代码压缩

astro-compress 一站式压缩 CSS、JS、HTML。配置里我开启了所有选项:

  • CSS 压缩 + 移除注释
  • HTML 移除空白 + 移除注释
  • JS 压缩 + 移除 console.log

最后再加上 EdgeOne Pages 对网站的优化,gzip/brotli,效果明显。

2. 图片优化

图片通常是网站最大的资源。Astro 的 Image 组件会自动把图片转换成 WebP 格式(现代浏览器支持),并且按需加载。

或者可以通过 getImage API 手动处理图片压缩为 WebP/Avif 格式,也支持配置质量。

astro 复制代码
---
import { Image } from 'astro:assets'
import cover from '../images/cover.jpg'
---

<Image src={cover} alt="封面" />

好处:

  • 自动裁剪不同尺寸,适配不同屏幕
  • 懒加载,滚动到视口才加载
  • WebP 格式比 JPEG 小 30% 左右

3. Vite 配置优化

Astro 底层用 Vite,所以可以配置 Vite 的构建选项:

javascript 复制代码
vite: {
  build: {
    minify: 'esbuild',      // 快速压缩
    cssCodeSplit: true,     // CSS 分割,按需加载
    target: 'esnext',       // 只针对现代浏览器
  }
}

4. SEO 与分享卡片优化

博客写好了。微信、QQ、Twitter 上看到的链接预览卡片,都是靠 SEO 标签实现的。

astro 复制代码
<!-- src/pages/blog/[slug].astro -->
<head>
  <meta property="og:title" content={frontmatter.title} />
  <meta property="og:description" content={frontmatter.description} />
  <meta property="og:image" content={ogImage} />
  <meta property="og:url" content={Astro.url} />
  <meta property="og:type" content="article" />
  <meta property="article:published_time" content={frontmatter.pubDate} />
  <meta property="article:author" content={frontmatter.author} />
</head>

效果: 分享链接时,自动显示文章标题、描述和封面图。

后端 API 服务:简单的力量

个人动态需要登录、发布、删除,所以需要一个后端。

我的原则是:够用就行,不搞复杂

技术选型

技术 用途 选择理由
Hono Web 框架 轻量、快速、TypeScript 友好
better-sqlite3 数据库 单文件、零配置、性能够用
jose JWT 鉴权 现代、安全、API 简单

Hono 是我第一次用,体验很好。它的设计理念是"小",整个框架只有几十 KB,但功能完整。

核心代码结构

bash 复制代码
apps/server/
├── src/
│   ├── db/           # 数据库连接
│   ├── middleware/   # JWT 鉴权中间件
│   ├── routes/       # API 路由
│   └── index.ts      # 入口

入口文件简单:

typescript 复制代码
import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()
app.use('/api/*', cors()) // 允许跨域
app.route('/api/auth', authRoutes) // 登录接口
app.route('/api/moments', momentRoutes) // 动态接口

export default app

JWT 鉴权中间件:

原理是:用户登录后发一个 JWT token,之后每次请求都带上这个 token。服务端验证 token 有效性,有效才允许操作。

typescript 复制代码
export async function authMiddleware(c, next) {
  const token = c.req.header('Authorization')?.replace('Bearer ', '')

  if (!token) return c.json({ error: 'Unauthorized' }, 401)

  try {
    const payload = await verify(token, JWT_SECRET)
    c.set('user', payload)
    await next()
  } catch {
    return c.json({ error: 'Invalid token' }, 401)
  }
}

这段代码保护了所有需要登录的接口。

部署流程:从本地到线上

部署策略:

  • 前端(web):静态部署到腾讯云 EdgeOne Pages,CDN 加速,足够快而且免费够用
  • 后端(server):跑在腾讯云轻量应用服务器,包年也便宜

这样设计的好处是:前端完全静态,几乎零成本;后端按需部署,资源占用低。

域名

我用的是腾讯云的域名,绑定了两个子域名:

  • blog.jingshihang.cn:博客主站
  • server.jingshihang.cn:API 服务

图片/文件

图片和文件都放到腾讯云的 COS 对象存储里,访问速度快,便宜够用。

开启图片防盗链,只允许指定域访问图片

bash 复制代码
# 用 PM2 守护进程
pm2 start dist/index.js --name blog-server

PM2 是个进程管理工具,可以保证服务挂了自动重启。

Nginx 反向代理:

我用 Nginx 做反向代理,把 server.jingshihang.cn 的请求转发到本地 3001 端口。

nginx 复制代码
server {
    listen 443 ssl;
    server_name server.jingshihang.cn;

    location / {
        proxy_pass http://localhost:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

踩坑记录:都是真金白银换来的

坑 1:Swup 页面切换后组件失效

用 Swup 做页面无刷新切换时,遇到一个问题:切换页面后,代码块复制按钮、评论系统、统计脚本都不工作了。

原因: Swup 只替换页面内容区域(<main id="swup">),但不会重新执行 <script> 标签。那些依赖 DOM 加载后初始化的脚本,在切换页面后就失效了。

解决: 监听 Swup 的 swup:content:replace 事件,在内容替换后重新初始化:

javascript 复制代码
// 代码块复制按钮、评论系统、统计脚本、标题锚点滚动
document.addEventListener('swup:content:replace', initSomething)

关键点: 把需要初始化的逻辑封装成函数,首次加载时执行一次,每次 swup:content:replace 时再执行一次。

这个事件是 Swup 内容替换完成后触发的,是重新绑定事件、重新初始化组件的最佳时机。

坑 2:Shiki 代码高亮主题切换

Astro 默认用 Shiki 做代码高亮,支持双主题(亮色/暗色)。但自定义样式时踩了个坑:样式覆盖不了

问题: 我想让代码块的背景色和网站主题更搭,但无论怎么写 CSS,Shiki 的内联样式都优先生效。

原因: Shiki 会在 <pre><span> 标签上直接写内联样式(style="background-color: #xxx"),优先级比外部 CSS 高。

解决: 必须用 !important 强制覆盖,而且暗色模式要用 Shiki 提供的 CSS 变量:

css 复制代码
/* 亮色模式 - Shiki 默认用 --shiki-light */
.markdown-body pre.astro-code,
.markdown-body pre.astro-code span {
  background-color: var(--shiki-light-bg) !important;
}

/* 暗色模式 - 必须用 --shiki-dark */
html.dark .markdown-body pre.astro-code,
html.dark .markdown-body pre.astro-code span {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg-custom) !important;
}

关键点:

  • !important 是必须的,不然优先级不够
  • 暗色模式要用 var(--shiki-dark),这是 Shiki 生成的变量
  • 亮色模式默认用 var(--shiki-light),可以不写

这个坑让我明白了:Shiki 的双主题机制是靠 CSS 变量 + 类名切换实现的 ,要覆盖样式就得用同样的方式,而且必须加 !important

最后

博客搭好了,地址:就是当前看到的页面 blog.jingshihang.cn

整体用下来,Astro 给我的感觉是很友好的。

不做多余的事,不强迫用它的生态,不绑死在固定技术栈上。这种反而让它变得更灵活。

Monorepo 架构让前后端分离更清晰,部署也方便。虽然一开始配置麻烦了点,但方便后续维护,长期来看是值得的。

如果你也想搭博客

我的建议是:

  1. 先想清楚需求 ------ 你需要一个什么样的博客?
  2. 选对工具 ------ Astro 适合博客,Next.js 适合复杂应用
  3. 别过度设计 ------ 够用就行,后续可以迭代,重要的是执行,不能空想

未来规划

博客不是一蹴而就的,我还有一些计划:

  • 文章目录(TOC)组件和目录跳转
  • 照片墙页面
  • 评论系统集成(当前使用的 Giscus)
  • 国际化支持(如果有外国朋友想看)

参考资源:


🔗链接: blog.jingshihang.cn _如果这篇文章对你有帮助,欢迎分享和交流!

相关推荐
weixin_382395237 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__8 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.8 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~9 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华10 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子12 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅12 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni13 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学14 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端