Table of Contents

(意义不明的前言)又是一篇博客流程博客,依稀记得上一篇博客也是介绍我的工作流。但是!这次我带来了更优秀的工作流,我会坚持发布博客的。

1 总体方案 Link to 1 总体方案

这次的博客涉及多个流程,主要分为导出和发布两个阶段。

  • 导出
    • 在 Obsidian 编写博客内容(包含图片,生成 obsidian 管理的 metadata )
    • 导出(将 md 导出,替换图片链接为 html 形式,将相关图片放到对应目录下,将图片处理为 webp )
    • 脚本处理(将图片上传到 R2 对象存储中,替换文章中的链接;调用大模型 api 生成文章的 description;处理博客的 metadata 为 blog 发布形式)
  • 发布
    • 将 md 文件 push 到文章仓库
    • 在博客仓库同步并发布

2 准备仓库 Link to 2 准备仓库

原本的博客框架是:EveSunMaple/Frosti,是 MIT 协议,但是我希望我的博客内容遵守 CC-BY-NC-SA 4.0 所以分别放在两个仓库里面,以 submodel 的形式链接

SHELL
1
git submodule add <子模块仓库URL> <子模块路>

仓库说明:

3 博客导出和处理 Link to 3 博客导出和处理

3.1 自动维护 yaml Link to 3.1 自动维护 yaml

任务一:修正 data 为 pubData

obsidian 生成的 metadata 的 key 是data但是博客系统里需要读取的是pubData,有一点点差异,所以在处理脚本里找到对应的data替换成pubData即可

任务二:添加 description

我的 obsidian 里面是不包含 description 这个 metadata 的,但是这个博客框架里面这个属于必须字段。而且我觉得确实是需要在页面里填充一些字才会显得不那么空,所以选择在导出后加上一个字段

但是如果自己写 description 又比较麻烦,所以还是做个自动化脚本来做这个事情吧

目前只是把文章的第一段的内容作为 description ,稍微有些单调。生成 description 这个事情后续可以用大模型来处理。(ToDo)用一个免费的小模型来做生成工作

PYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import os
import yaml

def update_yaml_in_md_files(directory):
    for root, _, files in os.walk(directory):
        for file in files:
            if file.endswith(".md"):
                file_path = os.path.join(root, file)
                with open(file_path, 'r', encoding='utf-8') as f:
                    lines = f.readlines()

                # Check if the file has YAML front matter
                if lines[0].strip() == '---':
                    # Find the end of the YAML front matter
                    yaml_end_index = next((i for i, line in enumerate(lines[1:], start=1) if line.strip() == '---'), None)
                    if yaml_end_index is not None:
                        yaml_content = ''.join(lines[1:yaml_end_index])
                        content = ''.join(lines[yaml_end_index + 1:])

                        # Parse and update YAML
                        yaml_data = yaml.safe_load(yaml_content)
                        if 'date' in yaml_data:
                            yaml_data['pubDate'] = yaml_data.pop('date')
                        if 'description' not in yaml_data:
                            # 取出文章前 100 个字符作为文章描述
                            yaml_data['description'] = content[:100]

                        updated_yaml = yaml.dump(yaml_data, sort_keys=False)

                        # Write updated content back to the file
                        with open(file_path, 'w', encoding='utf-8') as f:
                            f.write('---\n')
                            f.write(updated_yaml)
                            f.write('---\n')
                            f.write(content)

if __name__ == "__main__":
    content_dir = os.path.join(os.path.dirname(__file__), "../content")
    update_yaml_in_md_files(content_dir)

3.2 自动把图片上传到 cloudflare R2 Link to 3.2 自动把图片上传到 cloudflare R2

另外我的图片如果全部存到 Github 仓库里的话,100M 的空间很快就用完了,这样不太好。

所以我选择使用 cloudflare R2 的对象存储作为图床,先根据官网提示注册和创建对象存储。

然后写一个脚本上传图片到 R2 的 bucket 里,然后再替换图片的链接就可以了。因为在 obsidian 里面已经把名字做了 hash 处理了,所以在桶里面是不可能重名的,所以不需要再建立子目录,直接传到同一个目录里就好了。

大部分图片是文章目录里的 attachment 目录,逐个扫一遍即可;还有一些图片是头图,头图我全部放在根目录的 attachment 里,所以也要扫一下,头图单独放到一个目录里。

