知行记

如何搭建属于自己的知识管理体系【基于Github的内容输出博客发布体系】

2023-09-01
0
0

字数:16488


目前最终展示的平台是独立博客,而自己又有不止一个博客,所以最终打造了这样一个流程。 image.png 它的价值在于:

  • 只需要在OB中对应输出目录中把内容写好,提交到Git上,后面的事就都不用管了
  • 写内容不用关注最终的样式,不同博客可以有不同的样式,可以是Hexo也可以是Jekyll,也可以是其他的

有了这个流程以后,写文章回归到了本质。

下面会介绍为什么这样做,以及如何实现,还有以前的一些尝试思考。

1、只关注内容减少内耗

1.1、输出平台的选择和考虑,为什么没有选公众号等

为什么要输出

这算是老生常谈了,对我来说,最大的感触是,当我开始写的东西,会发现知识有很多卡点,不顺畅,也说不清楚,通过输出能非常好理顺,理清自己的知识体系和结构。随着时间的推移。写作能力会提升,学东西的速度也会加快。决定开始输出,选择什么平台输出呢?其实不重要。随便找个自己认为合适的平台开始写就好了。而我现在选择的方案是自己建立独立博客。原因如下:

image.png

创作自由度考虑

选择自己建站发布个人博客是为了追求创作的自由度和灵活性。在传统的自媒体平台上,虽然也可以自定义一些样式和功能,但是总有一些限制,比如特定的模板、插件或者广告。而在 个人博客上,我们几乎可以做任何你想做的事情。可以完全掌控博客的外观和功能,从页面布局到交互效果,全部由自己来决定。这种自由度意味着可以表达自己独特的风格和想法,让博客更符合个人品味和需求。

随时更新和修改

另一个优势是可以随时修改和更新。在传统的自媒体平台上,一旦发布了文章,想要进行修改就会稍显麻烦,可能需要登录到平台后台,进行编辑并保存。公众号文章更是只能修改少数几个字。 而在自己博客上,可以直接在本地编辑文章,然后通过 Git 工具将修改推送到 GitHub 仓库,整个过程非常简单。这意味着可以随时根据需要对文章进行修改和更新,保持内容的新鲜和准确。

这个说起来是优势也是劣势,特别是早期刚开始写的时候,尽量不要让自己心理有这个预期,好像自己随时可以改。尽量让自己一次性搞定。

但是没人看呀

然而,与传统的博客平台相比,自己建站发布个人博客也存在一些劣势。首先是流量问题。相对而言,我们的博客可能会面临流量较少的情况。这意味着需要通过其他途径,比如社交媒体、搜索引擎优化等手段来吸引读者,增加博客的曝光度和访问量。

不来钱啊

其次,个人博客相对于一些专业的博客平台来说,变现能力较弱。传统的博客平台通常提供了各种各样的变现方式,比如广告投放、付费订阅等,可以让博主通过写作获得一定的经济收益。而在自己建站上,由于平台本身的性质和限制,变现的方式相对有限,需要博主通过其他途径来实现。

千金难买我乐意

如何平衡这些劣势呢?一方面,可以通过持续优质内容的输出来吸引读者,提升博客的影响力和知名度,从而间接增加流量。另一方面,可以考虑在博客中设置捐赠通道或者链接到其他平台,比如 Patreon 或者 Ko-fi,让支持者可以通过其他方式给予支持和回报。最重要的是,保持对自己创作的热情和信心,坚持不懈地输出有价值的内容,这才是长期发展的关键。

当然,有一个收益的目标总是好的。真要弄其他平台,这里写好再复制过去也可以,并不冲突。

1.2、背景介绍,多个博客咋办

以前有2个博客:

  • Jekyll
    • 在VS Code里面写,这个博客从2013年左右,开始接触时间管理,柳比歇夫开始就做了,当时的样式还比较简陋
    • Jekyll如果部署在GitHub上有个天然的要求,它必须公开,这点咋说呢
    • 如果部署在Github,目前看来只能有一个免费站点。
  • Hexo
    • 偶然的机会需要写点东西,不想放在Jekyll里面,年初的时候重新折腾过Jekyll,感觉还是有点费劲,很多功能也没有(主要是因为我不熟)。就换了Hexo,漂亮太多了。

问题就在这个时候出现了,我希望写东西在一个地方,但是不管是Jekyll还是Hexo,都会带有很多样式相关的文件夹。它们提交到Git的时候大多数都要带上,但是这2个博客有分属不同的Git仓库。 image.png

简单来说,无法分离辅助展示的内容,和真正的内容,让我一度陷入焦灼,也让进展一度停滞。

1.3、能不能自动同步文件夹

