Lyndra's Blog

Astro 博客搭建与部署

2025-01-14
工具教程 AstroGithubBlogSiyuan
22分钟
4387字
温馨提示:本文最后更新于 2025-03-11 ,部分信息可能因时间推移而不再适用,欢迎反馈。

Astro 博客搭建与部署

  基于 astro.build 搭建博客。可部署在服务器、www.netlify.com、github pages。

  在网上还看到有自动化工具:DevNow,基于 astro 一键式搭建博客,但主题无法自选。

  本文还是以常规的方式,选择主题,按照主题的要求修改源码配置,最后通过 github 的 action 自动部署到 github pages。

  本文在 Linux 下完成。

目标

  1. 搭建过程尽可能简单,不需要太复杂的技术,太绚丽的界面。
  2. 无需购买服务器,托管在 github,但开启自定义域名,通过自己的域名访问。
  3. 博客功能:有阅读目录TOC、清晰的代码块展示、归档和搜索的功能。
  4. 更新内容要方便。

主题选择

  自己设计主题和功能需要花费较多的时间,学习前端知识,学习astro。本文本着将博客当作一个工具来使用,而不是一个折腾、学习的项目的态度,尽可能的提供简洁的方案。

  本文选择主题 Astro Theme Yi,足够简洁,但不简单,需要的功能都有,能看得出来作者是按照自己的使用场景打造,作者自己的博客也确实就是基于这个主题建立的。相比之下,我之前基于 hugo 的 stack 主题功能上有一些简单,SEO不友好,还需要解决一些问题才能使用。

  感谢作者的努力!

域名注册

  我希望拥有一个自己的域名,单独用作博客,或者是自己的一些服务。如果不想购买域名,则可以直接使用 github 的 page 服务,或者部署在 netlify 上。

  我在 namesilo 上购买 top 域名,相比于 com 域名,会便宜很多。首年 1.88 美元,续费 4.88 美元。

  购买后,按照 cloudflare 的域名DNS解析指南,在 cloudflare 托管域名,将 namesilo 的域名的域名服务器,替换为 cloudflare 的。这样才能在 cloudflare 中管理自己的域名。

使用模板

  可以在 astro 官方网站中找自己喜欢的模板。下面以 Astro-yi 为例。

下载和配置

1
git clone https://github.com/cirry/astro-yi.git

  参考:Astro-Yi主题配置和写作技巧 原作者提供的主题配置,对模板进行修改。

本地小修改

  由于之前的 markdown 书写习惯是从二级标题开始,所以将 TOC 的标题改为从 H2 开始显示。

  修改文件 src/components/Toc.astro

1
tocbot.init({
2
tocSelector: ".toc-container",
3
contentSelector: ".markdown-body",
4
headingSelector: "h2,h3,h4",
5
hasInnerContainers: true,
6
headingsOffset: 80,
7
scrollSmoothOffset: -80,
8
scrollSmoothDuration: 200,
9
collapseDepth: 4,
10
onClick: function (){
11
return false
12
},
13
});

构建

  参考主题仓库的 readme,在终端中,运行下列命令。

  通过 npm 安装需要的组件,并且预览网站。

1
npm install -g pnpm
2
pnpm i
3
npm run dev # preview

  成功后,可在本地浏览器输入 http://localhost:4321/​ 预览

default

  将博客文章写在 src/content/blog​ 文件夹中,在 src/content/feed​ 文件夹中编写想发布的动态内容。通过下列命令编译打包内容。

1
npm run build # build

  打包完成后,在根目录中会生成一个 dist 文件夹。将 ‘dist’ 文件夹上传到 Web 服务器目录中,即可完成部署。对于 github 来说,可以使用 action 自动化实现打包部署的过程。

Action自动化部署

  参考:部署你的 Astro 站点至 GitHub Pages

  官方的教程是在源码仓库部署,而我的需求是将源码仓库设置为私有仓库,将 build 后的网站部署到另一个公开仓库。需要对官方的 action 脚本进行修改。

生成 PAT 和 Actions secrets

  由于是部署到另一个仓库,需要先获取 PAT,通过 PAT 提供另一个仓库的访问权限。可以参考:Hugo搭建 中的使用 git 模板​ 部分,生成 经典 Tokens​。需要注意,如果用细粒度令牌(Fine-grained Tokens)生成 PAT ,需要开启 Contents 的权限,否则在部署时会遇到权限错误。

  然后在仓库设置中,把 PAT 添加到 Actions secrets。

