像 11ty.js 一样编程
「Zest 的模版即代码——就像 11ty.js 中 JavaScript 模版在构建时执行,Zest 的 .zest.fsx 文件是真实 F# 脚本,在构建时编译求值。」
概述
Zest 的 .zest.fsx 文件不仅仅是模版——它们是真正的 F# 脚本。
就像 11ty.js 中 JavaScript 模版文件在构建时执行一样,Zest 使用 FSharp.Compiler.Service 的 FsiEvaluationSession 在进程内编译并执行 F# 代码,将结果合并到生成的 HTML 页面中。
页面结构
一个典型的 .zest.fsx 文件由两部分组成:
1. 元数据注释(// @ 前缀)
// @title 页面标题
// @layout 布局模板
// @description 页面描述
// @tags ["tag1"; "tag2"]
2. HTML 内容表达式
使用 render 函数将 DSL 构建的 HTML 节点列表渲染为字符串:
render [
h1 [ text "Hello" ]
p [ text "World" ]
ul [ li [ text "Item 1" ]; li [ text "Item 2" ] ]
]
构建时计算示例
这些值在构建时由 F# 编译器真实计算:
当前构建时间:
2026-06-20 20:11:56
列表运算结果:
1 到 10 中偶数的平方和 = 220
条件分支:
下午好!
HTML DSL 元素
Zest 提供了一套完整的 HTML 构造函数,可直接在 F# 中使用:
文本与内联元素
text "纯文本" // 纯文本节点
raw "原始HTML" // 原始 HTML(不转义)
strong [ text "粗体" ] //
span [ text "内联" ] //
a "https://example.com" [ text "链接" ] //
块级元素
h1 [ text "标题1" ]
h2 [ text "标题2" ]
h3 [ text "标题3" ]
p [ text "段落" ]
ul [ li [ text "项目" ] ]
ol [ li [ text "项目" ] ]
div [ text "容器" ]
divC "my-class" [ text "带类名的容器" ]
代码与预格式化
codeBlock "fsharp" "let x = 42" //
codeBlock "" "inline code" // 无语言标记
pre [ code [ text "code block" ] ] // 手动构建
render 辅助函数
setup 阶段定义了 `render` 辅助函数,它是 `HtmlRenderer.render` 的别名:
// 以下两种写法等价:
render [ h1 [ text "A" ]; p [ text "B" ] ]
HtmlRenderer.render [ h1 [ text "A" ]; p [ text "B" ] ]
完整示例
下面是一个完整的 .zest.fsx 页面文件:
// @title 我的文章
// @layout default
// @date 2026-06-20
// @tags ["demo"]
let pageTitle = "我的文章标题"
let items = ["F#"; "Zest"; "SSG"]
render [
h1 [ text pageTitle ]
p [ text "这是一篇由 F# 脚本生成的页面。" ]
p [ text (sprintf "共有 %d 个项目:" items.Length) ]
ul [ for i in items -> li [ text i ] ]
]
工作原理
Zest 的 F# 脚本执行流程:
检测文件是否以 F# 脚本开头(通过 isPageScript 启发式检查)
解析 // @ 元数据注释,提取 title/layout/permalink/tags/date
将所有页面数据序列化为 JSON 临时文件
生成 preamble 脚本(DSL 函数 + collections API),通过 @"..." 路径读取 JSON
拼接 preamble + 用户脚本,写入临时 .fsx 文件
启动 dotnet fsi 子进程执行脚本,捕获 stdout 作为 HTML 输出
将 HTML 内容与元数据合并,应用布局模板,写出最终文件
如果脚本求值失败,自动回退到 Markdown 传统模式