chore: 初始化项目仓库
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1067" height="600" viewBox="0 0 1067 600" style="max-width: 100%; height: auto; cursor: default;"><defs><pattern id="checkerboard" width="20" height="20" patternUnits="userSpaceOnUse"><rect width="10" height="10" fill="#e0e0e0"/><rect x="10" y="0" width="10" height="10" fill="#ffffff"/><rect x="0" y="10" width="10" height="10" fill="#ffffff"/><rect x="10" y="10" width="10" height="10" fill="#e0e0e0"/></pattern></defs><rect width="100%" height="100%" fill="url(#checkerboard)"/><rect width="100%" height="100%" fill="rgba(255, 255, 255, 1)"/><!----><foreignObject x="0" y="0" width="100%" height="100%" style="pointer-events: none;"><div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; gap: 22px; font-family: sans-serif; font-weight: 600;"><div style="order: 1; width: 123px; height: 123px; display: flex; align-items: center; justify-content: center; background-color: transparent; backdrop-filter: none; border-radius: 0px;"><div style="max-width: 103px; max-height: 103px; flex-shrink: 0; color: rgb(0, 0, 0); filter: drop-shadow(rgba(0, 0, 0, 0) 0px 0px 0px); display: flex; align-items: center; justify-content: center; border-radius: 6%; overflow: hidden;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 185" width="100%" height="100%" preserveAspectRatio="xMidYMid meet"><path fill="#2396ed" d="M250.716 70.497c-5.765-4-18.976-5.5-29.304-3.5c-1.2-10-6.725-18.749-16.333-26.499l-5.524-4l-3.844 5.75c-4.803 7.5-7.205 18-6.485 28c.24 3.499 1.441 9.749 5.044 15.249c-3.362 2-10.328 4.5-19.455 4.5H1.155l-.48 2c-1.682 9.999-1.682 41.248 18.014 65.247c14.892 18.249 36.99 27.499 66.053 27.499c62.93 0 109.528-30.25 131.386-84.997c8.647.25 27.142 0 36.51-18.75c.24-.5.72-1.5 2.401-5.249l.961-2zM139.986 0h-26.42v24.999h26.42zm0 29.999h-26.42v24.999h26.42zm-31.225 0h-26.42v24.999h26.42zm-31.225 0H51.115v24.999h26.421zM46.311 59.998H19.89v24.999h26.42zm31.225 0H51.115v24.999h26.421zm31.225 0h-26.42v24.999h26.42zm31.226 0h-26.422v24.999h26.422zm31.225 0H144.79v24.999h26.422z"/></svg></div></div><span style="order: 0; font-size: 86px; color: rgb(0, 0, 0); text-shadow: rgba(0, 0, 0, 0.09) 0px 0px 0px; line-height: 1; white-space: nowrap;">编译 musl</span><span style="order: 2; font-size: 86px; color: rgb(0, 0, 0); text-shadow: rgba(0, 0, 0, 0.09) 0px 0px 0px; line-height: 1; white-space: nowrap;">可执行文件</span></div></foreignObject><g class="ratio-guide" style="display: none;"><rect x="0.16666666666674246" y="0" width="1066.6666666666665" height="600" fill="none" stroke="rgba(255,0,0,0.5)" stroke-width="2" stroke-dasharray="10 5"/><text x="10.166666666666742" y="30" fill="rgba(255,0,0,0.5)" font-size="20">16:9</text></g><rect x="0" y="0" width="100%" height="100%" fill="none" stroke="rgba(255,0,0,0.8)" stroke-width="2" class="canvas-border" style="display: none;"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: Docker 编译 musl 可执行文件
|
||||
published: 2025-03-27
|
||||
image: /posts/docker-musl-build/img/cover.svg
|
||||
description: 解决 Go 语言跨平台编译问题,使用 Docker + musl 编译静态链接的可执行文件。
|
||||
tags: ['Docker', 'Golang', '运维']
|
||||
draft: false
|
||||
---
|
||||
|
||||
## 解决问题
|
||||
|
||||
用 Go 进行跨平台编译时往往步骤复杂,用 Docker 可以轻松解决。但 Docker 的当前最小镜像 Alpine 其动态链接库采用 musl,与主流的 glibc 不同,往往不能方便执行。
|
||||
|
||||
## 查阅资料
|
||||
|
||||
```bash
|
||||
# 启动容器并挂载当前目录到 `/app`
|
||||
docker run -it --rm -v "$PWD":/app -w /app ubuntu:20.04 bash
|
||||
|
||||
# 在容器内执行以下操作:
|
||||
apt-get update && apt-get install -y musl-tools golang-go # 安装 musl 和 Go
|
||||
export CC=musl-gcc # 指定 musl 编译器
|
||||
export CGO_ENABLED=1 # 启用 CGO
|
||||
# 设置代理
|
||||
go env -w GOPROXY=https://goproxy.cn,direct
|
||||
go build -ldflags '-linkmode external -extldflags "-static"' -o myapp # 静态编译
|
||||
exit # 退出容器
|
||||
|
||||
# 验证结果(在宿主机执行)
|
||||
file myapp # 应显示 "statically linked"
|
||||
ldd myapp # 应显示 "not a dynamic executable"
|
||||
|
||||
wget https://go.dev/dl/go1.21.linux-amd64.tar.gz
|
||||
|
||||
ENV PATH="/data/go/bin:$PATH"
|
||||
```
|
||||
|
||||
### 遇到问题
|
||||
|
||||
1. `go mod tidy` 时报错 `tls: failed to verify certificate: x509: certificate`
|
||||
|
||||
解决:`RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*`
|
||||
|
||||
基础镜像的证书未更新,导致 https 的请求出现类似错误。
|
||||
|
||||

