引言
曾经我写过一篇文章叫做:Fuwari静态博客搭建教程。文中的Fuwari是基于Astro的,并且使用了服务器+客户端的混合渲染,尽管UI确实好看,但因为本人不会写Astro导致日后维护特别困难(比如手动添加Giscus评论后和上游分支发生冲突需要手动解决冲突才能合并上游)。最后我放弃了,既然我就是菜我为什么不找一个原生使用HTML+JS+CSS的框架呢?于是我便询问AI,Claude推荐我使用Hugo。其实我早就曾听闻Hugo的大名,但是并没有深入研究,但是Claude又告诉我Hugo采用Go语言进行编译,速度快,而且想要二次开发也只需要改改我最熟悉的HTML+JS+CSS。于是我便花了2小时深入研究、部署、调优。发现Hugo确实很强大:迁移方便,二改简单,构建迅速
正式开始
请全程在Windows上操作
我们首先需要安装Scoop,这是一个适用于Windows的包管理器,个人认为非常好用
Scoop默认会安装到C盘,如果你想要换盘请按需更改
$env:SCOOP='D:\Scoop'
$env:SCOOP_GLOBAL='D:\ScoopApps'
[Environment]::SetEnvironmentVariable('SCOOP', $env:SCOOP, 'User')
[Environment]::SetEnvironmentVariable('SCOOP_GLOBAL', $env:SCOOP_GLOBAL, 'Machine')
安装Scoop:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
如果你以管理员的身份会安装失败,请切换为普通用户。若想强制以管理员身份安装Scoop请使用
出于安全考虑,默认情况下已禁用管理员控制台下的安装。如果您知道自己在做什么并希望以管理员身份安装Scoop,请下载安装程序并在提升的控制台中手动执行它,使用 -RunAsAdmin
参数。以下是示例:
irm get.scoop.sh -outfile 'install.ps1'
.\install.ps1 -RunAsAdmin [-OtherParameters ...]
# 如果你想要一行解决:
iex "& {$(irm get.scoop.sh)} -RunAsAdmin"
安装Hugo框架:
scoop install hugo
然后选择一个你喜欢的文件夹创建你的站点。 myblog
即你的站点文件夹名称
hugo new site myblog
cd myblog
安装PaperMod主题:
git clone https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
站点根目录会有一个 hugo.toml
。我推荐使用YAML。将文件重命名为 hugo.yaml
。粘贴并更改以下内容
baseURL: "https://站点url"
title: "网站标题"
LanguageCode: "zh-CN"
theme: "PaperMod"
# 启用首页个人简介展示
params:
# 是否启用评论。你需要自己配置,或者直接引入Giscus等评论系统
comments: false
# 是否显示代码复制按钮
ShowCodeCopyButtons: true
# 是否显示面包屑导航
ShowBreadCrumbs: false
# 是否显示阅读时间
ShowReadingTime: true
# 是否显示分享按钮
ShowShareButtons: true
# 分享按钮配置
# ShareButtons: ["linkedin", "twitter"]
# 是否禁用主题切换按钮
disableThemeToggle: false
assets:
favicon: "/你的/网站图标.jpg" # 需要在static文件夹放置对应的图片
iconHeight: 35
# 首页信息配置
homeInfoParams:
Title: "首页展示的标题"
Content: >
首页展示的文本
# 设置网站头像和首页头像
profileMode:
enabled: false # 设为 true 将完全替换 homeInfoParams
# 网站头像设置 (显示在导航栏)
label:
text: "左上角显示的文本"
icon: "/你的/左上角显示的图片.jpg" # 这将显示在导航栏标题旁边。需要在static文件夹放置对应的图片
iconHeight: 35
# 社交图标 (显示在简介下方)
socialIcons:
- name: bilibili
url: ""
- name: github
url: ""
- name: telegram
url: ""
# 可以添加更多社交图标 https://github.com/adityatelange/hugo-PaperMod/wiki/Icons
# 顶部导航栏的快捷链接
menu:
main:
- identifier: categories
name: 分类
url: /categories/
weight: 10
- identifier: tags
name: 标签
url: /tags/
weight: 20
- identifier: archives
name: 归档
url: /archives/
weight: 30
- identifier: search
name: 搜索
url: /search/
weight: 40
# 可以添加更多导航链接。weight的值越高排序越靠后
# 如果要启用搜索功能,需要添加这个
outputs:
home:
- HTML
- RSS
- JSON # 必须,用于搜索功能
然后我们需要分别配置分类、标签、归档和搜索页
创建 content\categories\_index.md
写入:
---
title: 分类
layout: categories
---
创建 content\tags\_index.md
写入:
---
title: 标签
layout: tags
---
创建 content\archives.md
写入:
---
title: 归档
layout: archives
---
创建 content\search.md
写入:
---
title: "搜索"
layout: "search"
---
然后我们要更改默认的文章创建模板
在 archetypes\default.md
写入:
---
title: {{ replace .File.ContentBaseName "-" " " | title }}
published: {{ .Date }}
summary: "文章简介"
cover:
image: "文章封面图。也支持HTTPS"
tags: [标签1, 标签2]
categories: '文章所处的分类'
draft: false
lang: ''
---
接下来我们就可以通过命令来创建文章,并开始写作了。注意,最终构建的文章URL是你的文章的文件名。比如:https://你的网站.com/posts/first
所以文章文件名尽量简短,这并不会影响你的文章标题
hugo new posts/first.md
当我们写完一篇文章想要预览网站,可以使用
hugo server
当我们想要将站点发布到Vercel、Cloudflare Pages等静态网站托管平台可以将我们的 myblog
作为一个Git存储库提交到Github
根目录:./
输出目录:public
构建命令:hugo --gc
环境变量: Key:HUGO_VERSION
Value:0.145.0
对象存储存图中间件代码:
import keyboard
import pyperclip
from PIL import ImageGrab, Image
import io
import boto3
from botocore.config import Config
import time
import uuid
import pyautogui
import os
from io import BytesIO
# 示例配置
# # R2 配置
# R2_CONFIG = {
# 'account_id': '11111111111111111',
# 'access_key_id': '11111111111111111',
# 'secret_access_key': '11111111111111111',
# 'bucket_name': '11111111111111111'
# }
# # OSS 配置
# OSS_CONFIG = {
# 'url': 'oss.onani.cn',
# 'prefix': '/fuwari-blog/img'
# }
#########################################################
# R2 配置
R2_CONFIG = {
'account_id': '',
'access_key_id': '',
'secret_access_key': '',
'bucket_name': ''
}
# OSS 配置
OSS_CONFIG = {
'url': '',
'prefix': ''
}
#########################################################
def init_r2_client():
"""初始化 R2 客户端"""
return boto3.client(
's3',
endpoint_url=f'https://{R2_CONFIG["account_id"]}.r2.cloudflarestorage.com',
aws_access_key_id=R2_CONFIG['access_key_id'],
aws_secret_access_key=R2_CONFIG['secret_access_key'],
config=Config(signature_version='s3v4'),
region_name='auto'
)
def get_image_from_clipboard():
"""从剪贴板获取图片"""
try:
image = ImageGrab.grabclipboard()
if image is None:
return None
# 如果是列表(多个文件),取第一个
if isinstance(image, list):
if len(image) > 0:
# 如果是图片文件路径,打开它
try:
return Image.open(image[0])
except Exception as e:
print(f"打开图片文件失败: {e}")
return None
return None
# 如果直接是 Image 对象
if isinstance(image, Image.Image):
return image
return None
except Exception as e:
print(f"获取剪贴板图片失败: {e}")
return None
def convert_to_webp(image):
"""将图片转换为 webp 格式"""
if not image:
return None
try:
buffer = BytesIO()
# 确保图片是 RGB 模式
if image.mode in ('RGBA', 'LA'):
background = Image.new('RGB', image.size, (255, 255, 255))
background.paste(image, mask=image.split()[-1])
image = background
elif image.mode != 'RGB':
image = image.convert('RGB')
image.save(buffer, format="WEBP", quality=80)
return buffer.getvalue()
except Exception as e:
print(f"转换图片失败: {e}")
return None
def upload_to_r2(image_data):
"""上传图片到 R2"""
if not image_data:
return None
client = init_r2_client()
# 生成基础文件名
base_filename = f"{uuid.uuid4()}.webp"
filename = base_filename
try:
# 检查文件是否已存在
attempt = 1
while True:
try:
# 尝试获取文件信息,如果文件存在会返回数据,不存在会抛出异常
client.head_object(
Bucket=R2_CONFIG['bucket_name'],
Key=f"{OSS_CONFIG['prefix'].strip('/')}/{filename}"
)
# 如果文件存在,修改文件名
name_without_ext = base_filename.rsplit('.', 1)[0]
filename = f"{name_without_ext}_{attempt}.webp"
attempt += 1
print(f"文件名已存在,尝试重命名为: {filename}")
except client.exceptions.ClientError as e:
# 如果是 404 错误,说明文件不存在,可以使用这个文件名
if e.response['Error']['Code'] == '404':
break
raise e # 其他错误则抛出
# 上传文件
client.put_object(
Bucket=R2_CONFIG['bucket_name'],
Key=f"{OSS_CONFIG['prefix'].strip('/')}/{filename}",
Body=image_data,
ContentType='image/webp'
)
return filename
except Exception as e:
print(f"上传失败: {e}")
return None
def generate_markdown_link(filename):
"""生成 Markdown 图片链接"""
if not filename:
return None
url = f"https://{OSS_CONFIG['url']}{OSS_CONFIG['prefix']}/{filename}"
return f""
def type_markdown_link(markdown_link):
"""模拟键盘输入 Markdown 链接"""
if not markdown_link:
return
pyperclip.copy(markdown_link)
pyautogui.hotkey('ctrl', 'v')
def handle_upload():
"""处理图片上传的主函数"""
print(f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] 收到粘贴请求")
print("正在检查剪贴板...")
# 获取剪贴板图片
image = get_image_from_clipboard()
if not image:
print("❌ 剪贴板中没有图片")
return
print("✅ 获取到剪贴板图片")
# 转换为 webp
print("正在转换为 WebP 格式...")
image_data = convert_to_webp(image)
if not image_data:
print("❌ 图片转换失败")
return
print(f"✅ 转换完成,大小: {len(image_data)/1024:.2f}KB")
# 上传到 R2
print("正在上传到 R2...")
filename = upload_to_r2(image_data)
if not filename:
print("❌ 上传失败")
return
print(f"✅ 上传成功,文件名: {filename}")
# 生成并输入 Markdown 链接
markdown_link = generate_markdown_link(filename)
if markdown_link:
print(f"生成的 URL: https://{OSS_CONFIG['url']}{OSS_CONFIG['prefix']}/{filename}")
print(f"模拟键入: {markdown_link}")
type_markdown_link(markdown_link)
print("✅ 操作完成")
def main():
"""主函数"""
print("=" * 50)
print("R2 图片上传插件已启动")
print(f"当前配置:")
print(f"- OSS 域名: {OSS_CONFIG['url']}")
print(f"- 存储路径: {OSS_CONFIG['prefix']}")
print(f"- R2 存储桶: {R2_CONFIG['bucket_name']}")
print("使用 Ctrl+Alt+V 上传剪贴板中的图片")
print("=" * 50)
# 注册快捷键
keyboard.add_hotkey('ctrl+alt+v', handle_upload)
# 保持程序运行
keyboard.wait()
if __name__ == "__main__":
main()