我是基于Kimi moonshot-v1-8k实现的AI助手,在此博客上负责整理和概括文章
本文介绍了如何在shokaX主题的博客中搭建和更换图床。首先,作者分享了使用CloudFlare R2和Telegram Bot搭建免费图床的方法,但指出速度一般。随后,为了提升图片加载速度,作者转向国内平台,使用阿里云OSS搭建图床,并指出了搭建过程中的一个常见错误。接着,文章详细描述了如何处理图片并自动上传到图床,包括将图片转换为WebP格式、重命名、存储和上传的过程。此外,作者还讨论了如何在shokaX博客中更换图床,包括修改配置文件和代码,以适应新的图床服务。最后,提到了如何通过修改代码和配置,实现分类卡片封面的远程封面链接配置,以及如何更新图片JSON文件格式。文章还简要提到了更换Butterfly博客图床的方法,即通过替换旧图床链接为新图床链接的方式。
编辑记录
2025-02-20 第一次编辑
- 正文
# CloudFlare 图床搭建
主要参考文档进行部署和搭建。当前已经有 v2.0
版本,但我使用的仍是 v1.0
版本,结合 Telegram Bot
和 Cloudflare R2
搭建图床。该图床的特点是:免费、容量足够、速度还行。但是如果对图片加载速度有追求,则不建议。
处理并上传图片的代码:
import os | |
import shutil | |
from PIL import Image | |
import requests | |
from requests.adapters import HTTPAdapter | |
from urllib3.util.ssl_ import create_urllib3_context | |
# 配置代理绕过规则 | |
os.environ["NO_PROXY"] = "cloudflare-imgbed-9os.pages.dev" | |
class CustomHttpAdapter(HTTPAdapter): | |
"""自定义适配器,用于解决 HTTPS 主机名检查问题""" | |
def __init__(self, *args, **kwargs): | |
self._ssl_context = create_urllib3_context() | |
super().__init__(*args, **kwargs) | |
def init_poolmanager(self, *args, **kwargs): | |
kwargs['ssl_context'] = self._ssl_context | |
return super(CustomHttpAdapter, self).init_poolmanager(*args, **kwargs) | |
def convert_to_webp(input_dirs, output_dir, folder_labels, quality=90): | |
""" | |
将多个文件夹中的图片转换为 WebP 格式,并按文件夹字符串重命名。 | |
Args: | |
input_dirs (list): 输入文件夹的路径列表。 | |
output_dir (str): 转换后的图片存储的总文件夹路径。 | |
folder_labels (dict): 每个文件夹对应的字符串标签,格式为 {文件夹路径: 标签字符串}。 | |
quality (int): WebP 图片质量。 | |
""" | |
if not os.path.exists(output_dir): | |
os.makedirs(output_dir) | |
print(f"开始转换图片,输出目录: {output_dir}") | |
for input_dir in input_dirs: | |
if input_dir not in folder_labels: | |
print(f"跳过未配置标签的文件夹: {input_dir}") | |
continue | |
label = folder_labels[input_dir] | |
count = 1 | |
for root, dirs, files in os.walk(input_dir): | |
# 忽略子文件夹 | |
if root != input_dir: | |
continue | |
for file in files: | |
input_path = os.path.join(root, file) | |
output_filename = f"{label}_{count}.webp" | |
output_path = os.path.join(output_dir, output_filename) | |
try: | |
with Image.open(input_path) as img: | |
img.convert("RGB").save(output_path, "webp", quality=quality) | |
print(f"转换成功: {input_path} -> {output_path}") | |
count += 1 | |
except Exception as e: | |
print(f"转换失败: {input_path}, 错误: {e}") | |
def upload_images(api_url, output_dir, params=None, user_agent=None): | |
""" | |
上传图片到 CloudFlare 图床。 | |
Args: | |
api_url (str): 上传接口 URL。 | |
output_dir (str): 包含图片的文件夹路径。 | |
params (dict): 可选的查询参数。 | |
user_agent (str): 自定义 User-Agent。 | |
""" | |
session = requests.Session() | |
session.mount("https://", CustomHttpAdapter()) | |
headers = {"User-Agent": user_agent or "Mozilla/5.0"} | |
for file in os.listdir(output_dir): | |
file_path = os.path.join(output_dir, file) | |
try: | |
with open(file_path, 'rb') as image_file: | |
files = {'file': image_file} | |
response = session.post(api_url, params=params, files=files, headers=headers) | |
if response.status_code == 200: | |
print(f"上传成功: {file}") | |
else: | |
print(f"上传失败: {file}, 状态码: {response.status_code}, 错误信息: {response.text}") | |
except Exception as e: | |
print(f"上传失败: {file}, 错误: {e}") | |
if __name__ == "__main__": | |
# 输入文件夹列表 | |
input_dirs = [ | |
"C:\\Users\\10116\\Pictures\\game\\summer pockets", | |
"C:\\Users\\10116\\Pictures\\EVA\\webp\\background", | |
# "C:\\Users\\10116\\Pictures\\EVA\\webp\\cover", | |
"C:\\Users\\10116\\Pictures\\KON\\webp", | |
"C:\\Users\\10116\\Pictures\\Ghibli", | |
] | |
# 每个文件夹对应的标签 | |
folder_labels = { | |
"C:\\Users\\10116\\Pictures\\game\\summer pockets": "sp", | |
"C:\\Users\\10116\\Pictures\\EVA\\webp\\background": "eva", | |
# "C:\\Users\\10116\\Pictures\\EVA\\webp\\cover": "eva", | |
"C:\\Users\\10116\\Pictures\\KON\\webp": "kon", | |
"C:\\Users\\10116\\Pictures\\Ghibli": "ghibli", | |
} | |
# 输出总文件夹 | |
output_dir = "C:/Users/10116/Pictures/BlogImages" | |
# API 上传配置 | |
api_url = "https://cloudflare-imgbed-9os.pages.dev/upload" | |
params = { | |
"authCode": "4869", # 替换为您的认证码 | |
"uploadNameType": "origin", | |
"returnFormat": "full" | |
} | |
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" | |
# 转换图片 | |
convert_to_webp(input_dirs, output_dir, folder_labels, quality=90) | |
# 上传图片 | |
upload_images(api_url, output_dir, params, user_agent) |
该代码功能是把目标文件夹的图片转换为 webp
并按标签重命名后保存到某个总文件夹后,将该文件夹的图片依次上传到图床。
# 阿里云 OSS 图床搭建
为了优化图片加载速度,选择用国内厂商的平台搭建图床,阿里云 OSS 新用户注册免费送三个月 20G 存储容量,且性价比高,因此选择作为图床平台。搭建流程参考文档,按步骤一步步操作最终可实现通过 PicGo 上传图片到图床,但注意该文档有一处错误:
这句话:划重点!这里一定要注意,设定存储区域里要填的是之前记下来的地域节点里面的第一个字段,比如你的地域节点值是 "oss-cn-shanghai.aliyuncs.com",那么这里只需要填"oss-cn-shanghai",切记,否则配置失败无法上传图片。
实际请在” 概览 “里的” 访问端口 “->” 外网访问 “->"Endpoint(地域节点)" 查看,例如为 oss-cn-wuhan-lr.aliyuncs.com
,则这里要填 oss-cn-wuhan-lr
而不是 "oss-cn-wuhan"!
# 处理图片并自动上传
实现:
- 遍历多个输入文件夹,将其中的图片转换为
WebP
格式(转换时会将图片转为RGB
模式), - 按照每个文件夹对应的标签和序号重命名(如
eva_1.webp
、kon_2.webp
等), - 将转换后的图片存储到统一的输出文件夹中,
- 最后将输出文件夹中所有
WebP
文件的路径(绝对路径)构造成一个JSON
对象, - 并通过 HTTP POST 请求上传到指定的接口(请求体格式为
JSON
,示例为 {"list": ["xxx.webp", ...] })。
import os | |
from PIL import Image | |
import requests | |
def convert_to_webp(input_dirs, output_dir, folder_labels, quality=90): | |
""" | |
将多个文件夹中的图片转换为 WebP 格式,并按文件夹标签重命名后存储到总文件夹中。 | |
Args: | |
input_dirs (list): 输入图片文件夹的路径列表。 | |
output_dir (str): 输出图片存储的总文件夹路径。 | |
folder_labels (dict): 每个输入文件夹对应的标签,例如 {文件夹路径: "标签字符串"}。 | |
quality (int): 输出 WebP 图片的质量(0-100)。 | |
""" | |
if not os.path.exists(output_dir): | |
os.makedirs(output_dir) | |
print(f"开始转换图片,输出目录: {output_dir}") | |
for input_dir in input_dirs: | |
if input_dir not in folder_labels: | |
print(f"跳过未配置标签的文件夹: {input_dir}") | |
continue | |
label = folder_labels[input_dir] | |
count = 1 | |
# 仅处理当前文件夹内的文件,不递归子文件夹 | |
for root, dirs, files in os.walk(input_dir): | |
if root != input_dir: | |
continue | |
for file in files: | |
input_path = os.path.join(root, file) | |
output_filename = f"{label}_{count}.webp" | |
output_path = os.path.join(output_dir, output_filename) | |
try: | |
with Image.open(input_path) as img: | |
# 转换为 RGB 后保存为 WebP 格式 | |
img.convert("RGB").save(output_path, "webp", quality=quality) | |
print(f"转换成功: {input_path} -> {output_path}") | |
count += 1 | |
except Exception as e: | |
print(f"转换失败: {input_path},错误: {e}") | |
def upload_images(api_url, output_dir): | |
""" | |
将输出文件夹中的所有图片路径以 JSON 格式上传到指定接口。 | |
上传请求的要求: | |
- HTTP 方法:POST | |
- URL:如 http://127.0.0.1:36677/upload(示例默认配置) | |
- 请求体:{"list": ["完整路径1", "完整路径2", ...]},必须为 JSON 格式 | |
Args: | |
api_url (str): 上传接口的 URL。 | |
output_dir (str): 存储转换后图片的文件夹路径。 | |
""" | |
# 获取输出目录下所有 WebP 文件(完整路径) | |
file_list = [ | |
os.path.join(output_dir, f) | |
for f in os.listdir(output_dir) | |
if f.lower().endswith(".webp") | |
] | |
if not file_list: | |
print("没有找到需要上传的图片。") | |
return | |
payload = {"list": file_list} | |
headers = {"Content-Type": "application/json"} | |
try: | |
response = requests.post(api_url, json=payload, headers=headers) | |
if response.status_code == 200: | |
print("图片上传成功!") | |
else: | |
print(f"上传失败,状态码: {response.status_code},响应: {response.text}") | |
except Exception as e: | |
print(f"上传过程中发生错误: {e}") | |
if __name__ == "__main__": | |
# 定义输入图片文件夹列表 | |
input_dirs = [ | |
r"C:\Users\10116\Pictures\test", | |
] | |
# 定义每个文件夹对应的标签 | |
folder_labels = { | |
r"C:\Users\10116\Pictures\test": "test", | |
} | |
# 输出图片存储的总文件夹 | |
output_dir = "C:\\Users\\10116\\Pictures\\tt" | |
# 上传接口 URL(默认配置示例) | |
api_url = "http://127.0.0.1:36677/upload" | |
# 先转换图片 | |
convert_to_webp(input_dirs, output_dir, folder_labels, quality=90) | |
# 再上传图片 | |
upload_images(api_url, output_dir) |
# 更换 shokaX 博客图床
选择在 _config.shokaX.yml
配置一个参数: imgbed
方便后续管理和切换。新增参数:
# imgbed: "https://cloudflare-imgbed-9os.pages.dev/file/" | |
imgbed: "https://miyano.oss-cn-wuhan-lr.aliyuncs.com/img/" |
于是需要修改下面的文件进行适配。
# 修改 themes\shokaX\layout\gallery.pug
方法:获取主题配置文件里的参数 imgbed
,如果 gallery.yml
里的 img
为链接形式则不动,否则进行拼接为 url。
extends ./_partials/layout.pug | |
block content | |
- var imgbed = theme.imgbed || ''; | |
div#gallery-container | |
div.gallery-group-main | |
each group in page.posts | |
a(href=group.url target="_blank") | |
figure.gallery-group | |
- var imgSrc = /^https?:\/\//.test(group.img) ? group.img : imgbed + group.img; | |
img.gallery-group-img(src=imgSrc alt=group.name) | |
figcaption | |
div.gallery-group-name= group.name | |
p= group.descr | |
include _partials/pagination.pug |
extends ./_partials/layout.pug | |
block content | |
div#gallery-container | |
//- h1.page-title 相册 | |
div.gallery-group-main | |
each group in page.posts | |
a(href=group.url target="_blank") | |
figure.gallery-group | |
img.gallery-group-img(src=group.img alt=group.name) | |
figcaption | |
div.gallery-group-name= group.name | |
p= group.descr | |
include _partials/pagination.pug |
# 修改 themes\shokaX\layout\gallery-item.pug
方法:同样获取参数 imgbed
,如果 gallery.yml
里的 photos
里的每个 photo
为链接形式则不动,否则进行拼接为 url。
extends ./_partials/layout.pug | |
block content | |
- var imgbed = theme.imgbed || ''; | |
div.gallery-container.gallery-items | |
each photo in page.photos | |
- var photoSrc = /^https?:\/\//.test(photo) ? photo : imgbed + photo; | |
a(href=photoSrc class="gallery-item" data-src=photoSrc) | |
img(src=photoSrc alt=page.group.name title=page.group.descr loading="lazy") |
extends ./_partials/layout.pug | |
block content | |
div.gallery-container.gallery-items | |
each photo in page.photos | |
a(href=photo class="gallery-item" data-src=photo) | |
img(src=photo alt=page.group.name title=page.group.descr loading="lazy") |
# 修改 source\_data\gallery.yml
只需要将原来的:
gallery_groups: | |
- name: KON | |
descr: KON一生推,毕业不是终点,今后也都是朋友。 | |
url: /miyano/gallery/kon/ | |
img: "https://cloudflare-imgbed-9os.pages.dev/file/kon_16.webp" | |
photos: | |
- "https://cloudflare-imgbed-9os.pages.dev/file/kon_16.webp" |
改为:
gallery_groups: | |
- name: KON | |
descr: KON一生推,毕业不是终点,今后也都是朋友。 | |
url: /miyano/gallery/kon/ | |
img: "kon_16.webp" | |
photos: | |
- "kon_16.webp" |
# 修改分类卡片的封面逻辑
原先的效果是,必须在分类文件夹里放一张名字为 cover.jpg
(或其他格式) 的图片才能渲染出分类卡片,但是这样的实际效果是渲染的速度对于质量高的图片不佳,于是决定进行修改,使卡片的封面可以通过读取图片 url 得到。
于是通过全局搜索 cover.jpg
再一步步串联找到需要修改的文件: themes\shokaX\layout\_mixin\card.pug
和 themes\shokaX\scripts\generaters\index.js
。
具体改进方法:在 _config.shokaX.yml
新设置参数: cover_map
,格式为:
cover_map: | |
Literature-Translation: "kon_16.webp" | |
Playful-Exploration: "kon_3.webp" | |
Scientific-Notes: "kon_8.webp" | |
Engine-Translation: "kon_11.webp" |
即为每个分类配置一个短图片链接。
对于 card.pug
,修改如下:
mixin CardRender(item) | |
- var timestamp = Date.now() | |
- var cover = item.cover ? item.cover : (url_for(theme.statics + item.slug + '/cover.jpg') + '?v=' + timestamp) | |
- var itemname = item.name |
mixin CardRender(item) | |
- var timestamp = Date.now() | |
- cover = url_for(theme.statics + item.slug + '/cover.jpg') + '?v=' + timestamp | |
//- - cover = url_for(theme.statics + item.slug + '/cover.jpg') | |
- itemname = item.name |
方法是:修改 mixin
部分,先检测传入的分类项是否有 cover
属性(即通过 cover_map
配置得到的远程封面链接),如果存在则直接使用,否则继续使用原有通过静态资源目录生成的本地封面链接。
对于 index.js
,修改如下:
// 从主题配置中获取远程封面相关参数 | |
const themeConfig = hexo.theme.config; | |
const coverMap = themeConfig.cover_map || {}; | |
const imgbed = themeConfig.imgbed || ""; | |
if (categories && categories.length) { | |
categories.forEach((cat) => { | |
// 优先判断 cover_map 配置 | |
if (coverMap[cat.slug]) { | |
// 使用远程封面链接 | |
cat.cover = imgbed + coverMap[cat.slug]; | |
// 处理分类的归属关系及子分类 / 文章数据(与本地封面处理逻辑一致) | |
const topcat = getTopcat(cat); | |
if (topcat._id !== cat._id) { | |
cat.top = topcat; | |
} | |
const child = categories.find({ parent: cat._id }); | |
let pl = 6; | |
if (child.length !== 0) { | |
cat.child = child.length; | |
cat.subs = child.sort({ name: 1 }).limit(6).toArray(); | |
pl = Math.max(0, pl - child.length); | |
if (pl > 0) { | |
cat.subs.push(...cat.posts.sort({ title: 1 }).filter(function(item, i) { | |
return item.categories.last()._id === cat._id; | |
}).limit(pl).toArray()); | |
} | |
} else { | |
cat.subs = cat.posts.sort({ title: 1 }).limit(6).toArray(); | |
} | |
catlist.push(cat); | |
} else { | |
// 没有配置 cover_map,则使用本地封面图片 | |
const localCoverDir = `source/_posts/${cat.slug}`; | |
if (fs.existsSync(localCoverDir + "/cover.avif")) { | |
covers.push({ | |
path: cat.slug + "/cover.avif", | |
data: function() { | |
return fs.createReadStream(localCoverDir + "/cover.avif"); | |
} | |
}); | |
} else if (fs.existsSync(localCoverDir + "/cover.webp")) { | |
covers.push({ | |
path: cat.slug + "/cover.webp", | |
data: function() { | |
return fs.createReadStream(localCoverDir + "/cover.webp"); | |
} | |
}); | |
} else if (fs.existsSync(localCoverDir + "/cover.jpg")) { | |
covers.push({ | |
path: cat.slug + "/cover.jpg", | |
data: function() { | |
return fs.createReadStream(localCoverDir + "/cover.jpg"); | |
} | |
}); | |
const topcat = getTopcat(cat); | |
if (topcat._id !== cat._id) { | |
cat.top = topcat; | |
} | |
const child = categories.find({ parent: cat._id }); | |
let pl = 6; | |
if (child.length !== 0) { | |
cat.child = child.length; | |
cat.subs = child.sort({ name: 1 }).limit(6).toArray(); | |
pl = Math.max(0, pl - child.length); | |
if (pl > 0) { | |
cat.subs.push(...cat.posts.sort({ title: 1 }).filter(function(item, i) { | |
return item.categories.last()._id === cat._id; | |
}).limit(pl).toArray()); | |
} | |
} else { | |
cat.subs = cat.posts.sort({ title: 1 }).limit(6).toArray(); | |
} | |
catlist.push(cat); | |
} | |
} | |
}); | |
} |
if (categories && categories.length) { | |
categories.forEach((cat) => { | |
const cover = `source/_posts/${cat.slug}`; | |
if (fs.existsSync(cover + "/cover.avif")) { | |
covers.push({ | |
path: cat.slug + "/cover.avif", | |
data: function() { | |
return fs.createReadStream(cover + "/cover.avif"); | |
} | |
}); | |
} else if (fs.existsSync(cover + "/cover.webp")) { | |
covers.push({ | |
path: cat.slug + "/cover.webp", | |
data: function() { | |
return fs.createReadStream(cover + "/cover.webp"); | |
} | |
}); | |
} else if (fs.existsSync(cover + "/cover.jpg")) { | |
covers.push({ | |
path: cat.slug + "/cover.jpg", | |
data: function() { | |
return fs.createReadStream(cover + "/cover.jpg"); | |
} | |
}); | |
const topcat = getTopcat(cat); | |
if (topcat._id !== cat._id) { | |
cat.top = topcat; | |
} | |
const child = categories.find({ parent: cat._id }); | |
let pl = 6; | |
if (child.length !== 0) { | |
cat.child = child.length; | |
cat.subs = child.sort({ name: 1 }).limit(6).toArray(); | |
pl = Math.max(0, pl - child.length); | |
if (pl > 0) { | |
cat.subs.push(...cat.posts.sort({ title: 1 }).filter(function(item, i) { | |
return item.categories.last()._id === cat._id; | |
}).limit(pl).toArray()); | |
} | |
} else { | |
cat.subs = cat.posts.sort({ title: 1 }).limit(6).toArray(); | |
} | |
catlist.push(cat); | |
} | |
}); | |
} |
方法是:在生成器中,从主题配置中读取 imgbed
和 cover_map
参数。遍历每个分类时,若 cover_map
中存在该分类的配置,则为该分类添加 cover
属性,并构建出完整的远程封面链接;否则则采用原有判断本地 cover
图片的逻辑。注意:当使用远程封面时,不再从本地读取文件,而是直接为分类对象设置封面链接,同时其它分类数据(如 top
、 subs
等)保持原有处理逻辑,并将该分类加入 catlist
。
通过上述修改,成功实现通过读取 cover_map
进行分类卡片的封面配置。
# 修改 updateYaml.js
需要更新该代码的逻辑,实现:
- 首页与文章图片生成
脚本先从images.json
中读取当前主题(由MyTheme
或默认随机选择)的home
与articles
数组,将imgbed
参数拼接后写入对应的 YAML 文件。 - 远程封面分配(
cover_map
功能)
当_config.shokaX.yml
中存在cover_map
配置时:
- 从
images.json
中读取当前主题的cover
数组。 - 随机选取与
cover_map
键数量相同的封面(确保不重复),并更新cover_map
的值。 - 将更新后的配置写回
_config.shokaX.yml
(以便后续生成器中使用远程封面链接)。 - 同时,对那些分类文件夹名称未包含在
cover_map
键中的,仍使用原来的本地封面复制逻辑生成cover.jpg
。
- 原有功能保留
当配置文件中没有cover_map
时,所有博客分类文件夹均通过随机选取本地主题封面图片生成cover.jpg
文件。
改进代码如下:
const fs = require("fs"); | |
const path = require("path"); | |
const yaml = require("js-yaml"); | |
// JSON 文件路径 | |
const jsonFilePath = path.join(__dirname, "themes/shokaX/images.json"); | |
// YAML 文件路径(用于生成首页与文章图片列表) | |
const indexYmlPath = path.join(__dirname, "themes/shokaX/_images_index.yml"); | |
const articleYmlPath = path.join(__dirname, "themes/shokaX/_images.yml"); | |
// 主题图片文件夹路径(本地封面备用) | |
const coverImgDir = path.join(__dirname, "source/_data/coverimg"); | |
// 博客文章文件夹路径 | |
const postsDir = path.join(__dirname, "source/_posts"); | |
// 主题配置文件路径 | |
const configFilePath = path.join(__dirname, "_config.shokaX.yml"); | |
/** | |
* 组合 imgbed URL 与 imageName,若 imageName 已为完整 URL 则直接返回 | |
*/ | |
const getFullImageUrl = (imgbed, imageName) => { | |
return /^https?:\/\//.test(imageName) ? imageName : `${imgbed}${imageName}`; | |
}; | |
/** | |
* 随机从数组中选取 n 个元素(不保证原数组顺序) | |
*/ | |
const getRandomElements = (array, n) => { | |
const shuffled = array.sort(() => 0.5 - Math.random()); | |
return shuffled.slice(0, n); | |
}; | |
const updateYmlFiles = async () => { | |
// 读取 _config.shokaX.yml 配置 | |
let config; | |
try { | |
config = yaml.load(fs.readFileSync(configFilePath, "utf8")); | |
} catch (error) { | |
console.error("Error reading _config.shokaX.yml:", error); | |
return; | |
} | |
// 获取 imgbed 参数 | |
const imgbed = config.imgbed || ""; | |
console.log("imgbed:", imgbed); | |
// 获取 MyTheme 参数(若为 default 则根据日期选取一个主题) | |
let theme = config.MyTheme || "default"; | |
if (theme === "default") { | |
const themes = ["kon", "sp", "eva", "ghibli"]; | |
const currentDate = new Date(); | |
// 转换为北京时间 | |
const beijingTime = new Date(currentDate.getTime() + 8 * 60 * 60 * 1000); | |
const dayOfYear = (date) => { | |
const start = new Date(date.getFullYear(), 0, 0); | |
const diff = date - start + (start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000; | |
const oneDay = 1000 * 60 * 60 * 24; | |
return Math.floor(diff / oneDay); | |
}; | |
const dayIndex = dayOfYear(beijingTime); | |
const themeIndex = dayIndex % themes.length; | |
theme = themes[themeIndex]; | |
} | |
console.log("Selected Theme:", theme); | |
// 读取 images.json 数据 | |
const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, "utf8")); | |
// 生成首页 (home) 与文章 (articles) 图片列表(拼接 imgbed URL) | |
const homeImages = (jsonData[theme]?.home || []).map((img) => getFullImageUrl(imgbed, img)); | |
const articleImages = (jsonData[theme]?.articles || []).map((img) => getFullImageUrl(imgbed, img)); | |
// 写入 YAML 文件 | |
fs.writeFileSync(indexYmlPath, homeImages.map((url) => `- ${url}`).join("\n"), "utf8"); | |
fs.writeFileSync(articleYmlPath, articleImages.map((url) => `- ${url}`).join("\n"), "utf8"); | |
console.log(`YAML files updated for theme: ${theme}`); | |
// 获取本地封面图片(备用方案)所在目录 | |
const themeCoverDir = path.join(coverImgDir, theme); | |
let themeImages = []; | |
if (fs.existsSync(themeCoverDir)) { | |
themeImages = fs.readdirSync(themeCoverDir).filter((file) => /\.(jpg|jpeg|png|webp)$/i.test(file)); | |
} else { | |
console.error(`Theme folder not found: ${themeCoverDir}`); | |
} | |
if (themeImages.length === 0) { | |
console.error(`No images found in theme folder: ${themeCoverDir}`); | |
} | |
// 获取博客文章文件夹(每个分类文件夹) | |
const postFolders = fs.readdirSync(postsDir).filter((item) => { | |
const itemPath = path.join(postsDir, item); | |
return fs.statSync(itemPath).isDirectory(); | |
}); | |
/** | |
* 当 _config.shokaX.yml 中配置了 cover_map 时, | |
* 则从 images.json 对应主题下的 cover 数组中随机选取且不重复的图片, | |
* 用于更新 cover_map 中各分类的封面值; | |
* 同时,对于 cover_map 中已定义的分类,不再生成本地 cover.jpg, | |
* 其他分类仍保持原来的随机复制方式。 | |
*/ | |
if (config.cover_map && Object.keys(config.cover_map).length > 0) { | |
const coverMapKeys = Object.keys(config.cover_map); | |
// 读取 images.json 中远程封面图片数组 | |
const coverImagesRemote = jsonData[theme]?.cover || []; | |
if (coverImagesRemote.length < coverMapKeys.length) { | |
console.error(`Not enough remote cover images available for cover_map assignment for theme: ${theme}`); | |
} else { | |
// 随机选取不重复的图片赋值给 cover_map 中的每个分类(键) | |
const shuffled = coverImagesRemote.sort(() => 0.5 - Math.random()); | |
coverMapKeys.forEach((key, index) => { | |
config.cover_map[key] = shuffled[index]; | |
}); | |
// 将更新后的 cover_map 写回 _config.shokaX.yml 文件 | |
fs.writeFileSync(configFilePath, yaml.dump(config), "utf8"); | |
console.log("Updated cover_map in _config.shokaX.yml:", config.cover_map); | |
} | |
// 对于本地封面复制,只处理不在 cover_map 中的分类文件夹 | |
const localFolders = postFolders.filter(folder => !coverMapKeys.includes(folder)); | |
if (localFolders.length > 0 && themeImages.length > 0) { | |
const selectedImages = getRandomElements(themeImages, localFolders.length); | |
localFolders.forEach((folder, index) => { | |
const postFolderPath = path.join(postsDir, folder); | |
const sourceImagePath = path.join(themeCoverDir, selectedImages[index]); | |
const outputImagePath = path.join(postFolderPath, "cover.jpg"); | |
fs.copyFileSync(sourceImagePath, outputImagePath); | |
console.log(`cover.jpg created for ${folder}`); | |
}); | |
} | |
} else { | |
// 若未配置 cover_map,则使用原来的方式,为所有分类文件夹随机复制本地封面图片 | |
if (themeImages.length > 0) { | |
const selectedImages = getRandomElements(themeImages, postFolders.length); | |
postFolders.forEach((folder, index) => { | |
const postFolderPath = path.join(postsDir, folder); | |
const sourceImagePath = path.join(themeCoverDir, selectedImages[index]); | |
const outputImagePath = path.join(postFolderPath, "cover.jpg"); | |
fs.copyFileSync(sourceImagePath, outputImagePath); | |
console.log(`cover.jpg created for ${folder}`); | |
}); | |
} | |
} | |
}; | |
updateYmlFiles(); |
const fs = require("fs"); | |
const path = require("path"); | |
const yaml = require("js-yaml"); | |
// JSON 文件路径 | |
const jsonFilePath = path.join(__dirname, "themes/shokaX/images.json"); | |
// YAML 文件路径 | |
const indexYmlPath = path.join(__dirname, "themes/shokaX/_images_index.yml"); | |
const articleYmlPath = path.join(__dirname, "themes/shokaX/_images.yml"); | |
// 主题图片文件夹路径 | |
const coverImgDir = path.join(__dirname, "source/_data/coverimg"); | |
// 博客文章文件夹路径 | |
const postsDir = path.join(__dirname, "source/_posts"); | |
// 主题配置文件路径 | |
const configFilePath = path.join(__dirname, "_config.shokaX.yml"); | |
// 读取 `_config.shokaX.yml` 并获取 `imgbed` 参数 | |
const getImgBedUrl = () => { | |
try { | |
const config = yaml.load(fs.readFileSync(configFilePath, "utf8")); | |
return config.imgbed || ""; // 如果 `imgbed` 没有配置,返回空字符串 | |
} catch (error) { | |
console.error("Error reading _config.shokaX.yml:", error); | |
return ""; | |
} | |
}; | |
// 读取 `_config.shokaX.yml` 并获取 `MyTheme` 参数 | |
const getTheme = () => { | |
try { | |
const config = yaml.load(fs.readFileSync(configFilePath, "utf8")); | |
return config.MyTheme || "default"; // 如果 `imgbed` 没有配置,返回空字符串 | |
} catch (error) { | |
console.error("Error reading theme config:", error); | |
return "default"; | |
} | |
}; | |
//// 获取当前主题逻辑 | |
// const getTheme = () => { | |
// try { | |
// const config = fs.readFileSync(configFilePath, "utf8"); | |
// const match = config.match(/MyTheme:\s*(\S+)/); | |
// return match ? match[1] : "default"; | |
// } catch (error) { | |
// console.error("Error reading theme config:", error); | |
// return "default"; | |
// } | |
// }; | |
// 组合 `imgbed` URL 和 `imageName` | |
const getFullImageUrl = (imgbed, imageName) => { | |
return /^https?:\/\//.test(imageName) ? imageName : `${imgbed}${imageName}`; | |
}; | |
// 随机从数组中选取 n 个元素 | |
const getRandomElements = (array, n) => { | |
const shuffled = array.sort(() => 0.5 - Math.random()); | |
return shuffled.slice(0, n); | |
}; | |
// 更新 YAML 文件 | |
const updateYmlFiles = async () => { | |
const imgbed = getImgBedUrl(); // 获取 `imgbed` 配置 | |
console.log("imgbed:", imgbed); | |
const theme = getTheme(); // 获取当前主题 | |
const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, "utf8")); | |
let selectedTheme = theme; | |
if (theme === "default") { | |
const themes = ["kon", "sp", "eva", "ghibli"]; | |
const currentDate = new Date(); | |
const beijingTime = new Date(currentDate.getTime() + 8 * 60 * 60 * 1000); // 转换为北京时间 | |
// 根据日期计算主题索引 | |
const dayOfYear = (date) => { | |
const start = new Date(date.getFullYear(), 0, 0); | |
const diff = date - start + (start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000; | |
const oneDay = 1000 * 60 * 60 * 24; | |
return Math.floor(diff / oneDay); | |
}; | |
const dayIndex = dayOfYear(beijingTime); | |
const themeIndex = dayIndex % themes.length; | |
selectedTheme = themes[themeIndex]; | |
} | |
console.log("Selected Theme:", selectedTheme); | |
// 获取 `home` 和 `articles` 图片,并拼接 `imgbed` | |
const homeImages = (jsonData[selectedTheme]?.home || []).map((img) => getFullImageUrl(imgbed, img)); | |
const articleImages = (jsonData[selectedTheme]?.articles || []).map((img) => getFullImageUrl(imgbed, img)); | |
// 写入 YAML 文件 | |
fs.writeFileSync(indexYmlPath, homeImages.map((url) => `- ${url}`).join("\n"), "utf8"); | |
fs.writeFileSync(articleYmlPath, articleImages.map((url) => `- ${url}`).join("\n"), "utf8"); | |
console.log(`YAML files updated for theme: ${selectedTheme}`); | |
// 获取主题对应的图片文件夹 | |
const themeCoverDir = path.join(coverImgDir, selectedTheme); | |
if (!fs.existsSync(themeCoverDir)) { | |
console.error(`Theme folder not found: ${themeCoverDir}`); | |
return; | |
} | |
// 获取主题图片列表 | |
const themeImages = fs.readdirSync(themeCoverDir).filter((file) => /\.(jpg|jpeg|png|webp)$/i.test(file)); | |
if (themeImages.length === 0) { | |
console.error(`No images found in theme folder: ${themeCoverDir}`); | |
return; | |
} | |
// 获取博客文章文件夹数量 | |
const postFolders = fs.readdirSync(postsDir).filter((item) => { | |
const itemPath = path.join(postsDir, item); | |
return fs.statSync(itemPath).isDirectory(); | |
}); | |
// 随机选取图片并生成 cover.jpg | |
const selectedImages = getRandomElements(themeImages, postFolders.length); | |
postFolders.forEach((folder, index) => { | |
const postFolderPath = path.join(postsDir, folder); | |
const sourceImagePath = path.join(themeCoverDir, selectedImages[index]); | |
const outputImagePath = path.join(postFolderPath, "cover.jpg"); | |
// 复制图片为 cover.jpg | |
fs.copyFileSync(sourceImagePath, outputImagePath); | |
console.log(`cover.jpg created for ${folder}`); | |
}); | |
}; | |
updateYmlFiles(); |
同时, themes\shokaX\images.json
的格式需改为类似:
{ | |
"kon": { | |
"home": [ | |
"kon_1.webp" | |
], | |
"articles": [ | |
"kon_16.webp" | |
], | |
"cover":[ | |
"kon_18.webp" | |
] | |
}, | |
} |
{ | |
"kon": { | |
"home": [ | |
"https://cloudflare-imgbed-9os.pages.dev/file/kon_1.webp" | |
], | |
"articles": [ | |
"https://cloudflare-imgbed-9os.pages.dev/file/kon_16.webp" | |
] | |
}, | |
} |
# 修改 themes\shokaX\scripts\helpers\engine.js
当修改了 source\_data\gallery.yml
里的 photos
里的图片为短链接时,例如 "kon_16.webp",会导致进入相册页面的图库时,页面的头图显示出错,此时的链接为: src="/miyano/kon_16.webp"
,导致出错。所以应该修改这里的判断逻辑,一步步顺藤摸瓜,"themes\shokaX\layout\_partials\layout.pug"->"themes\shokaX\scripts\helpers\engine.js",于是修改该文件:
hexo.extend.helper.register("_image_url", function(img, path = "") { | |
const { statics, imgbed } = hexo.theme.config; | |
const { post_asset_folder } = hexo.config; | |
if (img.startsWith("//") || img.startsWith("http")) { | |
return img; | |
} else if (imgbed && imgbed.trim() !== "") { | |
// 如果配置了 imgbed,则使用它拼接完整的 URL | |
const base = imgbed.endsWith('/') ? imgbed : imgbed + '/'; | |
return base + img; | |
} else { | |
return import_hexo_util.url_for.call(this, statics + (post_asset_folder ? path : "") + img); | |
} | |
}); |
hexo.extend.helper.register("_image_url", function(img, path = "") { | |
const { statics } = hexo.theme.config; | |
const { post_asset_folder } = hexo.config; | |
if (img.startsWith("//") || img.startsWith("http")) { | |
return img; | |
} else { | |
return import_hexo_util.url_for.call(this, statics + (post_asset_folder ? path : "") + img); | |
} | |
}); |
实现:当 imgbed
配置存在并且不是空时,直接使用 imgbed + img
来拼接 URL。如果 imgbed
配置为空,则继续使用原来的 statics
路径。从而避免因为 statics
拼接而产生的错误。此外,为确保 URL 拼接正确,还需要确认 imgbed 最后是否有斜杠,因此可以使用类似 imgbed.endsWith ('/') ? imgbed : imgbed + '/' 来保证这一点。
# 修改 themes\shokaX\layout\_partials\sidebar\overview.pug
为了使博客的头像也能使用 url 获取得到,而不是直接使用本地的图片,于是修改 overview.pug
,当判断到 theme.imgbed
存在且不为空时,会直接使用 imgbed + theme.sidebar.avatar
生成完整的 URL,否则仍使用原来的 url_for
拼接生成路径。
div(class="author" itemprop="author" itemscope itemtype="http://schema.org/Person") | |
- var avatarUrl = ""; | |
if theme.imgbed && theme.imgbed.trim() !== "" | |
- avatarUrl = theme.imgbed + theme.sidebar.avatar; | |
else | |
- avatarUrl = url_for(theme.statics + theme.assets + '/' + theme.sidebar.avatar); | |
img(loading="lazy" decoding="async" class="image" itemprop="image" alt=author src=avatarUrl) |
div(class="author" itemprop="author" itemscope itemtype="http://schema.org/Person") | |
img(loading="lazy" decoding="async" class="image" itemprop="image" alt=author | |
src=url_for(theme.statics + theme.assets + '/'+ theme.sidebar.avatar)) |
在 _config.shokaX.yml
中配置为:
sidebar: | |
# Sidebar Position. | |
position: left | |
# position: right | |
# Replace the default avatar image under <root>/source/_data/assets/ and set the url here. | |
avatar: ai-icon.webp |
实际上没有变化,只是由 themes\shokaX\source\assets\ai-icon.webp
变成了 imgbed+ai-icon.webp
。
# 更换 Butterfly 博客图床
对于旧的博客,代码结构化不好,于是采用了暴力替换图床字符串链接的方式,为每个旧图床链接进行了新图床的链接替换。