hexo-butterfly主题添加影评书评页面。
1. 说明
在25年1月份,我的博客站点新增了一个影评书评页面,这个页面是基于之前添加的装备页面的修改而来的。
有关添加装备页面的教程,可以参考这篇博客:【Hexo】hexo-butterfly主题添加装备展示页面 | 慕雪的寒舍
后续我在这个装备页面的基础上,让GPT帮忙修改了一下前端代码,实现了一个影评和书评的画廊视图页面。

本文将给出这个页面的css和pug文件,以及如何使用github action自动从我们的书评和影评文章来创建对应的yaml配置,用于最终生成这个页面。
2. 主题修改
本站的butterfly主题停留在古早的4.9.5版本,此项修改可能对最新版的butterfly主题无效!
另外,修改hexo主题的前提是使用GIT方式来安装hexo主题(主题文件在themes文件夹内),如果你使用npm方式安装主题,换了一个环境或者npm更新主题版本之后你的主题修改就丢失了。
2.1. 前端文件修改
这部分修改和添加装备页面需要做的修改一致。
修改blog/themes/butterfly/layout/page.pug
文件,在case page.type
的判断语句中新增一个rating的判断,添加在default之前即可。
1 2
| when 'rating' include includes/page/rating.pug
|
随后再创建一个blog/themes/butterfly/layout/includes/page/rating.pug
文件,写入如下内容
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
| #rating if site.data.rating each i in site.data.rating .rating-item h2.rating-item-title= i.class_name .rating-item-description= i.description .rating-item-content each item, index in i.rating_list .rating-item-content-item .rating-item-content-item-cover img.rating-item-content-item-image(data-lazy-src=url_for(item.image) onerror=`this.onerror=null;this.src='` + url_for(theme.error_img.flink) + `'` alt=item.name) .rating-item-content-item-info .rating-item-content-item-name= item.name .rating-item-content-item-specification // 定义评分渲染函数 - const renderRating = (rating) => { - const fullStar = '★'; - const emptyStar = '☆'; - const maxRating = 5; - let stars = ''; - for (let i = 0; i < maxRating; i++) { - stars += i < rating ? fullStar : emptyStar; - } - // 格式化评分数字(保留一位小数) - const formattedRating = Number(rating).toFixed(1); - return `评分:${formattedRating} ${stars}`; - } | #{renderRating(item.specification)} .rating-item-content-item-description= item.description .rating-item-content-item-toolbar if item.link.includes('https://') || item.link.includes('http://') a.rating-item-content-item-link(href= item.link, target='_blank') 详情 else a.rating-item-content-item-link(href= item.link, target='_blank') 查看文章
|
随后在source文件夹下创建一个rating文件夹,在该文件夹内创建一个index.md
文件,写入如下内容。其中title可以根据你的喜好修改,aside: false
的含义是关闭侧边栏。
1 2 3 4 5 6 7
| --- title: 影评 · 书评 date: 2025-01-04 16:45:12 aside: false type: rating ---
|
再在source/rating/
目录下创建一个rating.css
文件,写入如下内容
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
| .rating-item-content { display: flex; flex-direction: row; flex-wrap: wrap; margin: 0 -8px; }
.rating-item-content-item { width: calc(25% - 12px); border-radius: 12px; border: 2px solid #979797; overflow: hidden; margin: 8px 6px; background: var(--heo-card-bg); box-shadow: var(--heo-shadow-border); min-height: 400px; position: relative; }
@media screen and (max-width: 1200px) { .rating-item-content-item { width: calc(50% - 12px); } }
@media screen and (max-width: 768px) { .rating-item-content-item { width: 100%; } }
.rating-item-content-item-info { padding: 8px 16px 16px 16px; margin-top: 12px; }
.rating-item-content-item-name { font-size: 18px; font-weight: bold; line-height: 1; margin-bottom: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: fit-content; }
.rating-item-content-item-specification { font-size: 12px; color: var(--heo-secondtext); line-height: 1; margin-bottom: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rating-item-content-item-description { line-height: 20px; color: var(--heo-secondtext); height: 60px; display: -webkit-box; overflow: hidden; -webkit-line-clamp: 3; -webkit-box-orient: vertical; font-size: 14px; }
a.rating-item-content-item-link { font-size: 12px; background: var(--heo-gray-op); padding: 4px 8px; border-radius: 8px; cursor: pointer; }
a.rating-item-content-item-link:hover { background: var(--heo-main); color: var(--heo-white); }
h2.rating-item-title { line-height: 1; }
.rating-item-description { line-height: 1; margin: 4px 0 8px 0; color: var(--heo-secondtext); }
.rating-item-content-item-cover { width: 100%; height: 200px; background: var(--heo-secondbg); display: flex; justify-content: center; }
img.rating-item-content-item-image { object-fit: cover; height: 100%; }
div#rating { margin-top: 26px; }
.rating-item-content-item-toolbar { display: flex; justify-content: space-between; position: absolute; bottom: 12px; left: 0; width: 100%; padding: 0 16px; }
a.bber-reply { cursor: pointer; }
|
到这里,主题的修改就完成了。
2.2. 评分配置文件
接下来要做的是新增一个对应的yaml配置文件,在source/_data/
下新增一个rating.yml
,评分项目的格式如下,包含了书评影评的名字、描述、文章链接、封面、评分(1到5的整数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| - class_name: 电影世界 description: 光影交织,大梦一场 rating_list: - name: 桑尼的优势 specification: 5 description: 制作精良,剧情在线,神级反转,好看! image: https://img.musnow.top/i/2025/01/a35d66e1ea8ffae6bb5ff248d1f53c63.png link: /posts/1438650502/ - class_name: 书籍海洋 description: 行万里路,读万卷书 rating_list: - name: 占位符 specification: 4 description: 占位符 image: /img/bg/op32.jpg link: /
|
到这里,就应该搞定了,可以在本地hexo s
然后访问/rating
路径看看是否有新增的页面了。

3. Github Action自动化配置
现在页面已经创建好了,但我不想每次写个影评之后都要自己去修改rating.yml
文件,那样太过麻烦,所以写了一个python脚本,来自动化生成这个文件。
3.1. 新增的front-matter
既然需要python脚本来处理,那么第一步就是把yaml文件里面的内容写到markdown文件的front-matter里面。这里新增了下面几个字段
字段 | 含义 | 说明 |
---|
rating_name | 书籍或电影的名字 | 如果缺少此字段,则会使用title |
rating_desc | 书籍或电影的简述 | 如果缺少此字段,则使用description |
rating_point | 评分(1到5的整数) | 默认为0 |
rating_cover | 书籍或电影的封面 | 如果缺少此字段,则使用cover;若没有cover,则使用提前配置好的默认封面 |
书评和影评文章的link会根据front-matter里面的abbrlink来生成。
3.2. Python处理脚本
下面给出半GPT写的完整的Python脚本,顶部Config是需要配置的条目,分别是书评和影评的md文件路径、当没有设置rating_cover时使用的默认封面、abbrlink的前缀。
其中POST_LINK_PREFIX
参数是abbrlink前缀,在我的站点中,文章的链接都是/posts/<abbrlink>/
,abbrlink的前缀就是/posts/
。这个参数根据你自己站点内abbrlink插件的配置来修改这个配置项。
剩下的代码,如果你不知道它们是怎么工作的,请不要修改。你可以丢给GPT让它根据你的需要来微调代码。
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
| import yaml import sys import re import os
class Config: MOVIE_FILE_PATH = "Notes/CODE/blog/rating/movies" BOOK_FILE_PATH = "Notes/CODE/blog/rating/books" DEFAULT_COVER = '/img/bg/op32.jpg' POST_LINK_PREFIX = '/posts/'
class Rating: """评价列表类""" MOVIE = '电影世界' MOVIE_DESC = '光影交织,大梦一场' BOOK = '书籍海洋' BOOK_DESC = '行万里路,读万卷书'
class Item: def __init__(self, name, specification, description, image, link): self.name = name self.specification = specification self.description = description self.image = image self.link = link
def to_dict(self): """将 Rating 对象转换为字典格式""" return { 'name': self.name, 'specification': self.specification, 'description': self.description, 'image': self.image, 'link': self.link } @staticmethod def default(): """获取到一个默认的item""" return Rating.Item('占位符',0,'占位符',Config.DEFAULT_COVER,'/')
@staticmethod def create_class(class_name, description, rating_list): return { 'class_name': class_name, 'description': description, 'rating_list': rating_list }
class MarkdownParser: """md文件处理""" @staticmethod def extract_front_matter(file_path): """ 提取 Markdown 文件中的 front-matter 内容。 假设 front-matter 是以 '---' 包围的 YAML 格式内容。 """ with open(file_path, 'r', encoding='utf-8') as file: content = file.read() match = re.match(r'---\n(.*?)\n---\n', content, re.DOTALL) if match: front_matter = match.group(1) return yaml.safe_load(front_matter) else: return None
@staticmethod def extract_front_matter_from_dir(dir_path): """ 遍历指定目录及其子目录下的所有 .md 文件,提取它们的 front-matter 内容,并将所有内容添加到列表中。 """ front_matter_list = [] for root, dirs, files in os.walk(dir_path): for filename in files: file_path = os.path.join(root, filename) if filename.endswith('.md'): front_matter = MarkdownParser.extract_front_matter(file_path) if front_matter: front_matter_list.append(front_matter) return front_matter_list
def generate_rating_list(file_path:str): """遍历目录下的所有md文件,构建rating列表""" front_matter_list = MarkdownParser.extract_front_matter_from_dir(file_path) if not front_matter_list: print(f"Err: no md file in {file_path}") return [Rating.Item.default().to_dict()]
rating_list = [] for fm in front_matter_list: name = fm.get('rating_name', fm.get('title', None)) desc = fm.get('rating_desc', fm.get('description', None)) if not name and not desc: continue rating_point = abs(fm.get('rating_point', 0)) if rating_point > 5: rating_point = 5 cover = fm.get('rating_cover', fm.get('cover', Config.DEFAULT_COVER)) link = Config.POST_LINK_PREFIX + str(fm.get('abbrlink','')) + '/' item = Rating.Item(name, rating_point, desc, cover, link) rating_list.append(item.to_dict())
if not rating_list: print(f"Err: no validate md file in {file_path}") rating_list.append(Rating.Item.default().to_dict()) return rating_list
def generate_rating_yaml(): movie_ratings = generate_rating_list(Config.MOVIE_FILE_PATH) book_ratings = generate_rating_list(Config.BOOK_FILE_PATH) classes = [ Rating.create_class(Rating.MOVIE, Rating.MOVIE_DESC, movie_ratings), Rating.create_class(Rating.BOOK, Rating.BOOK_DESC, book_ratings), ] return classes
def save_yaml(data, file_path): with open(file_path, 'w', encoding='utf-8') as file: yaml.dump(data, file, allow_unicode=True, default_flow_style=False)
if __name__ == '__main__': if len(sys.argv) < 2: print("Input Err: Please provide the YAML file path.") sys.exit(1) file_path = sys.argv[1] data = generate_rating_yaml() save_yaml(data, file_path) print(f'Rating Yaml save to {file_path}')
|
脚本依赖于pyyaml库,使用pip install pyyaml
安装了这个库后,使用如下命令调用脚本
1
| python3 test.py 目标YAML文件路径
|
最终会在给定的目标文件路径写入生成的yaml内容,比如python3 test.py ./rating.yml
就会在当前目录创建一个rating.yml
文件并写入生成的内容。
3.3. Github Action文件
配置Github Action之前,请先参考【博客】使用GithubAction自动同步obisidian和hexo仓库 | 慕雪的寒舍一文配置obsidian和hexo仓库的自动同步。后续的Action配置是在这个配置的基础之上的。
首先需要添加两个step,配置python环境并安装解析yaml文件需要的pyyaml库
1 2 3 4 5 6 7 8 9 10 11
| - name: Set up Python 3.10 uses: actions/setup-python@v4 with: python-version: '3.10'
- name: Install pip dependencies run: | python -m pip install --upgrade pip pip install pyyaml
|
然后再在push之前添加一个脚本调用就可以了。脚本提供的参数是目标yaml文件的路径。
1 2 3 4
| - name: Generate rating.yml from raw markdown run: | python Data/python_scripts/gen_rating.py HexoBlog/source/_data/rating.yml
|
最终的完整action文件如下。这个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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| name: Sync CODE to _posts
on: push: paths: - 'Notes/CODE/**'
jobs: sync: runs-on: ubuntu-latest
steps: - name: Checkout obsidian repository uses: actions/checkout@v3
- name: Set up Python 3.10 uses: actions/setup-python@v4 with: python-version: '3.10'
- name: Install pip dependencies run: | python -m pip install --upgrade pip pip install pyyaml
- name: Set up Git env: ACTIONS_KEY: ${{ secrets.HEXO_PRI_KEY }} run: | mkdir -p ~/.ssh/ echo "$ACTIONS_KEY" > ~/.ssh/id_rsa chmod 700 ~/.ssh chmod 600 ~/.ssh/id_rsa ssh-keyscan github.com >> ~/.ssh/known_hosts git config --global user.name "musnows" git config --global user.email "ezplayingd@126.com" git config --global core.quotepath false git config --global i18n.commitEncoding utf-8 git config --global i18n.logOutputEncoding utf-8
- name: Checkout HexoBlog repository run: | git clone git@github.com:musnows/Hexo-Blog.git HexoBlog
- name: Sync files from CODE to _posts run: | rsync -av --delete Notes/CODE/ HexoBlog/source/_posts/
- name: Generate rating.yml from raw markdown run: | python Data/python_scripts/gen_rating.py HexoBlog/source/_data/rating.yml
- name: Commit and push changes to HexoBlog repository run: | cd HexoBlog git add . git commit -m "Sync CODE to _posts at $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M:%S')" git push origin hexo
|
3.4. 测试Action
将脚本和更新后的action文件push到远端,触发action之后,就能看到结果了,成功根据md文件里面的配置生成出了yaml文件。