因为最后发布到平台这个步骤免不了,我希望的效果:

  • OB是很纯粹的知识梳理、整理工具
  • 发布是另外的工具干的

    能不能用OB插件

    这个时候我希望有这样的工具,借助OB的插件,自动/手动把文章复制到对应的文件夹。但是:

  • 如果博客内容有改动,或者标题变了,那个目录就很难知道。
  • 当然也可以把东西全删了,重新来过那也可以。但是毕竟不直观

当时也想过自己开发个OB插件,无奈水平不到,就把这件事就这么一直给拖了下去。后来听说OB有数据库的功能,一度跃跃欲试,但是考虑到新出的功能,另外似乎这也不是正确的解决办法。

能不能用rsync,rclone之类的工具

这个时候想到另外的一个办法,能不能借助这样的工具呢,理论上完全是可以的,但是这样一来:

  • 本机一直要启动一个程序为了它,消耗资源,啥时候死了也不知道。
  • 后面还是需要手工提交增加了难度

1.4、转机在Github Action

因为一直想实现CI/CD,但是我的知识储备有限,无奈还是不死心,所有就想着有什么办法可以做这个事,理论上应该是可以的,即使没有Github Action这样的东西,另外做个服务去同步应该也是可行的。

image.png

2、从OB到Github到Vercel

为了解决这个问题,首先确定好整体目标:

  • 希望自己更多关注在内容上,而不是工具的使用上。
  • 而在具体内容创作上,也不希望因为格式而分心。
  • 希望仓库私有。

所有就开始这样设计

  • 整体上把所有博客和Obsidian的仓库分来开,都做成私有库
  • Obsidian里面不存放和展示,theme相关的内容,只放纯粹的内容
  • 每个博客站点一个私有库,这样的好处是各个站点独立管展示样式,随时可以换
  • 为什么随时换很重要呢,因为最初的Jekyll博客后来完全无法适应移动终端……

2.1、重新找几个好看的样式

Jekyll的样式比较少,就一直沿用当前这个样式,在Github上直接拿来用就可以了,Hexo的样式比较多,我们选择的参考大概是这样:

  • 搜索top 10,最新hexo样式…… 可以找出来很多
  • 看这个hexo库在Github上的Star和最后更新时间。如果Star少,长久不更新,遇到问题很难快速解决

闪烁之狐

image.png

ookamiAntD

image.png

辣椒の酱

image.png

这几个样式和功能都是很ok的,但是最后没有选,可能是念旧吧。

2.2、创建Github私有库并部署到Vercel

基本上直接拷贝过来就可以了,Vercel里面直接部署。这里一般没有太多问题,后面会介绍一些这次部署Jekyll遇到的几个问题。 image.png

sh: jekyll:command not found

这个问题是因为从老博客拷贝过来的时候,希望文件少点,结果Gemfile没拷贝,就报错了

ERROR: YOUR SITE COULD NOT BE BUILT

这个是需要在_config.yml里面添加github仓库名,有个属性设置上就可以了

这里说明一点,用vercel去部署的时候,_config.yml里面的内容不能重复,以前用Github Pages的时候,属性重复它会认最后那个,但是Vercel里面就直接报错了

主页打开了,但是具体文章打不开,发现链接没有后缀

检查发现是我为了简单不想弄文件夹,把permlink改了。 这个属性是jekyll生成文件的一个依据,最后改成 permalink: /:categories/:title/ 可以了。 实际上,jekyll是通过它来生成文件结构的,最后这个/很重要,它会在这个文件夹下面生成一个 Index.html

如何看文件输出结构

image.png 在这里有个Source,它是针对每次发布的,从发布那里有个菜单看 view source就可以看到了。下面我们再往前倒推,如何从OB的仓库到博客的仓库呢?

2.3、如何从Ob的仓库到博客的仓库

答案就是利用Github Action。简单理解它就是一个自动化程序。目前对它的理解是,当我们的仓库内容发生变化时候,就会去看这里有哪些Action,调用它。 下面这个代码的逻辑大概是这样的

image.png

下面是我写的Action代码,一个name就对应一个步骤

name: 同步内容到ActiLearn的_posts文件夹

