Notist
On this page
  1. 从 Cargo examples 生成站点(Experimental)
  2. 文件布局
  3. 配置扩展
  4. 实现事件处理
  5. 页面模板
  6. 构建与检查

从 Cargo examples 生成站点(Experimental)

本例让 Notist SSG 在构建时读取 Cargo.toml 的 [[example]] 和 Rust 源文件,为每个 example 生成一个页面及模板数据。增减 Cargo targets 后重新构建即可,无须为每项维护独立的文档与 data 目录中的 TOML 文件。

本例使用 SSG 的 beforecompile 生成源码,在同一进程中保留元信息,随后在 beforetemplate 生成模板数据。协议见 SSG 站点扩展。

文件布局

project/
  Cargo.toml
  examples/
    hello/main.rs
  website/
    Notist.toml
    content/index.not
    scripts/examples.py
    templates/page.tera

准备一个最小 Cargo.toml:

[package]
name = "demo"
version = "0.1.0"
edition = "2024"

[[example]]
name = "hello"
path = "examples/hello/main.rs"

examples/hello/main.rs:

fn main() {
    println!("Hello from an example!");
}

配置扩展

website/Notist.toml:

[package]
name = "example-site"

[site]
title = "Rust examples"
content = "content"
output = "public"

[site.extensions.examples]
command = ["python3", "scripts/examples.py", "../Cargo.toml"]
watch = ["scripts", "../Cargo.toml", "../examples"]

脚本在 website 目录运行,因此 ../Cargo.toml 指向项目清单。watch 中的路径也相对于 website:清单和 Rust 源码位于站点根之外,需要显式声明才能触发预览重建。依赖目录递归监听,新增 example 目录也会触发重建。脚本需要 Python 3.11+,使用标准库中的 tomllib 解析 TOML。

website/content/index.not 是唯一需要手写的内容页:

= Rust examples

[浏览 example 目录](examples/index.md)。

链接目标稍后由扩展创建,可以在构建中正常解析。

实现事件处理

将下面代码保存到 website/scripts/examples.py:

import json
import sys
import tomllib
from pathlib import Path


def compile_sources(request, manifest_path):
    manifest = tomllib.loads(manifest_path.read_text())
    documents = request["documents"]

    # 本扩展管理 examples/ 页面和 site.data.examples。
    for path in list(documents):
        if path.startswith("examples/"):
            del documents[path]
    examples = {}
    links = ["# Examples", ""]

    for target in manifest.get("example", []):
        name = target["name"]
        source = (manifest_path.parent / target["path"]).read_text()
        quoted_name = json.dumps(name, ensure_ascii=False)
        documents[f"examples/{name}.md"] = (
            f"@!(title: {quoted_name}, example: {quoted_name})\n\n"
            f"# {name}\n\n```rust\n{source.rstrip()}\n```\n"
        )
        examples[name] = {"name": name, "code": source}
        links.append(f"- [{name}]({name}.md)")

    documents["examples/index.md"] = "\n".join(links) + "\n"
    return documents, examples


examples = {}
for line in sys.stdin:
    request = json.loads(line)
    response = {"version": 2, "event": request["event"]}
    try:
        if request["version"] != 2:
            raise ValueError("unsupported site extension request")
        if request["event"] == "before_compile":
            response["documents"], examples = compile_sources(request, Path(sys.argv[1]))
        elif request["event"] == "before_template":
            data = request["data"]
            data["examples"] = examples
            response["data"] = data
        else:
            raise ValueError("unsupported event")
    except Exception as error:
        response["errors"] = [str(error)]
    print(json.dumps(response), flush=True)

documents 的键相对于 content:examples/hello.md 生成 examples/hello.html。根注解将 example 名称提供给模板中的 page.attrs.example。

本扩展重新生成 examples/ 下的页面与 site.data.examples,保留其他内容。请将手写页面放在 examples/ 以外,避免被覆盖。

页面模板

website/templates/page.tera:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{{page.title}} · {{site.title}}</title>
    <link rel="stylesheet" href="{{assets.highlight}}">
  </head>
  <body>
    <nav><a href="{{url(path='index.html')}}">Home</a></nav>
    <main>
      {{page.html | safe}}
    </main>
    {% if assets.components %}
      <script type="module" src="{{assets.components}}"></script>
    {% endif %}
  </body>
</html>

模板通过 page.html 放置生成的正文,包括 Rust 代码高亮。还可以通过 site.data.examples 访问脚本生成的数据,见 模板。

构建与检查

从 project 目录执行:

notist build --config website/Notist.toml
notist serve --config website/Notist.toml --port 8000

输出结构为:

website/public/
  index.html
  examples/
    index.html
    hello.html

content 中仍只有手写的 index.not,data 目录不需要创建。添加新的 Cargo example target 及源码后,预览会生成相应页面;删除 target 后,再次成功构建会清理旧输出。

调试脚本时,日志写到 stderr,例如 print("loading manifest", file=sys.stderr)。stdout 只能返回协议响应。清单缺失或无法解析时,本例返回 errors,构建失败,CLI 保留已有输出;修复被 watch 观察的文件后,预览会重新构建。

单独执行 notist check website/content/index.not 只检查现有单文档,不执行站点扩展,因此应通过 build 检查生成页面之间的链接和完整站点。

仓库还提供 examples/ssg-extensions,包含分组目录、相邻 scenes.rs 源码读取,以及保留现有媒体数据的生成版本。从 Notist 仓库根目录可直接执行:

cargo run -p notist-cli -- build -p ssg-extensions

该示例使用 ranim 的 example 目录布局。它是 Notist 仓库 workspace 的成员,通过 -p 按包名选择,默认输出到仓库根的 notist-output/ssg-extensions/。上面的 website/public/ 则来自教程清单中显式配置的 output = "public"。