看到一个很棒的博客:
https://koobai.com/ ,博客原作者也很大方地将源码公开放到了Github上。对电影页面非常眼馋,征得作者同意后,我对他的模版进行了一些小改造,发布到了Github上,也将自己的博客也从原Hexo引擎迁移到了Hugo。
迁移和改造
文章迁移
Hexo与Hugo都基于MD文件的生成,所以除了修改了Front Matter中date时间格式外,迁移成本几乎为0,把之前的文章(.md)都挪到hugo的博客文件夹下就行了。
原: source/_post
现: content/posts
复制Koobai Github项目中theme/jingzhe_2,并在博客配置文件hugo.toml
中指定即可。
theme = "jingzhe_2"
themesDir = "themes"
电影页面的生成
Github Action配置
看了一下电影页面,是使用了Github Action自动化执行功能,从自己的豆瓣账号去拉取数据存在本地。有现成的轮子可以用,自己配置一个也很简单。
在项目的根目录下新建文件夹.github/workflows
(注意拼写),新建douban.yaml
name: douban
on:
push:
branches:
- main
workflow_dispatch:
inputs:
douban-name:
description: 'DouBan Id'
required: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
sync:
name: Douban Sync
runs-on: ubuntu-latest
env:
DOUBAN_NAME: ${{ github.event.inputs.douban-name || secrets.DOUBAN_NAME }}
REF: ${{ github.ref }}
REPOSITORY: ${{ github.repository }}
YEAR: ${{ vars.YEAR }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r scripts/douban-requirements.txt
- name: Douban Movie sync
run: |
python -u scripts/douban.py
- name: Commit changes
uses: EndBug/add-and-commit@v8
with:
author_name: DOUBAN Sync # 提交者的GitHub用户名
author_email: DOUBAN Sync # 提交者的电子邮件
message: 'Automatically commit changes' # 提交信息
add: '.' # 添加当前目录下的所有变更
为了方便调试,我增加了workflow_dispatch的触发方法(可从Github页面手动触发),并支持在手动触发输入豆瓣账号ID。非手动触发时,应将DOUBAN_NAME配置到secrets中。
工作流的默认工作目录是项目的根目录,我们在根目录下新建文件夹scripts,并添加执行脚本douban.py(省略部分内容):
image_save_folder = 'static/images/douban/'
json_movie_path = 'data/douban/movie.json'
json_book_path = 'data/douban/book.json'
movie_status = {
"mark": "想看",
"doing": "在看",
"done": "看过",
}
@retry(stop_max_attempt_number=3, wait_fixed=5000)
def fetch_subjects(user, type_, status):
offset = 0
page = 0
url = f"https://{DOUBAN_API_HOST}/api/v2/user/{user}/interests"
total = 0
results = []
/** 调用豆瓣接口获取数据 **/
def downloadImgs(image_url, id):
# 确保文件夹路径存在
os.makedirs(image_save_folder, exist_ok=True)
file_name = "{id}.jpg".format(id=id)
save_path = os.path.join(image_save_folder, file_name)
def insert_movie():
results = []
for i in movie_status.keys():
results.extend(fetch_subjects(douban_name, "movie", i))
# 确保文件的父目录存在
os.makedirs(os.path.dirname(json_movie_path), exist_ok=True)
# 检查文件是否存在
if not os.path.exists(json_movie_path):
# 文件不存在时,创建文件
open(json_movie_path, 'w').close() # 创建一个空文件
print("File created.")
json_data = []
for item in results:
# 下载图片文件
downloadImgs(item["subject"]["pic"]["large"], item["id"])
# 准备要追加的数据
new_data = {}
json_data.append(new_data)
with open(json_movie_path, mode='w', newline='', encoding='utf-8') as file:
json.dump(json_data, file, indent=4, ensure_ascii=False)
if __name__ == "__main__":
douban_name = os.getenv('DOUBAN_NAME')
if not douban_name:
print('Douban name is not set')
sys.exit(1)
else:
print(f"DOUBAN_NAME = {douban_name}")
insert_movie()
爬取豆瓣数据,并以json格式存储。这里为了生成海报,将封面海报下载到了静态仓库内。这里也可以不下,渲染时使用一些三方API或用自己图床/代理worker,由于我豆瓣标记也不是很多,所以就选择静态文件了。
页面渲染
原页面基于csv渲染,改成从json文件渲染需要一些小改动,将getCSV改成getJSON,从下标读改为从属性即可。
增加了基于css选择器的过滤。
照葫芦画瓢的读书页
本着对豆瓣爬都爬了都心理,顺便也生成了读书页。基本是对电影页面的复制粘贴。
制作归档页
在pages/ 下新增了归档.md
---
title: '归档'
url: "archive"
date: 2023-01-30
layout: archive
menu:
main:
name: "归档"
weight: 12
---
在主题文件夹下layouts/_default中,新增archive.html,基于标签和年份进行归档。
<div class="content_zhengwen">
<div class="tabs">
<button id = "tagsButton" class="tab-button" onclick="openTab(event, 'Tags')">标签</button>
<button id = "yearsButton" class="tab-button" onclick="openTab(event, 'Years')">年份</button>
</div>
<div class="content">
<div id="Years" class="tab-content">
{{ range (.Site.RegularPages.GroupByDate "2006") }}
<h3>{{ .Key }}</h3>
<ul class="archive-list">
{{ range (where .Pages "Type" "posts") }}
<li>
{{ .PublishDate.Format "2006-01-02" }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
</li>
{{ end }}
</ul>
{{ end }}
</div>
<div id="Tags" class="tab-content">
<!-- 标签词云放这里 -->
<!-- 标签归档开始 -->
{{ $allPages := .Site.RegularPages }}
{{ range $index, $tag := .Site.Taxonomies.tags }}
<h3>{{ $index }}</h3>
<ul class="tag-archive-list">
{{ range $allPages }}
{{ if in .Params.tags $index }}
<li>
{{ .PublishDate.Format "2006-01-02" }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
</li>
{{ end }}
{{ end }}
</ul>
{{ end }}
<!-- 标签归档结束 -->
</div>
Hugo的优势
启动更快
构建速度的确更快。
令人惊喜的短代码
在hexo中使用插件不是一件容易的事情,需要编写复杂的正则。生态也不是很好。
Hugo的短代码相对来就简单很多,也很像现在前端框架里的组件概念。
编写和使用都很简单,以douban卡片举例:
在主题文件夹中,/layout/shortcodes/下,新建一个douban.html 。接受一个url,通过正则,获取url中的subjectId,从本地的book.json中遍历找到对应书的信息,然后渲染到模版代码里。
{{ $dbUrl := .Get 0 }}
{{ $dbType := replaceRE `https://(movie|book).douban.com/subject/.*` "$1" $dbUrl }}
{{ $dbID := replaceRE `.*douban.com/subject/([0-9]+)/.*` "$1" $dbUrl }}
{{/* {{ printf "Page Params: %#v\n" $dbID }} */}}
{{ if eq $dbType "book" }}
{{$items := getJSON "data/douban/book.json" }}
{{range $item := $items}}
{{ $subjectId := string $item.subject_id}}
{{if eq ($subjectId) $dbID }}
{{ $rating := float ($item.douban_score) }}
<!--封面地址替换 -->
{{ $imagePath := printf "images/douban/%s.jpg" (path.Base ($subjectId)) }}
{{ $defaultImg := "images/default/default_poster.jpg"}}
<div class="db-card">
<div class="db-card-subject">
<div class="db-card-post"><img loading="lazy" decoding="async" referrerpolicy="no-referrer" src="{{ $imagePath | absURL }}" ></div>
<div class="db-card-content">
<div class="db-card-title"><a href="{{ $dbUrl }}" class="cute" target="_blank" rel="noreferrer">{{ $item.name }}</a></div>
<div class="rating"><span class="allstardark"><span class="allstarlight" style="width:{{mul 10 $rating }}%"></span></span><span class="rating_nums">{{$rating}}</span></div>
<div class="db-card-abstract">{{ $item.card_subtitle }}</div>
<div class="db-card-comment">{{ $item.intro}}</div>
</div>
</div>
</div>
{{end}}
{{end}}
{{ end }}
做一个短代码来支持LivePhoto吧!
知道了原理就可以很容易开发短代码了。我做了一个LivePhoto的。
Apple的LivePhoto本质就是一张图 +一小段视频 (.mov),我们可以从icloud将图和视频下载下来。
注意要选择兼容性最好的选项,不然可能显示不出来。
下载下来后是一个压缩包zip,里面包含一个jpeg文件和一个mov文件。解压后传到自己图床上。
短代码中需要引入apple的cdn文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Live Photo Example</title>
<script src="https://cdn.apple-livephotoskit.com/lpk/1/livephotoskit.js"></script>
</head>
<body>
<!-- Live Photo Element with dynamic ID -->
<live-photo id="{{ .Get "id" | default "default-live-photo-id" }}"
photo-src="{{ .Get "photo" }}"
video-src="{{ .Get "video" }}"
style="width: {{ .Get "width" | default "300" }}px; height: {{ .Get "height" | default "400" }}px;">
</live-photo>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Using the dynamic ID to create the LivePhotosKit player
const livePhotoId = "{{ .Get "id" | default "default-live-photo-id" }}";
const player = LivePhotosKit.Player(document.getElementById(livePhotoId));
// Set photo and video sources dynamically
player.photoSrc = "{{ .Get "photo" }}";
player.videoSrc = "{{ .Get "video" }}";
// Event listeners for player states
player.addEventListener('canplay', evt => console.log('Player is ready', evt));
player.addEventListener('error', evt => console.log('Player load error', evt));
player.addEventListener('ended', evt => console.log('Player finished playing through', evt));
// Playback controls and styles
player.playbackStyle = LivePhotosKit.PlaybackStyle.FULL;
player.play();
// Error handling specific to live photo elements
player.addEventListener('error', (ev) => {
if (typeof ev.detail.errorCode === 'number') {
switch (ev.detail.errorCode) {
case LivePhotosKit.Errors.IMAGE_FAILED_TO_LOAD:
console.error('Image failed to load');
break;
case LivePhotosKit.Errors.VIDEO_FAILED_TO_LOAD:
console.error('Video failed to load');
break;
}
} else {
console.error('Unexpected error:', ev.detail.error);
}
});
});
</script>
</body>
使用时传入刚刚的mov和jpeg图片地址即可。
Live Photo Example
路边一只被抛弃的可怜的小狗
在请求自己的图床时,发生跨域错误,需要到Cloudflare上配置一个Worker和Worker Rule,解决跨域问题。
新建worker,并配置图床绑定的子域名下/*的woker rule
允许本地开发ip和部署后的ip即可。(要注意地址末尾没有斜杠)
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const origin = request.headers.get("Origin");
let response = await fetch(request);
// Create a new response by cloning the original response
response = new Response(response.body, response);
// Check if the origin is one of the allowed origins
const allowedOrigins = ["https://ygria.site", "http://127.0.0.1:5500","http://127.0.0.1:1313"];
if (allowedOrigins.includes(origin)) {
// Set CORS headers
response.headers.set("Access-Control-Allow-Origin", origin);
response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.headers.set("Access-Control-Allow-Headers", "Content-Type");
}
// Handle OPTIONS preflight request
if (request.method === "OPTIONS") {
response = new Response(null, {
status: 204,
headers: response.headers
});
}
return response;
}
其他折腾
Notion的快速建站
Notion页面支持直接发布成网页,我将自己的读书和播客页面都进行了发布,并绑定了子域名。
在Cloudflare控制台增加一条CNAME,指向notion.so
新增一个worker,同样配置worker rule,将所有访问notion.ygria.site/*的请求都定向到worker。worker内容可以访问
生成worker。
如下图,可以定义多个二级路由和notion页面的关系。
现在,通过notion.ygria.site/podcast 和 notion.ygria.site/weread 就可以访问到我的两个notion公开页面了。并且都是实时更新的,真不错~
Emoji 页头
参考
把 emoji 当作 favicon ,加入了这一小小的功能彩蛋。
总结
-
搜索教程过程中,看到了很多做得很棒的个人博客。不过对于我来说,坚持写博客才是最重要的~
-
评论和memos等等都暂时还没做,对我来说也不是核心功能。
-
chatgpt、github和cloudflare,帮助折腾的利器。自动化流、workspace、函数计算、页面托管、S3存储,都是免费的,还都很好用。