我折腾了一套博客工作流,Astro + VS Code Web + 自建图床
我最近一直想把自己的博客重新搞一下。之前的方案有点重,每次写文章都要开本地编辑器,插个截图还要折腾半天。我就想,能不能搞一套轻量一点的,写博客尽量在线,插图片尽量省事。
折腾了几天,跑通了一套自己还比较满意的方案。
用 Astro 搭站点,用 VS Code Web 在线写 Markdown,用 Cloudflare Worker 加 R2 做图床,最后用 Raycast 或者 shell 脚本实现复制图片一键上传,自动返回 Markdown 链接。
坦率的讲,这套东西不是什么高深技术,就是把几个现成的工具串起来。但跑通之后写文章的体验确实顺了很多。我也不知道对大家有没有用,但我已经把整个过程和踩过的坑都记下来了,如果你觉得有点参考价值,就当是多个思路。
先聊聊为什么选 Astro。
我之前也试过 Hexo、Hugo、Next.js,最后停在 Astro 上,原因很直接。这玩意做博客真的太合适了。静态站点,默认产物干净,首屏快。对 Markdown 和 MDX 的支持是原生级别的,写文章不需要折腾。部署也简单,Cloudflare Pages、Vercel、Netlify 都能一键搞定。
如果你做的是内容输出的个人博客,不是那种复杂的前后端系统,Astro 基本是稳的。我自己一直觉得,博客的核心是阅读体验,不是花哨效果。排版舒不舒服,搜索好不好用,归档清不清晰,代码高亮和图片展示到不到位,这些才是关键。
初始化项目特别简单。
npm create astro@latest
跟着提示走,选个博客模板,或者先弄个空项目自己搭。进去之后 npm install,然后 npm run dev,打开 localhost:4321 就能看到页面了。
说到内容组织这块,Astro 有两种方式。
一种是在 src/pages 里直接写页面,适合文章少、不怎么更新的简单站点。另一种是用 Content Collections,这个我更推荐。把文章放在 src/content/blog 下面,每个 md 文件带一个 frontmatter,标题、日期、标签都写在里面。
---
title: 用 Astro 搭建博客
description: 记录我的博客工作流
pubDate: 2026-05-09
tags: ["Astro", "Blog", "Cloudflare"]
---
正文从这里开始...
然后在 src/content/config.ts 里定义一下 schema,Astro 会自动帮你校验 frontmatter,还能拿到类型提示。长期维护博客的话,这个结构靠谱很多。
这块需要注意一下,Content Collections 的 schema 定义长这样。
import { defineCollection, z } from 'astro:content'
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).optional(),
}),
})
export const collections = { blog }
选主题这块,如果你不想从零设计样式,最省事的方式是直接挑一个 Astro 主题。Astro Themes 上有很多选择。我的建议是,先选结构简单的,不要一开始就过度设计。博客跑起来比长得好看重要。
博客最核心的东西,说真的,就是阅读体验。排版、搜索、归档、图片和代码展示。花哨效果那都是后面的事。
然后就到了图片这块。插图片是博客里最高频的需求之一。
最常见的做法有两种。一种是把图片放在 public/images 目录里,Markdown 里直接写相对路径。简单直接,适合本地写作。但缺点也明显,图片跟着仓库走,管理大量截图会很麻烦。
另一种是用图床外链。Markdown 里写完整的 https 链接,图片托管在外面。不占仓库体积,在线编辑体验好。
我最后选的是第二种。Markdown 写外链图片,图片托管在 Cloudflare R2。
说到 VS Code Web,我这里用的不是本地桌面版,是浏览器里的那个版本。
好处很明显。随时在浏览器里改文章,跟 GitHub 仓库配合方便,不依赖本地完整开发环境,对纯 Markdown 写作很友好。我自己特别喜欢的一点是,不管在哪台电脑前,打开浏览器就能写。
但它也有明确的限制。很多桌面扩展不能用。PicGo 这种依赖本地能力的插件不支持。粘贴图片自动上传图床这类能力比较弱。
所以我一开始也踩过坑。很多教程推荐用 PicGo,或者 VS Code 的图床扩展。但这些基本都是围绕桌面环境设计的。VS Code Web 的限制是不能依赖本地可执行程序,很多扩展不支持 Web 平台。装上去会直接提示不可用于 Web 平台。
所以 VS Code Web 用户的正确思路不是硬上 PicGo,而是换个想法。把图片上传和编辑解耦。VS Code Web 负责写文章,独立脚本负责上传图片,上传后把 Markdown 链接粘贴回来。
这个思路一转,后面就顺了。
那怎么让图片上传尽量轻量呢?我用了 Cloudflare Worker 加 R2。
整体流程是这样的。本地脚本或者 Raycast 把图片 POST 到 Worker,Worker 把图片存到 R2,返回图片 URL,然后插入 Markdown 链接。
先在 Cloudflare 后台创建一个 R2 bucket,比如叫 blog-images。然后创建一个 Worker,绑定这个 bucket。
Worker 的代码其实不复杂,职责很简单。POST 请求接收上传文件,按日期路径存到 R2,返回图片 URL。GET 请求从 R2 读取图片返回。就这样?就这样。
export default {
async fetch(request, env) {
const url = new URL(request.url)
if (request.method === 'POST') {
// 鉴权检查
if (env.UPLOAD_TOKEN) {
const auth = request.headers.get('Authorization')
if (auth !== `Bearer ${env.UPLOAD_TOKEN}`) {
return new Response('Unauthorized', { status: 401 })
}
}
const formData = await request.formData()
const file = formData.get('file')
if (!file) {
return Response.json({ success: false, error: 'No file provided' }, { status: 400 })
}
// 按日期路径存储
const date = new Date()
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const key = `${year}/${month}/${day}/${file.name}`
await env.BUCKET.put(key, file)
const imageUrl = `${url.origin}/${key}`
return Response.json({ success: true, key, url: imageUrl })
}
if (request.method === 'GET') {
const key = url.pathname.slice(1)
if (!key) return new Response('Not Found', { status: 404 })
const object = await env.BUCKET.get(key)
if (!object) return new Response('Not Found', { status: 404 })
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('etag', object.httpEtag)
headers.set('Cache-Control', 'public, max-age=31536000')
return new Response(object.body, { headers })
}
return new Response('Method Not Allowed', { status: 405 })
},
}
wrangler.toml 这样配。
name = "blog-image-worker"
compatibility_date = "2026-01-01"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "blog-images"
部署就是 npx wrangler deploy。
鉴权这块我建议加上。用 npx wrangler secret put UPLOAD_TOKEN 设置一个 token,上传的时候带上 Authorization header。不然你的 Worker 开放着,被别人当公共图床用就不好了。。。想想就头疼。
上传成功之后返回的 JSON 长这样。
{
"success": true,
"key": "2026/05/09/test.jpg",
"url": "https://blog-image-worker.xxx.workers.dev/2026/05/09/test.jpg"
}
Markdown 里直接写这个 url 就行。
你可能想问,为什么不直接把图片存在 Astro 仓库里?
这个我也想过。如果你经常插截图,会遇到几个很现实的问题。仓库体积不断变大,git clone 越来越慢。在线编辑的时候图片不方便同步。图片管理也容易混乱。对于博客来说,图片独立托管确实更省心。
然后就到了整个工作流最关键的部分,上传脚本。
我的目标很明确。复制一张图片,快速上传,拿到 Markdown 格式链接。
在 macOS 下,我用 pngpaste 把剪贴板里的图片保存为文件。brew install pngpaste 装一下,然后 pngpaste /tmp/test.png 测试,剪贴板里有图片就会生成 PNG 文件。
图片导出成文件后,上传就简单了,一个 curl 命令搞定。
curl -F "file=@/tmp/test.png" https://your-worker.workers.dev
这里有一个很值得记录的坑,我折腾了好久。
一开始本地上传脚本一直没有输出,R2 里也没有文件。排查了半天,问题根本不在 Worker,也不在脚本逻辑,在代理。
最开始 curl 走的是 socks5://127.0.0.1:13659。结果会卡在 TLS 握手阶段,或者连接超时。
后来发现关键区别在于,socks5:// 是本地解析 DNS,socks5h:// 是让代理远端解析 DNS。对于 workers.dev 这类地址,前者可能会拿到异常 IP,导致 TLS 失败。后者就正常。
所以最终正确写法是 curl -x socks5h://127.0.0.1:13659。
这个坑真的浪费了我不少时间。如果你不用代理,直接留空就行。
完整的脚本长这样。
#!/bin/bash
set -euo pipefail
UPLOAD_URL="https://blog-image-worker.xxx.workers.dev"
UPLOAD_TOKEN="your-secret-token"
PROXY="socks5h://127.0.0.1:13659"
TMP_FILE="/tmp/clip-$(date +%s).png"
FILENAME="shot-$(date +%Y%m%d-%H%M%S).png"
if ! command -v pngpaste >/dev/null 2>&1; then
echo "pngpaste 未安装,请先执行: brew install pngpaste"
exit 1
fi
if ! pngpaste "$TMP_FILE"; then
echo "剪贴板中没有图片"
exit 1
fi
CURL_CMD=(curl --connect-timeout 10 --max-time 30 -sS)
if [ -n "$PROXY" ]; then
CURL_CMD+=(-x "$PROXY")
fi
if [ -n "$UPLOAD_TOKEN" ]; then
CURL_CMD+=(-H "Authorization: Bearer $UPLOAD_TOKEN")
fi
RESPONSE=$("${CURL_CMD[@]}" \
-F "file=@${TMP_FILE};filename=${FILENAME}" \
"$UPLOAD_URL") || {
echo "上传失败 | 无法连接 Worker"
rm -f "$TMP_FILE"
exit 1
}
URL=$(python3 -c "import sys, json; print(json.load(sys.stdin)['url'])" <<< "$RESPONSE") || {
echo "返回解析失败 | $RESPONSE"
rm -f "$TMP_FILE"
exit 1
}
MARKDOWN=""
echo -n "$MARKDOWN" | pbcopy
echo "$MARKDOWN"
rm -f "$TMP_FILE"
使用方法特别简单。截图或者复制图片到剪贴板,运行脚本,Markdown 图片链接就自动复制到剪贴板了,直接粘贴到 VS Code Web 里就行。就这么简单,真的。
如果你是 macOS 用户,Raycast 可以把这个脚本进一步变成快捷键工作流。
复制图片,打开 Raycast,执行 Upload Image to Markdown,自动上传并复制链接,回到 VS Code Web 粘贴。比手动找图床上传再复制链接要顺手很多。
在 Raycast 里创建 Script Command 的方式很简单。把脚本保存好,在头部加上 Raycast 的 metadata。
#!/bin/bash
# @raycast.schemaVersion 1
# @raycast.title Upload Image to Markdown
# @raycast.mode silent
# @raycast.packageName Blog Tools
# @raycast.icon 📸
# 然后接上面的脚本内容...
在 Raycast Settings 的 Extensions 里添加这个脚本就能用了。然后呢?然后就能直接在 Raycast 里搜索并运行了。
这套方案跑通之后,实际写博客的过程大概是这样的。
在 VS Code Web 里编辑 Markdown,写好 frontmatter 和正文。需要插图片的时候,截图或者复制图片,运行 Raycast 上传脚本,得到 Markdown 链接,直接粘贴到文章里。整个过程很顺。
我还想补充两个建议。
一个是自定义域名。虽然 workers.dev 可以直接用,但绑定自己的域名会更好。比如 img.example.com。链接更短更好看,更适合正式博客,也降低对 workers.dev 访问稳定性的依赖。在 Cloudflare Dashboard 的 Workers 里选你的 Worker,Triggers 那里加 Custom Domains 就行。
另一个就是上传鉴权,前面已经提过了,加个 Bearer token 就行,安全第一。
我自己也踩过一些坑,坦诚地讲,一开始配置 R2 和 Worker 可能需要花点时间,特别是如果你之前没接触过 Cloudflare 的话。鉴权设置、wrangler 部署这些,头两次可能会有点笨拙,花的时间比预想的长。但跑通之后基本就不用管了,非常稳定。
还有就是 socks5 和 socks5h 那个坑,我真的是花了半天才定位到问题。如果你用代理的话一定要注意这个,别跟我一样浪费时间。
顺着上面的再聊聊,这套东西适合什么人。用 Astro 写静态博客,文章主要是 Markdown,希望能在线编辑,用 VS Code Web,不想把大量图片塞进 Git 仓库,希望图片上传过程轻量自动化。这些场景如果你都中了,那这套方案应该挺合适。
如果你本来就是本地桌面版 VS Code 加 PicGo 用户,那不一定需要折腾这套。但如果你跟我一样,偏向浏览器写作和轻量工作流,这套方案真的很顺手。
核心就四件事。
Astro 搭站点,性能好,Markdown 支持好。VS Code Web 编辑,随时随地写文章。Cloudflare R2 托管图片,免费、稳定、简单。Raycast 或者脚本上传,复制图片一键生成 Markdown 链接。
把这些串起来,整个博客写作体验会顺很多。
还有个建议。虽然 workers.dev 可以直接用,但绑定自己的域名会更好,比如 img.example.com。链接更短更好看,也降低对 workers.dev 访问稳定性的依赖。在 Cloudflare Dashboard 的 Workers 里选你的 Worker,Triggers 那里加 Custom Domains 就行。上传鉴权前面提过了,加个 Bearer token 就行,安全第一。
其实回头想想,这套工作流就是把每个工具放在它最适合的位置上。Astro 天生适合内容站点,VS Code Web 天生适合轻量编辑,Cloudflare 天生适合边缘计算和存储。不是要把一个工具用到极致,而是让每个工具干它最擅长的事。
信息差这件事也是这样。很多看起来复杂的方案,拆开来看每一步都不难。难的是知道可以这样组合。磨平一些信息差,大概就是这个意思。
以上。