|
脚本详解:generate-archive.ps1
一脚本四产出:最新文章 + 文章归档 + 标签云 + 搜索索引
generate-archive.ps1 是整个构建流程的第一阶段。它必须在 build.ps1 之前运行,因为它负责生成那些会被"注入"到侧边栏和独立页面的数据文件。一个脚本,四种产出,是项目中最"高产"的模块。
1. 元数据扫描
脚本的第一步是遍历博客内容目录下的所有 content-*.html 文件,用正则提取三段关键元数据:
$dateMatch = [regex]::Match($raw, '')
$titleMatch = [regex]::Match($raw, '')
$tagsMatch = [regex]::Match($raw, '')
$isDraft = $raw -match ''
日期格式兼容两种写法:2026-04-28(精确到日)和 2026-04-28 23:30(精确到分钟)[1]。标签用逗号分隔,例如 教程, 网站建设, 脚本 会被解析为数组 @("教程", "网站建设", "脚本")。
草稿(draft: true)会被跳过,不出现在任何产出中。这意味着草稿文章对网站访问者完全不可见。
所有文章信息存入一个 PSCustomObject 数组,按日期降序排列。
|
2. 产出 A:最新文章组件
取前 5 篇文章,生成 src/components/latest-posts.html:
$recentPosts = $posts | Select-Object -First 5
这里有一个精巧的标题截断算法,按像素宽度估算而非字符数:
foreach ($c in $title.ToCharArray()) {
$code = [int]$c
if ($code -gt 127) { $estWidth += 11 } # CJK 字符 ~11px
elseif ($c -eq ' ') { $estWidth += 4 } # 空格 ~4px
else { $estWidth += 6 } # ASCII ~6px
}
if ($estWidth -gt 110) {
$title = $title.Substring(0, 10) + "..." # 截断 10 字符
}
为什么用像素估算而非简单字符截断?因为侧边栏宽度只有 140px,一个中文字大约 11px 宽,英文字母大约 6px,同样的字符数可能宽度差一倍。如果不做像素估算,"AAAAAAAAAAAA" 会溢出,"中中中中中中中" 则还有余量。这种"手工宽度计算"正是 90s web 开发的典型手法[2]。
输出文件使用 UTF-8 without BOM 编码——因为它会被注入到已有 BOM 的页面中。如果带了 BOM,注入位置会出现乱码字符。
|
3. 产出 B:文章归档页
归档页的核心逻辑是按年-月分组:
$grouped = $posts | Group-Object { $_.Date.Substring(0, 7) }
| Sort-Object Name -Descending
日期格式统一为 YYYY-MM-DD,取前 7 个字符就是 YYYY-MM。分组按键降序排列,确保最新的月份排在最上面。
每个月份的渲染分为:
月份标题:用 size="4" color="#ff99ff" 显示"2026年4月"
文章表格:两列布局——日期列(140px 宽,黄色) + 标题列(青色链接)
归档页输出到 dist/archive.html(中文)或 dist/en/archive.html(英文),使用与其他页面一致的表格式布局和侧边栏。
|
4. 产出 C:搜索索引
搜索索引是一个纯文本文件 data/search_index.txt,被 cgi-bin/search.py 读取用于全文搜索[3]:
TITLE: 脚本详解:build.ps1
DATE: 2026-04-28 23:30
LINK: blog-build-script.html
TEXT: build.ps1 是整个静态站点生成系统的核心引擎...
---
文本提取的流程是:
读取 HTML 源文件
用正则 <[^>]+> 剥离所有 HTML 标签
替换 为空格
压缩连续空白为单个空格
每篇文章之间用 --- 分隔,search.py 解析时按此分割为独立的文档记录。
文本提取不会解析元数据注释——因为注释中的 date、title 已经被结构化提取到了 TITLE 和 DATE 字段中。
|
5. 产出 D:标签云页面
标签云是四个产出中逻辑最复杂的一个,需要构建标签→文章的反向索引:
$tagMap = @{} # 哈希表:标签名 → 文章列表
foreach ($post in $posts) {
foreach ($tag in $post.Tags) {
if (-not $tagMap.ContainsKey($tag)) {
$tagMap[$tag] = @()
}
$tagMap[$tag] += $post
}
}
标签云的字号分级是一个简单的启发式规则:
$count = $tagMap[$tag].Count
$size = if ($count -ge 3) { 4 } # 3+ 篇文章:大号
elseif ($count -ge 2) { 3 } # 2 篇文章:中号
else { 2 } # 1 篇文章:小号
标签云下方,每个标签对应一个文章列表区域,用 <a name="tag名"> 做锚点定位,点击标签云中的标签即可跳转到对应的文章列表。
输出到 dist/tags.html(中文)或 dist/en/tags.html(英文)。
|
6. 双语支持
脚本通过 -Lang 参数(默认 "zh")控制语言:
$isEn = ($Lang -eq "en")
$blogDir = if ($isEn) {
Join-Path $projectRoot "src\content\pages\blog\en"
} else {
Join-Path $projectRoot "src\content\pages\blog"
}
英文模式下:
博客内容从 blog/en/ 目录读取
组件输出到 latest-posts-en.html
页面输出到 dist/en/ 子目录
界面文字("最新文章"→"Latest Posts","文章归档"→"Article Archive")自动切换
搜索索引导出到 data/search_index_en.txt
中文和英文的归档/标签是完全独立的——这保证了英文访问者看到的是英文的界面和文章列表,不会中英混杂。
|
7. 在构建流程中的位置
generate-archive.ps1 必须在 build.ps1 之前运行,原因是:
build.ps1 读取 latest-posts.html 注入侧边栏——如果还没生成,注入的就是旧数据
归档页和标签页是独立页面,也需要在访问者看到之前重新构建
搜索索引需要在 search.py 处理查询前更新,否则新文章搜不到
rebuild-all.ps1 负责按正确顺序调用这些脚本,确保数据依赖关系不被打破[4]。
|
小结
generate-archive.ps1 体现了一种"数据预处理"的设计模式:将博客元数据一次性扫描,生成多种下游格式,避免每个消费者重复解析。
如果把这个脚本拆成四个独立的脚本,代码会更模块化,但每次全站构建就需要扫描 4 次博客目录。一次扫描、四处输出的设计减少了 I/O,换来的是构建速度的提升——在这个有数十篇文章的站点上,差异可能只有几百毫秒,但它展示了"提前想好数据流"的工程思维[5]。
|
| [1] | 分钟精度的日期格式主要用于"最近在干什么"这类按月更新的日志文章,精确到分钟可以更好地控制文章排序。 |
| [2] | 在 CSS text-overflow: ellipsis 出现之前,后端截断是唯一的方案。IE5.5 不支持 text-overflow,所以这个手工算法恰好是最兼容的做法。 |
| [3] | 搜索是完全免 JavaScript 的——搜索框是一个 HTML 表单,提交到 /cgi-bin/search.py,服务端读取 search_index.txt 做全文匹配,返回带高亮的结果页面。整个过程与 2002 年的搜索引擎工作原理一致。 |
| [4] | 实际上 rebuild-all.ps1 不仅管理了脚本调用顺序,还会在调用 generate-archive.ps1 之后调用 build.ps1 重建所有页面。详见关于 rebuild-all.ps1 的博文。 |
| [5] | PowerShell 的 Group-Object、Sort-Object、Select-Object 等管道命令在这里发挥了类似 SQL 中 GROUP BY、ORDER BY、LIMIT 的作用——只不过操作的是内存中的对象数组而非数据库表。 |
|