on:
  push:
    paths:
      - '02.Area领域/230、写作输出/ActiLearn/**'

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: 先拿到当前库内容
        uses: actions/checkout@v2
        with:
          fetch-depth: '0'
 
      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "$" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H github.com >> ~/.ssh/known_hosts
          # Replace TARGET_HOST with your target's hostname or IP address

      - name: 设置 Python 环节
        uses: actions/setup-python@v2
        with:
          python-version: '3.x'

      - name: 安装python依赖
        run: |
          pip install pypinyin # 假设 convert_titles.py 需要 pypinyin 库
          
      - name: 配置信息
        run: |
          git config --global user.email "[email protected]"
          git config --global user.name "timetoeasy"
          # 替换为您的邮箱和用户名

      - name: 克隆 修改 同步
        run: |
          # 创建临时目录
          # 克隆目标路径
          # 克隆 rep_hexo 仓库,确保替换为您的实际仓库地址和分支名称
          git clone $  ActiLearn # $tmp_dir
          cd ActiLearn
          # 切换到 hexo 仓库的目标分支
          git checkout main
          
      - name: 同步文件信息
        run: |
          # 同步  内容到 hexo 仓库的 source/_posts 目录
          # 注意:这里使用 rsync 命令进行同步,并删除目标中多余的文件
          rsync -av --delete ./02.Area领域/230、写作输出/ActiLearn/ ActiLearn/_posts

      - name: 执行文件名转换
        run: |
          echo "当前路径是 $(pwd)"
          cd ActiLearn
          echo "列举转换前目录的所有文件:"
          ls -lah _posts
          python ./convert_titles.py # 根据实际路径调整
          echo "列举转换后目录的所有文件:"
          ls -lah _posts
          
      - name: 执行移动目录
        run: |
          echo "当前路径是 $(pwd)"
          cd ActiLearn
          echo "列举转换前目录的所有文件:"
          ls -lah _posts
          python ./move_category.py # 根据实际路径调整
          echo "列举转换后目录的所有文件:"
          ls -lah _posts
          
      - name: 正式上传
        run: |
          # 转到对应的路径
          cd ActiLearn
          echo "当前路径是 $(pwd)"
          echo "列举当前目录的所有文件:"
          ls -lah
          # Add, commit, and push changes
          git add .
          git commit -m "Sync updates from obsidian_src"
          git push origin main
      - name: 清理文件
        run: |
          # 清理文件
          echo "清理文件"
          rm -rf $tmp_dir

执行完的结果大概是这样 image.png

简单说明一下: Github的Action就是Git为我们构建了一个虚拟的执行环境,把当前仓库的内容弄进去,然后开始操作。

代码比较简单,目前来说够用了。Jekyll对目录有一些要求,最早的博客是用Rakefile来生成一个英文文件名,非常不方便管理,这次既然分离了,就通过Github Action去调用python脚本去修改文件名。

3、图床的选择和考虑

图床是这次才开始使用的,以前一直觉得很难,原因可能是10多年前弄七牛云的时候伤到了,没搞定,另外收费也不低。就一直觉得这事很难,其实PicGo一直是知道的,但是一直觉得它就是一个客户端插件,这次用下来发现完全不是那么回事。

使用的要求基本就一条:无感, 不要去折腾就可以了,以前每次截图都很费劲,用的是VS Code的插件,总会记错快捷……

3.1、为什么用图床:

  • 如果图片和文章放一起,那么markdown里面的图片引用就是相对路径,如果换个文件夹就要改很多地方
  • 随着时间的推移,这个文件夹会越来越大
  • 图片的访问可能会把博客流量撑死

    3.2、图床的价值

  • 图片存放在对方服务器上,一般来说不容易死
  • 链接用他们的就可以了
  • cdn加速可以用到,简单理解就是不同省份,不同地域的人访问,它可能会缓存过去

3.3、我选择图床的考虑

  • 不自己建了,自建当然是可以的,但是会发现其实不稳定,费心费力
    • 从时间成本上看也不划算
    • 另外,如果图床是和数据库挂钩了,最好别折腾了,多了一个风险点
  • 商业公司至少稳妥,花点钱买保险
  • 费用合理

最后选择的方向有几个:

  • 阿里云OSS,腾讯云的COS
    • 其实没用过,因为还没研究就找了另外的方案
  • Github+jsdelivr
    • 一直想用,但是也没深入研究
    • 免费1G
  • AWS+CF
    • 原因是因为它有免费的10G,最后选了它

3.4、选择插件

这方面的资料比较多,不再赘述,如果有需要可以在 关于里面发邮件给我。 目前主要有Picgo和PicList,没有深究他们的差异,相对来说使用上PicList更简单一些。 image.png

不管是哪个,所有复杂的事都被它给干了,它只是给了我们一个url地址。

使用过程中如果遇到图片无法上传,基本上可以通过卸载重装等搞定,因为它只是一个中间环节,不保留数据,所以就还好。另外它也有个地方可以看日志,辅助问题排查。 image.png

4、旧博客迁移

这里说的迁移是以前的博客有2个痛点:

  • 文件名是拼音的
  • 图片在本地

所以也做了一个图片迁移的代码:

import os
import re
import requests
import time