default

编写 deploy.yml

  在私有仓库,也就是源码仓库,新建 .github/workflows/deploy.yml ,并按照自己的仓库、分支名称,填写如下,其中 secrets.ASTRO_DEPLOY 就是生成的 PAT。

1
name: Deploy to GitHub Pages
2
3
on:
4
# 每次推送到 `main` 分支时触发这个"工作流程"
5
# 如果你使用了别的分支名,请按需将 `main` 替换成你的分支名
6
push:
7
branches: [ main ]
8
# 允许你在 GitHub 上的 Actions 标签中手动触发此"工作流程"
9
workflow_dispatch:
10
11
# 允许 job 克隆 repo 并创建一个 page deployment
12
permissions:
13
contents: read
14
pages: write
15
id-token: write
24 collapsed lines
16
17
jobs:
18
build:
19
runs-on: ubuntu-latest
20
steps:
21
- name: Checkout your repository using git
22
uses: actions/checkout@v4
23
- name: Install, build, and upload your site
24
uses: withastro/action@v3
25
with:
26
# path: . # 存储库中 Astro 项目的根位置。(可选)
27
# node-version: 20 # 用于构建站点的特定 Node.js 版本,默认为 20。(可选)
28
package-manager: pnpm@latest # 指定使用 pnpm 8.x 版本
29
# package-manager: pnpm@latest # 或者使用最新版本的 pnpm
30
- name: Deploy to GitHub Pages
31
id: deployment
32
uses: JamesIves/github-pages-deploy-action@v4 # 一个自动发布github pages的action
33
with:
34
repository-name: 用户名/公开仓库名。例如:codegithub/codegithub.github.io
35
token: ${{ secrets.ASTRO_DEPLOY }}
36
branch: main
37
folder: dist
38
clean: true
39
single-commit: true

跳过 github 的 Jekyll 流程

  在 GitHub Pages 部署 Astro 網站要注意的事情Bypassing Jekyll on GitHub Pages

  astro 编译后,会将样式文件放在 _astro​ 文件夹下,但 github 如果在仓库中选择基于分支构建 Pages,则会自动采取 Jekyll 的方式构建 Pages。而 Jekyll 会跳过前缀是下划线的文件夹的处理,在访问博客时,该文件夹下的内容就无法访问。如果采取 action 作为构建 Pages 的方式,就不受影响,astro 官方就是采用 action 作为构建 Pages 的方式。

  解决方式是在作为 pages 的仓库的根目录下创建一个名为 .nojekyll​ 空文件,这样会绕过 Jekyll 构建过程并直接部署内容。所以,在本文的场景中,需要在源码 astro 仓库的 public 文件夹创建 .nojekyll​ 空文件。这样部署到公开仓库后,公开仓库的根目录下就会有 .nojekyll​ 空文件。

default

github添加自己域名

  由于不想购买服务器,不想让个人使用的服务和代理 VPS 混合在一起使用,可以将博客部署到 github pages,然后为 github pages 配置自己的域名,就可以通过自己的域名访问了。

为 github pages 启用自己的域名

  参考:【官方】验证 GitHub Pages 的自定义域【教程】Github Page 添加自定义域名多项目部署为同一个GitHub Pages

  官方教程有些晦涩,中文翻译不完整,例如 apex 域名(顶点域名),标题里面没翻译,正文里面又翻译了,导致我以为 apex 是 github 的配置。

验证

  先在 github 的设置中,添加自己的域名。

  点击自己的头像,进入设置界面,选择 Pages,添加自己的域名。

default

  然后会显示如下内容,意思是,你现在需要去自己的域名托管网站,为自己的域名添加一条包含 固定的名称和内容 的 TXT 类型的 DNS 解析记录。

default

  比如在 cloudflare 中,添加一条 TXT 的 DNS 解析记录

default

  添加完成后,稍等一会,点击 Verify,github 就会自动去验证。

