针对 build.ps1 这个脚本的详细解说
单页面构建解析
build.ps1 此脚本是整个静态站点生成系统里面的核心。它会把各式各样的 HTML 组件加载进来,然后进行占位符的替换工作,把动态数据注入进去,最后输出一个完整的 .html 文件。它支持两种语言(中文和英文),两种页面类型(主页和博客页),以及草稿模式。通过不同的参数组合,可以满足各种构建需求:构建主页、构建博客文章、包含草稿、生成英文版等等。
1. 参数设计
脚本会去接收 5 个命名参数,覆盖到了以下场景:
param(
[string]$PageName = "index", # 输出文件名(不含 .html)
[string]$Title = "DragonRS Void", # <title> 标签内容
[string]$ContentFile = "", # 正文内容文件路径
[switch]$IncludeDrafts, # 是否包含草稿
[string]$Lang = "zh" # 语言:zh / en
)
这里面 $PageName 直接决定了输出的路径:index 会去输出到项目的根目录,其他的页面就会输出到 dist/(中文)以及 dist/en/(英文)[1]。
2. 组件加载
脚本的第一步,就是把所有的 HTML 组件模板都给加载进来。这些模板存放于 src/components/ 目录当中,运用 Get-Content -Raw -Encoding UTF8 这个命令,把整份文件以字符串形式读取出来:
$header → src\components\header.html
$banner → src\components\banner.html
$sidebarLeft → src\components\sidebar-left.html
$sidebarRight → src\components\sidebar-right.html (或 -en.html 英文版)
$footer → src\components\footer.html
另外还拥有 6 个"子模板",是用来注入以下动态内容:
$tmplGuestbookItem → guestbook-item.html 单条留言的 HTML
$tmplGuestbookContainer → guestbook-container.html 留言滚动容器
$tmplChangelogItem → changelog-item.html 单条更新日志
$tmplChangelogContainer → changelog-container.html 更新日志滚动容器
$tmplPostNav → post-nav.html 上一篇/下一篇导航条
英文页面(-Lang en)会自动把 sidebar-right-en.html 加载进来。
此外,为了支持博客页面的独立侧边栏布局(左侧 TOC 目录 + 右侧精简栏),脚本还会额外加载 6 个博客专属模板:
$sidebarLeftBlog → sidebar-left-blog.html 博客左栏(TOC目录,中文)
$sidebarLeftBlogEn → sidebar-left-blog-en.html 博客左栏(TOC目录,英文)
$sidebarRightBlog → sidebar-right-blog.html 博客右栏(搜索+文章+标签+功能)
$sidebarRightBlogEn → sidebar-right-blog-en.html 博客右栏(英文版)
$tmplTocItem → toc-item.html TOC 条目模板
$tmplTocContainer → toc-container.html TOC 滚动容器模板
这些模板与主页的侧边栏是独立的——主页左侧依旧是留言板,右侧依旧是搜索+最新文章+更新日志+徽章[2]。
3. 留言板注入
脚本会从 data/runtime/guestbook.txt 里面把留言数据读取出来,取出最后 $MaxMessages(默认是 20)条,并且把顺序反转过来,让最新的那条排在最前面,开展解析工作:
# 数据格式(管道符分隔):
name|email|content|ip|time|show_ip # 新格式 6 字段
name|content|ip|time|show_ip # 旧格式 5 字段(兼容)
每一条留言,都会借助 -replace '{{placeholder}}', $value 的方式,填进 guestbook-item.html 这个模板。要是用户填写了邮箱,那么名字就会变成 <a href="mailto:..."> 这样可点击的链接。所有的条目在拼接完之后,再填到 guestbook-container.html 的 {{messages}} 占位符当中,形成一个带着滚动条的留言容器。
最后再通过替换 <!-- GUESTBOOK_MESSAGES -->,把它注入到左侧边栏里面去。
4. 更新日志注入
跟留言板相比,流程是类似的,但有两个关键的区别:
数据源是 data/changelog.txt,格式为 YYYY-MM-DD|内容
条目并不需要进行反转——changelog.txt 本来就是按照时间正序(旧→新)去排列的,取最后 N 条之后,得到的就是最近的内容了
最大显示的条目数量是 $MaxChangelog(默认 300 条)
在英文模式下,容器标题"更新日志"会被替换成"Changelog"。最终通过 <!-- CHANGELOG --> 这个标记注入到右侧边栏。
5. 最新文章注入
最新文章的列表并不是由 build.ps1 来生成的——它是由 generate-archive.ps1 预先构建好,并输出到 src/components/latest-posts.html(或者是 latest-posts-en.html)里面去的。build.ps1 只负责去读取它,然后再注入到右侧边栏的 <!-- LATEST_POSTS --> 占位符那个位置。
"预生成 + 注入"设计,避免了 build.ps1 每次都要去扫描全部的博客文件,从而提升了单页面构建的速度[3]。
6. 草稿检测
在把内容文件读取出来以后,脚本就会去检测正文里面是不是包含了 draft: true:
$isDraft = $content -match '<!--\s*draft:\s*true\s*-->'
if ($isDraft -and -not $IncludeDrafts) {
Write-Host "Skipped draft: $PageName" -ForegroundColor Yellow
return # 直接退出,不生成页面
}
if ($isDraft -and $IncludeDrafts) {
$PageName = "draft-$PageName" # 输出为 draft-blog-xxx.html
}
这就意味着:
正常去运行 build.cmd 的时候,草稿会被跳过,不会出现在网站上
要是使用了 -IncludeDrafts 这个参数,草稿就会以 draft- 作为前缀输出成文件
草稿并不会出现在归档、标签云、RSS 和搜索索引当中
7. 语言切换链接
右侧边栏里面有一个 <!-- LANG_SWITCH_URL --> 占位符。脚本会根据当前所采用的语言,把相应的切换链接计算出来:
if ($Lang -eq "en") {
# 英文页面 → 切换到中文版
$sidebarRight = $sidebarRight -replace
'<!-- LANG_SWITCH_URL -->', "/$PageName.html"
} else {
# 中文页面 → 切换到英文版
$sidebarRight = $sidebarRight -replace
'<!-- LANG_SWITCH_URL -->', "/en/$PageName.html"
}
语言切换现在会同时注入到所有侧边栏——主页右侧栏、博客右侧栏(中英文)都会被替换,确保无论在哪个页面,语言切换链接都指向正确的目标[4]。
8. 博客侧边栏分离与 TOC 目录生成
这是最近一次重构中新增的功能。博客页面不再使用主页的侧边栏,而是加载一套独立的布局:
左侧栏:从"留言板"变为 TOC 目录——自动从正文标题生成,点击可跳转
右侧栏:保留搜索 + 最新文章,去掉更新日志和徽章,新增文章标签和功能按钮(RSS订阅、GitHub源码等)
侧边栏选择逻辑:
# Select sidebars based on page type
if ($PageName -like "blog-*") {
if ($Lang -eq "en") {
$pageLeft = $sidebarLeftBlogEn
$pageRight = $sidebarRightBlogEn
} else {
$pageLeft = $sidebarLeftBlog
$pageRight = $sidebarRightBlog
}
} else {
$pageLeft = $sidebarLeft # 主页保持原样
$pageRight = $sidebarRight
}
TOC 目录的生成是本模块最精巧的部分。它从正文中提取所有 <font size="5"> 标题标签(这正是博客文章中的章节标题格式),然后为每个标题自动分配锚点 ID 并构建目录列表:
$headingPattern = '<font[^>]*size\s*=\s*"5"[^>]*color\s*=\s*"#ff66cc"[^>]*>\s*(.*?)\s*</font>'
$tocMatches = [regex]::Matches($content, $headingPattern)
这里有一个关键的实现技巧:从后往前处理。因为每次在标题前插入 <a name="toc-N"> 锚点时,字符串长度会发生变化,后面所有匹配的索引都会偏移。如果从前向后处理,第一个替换之后的索引就全错了。所以脚本倒序遍历匹配结果,从文档末尾向开头逐个插入锚点:
for ($ti = $tocMatches.Count - 1; $ti -ge 0; $ti--) {
$tocNum = $ti + 1 # 从后往前,编号始终正确
$tm = $tocMatches[$ti]
$anchorId = "toc-$tocNum"
$headingText = $tm.Groups[1].Value
# 在原标题前插入锚点
$replacement = '<a name="' + $anchorId + '"></a>' + $tm.Value
$content = $content.Substring(0, $tm.Index)
+ $replacement
+ $content.Substring($tm.Index + $tm.Length)
# TOC 条目往前插入(因为在倒序遍历)
$tocItem = $tmplTocItem -replace '{{anchor}}', $anchorId
$tocItem = $tocItem -replace '{{text}}', $headingText
$tocItems = $tocItem + $tocItems # 前插
}
$tocHtml = $tmplTocContainer -replace '{{items}}', $tocItems
最终 TOC 通过 <!-- TOC_ITEMS --> 注入左侧栏,渲染为带锚点链接的目录列表[5]。
文章标签注入则简单得多——从内容文件的 <!-- tags: xxx --> 元数据中提取标签列表,生成指向 /tags.html#标签名 的链接,注入到博客右侧栏的 <!-- ARTICLE_TAGS --> 占位符中。
所有的注入(最新文章、语言切换、TOC、文章标签)都同时作用于中英文博客侧边栏,确保两种语言版本的页面都获得完整的功能[6]。
9. 脚注系统(最复杂的部分)
脚注的处理可以说是本脚本里面最精妙的一段逻辑了,它会分成三步来完成:
第一步:提取定义。 运用正则去匹配正文当中的 [^N]: 内容 这种格式,然后把它存入哈希表 $fnDefs。正则还需要去处理跨行以及内嵌标签的情况[7]。
第二步:替换引用。 把正文里面的 [^N](后面不紧跟冒号,以此来排除定义行)替换成带着锚点的上标链接:
<small><sup>
<a href="#fn1" id="fnref1" style="">[1]</a>
</sup></small>
第三步:构建定义列表。 在文末生成一个表格,每一行都包含了脚注编号(可以链接回引用的锚点)以及对应的内容。脚注会用 size="1"(也就是最小号的字体)来显示。
双向锚点确保了读者可以去点击 <sup><a href="#fn-8" id="ref-8">[8]</a></sup> 跳转到文末去查看注释,然后还可以再点击脚注编号跳回到原文当中的那个位置且不需要JS参与[9]。
10. 文章导航(上一篇/下一篇)
对于那些博客页面(也就是 $PageName -like "blog-*"),脚本会自动把上一篇/下一篇的导航生成出来。具体流程如下:
扫描博客内容目录下面的所有 content-*.html 文件
从每一个文件中把 date 以及 title 的元数据提取出来
将草稿过滤掉(除非使用了 -IncludeDrafts)
按照日期进行降序排列
找到当前这篇文章在排序列表中的位置 $idx
$idx+1 对应的是更早的文章("上一篇"),$idx-1 对应的是更新的文章("下一篇")
得到的结果会通过 post-nav.html 模板去进行渲染,把 {{prev}}、{{home}}、{{next}} 这三个占位符给替换掉。
11. 页面组装与输出
到了最后,所有的部分都会被拼接起来,形成一个完整的 HTML 文档:
# 博客页面使用独立侧边栏,主页保持原样
$page = $header # <html><head>...</head><body>
+ $banner # 顶部横幅(960×182 图片)
+ "<table width=980><tr>"
+ $pageLeft # 左栏(主页=留言板,博客=TOC目录)
+ "<td width=700>" + $content + $postNavHtml + "</td>"
+ $pageRight # 右栏(主页=搜索+最新+更新日志+徽章
+ "</tr></table>" # 博客=搜索+最新+标签+功能)
+ $footer # 页脚(含最后更新时间)
+ "</body></html>"
$pageLeft 和 $pageRight 不再是固定的变量,而是根据页面类型在组装前就已选择好。这使得同一套构建流程能产出两种不同布局的页面,而无需维护两套组装代码。
在输出之前,还有几项收尾的工作要做:
资源路径修正:把 assets/ 替换为 /assets/(从而确保是从根目录去访问的)
将最后更新时间注入到页脚当中:<!-- LAST_UPDATED --> → 内容文件的修改时间
标题替换:<title>DragonRS Void</title> → 文章的标题
最后,运用 Set-Content -Encoding UTF8 把它输出成一个带有 BOM 的 UTF-8 文件[10]。
小结
build.ps1 是一个典型的模板引擎 + 数据注入架构。它不依赖于任何外部库,完全凭借纯 PowerShell 去实现,其核心的思路就是把 HTML 组件化,然后借助占位符的机制,把动态的数据填入到静态的模板当中去。
兼容功能如下:
对老格式 5 字段留言的自动兼容,这样就避免了历史数据的丢失
脚注的定义采用了正则进行精确匹配,防止把正文里的方括号错认成脚注
文章导航是在构建的时候计算好的,而不是在运行时
博客页面与主页的侧边栏通过 $PageName -like "blog-*" 判断完成切换,TOC 目录从正文标题自动生成,文章标签从元数据提取后链接到标签云页面
资源路径的全局替换,保证了 dist/ 子目录当中的页面也能正确地引用 /assets/
目前 Get-Content -Raw 这个命令需要 PowerShell 3.0 或以上的版本,这就意味着构建脚本本身没办法在真正的 Windows 95 上面去运行[11]。
| [1] | 输出路径的判断逻辑还把语言因素考虑进去了:英文 index 输出到 dist/en/index.html,中文 index 输出到项目根目录的 index.html;其他页面按语言分别进入 dist/ 或 dist/en/。
|
| [2] | 主页左侧依旧是留言板 + 留言表单,右侧是搜索+最新文章+更新日志+88x31徽章全集。博客页面左侧变为 TOC 目录,右侧精简为搜索+最新文章+标签+功能按钮。两者的侧边栏 HTML 模板完全独立,通过 $PageName -like "blog-*" 一个条件分支完成切换。
|
| [3] | 单页面构建时只替换占位符,不重新扫描博客目录。但如果新增了文章,需要先运行 generate-archive.ps1 更新 latest-posts 组件,再运行 build.ps1。
|
| [4] | 语言切换链接现在会注入到所有侧边栏变量($sidebarRight、$sidebarRightBlog、$sidebarRightBlogEn),确保中英文切换在主页和博客页面都能正常工作。
|
| [5] | TOC 生成的锚点跳转同样是纯 HTML 实现——<a name="toc-N"> 作为目标,<a href="#toc-N"> 作为链接。这与脚注系统采用了相同的锚点跳转机制,是 HTML 3.2 时代的标准做法。
|
| [6] | 最新文章注入和语言切换注入也都同时作用于中英文博客侧边栏,确保英文读者看到的是 Latest Posts 和正确的语言切换链接,而非中文标签或错误的 URL。
|
| [7] | 核心脚注正则:\[\^(\d+)\]:\s<em>([^\n]</em>?)(?:</(?:p|li|td)>)?(?=\s<em>\[\^\d+\]:|</font>|<hr|\s</em>$|\n) —— 匹配脚注定义直到遇到下一个脚注定义或段落结束标签。跨行匹配和标签内嵌是两个最容易出 bug 的地方。
|
| [8] | 正文中提到的 <sup><a href="#fn-8" id="ref-8">[8]</a></sup> 本身就是一个脚注引用——点击它可以跳到文末这条注释,再点击编号可以跳回去。
|
| [9] | 目前本站所有交互走纯 HTML,不依赖 JavaScript。锚点跳转(<a name> + <a href="#name">)在 IE5.5 甚至更早的浏览器上都能完美工作。
|
| [10] | 服务器和浏览器依赖 UTF-8 BOM 来正确识别中文编码。没有 BOM 的话,IE5.5 可能将页面当作 Latin-1 渲染,导致中文全部变成乱码。PowerShell 的 Set-Content -Encoding UTF8 默认输出带 BOM 的 UTF-8。
|
| [11] | PowerShell 1.0 发布于 2006 年,仅支持 Windows XP/2003/Vista。Windows 95 自带的脚本环境是 DOS 批处理和 VBScript。而 Get-Content -Raw 至少需要 PowerShell 3.0(2012 年)。
|
|