这是一个“配置驱动”的轻量静态站点生成器,面向 Typora 导出的 HTML 笔记:
生成一个克制风格的首页索引(文章列表 / 分组 / 标签筛选 / 最近更新 / 推荐阅读)
为文章页注入一个浮动操作面板(返回主页 / 回到顶部 / 到达底部,移动端默认收起)
项目的设计目标是“尽量不侵入笔记内容”:不搬运图片、不改写文章内部链接与图片引用,仅在文章 HTML 中插入少量资源引用(CSS/JS)和一个 meta 元素作为“返回主页”的锚点。
你用 Typora 写作并导出 HTML(默认会生成 xxx.html,需要手动迁移图片资源文件夹xxx.assets/)
你希望在本地或静态托管(GitHub Pages、个人服务器等)上查看这些 HTML
你希望自动得到一个可浏览/可筛选的首页,同时给文章页统一加上轻量导航控件
Python 3.10+(脚本使用了 str | Path 这类语法)
无第三方依赖(纯标准库)
保持 Typora 默认导出结构即可(同级资源目录):
xxxxxxxxxx51<某个目录>/2文章A.html3文章A.assets/4文章B.html5文章B.assets/
本仓库示例是把“站点输出”和“文章目录”都放在 site/ 下,你也可以把文章放到任意目录,只要配置 scan 指向它即可。
在仓库根目录创建/修改 site.config.json,最小可用版本如下:
xxxxxxxxxx71{2 "site": { "title": "Notebook", "output": "site/index.html" },3 "scan": ["site"],4 "home": { "recent_count": 5, "section_preview_count": 8, "recommended": [] },5 "cache": { "path": "site/.cache/index_cache.json" },6 "ui": { "assets_dir": "site/assets/ui" }7}说明:
所有相对路径都以“配置文件所在目录”为基准解析(通常就是仓库根目录)
scan 必须是非空数组,否则会报错
xxxxxxxxxx11python build_site.py --config site.config.json会产生/更新:
首页:site.output(示例为 site/index.html)
增量缓存:cache.path
UI 静态资源:ui.assets_dir/float-panel.css 与 ui.assets_dir/float-panel.js
文章页:对“变更过的文章”(或配置变更触发全量)注入浮动面板引用(幂等,可重复运行)
推荐用 HTTP 方式预览(避免 file:// 下相对路径、脚本加载等差异):
方式 A:以仓库根目录作为站点根
xxxxxxxxxx11python -m http.server 8000浏览器打开:
http://localhost:8000/site/index.html
方式 B:直接以 site/ 作为站点根(更接近部署到子目录时的体验)
xxxxxxxxxx11python -m http.server 8000 --directory site浏览器打开:
http://localhost:8000/index.html
首页(index):由 build_site.py 扫描文章列表并渲染为单页 HTML,内置轻量 JS 做标签筛选与“分组预览折叠”
文章页增强:由 inject_ui.py 向 Typora 导出的 HTML 注入 CSS/JS 引用,并写入 meta[name="site-home"] 作为“返回主页”的链接来源
这里的“主 / 副标签”是一个分工明确的模型,目的是:既能用目录做分组(稳定、低维护),也能用侧车做主题标注(不侵入正文)。
主标签(primary_tag,分组标签):由文章所在目录名决定,用于首页的“分组标题”
当扫描目录不是站点输出目录时:primary_tag = scan_dir.name
当扫描目录恰好等于首页输出所在目录(例如都在 site/)时:
site/*.html 会被归为 未分类
site/<子目录>/*.html 会以 <子目录> 作为主标签(且可通过 exclude_dirs 排除 .cache、assets 等目录)
副标签(secondary_tags,侧车标签):通过“侧车文件”提供,适合表达“主题/关键词”,不需要改动文章内容
一个直观的例子:
site/教程设计/Typora教程.html 的主标签是 教程设计
如果旁边有 site/教程设计/Typora教程.tags.txt(或 Typora教程.html.tags.txt),其中写了 写作, 工具,那么副标签就是 写作、工具
最终用于“标签筛选与计数”的是 effective_tags:
若 tags.merge_primary=true(默认):effective_tags = [primary_tag] + secondary_tags(去重 + 隐藏过滤)
否则:只使用 secondary_tags(此时主标签只负责分组,不参与筛选导航)
对每一篇文章 xxx.html,可以创建同名标签侧车文件(两种命名都支持):
新风格:xxx.tags.txt
兼容旧风格:xxx.html.tags.txt
内容写法:
支持“一行一个标签”
也支持“逗号分隔”
自动把中文逗号 , 视为 ,
自动去重与去空白
示例:
xxxxxxxxxx21信号处理, 嵌入式2ADC
构建时会读写 cache.path(JSON 文件),缓存每篇文章的关键信息:
HTML 文件签名:mtime_ns + size
侧车文件签名:mtime_ns + size + tags_path
从 HTML 提取到的标题、主标签、副标签列表
因此:
文章未变更时,不重复解析标题与侧车标签
文章变更时才会触发注入(或当配置文件内容变化时全量注入)
一个额外的小细节:注入时会尽量保留文章文件的原始 mtime(避免“注入导致全站文件时间被改写”,影响你对真实更新时间的判断)。
首页每一条文章项都会带上 data-tags='[...]'(JSON 数组),前端通过 URL Hash 做筛选:
#tag=某标签:只显示命中标签的文章项,同时隐藏“最近更新/推荐阅读”区域
# 或空:显示全部,并可按 home.section_preview_count 对每个分组做“预览折叠”
为了避免筛选后列表末尾出现多余分隔线,脚本会动态为“每个分组内最后一个可见项”打上 .last-visible 类来修正样式。
python build_site.py --config site.config.json 的逻辑可以概括为:
读取配置(site_config.py):路径按配置文件所在目录解析为绝对路径
准备 UI 资源(site_ui_assets.py):把 assets/ui/float-panel.css/js 复制到 ui.assets_dir
扫描文章(非递归):只扫描每个目录的第一层 *.html(特殊情况:当 scan_dir==output_dir 会多扫一层子目录)
为每篇文章生成元数据:
标题:优先取 HTML 中的 h1,其次 h2,再其次 <title>,最后回退为文件名
标签:主标签来自目录名;副标签来自 *.tags.txt
归一化/别名/隐藏:由 tags.normalize、tags.aliases、tags.hide_tags 控制
增量注入(并发):对“需要注入”的文章页插入/更新浮动面板所需的标签
渲染首页:
顶部区:最近更新(home.recent_count)、推荐阅读(home.recommended)
导航区:所有标签及文章数量(按数量降序)
分组区:按主标签分组显示文章列表
写入缓存与首页输出:
如果首页内容无变化,会提示“主页无变化”并避免重复写文件
python inject_ui.py <文章.html> --config site.config.json 会在文章 HTML 中:
在 <head> 中替换或插入(幂等):
<meta name="site-home" content="...">:返回主页的相对路径
<link rel="icon" ... data-site-ui="favicon">(可选):站点图标(由 ui.favicon 控制)
<link rel="stylesheet" ... data-site-ui="float-panel">
在 </body> 前替换或插入(幂等):
<script src="..." defer data-site-ui="float-panel"></script>
注入的路径全部使用“相对文章所在目录”的相对路径计算,因此:
文章和首页、资源目录的相对位置发生变化时,只要重新运行一次注入即可自洽
更推荐用 HTTP 预览/部署(而不是双击 file:// 打开)
所有路径字段都支持绝对路径与相对路径;相对路径会以配置文件目录为基准解析。
(本文提到的“相对路径”,如果是在说配置项的取值,默认都是相对于 site.config.json 所在目录。)
site.title:站点标题(默认 Notebook)
site.output:首页输出文件(默认 index.html)
header.social:页头右侧的社交按钮数组,每项包含:
type:图标类型(当前内置:github、email;未知类型会被忽略)
href:链接(必填;例如 GitHub URL、mailto:xxx@xx.com)
label:可读文本(用于 aria-label 与 title;可空,默认回退为 type)
footer.icp.text / footer.icp.href:ICP 备案号文本与链接(href 默认回退为 https://beian.miit.gov.cn/)
footer.gongan.text / footer.gongan.href:公安备案号文本与链接(href 可留空;留空则仅展示文本)
footer.gongan.show_icon:是否显示公安备案图标(默认 true)
footer.gongan.icon:公安备案图标来源(可选)
在线链接:填写 http/https URL
本地文件:填写本地路径(相对/绝对均可;相对路径以配置文件所在目录为基准);构建时会复制到 ui.assets_dir 的上级目录,并以相对路径引用
若启用图标但未设置该字段或解析失败:回退为内置占位图标
scan:要扫描的目录列表(必须是非空数组)
每个目录仅扫描第一层 *.html
若某个扫描目录恰好等于 site.output 所在目录,会额外扫描其下一层子目录
home.recent_count:最近更新展示数量(默认 12;<=0 视为不显示)
home.section_preview_count:每个分组在“未筛选”状态下的预览条数(默认 0:不折叠)
home.recommended:推荐文章列表(字符串数组)
建议写“相对路径(不以 / 开头)”,例如:blog/形式与政策课程论文.html
构建时会尝试用 scan 目录前缀进行匹配(兼容你写 blog/... 而实际扫描目录为 site 的情况)
exclude_names:扫描时忽略的 HTML 文件名集合(不区分大小写)
exclude_dirs:当扫描目录等于站点输出目录时,忽略的子目录集合(不区分大小写;为空则默认 {".cache","assets"})
cache.path:增量缓存文件路径(默认 .cache/index_cache.json)
ui.assets_dir:UI 资源输出目录(默认 assets/ui)
ui.css_filename / ui.js_filename:资源文件名(默认 float-panel.css/js)
ui.favicon:站点图标(可选),支持两种模式:
在线链接:填写 http/https URL,例如 https://example.com/favicon.ico
本地资源:填写一个本地文件路径(相对/绝对均可;相对路径以配置文件所在目录为基准),例如 assets/favicon.ico
构建/注入时会把该文件复制到 ui.assets_dir 的上级目录(例如 site/assets/),并在首页与文章页以“相对当前 HTML 文件所在目录”的方式引用
ui.mobile_notice.enabled / ui.mobile_notice.text:移动端提示(仅在小屏显示)
ui.home_font_family:首页字体栈(可空)
ui.home_font_css_url:首页额外字体 CSS 链接(仅允许 http/https;可空)
ui.home_meta_template:标题下方元信息模板(可空)
支持占位符:{time}(构建时间)、{count}(文章总数)
如果“文章集合与配置均未变化”,构建时间会沿用缓存中的上一次值(让“更新于/构建于”更符合真实变化)
ui.section_tags_mode:分组内文章行显示标签的策略
secondary_only:仅显示副标签(默认)
primary_and_secondary:显示主标签 + 副标签
用于“标签归一化/别名/隐藏”:
xxxxxxxxxx81{2 "tags": {3 "merge_primary": true,4 "normalize": "lower",5 "aliases": { "ADC": "adc", "嵌入式开发": "嵌入式" },6 "hide_tags": ["未分类"]7 }8}tags.merge_primary:是否把主标签并入可筛选标签(默认 true)
tags.normalize:归一化方式(默认 none)
none:不做大小写处理
lower / lower_ascii:大小写折叠(实现上使用 casefold())
tags.aliases:别名映射(会先归一化 key/value 再应用)
tags.hide_tags:隐藏标签列表(从展示与计数中剔除)
更推荐用 python -m http.server 预览。file:// 环境下,浏览器对脚本/资源加载、相对路径解析更敏感,容易出现“资源加载失败但不明显”的情况。
构建时间来自缓存:当“文章列表签名 + 配置文件内容”都未变化时,会沿用上一次构建时间。想强制更新时间变化,可以:
修改任意文章(或侧车标签文件)
或修改 site.config.json(配置变更会触发全量注入与重算签名)
可以,直接把 site.output 设为 site/index.html(本仓库示例就是这样)。同时建议把 cache.path 与 ui.assets_dir 一并放到同一个输出根下(例如 site/.cache 与 site/assets/ui),便于整体搬迁与部署。
首页 UI:build_site.py 内部内联 CSS/JS(适合保持“单文件首页”);改动后直接重新构建即可
浮动面板 UI:修改 assets/ui/float-panel.css 与 assets/ui/float-panel.js,然后重新运行构建(会复制到 ui.assets_dir)