Nuxt 搭建个人博客
2025-02-02 · 11min由于工作的原因,对Vue3进行了一段时间的康复训练。距离上次接触Vue3已经过去了4年,那会Vue3刚刚出,许多功能兼容性以及Vite都还不太完善。在使用Vue3着手开发了几个项目后,我个人的开发体验是觉得Vue3要比React好太多了,具体对比后面另外开篇文章细说吧。也因为这段体验,过年在家把4个月前才用Next搭建好的博客改用Nuxt重写了一遍(这次最后一次重写博客)。
技术选型
- Nuxt
- Typescript
- Unocss
- Nuxt Content
项目目录结构
├─ 📁assets
│ ├─ 📁css
│ │ └─ 📄main.css ───────────────── 全局CSS配置
│ ├─ 📁icons ───────────────────── unocss转换图标目录
│ └─ ......
│ └─ 📁svgo ───────────────────── svgo转换图表组件目录
│ └─ 📄logo.svg
├─ 📁components ───────────────── 组件目录
│ ├─ 📁content ────────────────── Nuxt Content Prose组件目录
│ │ ├─ 📄ProseA.vue
│ │ ├─ 📄ProseCode.vue
│ │ ├─ 📄ProseH2.vue
│ │ ├─ 📄ProseH3.vue
│ │ ├─ 📄ProseHr.vue
│ │ ├─ 📄ProseImg.vue
│ │ └─ 📄ProsePre.vue
├─ 📁content ───────────────── Nuxt Content Markdown读取目录
│ ├─ 📁blogs ──────────────── 文章目录
│ │ ├─ ......
│ └─ 📄index.md ───────────── 主页内容
├─ 📁layouts ──────────────── 布局目录
│ └─ 📄default.vue ─────────── 默认布局组件
├─ 📁pages ──────────────── 页面目录
│ ├─ 📁blogs
│ │ ├─ 📄index.vue ───────── 博客列表页
│ │ └─ 📄[slug].vue ───────── 博客详情页
│ └─ 📄index.vue ─────────── 主页
├─ 📁public ─────────────── 静态资源目录
│ ├─ 📁fonts ────────────── 字体目录
│ │ ├─ ......
│ ├─ 📁images ──────────── 图片目录
│ │ └─ ......
│ ├─ 📄favicon.ico
│ └─ 📄robots.txt
├─ 📁server ───────────── nuxt 服务端目录
│ └─ 📄tsconfig.json
├─ 📄.gitignore
├─ 📄app.vue ─────────── 入口组件
├─ 📄content.config.ts ───── Nuxt Content 配置文件
├─ 📄nuxt.config.ts ─────── Nuxt 配置文件
├─ 📄package.json
├─ 📄pnpm-lock.yaml
├─ 📄README.md
├─ 📄tsconfig.json
└─ 📄uno.config.ts ────── Unocss 配置文件
创建项目
完全跟着官方文档走
pnpm dlx nuxi@latest init my-blog
cd my-blog
pnpm run dev
打开localhost:3000 就可以看到了运行后的项目
Unocss安装
与 Tailwind CSS 相同的 CSS 原子类,但 Unocss 配置可读性更高些,而且 Unocss 也是 Vue 团队成员开发的,所以这次采用 Unocss 进行开发。
安装依赖
pnpm add -save -D unocss @unocss/nuxt @unocss/preset-mini
注册模块
export default defineNuxtConfig({
compatibilityDate: "2025-02-02",
devtools: { enabled: true },
modules: [
"@unocss/nuxt",
// ...
],
// ...
});
配置文件
在根目录下创建配置文件 uno.config.ts
import { defineConfig, presetAttributify, presetUno } from "unocss";
import presetMini from "@unocss/preset-mini";
import presetIcons from "@unocss/preset-icons";
import { FileSystemIconLoader } from "@iconify/utils/lib/loader/node-loaders";
export function createConfig({ strict = true, dev = true } = {}) {
return defineConfig({
envMode: dev ? "dev" : "build",
content: {
pipeline: {
include: [/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/],
},
},
presets: [
presetMini(),
presetAttributify({ strict }),
presetUno(),
],
});
}
export default createConfig();
这样就可以在项目中使用 Unocss 了。
SVG 文件自动识别转换成 CSS
我选用 Unocss 最大的原因,是因为 Unocss 官方提供了 preset-icons 模块进行图标预设,将 SVG 转成纯 CSS 来使用。
官方是以 Iconify 作为图标数据源,需要额外按照@iconify-json/* 模式在 devDependencies 中安装相应的图标集才能进行使用。
我这里没有使用 Iconify 的图标集,而是通过 @iconify/utils 提供的 FileSystemIconLoader方法,提前加载指定目录下的 SVG 文件,提供给 preset-icons转换成CSS类。
安装依赖
pnpm add -save -D @unocss/preset-icons @iconify/utils
读取 SVG 文件配置
import { defineConfig, presetAttributify, presetUno } from "unocss";
import presetMini from "@unocss/preset-mini";
import presetIcons from "@unocss/preset-icons";
import { FileSystemIconLoader } from "@iconify/utils/lib/loader/node-loaders";
export function createConfig({ strict = true, dev = true } = {}) {
return defineConfig({
envMode: dev ? "dev" : "build",
content: {
pipeline: {
include: [/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/],
},
},
presets: [
presetMini(),
presetAttributify({ strict }),
presetUno(),
presetIcons({
extraProperties: {
display: "inline-block",
"vertical-align": "middle",
width: "1em",
height: "1em",
},
collections: {
// 从文件系统加载图标的助手
// `./assets/icons` 下带有 `.svg` 扩展名的文件将按照文件名加载
// 您还可以提供一个转换回调来更改每个图标(可选)
icons: FileSystemIconLoader("./assets/icons/", (svg) => svg),
},
customizations: {
customize(props) {
props.height = "1em";
props.width = "1em";
return props;
},
},
}),
],
});
}
export default createConfig();
配置过后,会自动将根目录下的 assets/icons/*.svg 文件,按照文件名进行对于的 CSS 类转换。
需要注意的是 preset-icons 的纯CSS图标类名使用方式是:
<prefix><collection>-<icon><prefix><collection>:<icon>
因此我的 SVG 文件全部都才用破折号连接命名
使用示例
<template>
<!-- 对应 sun.svg -->
<div class="i-icons-sun?mask "></div>
</template>
preset-icons 这种转成纯 CSS 图标的解决方案,我觉得非常适合任何一个前端开发项目。对于独立开发者来说,可以使用 Iconify 的图标集作为数据源使用;对于项目开说,又可以通过 @iconify/utils 读取本地的 SVG 文件进行相同的转换。只需要从Figma上将图标按照命名规范进行导出到指定目录中,开发者就可以直接通过 CSS 类的方式对图标进行使用了,无疑是减少了开发者在图标浪费过多的时间。
Markdown内容渲染
Nuxt官方提供了 Nuxt Content 模块,通过配置就可以对项目的 content 目录下的内容文件(yml、json和markdown 等)进行数据读取,比提供组件渲染内容。
安装
pnpm add @nuxt/content
在Nuxt配置文件中注册模块,并进行样式配置
export default defineNuxtConfig({
// ...
modules: [
// ...
"@nuxt/content",
],
// 读取外部css文件
css: ["~/assets/css/main.css"],
// Nuxt Content 配置
content: {
build: {
markdown: {
// 代码块高亮样式配置
highlight: {
theme: {
// Default theme (same as single string)
default: "slack-ochin",
// Theme used if `html.dark`
dark: "slack-dark",
},
},
},
},
},
});
获取内容
在根目录下创建内容集合配置文件 content.config.ts,将 content 目录下的 markdown 文件读取创建集合,并命名为为 content。
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
export default defineContentConfig({
collections: {
content: defineCollection({
type: "page",
source: "**/*.md",
})
},
});
根目录下创建 content/index.md
---
title: Nanacai
date: 2020-11-11
---
主页内容
根目录下创建 pages/index.vue, Nuxt会自动读取pages下的vue文件转换成路由页面。
<script setup lang="ts">
const { data } = await useAsyncData(() => queryCollection("content").path("/").first());
useSeoMeta({
title: data.title,
description: data.description,
});
</script>
<template>
<ContentRenderer:value="data" />
</template>
Nuxt Content 提供 useAsyncData Hook请求内容集合数据,会以 Promise形式返回数据,将请求到的数据通过 ContentRenderer组件进行渲染。这里请求目标是 content/index.md。