r2 的密钥的都放在同目录的 r2_key.yaml 里了,不能公开出来了(/////////)

PYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import boto3
from botocore.config import Config
import os
import re

# 从 r2_key.yaml 文件中加载 R2 配置信息
import yaml

r2_key_path = f"{os.path.dirname(__file__)}/r2_key.yaml"

with open(r2_key_path, 'r', encoding='utf-8') as key_file:
    r2_config = yaml.safe_load(key_file)

access_key = r2_config['access_key']
secret_key = r2_config['secret_key']
url = r2_config['url']

visit_url = 'https://r2.artlesbol.top'
bucket_name = 'blog'

# 创建 S3 客户端
config = Config(signature_version='s3v4')
s3_client = boto3.client(
    's3',
    aws_access_key_id=access_key,
    aws_secret_access_key=secret_key,
    endpoint_url=url,
    config=config
)


def upload_to_r2(local_file_path, bucket_file_name):
    """上传文件到 R2"""
    try:
        s3_client.upload_file(local_file_path, bucket_name, bucket_file_name)
        print(f"Uploaded {local_file_path} to {bucket_file_name}")
        return f"{visit_url}/{bucket_name}/{bucket_file_name}"
    except Exception as e:
        print(f"Failed to upload {local_file_path}: {e}")
        return None

def process_directory(base_dir):
    """遍历目录并处理 attachment 文件夹和 .md 文件"""
    for root, dirs, files in os.walk(base_dir):
        if 'attachment' in dirs:
            attachment_dir = os.path.join(root, 'attachment')
            md_files = [f for f in files if f.endswith('.md')]

            # 上传 attachment 目录中的文件
            for file_name in os.listdir(attachment_dir):
                local_file_path = os.path.join(attachment_dir, file_name)
                bucket_file_name = f"content/img/{file_name}"
                bucket_file_name = bucket_file_name.replace("\\", "/")  # 替换为 R2 兼容路径
                r2_url = upload_to_r2(local_file_path, bucket_file_name)
                print(r2_url)

                # 替换同级 .md 文件中的图片路径
                if r2_url:
                    for md_file in md_files:
                        md_file_path = os.path.join(root, md_file)
                        replace_image_path(md_file_path, file_name, r2_url)

def replace_image_path(md_file_path, file_name, r2_url):
    """替换 .md 文件中的图片路径为 R2 路径"""
    try:
        with open(md_file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        # 替换图片路径
        updated_content = re.sub(
            #匹配图片路径,如:img src="attachment/d8fc267508bb5e274aaf90b19d486d3d.webp"
            fr'img src="attachment/{file_name}"',
            f'img src="{r2_url}"',
            content
        )

        with open(md_file_path, 'w', encoding='utf-8') as f:
            f.write(updated_content)

        print(f"Updated image path in {md_file_path}")
    except Exception as e:
        print(f"Failed to update {md_file_path}: {e}")

if __name__ == "__main__":
    content_dir = os.path.join(os.path.dirname(__file__), "../content")
    process_directory(content_dir)

参考文章:

4 编写 Github CI wrokflow config Link to 4 编写 Github CI wrokflow config

Astro 官方提供了一个部署脚本:部署你的 Astro 站点至 GitHub Pages | Docs

需要做一点点修改:由于我们分离仓库的操作,需要添加一个 cp 命令,这样目录里才有文章

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
name: Deploy to GitHub Pages

on:
  # 每次推送到 `main` 分支时触发这个“工作流程”
  # 如果你使用了别的分支名,请按需将 `main` 替换成你的分支名
  push:
    branches: [ main ]
  # 允许你在 GitHub 上的 Actions 标签中手动触发此“工作流程”
  workflow_dispatch:

# 允许 job 克隆 repo 并创建一个 page deployment
permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout your repository using git
        uses: actions/checkout@v4
        with:
          submodules: true  # 拉取子模块
      - name: copy contents
        run: cp -rf ./content/blog ./Frosti-myblog/src/content/blog
      - name: Install, build, and upload your site
        uses: withastro/action@v3
        with:
          path: ./Frosti-myblog # 存储库中 Astro 项目的根位置。(可选)
          node-version: 20 # 用于构建站点的特定 Node.js 版本,默认为 20。(可选)
          package-manager: pnpm@latest # 应使用哪个 Node.js 包管理器来安装依赖项和构建站点。会根据存储库中的 lockfile 自动检测。(可选)


  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

5 配置 Github Pages Link to 5 配置 Github Pages

这一步没什么好说的,就在仓库的配置页面里选上 Github Action 部署就行,也就是上面配的 CI。

6 编写博客和发布 Link to 6 编写博客和发布

万事俱备,只欠东风。

这时候我只需要几步就可以顺利发布文章了

  • 从 obsidian 里用 markdown-export 插件把我的文章导出到文章仓库里
  • 执行自动格式化脚本:bash runscript.sh
  • 添加和上传:git add && git push

等待 Github Action 执行完毕,刷新博客页就行了。

7 Dev Link to 7 Dev

如果还需要修改博客的样式之类的,可以在模板仓库来做

然后在文章仓库这边,如果需要更新,则使用同步的命令进行更新

SHELL
1
 git submodule update --remote --merge
Thanks for reading!

「Astro+Github Pages+CloudFlare R2」我的博客发布撰写和发布流程

2025年03月23日 周日
2130 字 · 13 分钟

© Artlesbol 版权所有,依据 CC-BY-NC-SA 4.0 协议开源