# 定义全局变量来追踪进度
total_files = 0
processed_files = 0
start_time = time.time()

# PicGo HTTP Server 地址
picgo_server = "http://127.0.0.1:36677/upload"

# 遍历指定文件夹中的所有Markdown文件
def process_markdown_files(folder_path):
    global total_files
    md_files = [os.path.join(root, file)
                for root, dirs, files in os.walk(folder_path)
                for file in files if file.endswith(".md")]
    total_files = len(md_files)
    
    for file_path in md_files:
        process_start_time = time.time()
        process_markdown_file(file_path)
        process_time = time.time() - process_start_time
        print(f"Processed {file_path} in {process_time:.2f} seconds.")
        
        global processed_files
        processed_files += 1
        elapsed_time = time.time() - start_time
        remaining_files = total_files - processed_files
        avg_time_per_file = elapsed_time / processed_files
        estimated_remaining_time = avg_time_per_file * remaining_files
        
        print(f"Processed {processed_files}/{total_files} files in {elapsed_time:.2f} seconds. Estimated remaining time: {estimated_remaining_time:.2f} seconds.")


# 处理单个Markdown文件
def process_markdown_file(file_path):
    with open(file_path, "r+", encoding="utf-8") as file:
        print('开始处理:' + file_path)
        content = file.read()
        updated_content, images = extract_and_upload_images(content)
        if updated_content:  # 有进行图片替换,则更新文件内容
            file.seek(0)
            file.write(updated_content)
            file.truncate()
            print(f"Processed {file_path} with {len(images)} images uploaded.")

# 从Markdown内容中提取图片并上传,返回更新后的内容
def extract_and_upload_images(content):
    pattern = r"!\[.*?\]\((images/.*?)\)"
    pattern = r"!\[.*?\]\((\.\./\.\./images/.*?\.(?:png|jpg|jpeg|gif))\)"

    matches = re.findall(pattern, content)
    if not matches:
        return None, []

    for relative_image_path  in matches:
        #full_image_path = os.path.join(os.getcwd(), image_path)  # 假设图片相对当前脚本位置
        print('relative_image_path:' + relative_image_path)
        # 移除路径中的 `../../`
        relative_image_path_cleaned = relative_image_path .replace("../", "")
        # 计算图片的实际文件系统路径,这里使用正斜杠以确保与网络请求兼容
        full_image_path = os.path.abspath(os.path.join(os.getcwd(), relative_image_path_cleaned)).replace("\\", "/")
        uploaded_url = upload_image_to_picgo(full_image_path)
        print('full_image_path:' + full_image_path)
        #uploaded_url = upload_image_to_picgo(full_image_path)
        if uploaded_url:
            content = content.replace(relative_image_path, uploaded_url)

    return content, matches

# 上传图片到PicGo
def upload_image_to_picgo(image_path):
    files = {'file': open(image_path, 'rb')}
    try:
        response = requests.post(picgo_server, files=files)
        response_json = response.json()
        # 检查上传是否成功
        if response.status_code == 200 and response_json.get('success'):
            # 提取第一个结果的 URL
            return response_json['result'][0]  # 假设我们总是关心第一个结果
        else:
            print(f"Upload failed for {image_path}. Response: {response_json}")
    except Exception as e:
        print(f"Error uploading {image_path}: {e}")
    finally:
        files['file'].close()  # 确保文件在上传后被正确关闭
    return None


if __name__ == "__main__":
    folder_path = "_post"  # 指定Markdown文件夹路径
    process_markdown_files(folder_path)

时间还是比较久的

5、聚焦写作的几个小技巧

技巧的目的,是为了让我们整体思路比较连贯写完。

  • 先写提纲,大概要写什么先写了
    • 如果可以的话先写概述,让自己不偏离
  • 如果需要插图等,我就写图图
    • 空那里,或者描述一下大概是什么图
    • 回头再写
  • 标题前面的数字最后放
    • 中间不要为这个事分心,最多弄个级别啥的

6、后记

这篇文章其实拖了好几个月,迟迟没开始,写了也就写了,以前囿于流程不畅,费了很长时间。用过的工具很多,vs code可能算是比较奇葩的了,我居然用了10来年,后面用了bear,typora,妙言等,最后还是决定放在OB里面,这个地方放深度知识。方便整理和梳理。

7、Hexo和Jekyll细节记录

7.1、Hexo的db.json

这个文件应该是自动生成的,系统依赖它来产生内容。 因为发现从另外一个博客拷贝过来的时候,它居然保留了原来的内容,通过opdus搜出来才发现是这么回事……


Similar Posts

支付宝打赏 微信打赏

您的打赏是对我最大的鼓励!

Comments