博客列表
在 content 目录下在创建 blogs 目录,用于存放需要 Blog 内容,并在 content.config.ts 中注册集合:
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
export default defineContentConfig({
collections: {
content: defineCollection({
type: "page",
source: "**/*.md",
schema: z.object({
date: z.string(),
}),
}),
blogs: defineCollection({
type: "page",
source: "blogs/*.md",
schema: z.object({
date: z.string(),
}),
}),
},
});
创建 pages/blogs/index.vue ,通过 queryCollection 即可获取博客列表的数据:
<template>
<div class="flex flex-col w-full max-w-80ch md:max-w-80ch gap-4xl page-appear">
<div class="w-full pb-4 b-b-solid b-b b-b-gray-3 dark:b-b-gray-9 text-4xl font-500 c-gray-11 dark:c-gray-1 m-0">
Blogs
</div>
<div v-if="Array.isArray(allPosts)" class="flex flex-col gap-2xl ">
<NuxtLink
v-for="post in allPosts"
:key="`blog-post-${post.id}`"
class="flex flex-col md:flex-row md:items-end gap-2 text-lg group cursor-pointer"
:to="post.path"
>
<span
class="text-18px font-bold c-gray-8 dark:c-gray-4 transition-all group-hover:c-gray-11 dark:group-hover:c-gray-0 md:lh-none"
>
{{ post.title }}
</span>
</NuxtLink>
</div>
</div>
</template>
<script setup lang="ts">
import type { ReadTimeResults } from "reading-time";
useSeoMeta({
title: "Blogs - Nanacai",
});
const { data: allPosts } = await useAsyncData("all-post", () => queryCollection("blogs").order("date", "DESC").all());
</script>
进入 blogs 路由地址,博客列表如下:

博客详情
在 pages/blogs 目录下,创建 [slug].vue
<script setup>
const slug = useRoute().params.slug;
const { data: post } = await useAsyncData(`blog-${slug}`, () => {
return queryCollection("blogs").path(`/blogs/${slug}`).first();
});
</script>
<template>
<!-- Render the blog post as Prose & Vue components -->
<MakrdownRender :value="post" />
</template>
在博客列表点击博客后,会跳转到对应该组件,并渲染对应的博客内容:

页面过渡
Nuxt 的官方文档中就有说明,只需要在 nuxt.config.ts 中配置下,就可以像 Vue Transition 一样进行页面过渡。
export default defineNuxtConfig({
app: {
pageTransition: { name: "page", mode: "out-in" },
},
// ...
});
在 App.vue 中添加过渡样式
<style>
.page-enter-active {
animation: pageTransition ease-out 400ms;
}
.page-leave-active {
animation: pageTransition ease-in 400ms reverse;
}
@keyframes pageTransition {
from {
clip-path: xywh(0 0 100% 0);
opacity: 0;
filter: blur(1rem);
}
to {
clip-path: xywh(0 0 100% 100%);
opacity: 1;
filter: blur(0);
}
}
</style>
自动化部署
自动化部署通过 Github Action 实现。
在仓库页面,进入 Action,点击 set up a workflow yourself 创建工作流。

工作流是一个 Yml 文件,命名无所谓。
# 自动化部署名称
name: Publish And Deploy Demo
# 触发部署的条件: 当 master 分支, 有 push 的操作, 即可触发。
on:
push:
branches: # 此处不仅限于监听分支, 还监听 tag
- master
tags:
- "v*"
paths-ignore: # 下列文件的变更不触发部署,可以自行添加
- README.md
- LICENSE
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install Dependencies
run: npm install
- name: Build Project
run: npm run build
- name: Package Artifacts
run: |
tar -zcvf release.tgz .output pm2.config.json
ls -lh release.tgz
- name: Copy release to server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.ALIYUN_HOST }}
username: ${{ secrets.ALIYUN_USERNAME }}
password: ${{ secrets.ALIYUN_PASSWORD}}
port: ${{ secrets.ALIYUN_PORT || 22 }}
source: 'release.tgz'
target: '/www/blog/'
- name: SSH Post Commands
uses: appleboy/ssh-action@v1.2.1
with:
host: ${{ secrets.ALIYUN_HOST }}
username: ${{ secrets.ALIYUN_USERNAME }}
password: ${{ secrets.ALIYUN_PASSWORD}}
script: |
source /etc/profile
cd /www/blog
ls -l release.tgz # 检查文件是否存在(关键调试步骤
pm2 stop nuxt-blog || true
tar zxvf release.tgz
pm2 reload pm2.config.json
rm -f release.tgz # 清理临时文件
这样当推送 tag 时,就会触发 Github Action。依次执行安装依赖、打包、压缩打包产物,通过 scp 传输压缩包到指定目录, 连接服务器解压压缩包,并重启pm2,即可完成自动化部署。