Hugo-PaperMod在首页添加热力图

<!DOCTYPE html> Responsive Image 实现功能: 根据颜色深浅表现当日字数的多少鼠标悬停展示当日总字数、文章数和瞬间数点击弹出当天写的文章列表 Step 1 - 使用脚本生成 json 文件 heatmap.py(我放在网站项目根目录) import os import re import json import jieba import frontmatter from datetime import datetime, date from dateutil.parser import parse as parse_date from collections import defaultdict # 配置 POSTS_DIR = 'content/posts' # Hugo posts 目录 MOMENTS_DIR = 'content/moments' # Hugo moments 目录 OUTPUT_FILE = 'static/data/posts_heatmap.json' # 输出 JSON 文件路径 if os.path.exists(OUTPUT_FILE): os.remove(OUTPUT_FILE) # 初始化数据结构: key=日期(字符串),value=字典 heatmap_data = defaultdict(lambda: { 'posts': [], 'moments': [], 'totalWords': 0.0, 'momentCount': 0 }) def compute_words_count_by_jieba(markdown_content): """ 示例: 使用 jieba 对文本进行分词,统计词数并换算成“千字”。 """ text = re.sub(r'(```.+?```)', ' ', markdown_content, flags=re.S) text = re.sub(r'(`[^`]+`)', ' ', text) text = re.sub(r'\[[^\]]+\]\([^\)]+\)', ' ', text) tokens = list(jieba.cut(text, cut_all=False)) tokens = [tok for tok in tokens if tok.strip()] # 去掉纯空白 return round(len(tokens)/1000.0, 2) def convert_dates(obj): """ 递归地将字典或列表中的 date/datetime 对象转换为字符串。 防止写入 JSON 时出现 “Object of type date is not JSON serializable”。 """ if isinstance(obj, dict): return {k: convert_dates(v) for k, v in obj.items()} elif isinstance(obj, list): return [convert_dates(item) for item in obj] elif isinstance(obj, (date, datetime)): return obj.strftime('%Y-%m-%d') else: return obj def process_files(directory, category): """ 遍历指定目录,读取 Markdown 文件并插入 heatmap_data。 category 为 'posts' 或 'moments'。 """ if not os.path.exists(directory): print(f"目录不存在: {directory}") return for root, dirs, files in os.walk(directory): for filename in files: if category == 'posts': # 仅处理 'index.md' + 排除 _index.md if filename == 'index.md' and not filename.startswith('_'): file_path = os.path.join(root, filename) else: continue else: # moments: 处理 .md 文件,排除 _index.md if filename.endswith('.md') and not filename.startswith('_'): file_path = os.path.join(root, filename) else: continue try: with open(file_path, 'r', encoding='utf-8') as f: post = frontmatter.load(f) date_field = post.get('date') if not date_field: print(f"文件 {file_path} 缺少 'date' 字段,跳过。") continue # 统一解析 date 并存成字符串 try: # 先转成字符串,再用 parse_date post_date = parse_date(str(date_field)) date_str = post_date.strftime('%Y-%m-%d') except (ValueError, TypeError) as e: print(f"无法解析文件 {file_path} 的日期:{date_field} => {e}") continue # 计算字数 words_count = compute_words_count_by_jieba(post.content) # 获取 slug slug = post.get('slug') if not slug: if category == 'posts': slug = os.path.basename(root) else: slug = os.path.splitext(filename)[0] # 获取 title if category == 'moments': title = slug # moments 不关心 title => 用 slug else: title = post.get('title', '未命名') # 链接(仅 posts) link = f"/posts/{slug}/" if category == 'posts' else "" # 初始化 date_str if date_str not in heatmap_data: heatmap_data[date_str] = { 'posts': [], 'moments': [], 'totalWords': 0.0, 'momentCount': 0 } if category == 'posts': heatmap_data[date_str]['posts'].append({ 'title': title, 'words': words_count, 'link': link }) else: # moments heatmap_data[date_str]['moments'].append({ 'title': title, 'words': words_count }) except Exception as e: print(f"处理文件 {file_path} 时发生错误:{e}") def main(): # 分别处理 posts 和 moments process_files(POSTS_DIR, 'posts') process_files(MOMENTS_DIR, 'moments') # 统计 totalWords 和 momentCount for date_str, data in heatmap_data.items(): total = 0.0 total += sum(x['words'] for x in data['posts']) total += sum(x['words'] for x in data['moments']) data['totalWords'] = round(total, 2) data['momentCount'] = len(data['moments']) # 将 defaultdict 转为普通字典 heatmap_dict = dict(heatmap_data) for k in heatmap_dict: heatmap_dict[k] = dict(heatmap_dict[k]) # 在写出之前,递归地将 date/datetime 全转成字符串 # 因为 heatmap_dict 的 key 已经是字符串 date_str,所以不需特地改它们 # 但为保险起见,依然可以把里面的任何残余 date 转掉 final_data = convert_dates(heatmap_dict) # 写入 JSON os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True) try: with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: json.dump(final_data, f, ensure_ascii=False, indent=2) print(f"\n已成功生成 {OUTPUT_FILE}") except Exception as e: print(f"写入 JSON 文件时出错: {e}") if __name__ == "__main__": main() 生成的static/data/posts_heatmap.json应该类似这样: ...