映射

  先配置顶点域名,将顶点域名映射到 github pages 的站点 ip 地址。

  官方文档中给出了 github pages 的 ipv4 和 ipv6 的所有 ip 地址,需要将顶点域名(也就是购买时获得的域名,没有任何子域名的。例如 your.top 和 www.your.top,前者是顶点域名,后者是子域名)解析到给出的几个 ip 地址。一般来说,名称填入 @ 就表示顶点域名。

default

  然后配置子域名,配置子域名时,就不用解析到 ip 地址了,直接选择 CNAME 类型,将自己的子域名映射到 your_github_useranme_.github.io 即可。

  配置完毕后如下(注意,如果是 cloudflare 配置,需要取消代理,即不点亮橙色云,否则无法强制开启 https):

default

  最后还需要在启用 github pages 的仓库(如果想在源码仓库开启 pages 也可以,需要将仓库权限改为 Public。否则,就需要新建一个 Public 仓库)的设置中,配置自己的域名。

default

强制 HTTPS

  参考:Github pages keep saying it cant enforce httpsUnable to enforce https on my site.

  如果需要让自定义域名强制支持 https,则需要等 github 自动设置证书生效后,勾选 Enforce HTTPS。

  注意,在 cloudflare 中,不能点亮小橙云,否则 github 不会自动申请证书。下图为关闭小橙云后,github 自动申请证书时的截图。

default

撰写文章并更新

思源笔记导出转换

  之前我的博客使用 hugo 搭建的,先在思源笔记上写文章,然后导出 md 压缩包,手动解压然后移动到对应的仓库中,并修改部分内容适配 hugo。

  现在换到 astro,由于 astro 不支持相对路径引用图片,写了一个自动化转换思源的 md 压缩包到 astro 博客框架的脚本。这个脚本会自动解压缩、替换图片路径、将图片移动到 public/images 下、修改文件命名等。

  脚本内容如下:

1
#!/usr/bin/env bash
2
set -eo pipefail
3
4
# 颜色配置
5
RED='\033[0;31m'
6
GREEN='\033[0;32m'
7
YELLOW='\033[1;33m'
8
NC='\033[0m'
9
10
# 基础路径配置
11
BLOG_PATH="/home/ling/Desktop/blog/astro-yi"
12
CONTENT_PATH="$BLOG_PATH/src/content/blog"
13
PUBLIC_IMAGES_PATH="$BLOG_PATH/public/images"
14
15
# 命令配置:格式「命令名="描述::处理函数::选项声明"」
353 collapsed lines
16
declare -A COMMAND_MAP=(
17
["unzip"]="解压思源笔记导出的md压缩包::handle_unzip::-f,--file:指定压缩包文件路径(必须)::-o,--output:输出目录(默认:与压缩包同名)"
18
["move-images"]="移动图片文件到公共目录::handle_move_images::-d,--dir:指定包含assets的目录(必须)"
19
["update-references"]="更新MD文件中的图片引用::handle_update_references::-f,--file:指定MD文件路径(必须)::-n,--name:文件夹名称(必须)"
20
["move-md"]="移动MD文件到博客根目录::handle_move_md::-f,--file:指定MD文件路径(必须)"
21
["process-all"]="执行完整处理流程::handle_process_all::-f,--file:指定压缩包文件路径(必须)::-d,--delete:处理完成后删除源压缩包(可选)"
22
)
23
24
# 错误处理函数
25
panic() {
26
echo -e "${RED}[错误]${NC} $1" >&2
27
exit 1
28
}
29
30
# 日志输出函数
31
log_info() {
32
echo -e "${GREEN}[信息]${NC} $1" >&2
33
}
34
35
log_warning() {
36
echo -e "${YELLOW}[警告]${NC} $1" >&2
37
}
38
39
log_success() {
40
echo -e "${GREEN}[成功]${NC} $1" >&2
41
}
42
43
##############################################
44
# 通用的帮助信息生成函数 #
45
##############################################
46
47
show_command_help() {
48
local cmd="$1"
49
local info="${COMMAND_MAP[$cmd]}"
50
51
# 1) 把所有 "::" 替换成一个不会在脚本中出现的分隔符,比如 '|'
52
info="${info//::/|}"
53
54
# 2) 这时才能安全地用 IFS='|' 来做分割
55
local desc handler options_str
56
IFS="|" read -r desc handler options_str <<< "$info"
57
58
echo -e "${YELLOW}命令: ${GREEN}$cmd${NC}"
59
echo -e "${YELLOW}描述: ${NC}$desc"
60
echo -e "${YELLOW}选项:${NC}"
61
62
# 3) 选项声明里可能有多段「::」分隔,继续替换并拆分
63
options_str="${options_str//::/|}"
64
IFS="|" read -ra opt_lines <<< "$options_str"
65
66
for opt_line in "${opt_lines[@]}"; do
67
[[ -z "$opt_line" ]] && continue
68
69
# opt_line 类似 "-p,--package:指定包名(必须)"
70
# 用 ":" 分割出(短+长选项)和描述
71
IFS=":" read -r flags desc <<< "$opt_line"
72
printf " ${GREEN}%-20s${NC} %s\n" "$flags" "$desc"
73
done
74
}
75
76
# 主帮助信息
77
show_help() {
78
echo -e "${YELLOW}使用方法:${NC}"
79
echo " $0 [命令] [选项]"
80
echo
81
echo -e "${YELLOW}可用命令:${NC}"
82
for cmd in "${!COMMAND_MAP[@]}"; do
83
local desc=${COMMAND_MAP[$cmd]%%::*}
84
printf " ${GREEN}%-15s${NC} %s\n" "$cmd" "$desc"
85
done
86
echo -e "\n使用 ${YELLOW}$0 命令 --help${NC} 查看具体命令帮助"
87
}
88
89
##############################################
90
# 命令处理函数 #
91
##############################################
92
93
handle_unzip() {
94
local file="" output=""
95
while [[ $# -gt 0 ]]; do
96
case $1 in
97
-f|--file)
98
file="$2"
99
shift 2
100
;;
101
-o|--output)
102
output="$2"
103
shift 2
104
;;
105
-h|--help)
106
show_command_help "unzip"
107
exit 0
108
;;
109
*)
110
panic "未知选项: $1"
111
;;
112
esac
113
done
114
115
[[ -z "$file" ]] && panic "必须指定压缩包文件路径"
116
[[ -f "$file" ]] || panic "文件不存在: $file"
117
118
# 如果输出目录未指定,则使用压缩包文件名作为目录名
119
if [[ -z "$output" ]]; then
120
# 去掉.md.zip后缀,获取文件名
121
output=$(basename "$file" .md.zip)
122
# 创建目标目录
123
mkdir -p "$CONTENT_PATH/$output"
124
output="$CONTENT_PATH/$output"
125
else
126
mkdir -p "$output"
127
fi
128
129
log_info "正在解压 ${YELLOW}$file${NC} 到 ${YELLOW}$output${NC}"
130
unzip -o "$file" -d "$output" >&2
131
log_success "解压完成"
132
133
# 只返回解压目录路径,不输出其他信息
134
echo "$output"
135
}
136
137
handle_move_images() {
138
local dir=""
139
while [[ $# -gt 0 ]]; do
140
case $1 in
141
-d|--dir)
142
dir="$2"
143
shift 2
144
;;
145
-h|--help)
146
show_command_help "move-images"
147
exit 0
148
;;
149
*)
150
panic "未知选项: $1"
151
;;
152
esac
153
done
154
155
[[ -z "$dir" ]] && panic "必须指定目录路径"
156
[[ -d "$dir" ]] || panic "目录不存在: $dir"
157
158
# 获取子文件夹名称
159
subfolder=$(basename "$dir")
160
161
# 检查是否存在 assets 文件夹
162
if [ -d "${dir}/assets" ]; then
163
# 创建目标文件夹(如果不存在)
164
target_dir="$PUBLIC_IMAGES_PATH/$subfolder"
165
mkdir -p "$target_dir"
166
167
# 移动所有图片文件
168
# 支持的图片格式:jpg, jpeg, png, gif, webp, svg
169
find "${dir}/assets" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.svg" \) -exec mv {} "$target_dir/" \;
170
171
log_success "已移动 ${YELLOW}${dir}/assets${NC} 中的图片到 ${YELLOW}$target_dir${NC}"
172
173
# 删除空的 assets 文件夹(可选)
174
rmdir "${dir}/assets" 2>/dev/null && log_info "已删除空的 assets 文件夹"
175
else
176
log_warning "目录 ${YELLOW}$dir${NC} 中不存在 assets 文件夹"
177
fi
178
}
179
180
handle_update_references() {
181
local file="" name=""
182
while [[ $# -gt 0 ]]; do
183
case $1 in
184
-f|--file)
185
file="$2"
186
shift 2
187
;;
188
-n|--name)
189
name="$2"
190
shift 2
191
;;
192
-h|--help)
193
show_command_help "update-references"
194
exit 0
195
;;
196
*)
197
panic "未知选项: $1"
198
;;
199
esac
200
done
201
202
[[ -z "$file" ]] && panic "必须指定MD文件路径"
203
[[ -f "$file" ]] || panic "文件不存在: $file"
204
[[ -z "$name" ]] && panic "必须指定文件夹名称"
205
206
log_info "正在更新 ${YELLOW}$file${NC} 中的图片引用"
207
208
# 步骤1: 使用 perl 将 assets/ 替换为 /images/文件夹名/
209
perl -i -pe 's/(\s*)!\[(.*?)\]\(assets\//\1![\2](\/images\/'"$name"'\//g' "$file"
210
211
# 步骤2: 将图片路径中的空格替换为 %20
212
# 匹配模式:查找 ![任意文本](/images/文件夹名/任意文本),然后处理路径部分的空格
213
perl -i -pe 's/(\!\[.*?\]\()([^)]*)( )([^)]*)(\))/$1$2%20$4$5/g' "$file"
214
215
# 上面的替换可能无法处理多个空格的情况,所以我们多执行几次
216
# 理论上重复执行直到没有空格为止,但为了简单起见,我们重复执行几次
217
for i in {1..5}; do
218
perl -i -pe 's/(\!\[.*?\]\()([^)]*)( )([^)]*)(\))/$1$2%20$4$5/g' "$file"
219
done
220
221
log_success "图片引用路径更新完成(包括空格处理)"
222
}
223
224
handle_move_md() {
225
local file=""
226
while [[ $# -gt 0 ]]; do
227
case $1 in
228
-f|--file)
229
file="$2"
230
shift 2
231
;;
232
-h|--help)
233
show_command_help "move-md"
234
exit 0
235
;;
236
*)
237
panic "未知选项: $1"
238
;;
239
esac
240
done
241
242
[[ -z "$file" ]] && panic "必须指定MD文件路径"
243
[[ -f "$file" ]] || panic "文件不存在: $file"
244
245
# 获取文件名和所在文件夹
246
file_name=$(basename "$file")
247
folder_path=$(dirname "$file")
248
folder_name=$(basename "$folder_path")
249
250
# 检查目标位置是否已经存在同名文件
251
if [ -f "$CONTENT_PATH/$file_name" ]; then
252
log_warning "根目录已存在同名文件 ${YELLOW}$file_name${NC},将重命名后移动"
253
# 生成新文件名(添加文件夹名称作为前缀)
254
new_file_name="${folder_name}-${file_name}"
255
mv "$file" "$CONTENT_PATH/$new_file_name"
256
log_success "已将 ${YELLOW}$file${NC} 移动并重命名为 ${YELLOW}$CONTENT_PATH/$new_file_name${NC}"
257
else
258
# 移动文件到根目录
259
mv "$file" "$CONTENT_PATH/"
260
log_success "已将 ${YELLOW}$file${NC} 移动到 ${YELLOW}$CONTENT_PATH/$file_name${NC}"
261
fi
262
263
# 检查子文件夹是否为空
264
if [ -z "$(ls -A "$folder_path" 2>/dev/null)" ]; then
265
# 文件夹为空,可以删除
266
rmdir "$folder_path"
267
log_info "已删除空文件夹 ${YELLOW}$folder_path${NC}"
268
else
269
log_info "文件夹 ${YELLOW}$folder_path${NC} 不为空,保留"
270
fi
271
}
272
273
handle_process_all() {
274
local file="" delete_zip=0
275
while [[ $# -gt 0 ]]; do
276
case $1 in
277
-f|--file)
278
file="$2"
279
shift 2
280
;;
281
-d|--delete)
282
delete_zip=1
283
shift
284
;;
285
-h|--help)
286
show_command_help "process-all"
287
exit 0
288
;;
289
*)
290
panic "未知选项: $1"
291
;;
292
esac
293
done
294
295
[[ -z "$file" ]] && panic "必须指定压缩包文件路径"
296
[[ -f "$file" ]] || panic "文件不存在: $file"
297
298
log_info "开始处理文件 ${YELLOW}$file${NC}"
299
300
# 1. 解压文件 - 捕获返回的目录路径而不是所有输出
301
dir=$(handle_unzip --file "$file")
302
log_info "文件已解压到 ${YELLOW}$dir${NC}"
303
304
# 获取文件夹名称
305
folder_name=$(basename "$dir")
306
307
# 2. 移动图片
308
handle_move_images --dir "$dir"
309
log_info "图片已移动到公共目录"
310
311
# 3. 查找MD文件 - 使用更安全的方式处理包含空格的文件名
312
# 不使用命令替换和for循环,而是使用 while read 循环
313
log_info "搜索MD文件..."
314
local found_files=0
315
316
while IFS= read -r -d '' md_file; do
317
# 4. 更新图片引用
318
found_files=1
319
log_info "找到MD文件: ${YELLOW}$md_file${NC}"
320
handle_update_references --file "$md_file" --name "$folder_name"
321
log_info "MD文件中的图片引用已更新"
322
323
# 5. 移动MD文件
324
handle_move_md --file "$md_file"
325
log_info "MD文件已移动到博客根目录"
326
done < <(find "$dir" -maxdepth 1 -type f -name "*.md" -print0)
327
328
if [[ $found_files -eq 0 ]]; then
329
log_warning "未找到MD文件"
330
fi
331
332
# 6. 删除源压缩包(如果选择了--delete选项)
333
if [[ $delete_zip -eq 1 ]]; then
334
if rm "$file"; then
335
log_success "已删除源压缩包 ${YELLOW}$file${NC}"
336
else
337
log_warning "删除源压缩包 ${YELLOW}$file${NC} 失败"
338
fi
339
fi
340
341
log_success "文件 ${YELLOW}$file${NC} 处理完成"
342
}
343
344
##############################################
345
# 主逻辑 #
346
##############################################
347
348
main() {
349
[[ $# -eq 0 ]] || [[ "$1" == "--help" ]] && show_help && exit 0
350
351
local cmd="$1"
352
shift
353
354
[[ -n "${COMMAND_MAP[$cmd]}" ]] || panic "未知命令: $cmd"
355
356
# 同样地,这里也要避免直接 IFS="::" 分割
357
# 先用 '|' 替换 "::" ,再做一次分割即可
358
local info="${COMMAND_MAP[$cmd]}"
359
info="${info//::/|}"
360
361
local desc handler options_str
362
IFS="|" read -r desc handler options_str <<< "$info"
363
364
# 调用对应的处理函数
365
$handler "$@"
366
}
367
368
main "$@"

  脚本将每一个功能都独立为一个命令选项,并提供整合选项 process-all​ 自动执行所有功能。

  例如

Terminal window
1
./siyuan_export_processor.sh process-all -d -f "年轻人的第一台 VPS 代理服务器.md.zip"

default

调整 frontmatter

  Astro 主题有一些额外的 frontmatter,思源默认的 frontmatter 可能没有这么多。需要手动调整一下,比如 分类和标签。

VSCODE 编写

  除了思源笔记以外,还可以通过 vscode 编写,虽然目前 vscode 已经自带了 markdown 的图片粘贴自动引用,但在 cursor 里没法用。需要安装扩展 Markdown Paste。

  可以在设置中的 Markdown Paste: Path​ 中修改自定义存放目录。例如在 Astro 的通常的文件结构中,存放在 /public/images 中。

Terminal window
1
${workspaceFolder}/public/images/${fileBasenameNoExtension}/

  直接使用 vscode 编写 md 文件,可以更好的利用主题提供的额外的 markdown 语法。

git 提交

  写完后,用 git 工具提交到仓库,会自动触发编译,并推动到公开仓库部署。

本文标题:Astro 博客搭建与部署
文章作者:Lyndra
发布时间:2025-01-14
总访问量
总访客数人次
Copyright 2025
站点地图