|
||||
|
||||
2. 直接安装 `golang-go` 其版本过低
|
||||
|
||||
解决: `PATH="/data/go/bin:$PATH"` 手动指定环境
|
||||
|
||||
## 最终方案
|
||||
|
||||
1. 下载 `go1.24.1.linux-amd64.tar.gz` 解压
|
||||
2. 编写 Dockerfile
|
||||
3. 执行 `docker build -t go-build:v1 .`
|
||||
|
||||
Dockerfile 内容:
|
||||
|
||||
```dockerfile
|
||||
FROM ubuntu:20.04
|
||||
|
||||
LABEL org.opencontainers.image.authors="sutong"
|
||||
|
||||
COPY go/ /go/
|
||||
|
||||
ENV PATH="/go/bin:$PATH"
|
||||
|
||||
RUN apt-get update && apt-get install -y musl-tools
|
||||
|
||||
## go mod tidy 时报错 tls: failed to verify certificate: x509: certificate
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
# 指定 musl 编译器
|
||||
ENV CC=musl-gcc
|
||||
# 启用 CGO
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
VOLUME /data
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
# docker build -t go-build:v1 .
|
||||
|
||||
# 直接执行编译 编译后删除
|
||||
docker run --rm -v .:/data go-build:v1 go build -o app_musl
|
||||
|
||||
# 构建编译环境
|
||||
docker run -it -d -v .:/data --name <容器名称> go-build:v1 /bin/bash
|
||||
# eg:docker run -it -d -v .:/data --name logbus-build go-build:v1 /bin/bash
|
||||
|
||||
docker exec -it <容器名称> go build -o <myapp>
|
||||
# eg:docker exec -it logbus-build go build -o logbus_musl_1
|
||||
```
|
||||
|
||||
### 额外注意
|
||||
|
||||
若没科学上网需要设置代理:
|
||||
|
||||
```bash
|
||||
docker exec -it <容器名称> /bin/bash
|
||||
go env -w GOPROXY=https://goproxy.cn,direct
|
||||
go build -ldflags '-linkmode external -extldflags "-static"' -o myapp # 静态编译
|
||||
go build # musl 库编译
|
||||
```
|
||||
|
||||
### 运行环境示例
|
||||
|
||||
```dockerfile
|
||||
FROM alpine:latest
|
||||
|
||||
LABEL org.opencontainers.image.authors="sutong"
|
||||
|
||||
COPY glog_musl /app/
|
||||
|
||||
COPY conf.ini /data/
|
||||
COPY ipname.ini /data/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN chmod 777 /app/glog_musl
|
||||
|
||||
CMD ["./glog_musl", "logsweb", "/data/conf.ini"]
|
||||
|
||||
# docker build -t glog:v1 .
|
||||
# docker run -p 6801:6801 glog:v1
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1067" height="600" viewBox="0 0 1067 600" style="max-width: 100%; height: auto; cursor: default;"><defs><pattern id="checkerboard" width="20" height="20" patternUnits="userSpaceOnUse"><rect width="10" height="10" fill="#e0e0e0"/><rect x="10" y="0" width="10" height="10" fill="#ffffff"/><rect x="0" y="10" width="10" height="10" fill="#ffffff"/><rect x="10" y="10" width="10" height="10" fill="#e0e0e0"/></pattern></defs><rect width="100%" height="100%" fill="url(#checkerboard)"/><rect width="100%" height="100%" fill="rgba(255, 255, 255, 1)"/><!----><foreignObject x="0" y="0" width="100%" height="100%" style="pointer-events: none;"><div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; gap: 22px; font-family: sans-serif; font-weight: 600;"><div style="order: 1; width: 123px; height: 123px; display: flex; align-items: center; justify-content: center; background-color: transparent; backdrop-filter: none; border-radius: 0px;"><div style="max-width: 103px; max-height: 103px; flex-shrink: 0; color: rgb(0, 0, 0); filter: drop-shadow(rgba(0, 0, 0, 0) 0px 0px 0px); display: flex; align-items: center; justify-content: center; border-radius: 6%; overflow: hidden;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 185" width="100%" height="100%" preserveAspectRatio="xMidYMid meet"><path fill="#2396ed" d="M250.716 70.497c-5.765-4-18.976-5.5-29.304-3.5c-1.2-10-6.725-18.749-16.333-26.499l-5.524-4l-3.844 5.75c-4.803 7.5-7.205 18-6.485 28c.24 3.499 1.441 9.749 5.044 15.249c-3.362 2-10.328 4.5-19.455 4.5H1.155l-.48 2c-1.682 9.999-1.682 41.248 18.014 65.247c14.892 18.249 36.99 27.499 66.053 27.499c62.93 0 109.528-30.25 131.386-84.997c8.647.25 27.142 0 36.51-18.75c.24-.5.72-1.5 2.401-5.249l.961-2zM139.986 0h-26.42v24.999h26.42zm0 29.999h-26.42v24.999h26.42zm-31.225 0h-26.42v24.999h26.42zm-31.225 0H51.115v24.999h26.421zM46.311 59.998H19.89v24.999h26.42zm31.225 0H51.115v24.999h26.421zm31.225 0h-26.42v24.999h26.42zm31.226 0h-26.422v24.999h26.422zm31.225 0H144.79v24.999h26.422z"/></svg></div></div><span style="order: 0; font-size: 86px; color: rgb(0, 0, 0); text-shadow: rgba(0, 0, 0, 0.09) 0px 0px 0px; line-height: 1; white-space: nowrap;">构建时</span><span style="order: 2; font-size: 86px; color: rgb(0, 0, 0); text-shadow: rgba(0, 0, 0, 0.09) 0px 0px 0px; line-height: 1; white-space: nowrap;">指定用户启动</span></div></foreignObject><g class="ratio-guide" style="display: none;"><rect x="0.16666666666674246" y="0" width="1066.6666666666665" height="600" fill="none" stroke="rgba(255,0,0,0.5)" stroke-width="2" stroke-dasharray="10 5"/><text x="10.166666666666742" y="30" fill="rgba(255,0,0,0.5)" font-size="20">16:9</text></g><rect x="0" y="0" width="100%" height="100%" fill="none" stroke="rgba(255,0,0,0.8)" stroke-width="2" class="canvas-border" style="display: none;"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Docker 构建时指定用户启动
|
||||
published: 2025-04-01
|
||||
image: /posts/docker-user-start/img/cover.svg
|
||||
description: 解决 Docker 容器默认以 root 用户运行导致的权限问题,指定非 root 用户启动应用。
|
||||
tags: ['Docker', '运维', 'Linux']
|
||||
draft: false
|
||||
---
|
||||
|
||||
## 前言
|
||||
|
||||
我们在 Linux 使用应用时,往往不会直接使用 root 用户作为应用启动项,但 Dockerfile 默认就是 root 用户,导致挂载生成的文件路径往往是 root 权限,非 root 用户访问就很不方便。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 安装用户管理工具
|
||||
|
||||
若镜像非自带用户管理:
|
||||
|
||||
```dockerfile
|
||||
# 权限管理
|
||||
RUN apk add --no-cache shadow
|
||||
```
|
||||
|
||||
### 2. 创建用户并修改权限
|
||||
|
||||
```dockerfile
|
||||
# 创建用户组和用户
|
||||
RUN addgroup -S <app_user_group> && adduser -S -G <app_user_group> <app_user>
|
||||
|
||||
# 修改目录权限
|
||||
RUN chown -R <app_user_group>:<app_user> /data
|
||||
```
|
||||
|
||||
### 3. 指定用户启动
|
||||
|
||||
执行 docker 时加 `--user` 参数:
|
||||
|
||||
```bash
|
||||
docker run -it --user <app_user> <app_image>:<image_version>
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```dockerfile
|
||||
FROM alpine:latest
|
||||
|
||||
# 国内源
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
|
||||
|
||||
RUN apk update --no-cache
|
||||
# 权限管理
|
||||
RUN apk add --no-cache shadow
|
||||
# 调试
|
||||
RUN apk add --no-cache bash
|
||||
# 设置时区
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 创建用户组和用户
|
||||
RUN addgroup -S glog && adduser -S -G glog glog
|
||||
|
||||
COPY ./glog_static_musl_1 /app/
|
||||
|
||||
RUN mkdir /data
|
||||
|
||||
VOLUME [ "/data" ]
|
||||
WORKDIR /data
|
||||
|
||||
# 修改权限
|
||||
RUN chown -R glog:glog /data
|
||||
RUN chown -R glog:glog /app
|
||||
|
||||
# 指定用户启动
|
||||
USER glog
|
||||
CMD [ "/app/glog_static_musl_1", "logsweb", "./conf.ini" ]
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
关键步骤:
|
||||
|
||||
1. `apk add --no-cache shadow` - 安装用户管理工具
|
||||
2. `addgroup` + `adduser` - 创建用户
|
||||
3. `chown -R` - 修改目录权限
|
||||
4. `USER <username>` - 指定运行用户
|
||||
|
||||
这样容器内的应用就不再以 root 身份运行,更加安全。
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: Hello World - 欢迎来到尼克的小窝
|
||||
published: 2026-05-31T08:00:00
|
||||
description: 博客上线啦!这篇文章介绍本站的技术架构、功能特性和搭建过程中的一些思考。
|
||||
image: /posts/hello-world/img/cover.jpg
|
||||
pinned: true
|
||||
tags: ['博客', 'Nuxt']
|
||||
draft: false
|
||||
---
|
||||
|
||||
## 博客上线啦!
|
||||
|
||||
Hello World!欢迎来到**尼克的小窝**,这是我的第一篇博客文章。
|
||||
|
||||
经过一段时间的折腾,这个基于 Nuxt 3 的个人博客终于上线了。在这里记录一下这个项目的技术选型和功能特性。
|
||||
|
||||
## 技术栈
|
||||
|
||||
本站使用了以下技术:
|
||||
|
||||
| 技术 | 用途 |
|
||||
|------|------|
|
||||
| **Nuxt 3** | Vue 3 全栈框架,提供 SSG、路由、API 等能力 |
|
||||
| **TypeScript** | 类型安全,提升开发体验 |
|
||||
| **Tailwind CSS v4** | 原子化 CSS,快速构建 UI |
|
||||
| **@nuxt/content** | Markdown 驱动的内容管理 |
|
||||
| **Shiki** | 代码语法高亮 |
|
||||
| **Giscus** | 基于 GitHub Discussions 的评论系统 |
|
||||
|
||||
## 功能特性
|
||||
|
||||
目前已实现的功能:
|
||||
|
||||
- **博客系统** - Markdown 文章,支持代码高亮、目录导航、搜索
|
||||
- **暗色模式** - 跟随系统或手动切换
|
||||
- **文章搜索** - 支持标题、描述、正文、标签多维度搜索
|
||||
- **标签系统** - 文章标签分类,高频标签快速筛选
|
||||
- **友链页面** - 展示友情链接,支持申请
|
||||
- **赞助页面** - 微信收款码展示
|
||||
- **封面制作** - 在线生成封面图片工具
|
||||
- **网易云热评** - 首页随机展示音乐评论
|
||||
- **访客统计** - 不蒜子计数器,带数字滚动动画
|
||||
- **SEO 优化** - 自动生成站点地图、Open Graph 标签
|
||||
|
||||
## 代码示例
|
||||
|
||||
本站的配置非常集中,一个文件搞定全站定制:
|
||||
|
||||
```typescript
|
||||
// config/site.ts
|
||||
export const siteConfig = {
|
||||
siteName: '尼克的小窝',
|
||||
url: 'https://nixus.top/',
|
||||
avatar: 'https://q2.qlogo.cn/headimg_dl?dst_uin=xxx&spec=100',
|
||||
// ... 改这一个文件,全站生效
|
||||
}
|
||||
```
|
||||
|
||||
文章的 frontmatter 也很简洁:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 文章标题
|
||||
published: 2026-05-31T08:00:00
|
||||
description: 文章简介
|
||||
tags: ['标签1', '标签2']
|
||||
---
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
本站使用 SSG 静态生成,构建后直接部署到静态托管即可:
|
||||
|
||||
```bash
|
||||
npx nuxi generate
|
||||
```
|
||||
|
||||
构建产物在 `.output/public` 目录下,可以直接丢到 Nginx、Vercel、Netlify 等平台。
|
||||
|
||||
## 后续计划
|
||||
|
||||
- [ ] RSS 订阅
|
||||
- [ ] 文章目录优化
|
||||
- [ ] 更多小工具
|
||||
- [ ] 持续输出内容
|
||||
|
||||
## 致谢
|
||||
|
||||
感谢 [二叉树树](https://2x.nz) 的开源项目,本站的 UI 设计参考了他的项目。
|
||||
|
||||
---
|
||||
|
||||
**感谢阅读!如果觉得不错,欢迎点个 Star 或者留个评论 (☞゚ヮ゚)☞**
|
||||
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: Nuxt Content v3 踩坑记录:目录、图片与那些反直觉的设计
|
||||
published: 2026-05-31T14:00:00
|
||||
description: 从 SvelteKit 迁移到 Nuxt 3 的过程中,遇到了不少 Nuxt Content v3 的坑。这篇文章记录了目录导航、图片路径、ProseImg 组件等问题的排查和解决过程。
|
||||
image: /posts/nuxt-content-lessons/img/cover.jpg
|
||||
tags: ['Nuxt', '踩坑', 'Nuxt Content']
|
||||
draft: false
|
||||
---
|
||||
|
||||
# 背景
|
||||
|
||||
最近在用 Nuxt 3 + Nuxt Content v3 搭建一个个人博客,参考了一个 SvelteKit 项目(svaf)的架构。原以为换个框架只是语法差异,没想到在几个关键功能上踩了不少坑。
|
||||
|
||||
这篇文章记录了三个核心问题的排查过程和最终方案。
|
||||
|
||||
# 问题一:目录导航(TOC)只有 h2
|
||||
|
||||
## 现象
|
||||
|
||||
Nuxt Content 内置了 TOC 数据,挂在 `post.body.toc` 上。直接用它渲染目录,发现**只有 h2 标题,h3 全部丢失**。
|
||||
|
||||
## 排查
|
||||
|
||||
查了官方文档,发现 `build.markdown.toc.depth` 可以控制深度:
|
||||
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
content: {
|
||||
build: {
|
||||
markdown: {
|
||||
toc: { depth: 3 } // 包含 h3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
配置改了,清了缓存重启,**依然只有 h2**。这个配置似乎不起作用。
|
||||
|
||||
## 最终方案
|
||||
|
||||
放弃依赖 Nuxt Content 的 toc,直接从 `post.body` 的 AST 里递归提取所有标题:
|
||||
|
||||
```typescript
|
||||
function extractHeadings(node: any, list: Heading[] = []): Heading[] {
|
||||
if (!node) return list
|
||||
if (Array.isArray(node)) {
|
||||
// minimark 格式:["h2", {id: "xxx"}, "文本"]
|
||||
if (typeof node[0] === 'string' && /^h[1-6]$/.test(node[0])) {
|
||||
const level = Number(node[0].charAt(1))
|
||||
const text = extractText(node.slice(2)).trim()
|
||||
list.push({ id: node[1]?.id || slugify(text), text, level })
|
||||
}
|
||||
for (const child of node) extractHeadings(child, list)
|
||||
}
|
||||
return list
|
||||
}
|
||||
```
|
||||
|
||||
关键点:Nuxt Content v3 的 body 使用 **minimark** 格式,标题节点是数组 `["h2", {id: "xxx"}, "标题文本"]`,不是对象。
|
||||
|
||||
# 问题二:图片路径解析错误
|
||||
|
||||
## 现象
|
||||
|
||||
markdown 里用相对路径引用图片:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
HTML 输出的 `<img src="img/sshfs.avif">` 看起来没问题,但浏览器请求的却是 `/posts/img/sshfs.avif` 而不是 `/posts/sshfs/img/sshfs.avif`。
|
||||
|
||||
## 排查
|
||||
|
||||
1. 检查了 `<base>` 标签 —— 没有
|
||||
2. 检查了重定向 —— 没有
|
||||
3. 直接访问 `/posts/sshfs/img/sshfs.avif` —— 返回 200 OK
|
||||
4. 直接访问 `/posts/img/sshfs.avif` —— 404
|
||||
|
||||
服务器路由没问题,问题是**浏览器解析相对路径时基于了错误的基准 URL**。
|
||||
|
||||
这可能和 Nuxt 的客户端路由有关:SPA 导航时,浏览器的"当前页面"和 Vue Router 的"当前路由"不同步,导致相对路径解析出错。
|
||||
|
||||
## 最终方案
|
||||
|
||||
两步解决:
|
||||
|
||||
**第一步:自定义 ProseImg 组件**
|
||||
|
||||
创建 `components/content/ProseImg.vue`,覆盖 Nuxt Content 默认的图片渲染:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const route = useRoute()
|
||||
const resolvedSrc = computed(() => {
|
||||
if (!props.src) return ''
|
||||
if (props.src.startsWith('/') || props.src.startsWith('http')) return props.src
|
||||
const base = route.path.replace(/\/$/, '')
|
||||
return `${base}/${props.src}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img :src="resolvedSrc" :alt="alt" />
|
||||
</template>
|
||||
```
|
||||
|
||||
**第二步:启用 MDC prose 组件**
|
||||
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
mdc: {
|
||||
components: {
|
||||
prose: true // 让 Nuxt Content 使用自定义 ProseImg
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 问题三:content 目录的图片如何服务
|
||||
|
||||
## 现象
|
||||
|
||||
原站(SvelteKit)的图片放在 `content/posts/{slug}/img/` 下,markdown 用相对路径引用,一切正常。Nuxt 项目里,content 目录的文件不会被当成静态资源服务。
|
||||
|
||||
## 方案对比
|
||||
|
||||
| 方案 | 优点 | 缺点 |
|
||||
|------|------|------|
|
||||
| 复制到 public/ | 简单 | 文件重复,需同步 |
|
||||
| Vite 插件拦截 | 原站方案 | Nuxt 不直接支持 |
|
||||
| Server Route | 不复制文件 | 需要手动写路由 |
|
||||
|
||||
最终选择 **Server Route**,和原站的 Vite 插件思路一致:
|
||||
|
||||
```typescript
|
||||
// server/routes/posts/[slug]/img/[...file].ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const url = getRequestURL(event)
|
||||
const match = url.pathname.match(/^\/posts\/([^/]+)\/img\/(.+)$/)
|
||||
if (!match) return
|
||||
|
||||
const [, slug, filename] = match
|
||||
const filePath = resolve('content/posts', slug, 'img', filename)
|
||||
const data = await readFile(filePath)
|
||||
setResponseHeader(event, 'content-type', MIME[ext])
|
||||
return data
|
||||
})
|
||||
```
|
||||
|
||||
请求 `/posts/sshfs/img/sshfs.avif` → 读取 `content/posts/sshfs/img/sshfs.avif` → 返回图片。
|
||||
|
||||
# 总结
|
||||
|
||||
| 问题 | 根因 | 方案 |
|
||||
|------|------|------|
|
||||
| TOC 只有 h2 | `toc.depth` 配置不生效 | 从 body AST 手动提取 |
|
||||
| 图片路径错误 | SPA 路由导致相对路径解析基准错误 | ProseImg 组件 + mdc.prose 配置 |
|
||||
| content 图片 404 | content 目录不暴露给浏览器 | Server Route 直接读取返回 |
|
||||
|
||||
Nuxt Content v3 的设计理念是"图片放 public/,用绝对路径"。如果想像 SvelteKit 项目那样把图片和文章放一起,需要额外做不少工作。
|
||||
|
||||
不过一旦这些坑都踩过,整体开发体验还是很流畅的。Nuxt 的生态系统、自动导入、模块系统都比 SvelteKit 成熟很多。
|
||||
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Nuxt 3 + Tailwind CSS 客户端导航链接下划线闪烁问题
|
||||
published: 2026-05-31T18:47:00
|
||||
image: /posts/nuxt-tailwind-underline-fix/img/cover.png
|
||||
description: 记录 Nuxt 3 配合 Tailwind CSS 时,客户端导航导致链接文字出现下划线闪烁的问题及解决方案。
|
||||
tags: ['Nuxt', 'Tailwind CSS', 'CSS', '踩坑']
|
||||
author: Nixus
|
||||
---
|
||||
|
||||
## 问题描述
|
||||
|
||||
在 Nuxt 3 项目中使用 Tailwind CSS 时,发现一个奇怪的现象:
|
||||
|
||||
- 首次通过客户端导航(点击 `<NuxtLink>`)跳转到某个页面时,**所有链接文字都会出现下划线**
|
||||
- 手动刷新页面后,下划线消失,样式恢复正常
|
||||
|
||||
这个问题在开发模式和生产构建后都会出现。
|
||||
|
||||
## 原因分析
|
||||
|
||||
Tailwind CSS 的 Preflight(基于 modern-normalize)会重置链接样式:
|
||||
|
||||
```css
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
```
|
||||
|
||||
正常情况下,这会让链接继承父元素的 `text-decoration`(通常是 `none`),从而去掉浏览器默认的下划线。
|
||||
|
||||
问题出在 **Nuxt 的客户端导航机制**:
|
||||
|
||||
1. 用户点击 `<NuxtLink>`,Vue Router 拦截导航
|
||||
2. 新页面组件异步加载并渲染
|
||||
3. **在 CSS 完全加载/应用之前,页面已经渲染了**
|
||||
4. 此时浏览器使用默认样式(链接带下划线)
|
||||
5. CSS 加载完成后,下划线消失
|
||||
|
||||
这就是典型的 **FOUC(Flash of Unstyled Content)** 问题。
|
||||
|
||||
## 为什么刷新就正常了?
|
||||
|
||||
刷新页面时,浏览器会等待所有 CSS 加载完成后再渲染页面(SSR 模式下 HTML 和 CSS 是一起返回的)。所以刷新后样式是正确的。
|
||||
|
||||
而客户端导航是 JavaScript 驱动的,CSS 的加载和页面的渲染是异步的,就出现了时序差。
|
||||
|
||||
## 解决方案
|
||||
|
||||
尝试了多种方案:
|
||||
|
||||
- `@layer base` 覆盖 — 无效,CSS layers 的优先级问题
|
||||
- `corePlugins.preflight: false` — 无效,且破坏其他基础样式
|
||||
- `features.inlineStyles: true` — 无效
|
||||
- 页面过渡动画 — 只是掩盖,不能根治
|
||||
|
||||
最终有效的方案是在 `app.vue` 中用 `!important` 强制覆盖:
|
||||
|
||||
```vue
|
||||
<style>
|
||||
a:not(.prose a) {
|
||||
text-decoration-line: none !important;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**关键点:**
|
||||
- `!important` 是为了对抗 Tailwind CSS layers 的优先级
|
||||
- `:not(.prose a)` 排除文章正文区域,保留文章内链接的下划线(可读性需要)
|
||||
|
||||
## 总结
|
||||
|
||||
这是 Nuxt 3 + Tailwind CSS 的一个已知问题,本质上是 CSS 加载时序导致的 FOUC。`!important` 虽然不够优雅,但在这个场景下是务实的解决方案。
|
||||
|
||||
如果你有更好的方案,欢迎在 [Issues](https://github.com/wishesl/SuBlog/issues) 提出!
|
||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: SvelteKit 入门:为什么它比你想象的更简单
|
||||
published: 2026-05-30
|
||||
description: SvelteKit 是一个全栈 Svelte 框架,这篇文章带你了解它的核心概念和优势。
|
||||
image: /posts/sveltekit-intro/img/cover.jpg
|
||||
tags: ['SvelteKit', '前端框架']
|
||||
draft: false
|
||||
---
|
||||
|
||||
## 什么是 SvelteKit?
|
||||
|
||||
SvelteKit 是 Svelte 的官方全栈框架,类似于 React 之于 Next.js 的关系。它提供了路由、SSR、SSG 等能力,让开发者可以快速构建现代 Web 应用。
|
||||
|
||||

|
||||
|
||||
## 核心特点
|
||||
|
||||
### 1. 编译时框架
|
||||
|
||||
Svelte 和 React/Vue 最大的区别在于:**它在编译阶段就把组件转成原生 JS**,运行时没有虚拟 DOM。
|
||||
|
||||
```typescript
|
||||
// Svelte 组件(编译后变成直接的 DOM 操作)
|
||||
let count = 0
|
||||
function increment() {
|
||||
count += 1
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 文件路由
|
||||
|
||||
`src/routes/` 目录结构即 URL,不需要手动配置路由表:
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +page.svelte → /
|
||||
├── about/
|
||||
│ └── +page.svelte → /about
|
||||
└── posts/
|
||||
├── +page.svelte → /posts
|
||||
└── [slug]/
|
||||
└── +page.svelte → /posts/:slug
|
||||
```
|
||||
|
||||
### 3. 全栈能力
|
||||
|
||||
SvelteKit 内置了服务端渲染(SSR)、静态站点生成(SSG)、API 路由等功能,开箱即用。
|
||||
|
||||
## 和其他框架对比
|
||||
|
||||
| 特性 | SvelteKit | Next.js | Nuxt |
|
||||
|------|-----------|---------|------|
|
||||
| 学习曲线 | 低 | 中 | 中 |
|
||||
| 包体积 | 小 | 大 | 中 |
|
||||
| 运行时开销 | 无 | 有 | 有 |
|
||||
| 生态 | 小 | 大 | 大 |
|
||||
|
||||
## 总结
|
||||
|
||||
如果你追求**简洁的代码**和**极致的性能**,SvelteKit 是一个非常值得尝试的选择。它的学习曲线平缓,写起来就像在写原生 HTML/CSS/JS。
|
||||
|
||||

|
||||