2025-01-09 19:05 · 王明明

在Hugo-PaperMod中加入Waline评论区

❗️ 20250325更新: 本文已过时,请使用主题Drifting-PaperMod,参考文档Drifting-PaperMod主题文档 Waline服务端部署 LeanCloud 设置 参考Waline官方文档: LeanCloud 设置 (数据库) Vercel部署 参考Waline官方文档: Vercel 部署 (服务端) 绑定域名(可选) 参考Waline官方文档: 绑定域名 (可选) 在Hugo-PaperMod中引入Waline HTML 引入 (客户端) 在layouts/partials文件夹下新增comments.html: layouts/partials/comments.html <!-- layouts/partials/comments.html --> {{- if .Site.Params.comments }} <!-- 评论容器 --> <div class="waline-container" data-path="{{ .Permalink | relURL }}"></div> <link href="https://unpkg.com/@waline/client@v3/dist/waline.css" rel="stylesheet" /> <!-- 初始化 Waline 的脚本 --> <script> document.addEventListener("DOMContentLoaded", () => { // 初始化 Waline const walineInit = () => { import('https://unpkg.com/@waline/client@v3/dist/waline.js').then(({ init }) => { const walineContainers = document.querySelectorAll('.waline-container[data-path]'); walineContainers.forEach(container => { if (!container.__waline__) { const path = container.getAttribute('data-path'); container.__waline__ = init({ el: container, serverURL: '{{ .Site.Params.waline.serverURL }}', lang: '{{ .Site.Params.waline.lang }}', visitor: '{{ .Site.Params.waline.visitor | default "匿名者" }}', emoji: [ {{- range .Site.Params.waline.emoji }} '{{ . }}', {{- end }} ], requiredMeta: [ {{- range .Site.Params.waline.requiredMeta }} '{{ . }}', {{- end }} ], locale: { admin: '{{ .Site.Params.waline.locale.admin }}', placeholder: '{{ .Site.Params.waline.locale.placeholder }}', }, path: path, dark: '{{ .Site.Params.waline.dark | default "body.dark" }}', }); } }); }).catch(error => { console.error("Waline 初始化失败:", error); }); }; walineInit(); }); </script> {{- end }} 修改hugo.yaml配置 toml文件自行改写。 在params下: ...

2025-01-08 22:58 · 王明明

Obsidian+github+clouflarepages的Hugo一体式发布流程

<!DOCTYPE html> Responsive Image Cloudflare Pages 改成了使用 Vercel 也可以,部署步骤大同小异。 为什么使用 obsidian:Obsidian 有很多可用的插件,比如 excalidraw 这种流程图软件等等,可以一站实现所有的写文流程,不用再切换其他软件做图; 为什么使用 cloudflare pages:有现成的 Hugo 部署方案,结合 Github 可以实现自动化部署; QuickAdd 有什么作用:可以使用快捷键将 posts 添加到想要的位置,并且添加 front matter 的 yaml 模版; ...

2025-01-07 22:50 · 王明明