Astro主题使用技巧
记录使用 Astro 搭建博客的过程中的一些使用技巧,包括图片优化、代码高亮等等。
博客源代码:chensoul.github.io
图片优化
参考 Photosuite:一个为博客而生的图片处理方案,使用 Photosuite 优化博客图片。
使用 markdown 语法设置图片
注意图片路径前面没有 /,图片地址是相对路径。Photosuite 会将图片地址替换为 {imageBase}/example-picture.webp,imageBase 在 astro.config.ts 中由 photosuite 配置。
本文中的实际示例图使用站内绝对路径,避免构建文档时去请求外部 EXIF 服务。

如果使用绝对路径,比如 , Photosuite 不会进行替换。 渲染后的 html 如下:
<div class="photosuite-item"><a href="/images/example-picture.webp" data-fancybox="markdown"> <img src="/images/example-picture.webp" alt="example-picture" loading="lazy" decoding="async"></a> <div class="photosuite-caption">example-picture</div></div>也可以将 2 个或者 3 个图片排列到一起:

astro.config.ts 中配置
photosuite({ scope: '#article', imageBase: "https://cos.chensoul.cc/images", exif: { enabled: true, fields: [ 'Model', // Camera Model 'LensModel', // Lens Model 'FocalLength', // Focal Length 'FNumber', // Aperture 'ExposureTime', // Shutter Speed 'ISO', // ISO 'DateTimeOriginal' // Date Original ], separator: ' · ' // Separator },}),https://cos.chensoul.cc/images 对应的是 obs 存储里面的 images 目录,这里我使用的是 Cloudflare R2 对象存储。cos.chensoul.cc 是对象存储桶(桶名称为 cos)对应的自定义域名,该存储桶下目前有三个目录:
- dist:存储脚本库文件
- images:存储博客图片
- thumbs:存储博客缩略图
images 和 thumbs 在博客源码仓库的 public 目录有一个备份。使用 rclone 工具可以同步 public 目录下的这两个文件到 R2。
使用 Rclone 上传图片
安装和配置 rclone:
brew install rclonerclone config file~/.config/rclone/rclone.conf 文件如下:
[r2]type = s3provider = Cloudflareaccess_key_id = xxxxsecret_access_key = xxxxxendpoint = https://2e1980a0ef43f57b920ea28cdf9d38d1.r2.cloudflarestorage.comacl = privateno_check_bucket = true操作桶和对象:
rclone tree r2:cos
# 同步到桶里的子目录,例如 cos 桶下的 images。不删远程多余文件(默认就是这样), -P 显示进度rclone sync public/images r2:cos/images -Prclone sync public/thumbs r2:cos/thumbs -P
# 看会同步哪些文件,不真正执行(试跑)rclone sync public/images r2:cos/images -P --dry-run
# 多线程、显示传输进度rclone sync public/images r2:cos/images -P --transfers 4图片压缩和转换
在博客源代码仓库的 scripts/convert-to-webp.mjs 里合并了两种用法(均用 sharp):
- 默认:将 jpg/png 转为同名
.webp,默认删原图(可用--keep)。 --compress:就地压缩 jpg/png/webp,仅当体积变小才覆盖。
图片压缩
一键压缩 public/images 下所有 jpg/png/webp:
pnpm run compress-images只压缩指定子目录(相对项目根):
node scripts/convert-to-webp.mjs public/images/某个子目录 --compress说明:
- 使用项目里已有的 sharp,不新增依赖。
- 会处理 jpg / jpeg / png / webp,长边超过 1920px 会等比缩小。
- 只有压缩后体积更小才会覆盖原图,变大的会跳过并打印「跳过,压缩后更大」。
- 默认目录是 public/images,也可通过参数改成其它目录。
图片转换
把 public/images 下所有 jpg/png 转成 webp(转完后删除原图):
pnpm run convert-to-webp只转换指定目录:
node scripts/convert-to-webp.mjs public/images/某子目录保留原图,只多出一份 .webp:
pnpm run convert-to-webp -- --keep# 或指定目录node scripts/convert-to-webp.mjs public/images --keep说明:
- 只处理 jpg / jpeg / png,已经是 webp 的会跳过。
- 输出为同名 .webp(如 a.jpg → a.webp)。
- 长边超过 1920px 会先等比缩小再转 webp。
- 不加
--keep时会在转换成功后删除原图;若文章或代码里还在用/images/xxx.jpg,需要改成.webp或先加--keep再逐步改引用。
文章缩略图
文章缩略图需要在博客文章的 md 文件元数据中定义 cover 字段,以指定缩略图的图片地址(该地址以 / 开头),thumbs 在 public 下面。
cover: /thumbs/astro-logo.svg缩略图的渲染由 Card.astro 实现,其中最重要的一段代码是获取缩略图的地址:
const buildCoverSrc = (): string | undefined => { const resolveLocalSrc = (src: string): string => { const normalizedSrc = src.startsWith("/") ? src : `/${src}`;
if (import.meta.env.DEV) { return normalizedSrc; }
const imagesBase = (SITE.imageConfig.imagesUrl ?? "") .trim() .replace(/\/$/, ""); return imagesBase ? `${imagesBase}${normalizedSrc}` : normalizedSrc; };
const rawImage = typeof image === "string" ? image.trim() : "";
if (rawImage) { if (rawImage.startsWith("http://") || rawImage.startsWith("https://")) { return rawImage; } return resolveLocalSrc(rawImage); }构建封面图 URL 逻辑:
- image 为空:取首个分类对应缩略图 /thumbs/{category}.svg,无分类则 /thumbs/blank.svg,再按下方规则解析
- 以 http:// 或 https:// 开头:原样返回(绝对地址)
- 其他(相对路径或 /images、/thumbs 等):
/images、/thumbs:始终走站内同源路径,减少列表页跨域图片请求- 其余相对路径:开发环境走站内路径;生产环境若配置了
SITE.imageConfig.imagesUrl才拼接 CDN
config.ts 中定义了图片的 CDN 地址 https://cos.chensoul.cc
imageConfig: { imagesUrl: "https://cos.chensoul.cc",}提示:使用 convertio 网站可以将图片转换为 SVG,然后使用 SVG Viewer 可以调整 SVG 大小,最后作为文章的缩略图。
代码高亮
Expressive Code 对静态架构非常友好,只要你的博客支持 Rehype 插件,就可以安装使用。
| 功能 | 语法示例 | 说明 |
|---|---|---|
| 标题 | title="app.ts" | 显示编辑器标签标题 |
| 语法高亮(ANSI) | ansi | 渲染 ANSI 终端转义 |
| 行标注(高亮) | {3,5-9} | 高亮第 3 行与 5–9 行 |
| 行标注(新增) | ins={4-6} | 以“新增”样式高亮 |
| 行标注(删除) | del={7,12} | 以“删除”样式高亮 |
| 行标注 + 标签 | ins={"Init":3-6} | 为 3–6 行添加引用标签 |
| 内联标注(文本) | ins="inserted" del="deleted" | 高亮匹配到的片段 |
| 内联标注(正则) | ins=/foo$.+$/ | 用正则匹配片段 |
| 折叠区块 | collapse={1-5,12-14} | 折叠多段代码 |
| 行号开关 | showLineNumbers | 需安装行号插件 |
| 起始行号 | startLineNumber=5 | 从第 5 行开始计数 |
| 自动换行 | wrap | 自动换行 |
| 悬挂缩进 | wrap hangingIndent=2 | 换行时额外缩进 2 列 |
| 不保留缩进 | wrap preserveIndent=false | 换行后不缩进 |
示例:
```go title="main.go" {17-20,22} del={14} ins={15,4-7} collapse={2-7} "rand.Seed(time.Now().UnixNano())"func greet(id int, wg *sync.WaitGroup) { defer wg.Done()
delay := time.Duration(rand.Intn(3000)) * time.Millisecond time.Sleep(delay)
fmt.Printf("#%d Ping %v\n", id, delay)}
func main() { rand.Seed(time.Now().UnixNano())
var wg sync.WaitGroup numGoroutines := 10 numGoroutines := 5
for i := 1; i <= numGoroutines; i++ { wg.Add(1) go greet(i, &wg) }
wg.Wait()}```效果:
func greet(id int, wg *sync.WaitGroup) {6 collapsed lines
defer wg.Done()
delay := time.Duration(rand.Intn(3000)) * time.Millisecond time.Sleep(delay)
fmt.Printf("#%d Ping %v\n", id, delay)}
func main() { rand.Seed(time.Now().UnixNano())
var wg sync.WaitGroup numGoroutines := 10 numGoroutines := 5
for i := 1; i <= numGoroutines; i++ { wg.Add(1) go greet(i, &wg) }
wg.Wait()}