Snow静态博客生成器

linux snow

一个简单却可配置的静态博客生成器。 很早之前(三年前)就想写一个静态博客生成器,但苦于一直没有时间,最近把之前写的重构了一下,让其可以支持更多的定制化配置。

至于为什么要重新写一个:

  1. 大概是因为想要把之前未完成的轮子补充完整

  2. 我目前使用的博客系统使用的是 pelican,一个基于 Python 的静态博客生成器,目前里面包括了很多我自己写的插件,比如 Emacs org mode 的支持、文章加密、模版定制等,因为需要大量遍历所有文章,导致生成速度越来越慢,还有一点就是因为使用的是 Python,每次本地预览时都需要切换到虚拟环境

  3. 我习惯使用 Emacs + Org,除了前期的几篇文章,后面都是使用 org mode 书写,之前是因为 Python 没有一个好用的 org mode 解析库,所以专门写了一个 org-python 用来解析 org mode;最近我也是完善了另一个我很早之前就写的 org-golang 解析库(轮子+1),准备趁次机会利用一下这个库

  4. 至于为什么不用最近几年流行的 Hugo, 因为我想要保持和我使用 Pelican 时一样的功能,比如文章加密,而 hugo 并不支持插件,想要自定义插件必须复制大段大段的启动函数, 甚至需要修改源代码。同样我想要定制一个相同的模版,而对于使用过其它模版系统如Django,jinja2, 再来使用 Go内置模版 的人来说, hugo 的内置模版除了难用就是难用,这也是我此次选用 pongo2 的原因

  5. 我的设想是提供插件的接口,并提供一个足够简单的启动函数, 在有用户需要自定义插件时只需要自己创建一个包, 使用三两行代码就能注册自定义插件并重新编译自己的snow

快速开始

安装

使用 Homebrew:

1brew install honmaple/tap/snow

从源码安装:

1go install github.com/honmaple/snow@latest

手动编译:

1git clone https://github.com/honmaple/snow --depth=1
2cd snow
3go mod tidy
4go build .

创建站点

1snow init myblog

如果不传目录名,Snow 会在当前目录初始化站点。初始化过程会询问站点标题、作者、URL 前缀,以及是否创建第一篇文章。

初始化后,一个最小站点通常长这样:

1myblog/
2├── config.yaml
3└── content/
4    └── posts/
5        └── hello-snow.md

本地预览

1cd myblog
2snow server --autoload

也可以不切换目录,直接指定站点根目录:

1snow server --root-dir myblog --autoload

--autoload 会监听内容、模板、静态文件等变化,自动重新构建。

构建发布

1snow build

常见生产构建:

1snow build --mode publish --clean

--mode publish 会应用配置里的发布环境覆盖项;--clean 会在构建前清理输出目录里的非隐藏文件。

命令行

Snow 常用命令包括 initserverbuildhooks

init

1snow init
2snow init myblog

创建新站点。如果指定目录名,会在该目录下生成站点;否则在当前目录初始化。

server

1snow server
2snow server --listen 127.0.0.1:8088
3snow server --autoload
4snow server --root-dir myblog
5snow server --config other.yaml
6snow server --debug
7snow server --mode publish
8snow server --include-drafts

常用短参数:

1snow server -l 127.0.0.1:8088
2snow server -R
3snow server -r myblog
4snow server -c other.yaml
5snow server -D
6snow server -m publish

build

1snow build
2snow build --clean
3snow build --root-dir myblog
4snow build --output-dir dist
5snow build --config other.yaml
6snow build --dry-run
7snow build --debug
8snow build --mode publish
9snow build --include-drafts

--dry-run 会执行构建流程但不写入输出文件,适合排查配置和模板问题。

常用短参数:

1snow build -C
2snow build -r myblog
3snow build -o dist
4snow build -c other.yaml
5snow build -D
6snow build -m publish

hooks

查看已注册 Hook:

1snow hooks

输出会标记当前配置中启用的 Hook,例如:

1mount, snakecase, assets(enabled), pelican, rewrite, filter, encrypt(enabled), links(enabled), shortcode(enabled), minify, alias

目录结构

Snow 站点推荐结构如下:

 1mysite/
 2├── config.yaml
 3├── content/
 4│   ├── _index.md
 5│   ├── about.md
 6│   └── posts/
 7│       ├── _index.md
 8│       ├── hello.md
 9│       └── tutorials/
10│           └── _index.md
11├── static/
12├── templates/
13├── themes/
14│   └── snow/
15│       ├── theme.yaml
16│       ├── templates/
17│       ├── static/
18│       └── i18n/
19├── assets/
20├── data/
21└── i18n/

核心目录说明:

目录 说明
content/ 内容目录,存放 Markdown、Org-mode、HTML 页面
static/ 静态文件目录,原样复制到输出目录
templates/ 站点模板目录,优先于主题模板
themes/ 主题目录,theme 对应 themes/{name}/
assets/ assets hook 默认读取的资源目录
data/ 模板中 load_data 默认读取的数据目录
i18n/ 翻译文件目录
output/ 默认构建输出目录,可通过 output_dir 修改

内容识别规则:

文件/目录 识别为
*.md *.org *.html Page
_index.{md,org,html} Section 标记
index.{md,org,html} Page Bundle 标记
_* / .* 开头 默认忽略,_index.* 除外

包含 index.* 的目录会被视为 Page Bundle,目录里的图片等文件可以作为页面附属资源处理。

配置文件

Snow 使用 YAML 配置,默认读取站点根目录下的 config.yaml,也可以通过 --config 指定其它文件。

一个常用配置示例:

 1base_url: "http://127.0.0.1:8000"
 2title: "snow"
 3description: "snow is a static site generator."
 4author: "honmaple"
 5language: "zh"
 6output_dir: "output"
 7theme: "snow"
 8
 9modes:
10  publish:
11    base_url: "https://example.com"
12
13slugify:
14  lowercase: true
15  preserve_unicode: false
16
17markups:
18  _default:
19    style: "monokai"
20    show_toc: true
21    show_line_numbers: true
22    prevent_pre_code: true
23  markdown:
24    enabled: true
25    unsafe: false
26    directive_blocks: false
27  orgmode:
28    enabled: true
29  html:
30    enabled: false
31
32sections:
33  _default:
34    path: "{path:slug}/"
35    sort_by: "date desc"
36    paginate: 0
37  posts:
38    sort_by: "date desc"
39    paginate: 10
40  pages:
41    path: ""
42
43pages:
44  _default:
45    path: "{path:slug}/{slug}/"
46  posts:
47    path: "posts/{date:%Y}/{date:%m}/{slug}.html"
48  drafts:
49    draft: true
50
51taxonomies:
52  _default:
53    path: "{taxonomy}/"
54    sort_by: "name"
55    term:
56      path: "{taxonomy}/{term:slug}/"
57      sort_by: "date desc"
58  tags:
59  categories:
60  authors:
61
62hooks:
63  assets:
64    enabled: true
65  encrypt:
66    enabled: true
67  links:
68    enabled: true
69  shortcode:
70    enabled: true
71  minify:
72    enabled: false

新版配置已经不再使用旧版的 site.urlsite.titletheme.namemode.publishbuild_filter 这些写法。站点信息现在是扁平字段,例如 base_urltitleauthor;多环境配置统一放在 modes 下。

内容管理

Snow 的内容系统围绕三个核心概念:

概念 说明
Page 单篇文章或页面
Section 栏目或目录树
Taxonomy 标签、分类、作者、时间归档等分类系统

Page

Page 是最基本的内容单元。可以直接在 content/ 下创建 .md.org.html 文件。

1content/
2├── about.md
3└── posts/
4    ├── hello.md
5    └── bundle/
6        ├── index.md
7        └── image.png

常见 FrontMatter:

 1---
 2title: "文章标题"
 3slug: "custom-slug"
 4date: 2024-01-15 20:35:00
 5modified: 2024-02-01 10:00:00
 6draft: false
 7hidden: false
 8render: true
 9path: "custom/url/"
10template: "custom-post.html"
11aliases:
12  - "/old-url/"
13tags:
14  - go
15  - web
16categories:
17  - Programming/Go
18---

Org-mode 可以使用类似写法:

1#+TITLE: 文章标题
2#+DATE: 2024-01-15 20:35:00
3#+PROPERTY: MODIFIED 2024-02-01 10:00:00
4#+PROPERTY: TAGS go,web
5#+CATEGORY: Programming/Go

Page 配置示例:

 1pages:
 2  _default:
 3    path: "{path:slug}/{slug}/"
 4  posts:
 5    path: "articles/{date:%Y}/{date:%m}/{slug}.html"
 6  pages:
 7    hidden: true
 8    template: "page.html"
 9  drafts:
10    draft: true

路径变量:

变量 说明
{date:%Y}
{date:%m}
{date:%d}
{date:%H}
{lang} 语言代码
{lang:optional} 默认语言时为空
{path} 文件所在路径
{path:slug} slug 化后的路径
{slug} 页面 slug
{title} 页面标题

构建默认跳过 draft: true 的页面,需要包含草稿时使用:

1snow build --include-drafts
2snow server --include-drafts

Section

包含 _index.{md,org,html} 的目录会被识别为 Section。

1content/
2├── _index.md
3└── posts/
4    ├── _index.md
5    ├── article1.md
6    └── tutorials/
7        ├── _index.md
8        └── intro.md

_index.md 中的 FrontMatter 可以控制 Section:

1---
2title: "文章列表"
3sort_by: "date desc"
4paginate: 10
5template: "custom-section.html"
6---
7这里是栏目描述,可以在模板中通过 section.Content 输出。

Section 配置示例:

 1sections:
 2  _default:
 3    path: "{path:slug}/"
 4    sort_by: "date desc"
 5    paginate: 0
 6  posts:
 7    sort_by: "date desc"
 8    paginate: 10
 9    template: "section.html"
10  pages:
11    path: ""

配置查找顺序为 sections.{目录路径} -> 父目录 -> sections._default

常用模板变量:

变量 说明
section.Title 栏目标题
section.Content _index.* 正文
section.Path 相对链接
section.Permalink 绝对链接
section.Pages 当前栏目页面
section.HiddenPages 当前栏目隐藏页面
section.Children 子栏目
section.Parent 父栏目
section.AllPages() 当前栏目及子栏目所有普通页面

Taxonomy

Taxonomy 根据 Page FrontMatter 字段自动生成分类页面。常见字段是 tagscategoriesauthors

 1taxonomies:
 2  _default:
 3    path: "{taxonomy}/"
 4    sort_by: "name"
 5    term:
 6      path: "{taxonomy}/{term:slug}/"
 7      sort_by: "date desc"
 8  tags:
 9  categories:
10  authors:

页面中使用:

1---
2tags:
3  - go
4  - web
5categories:
6  - Programming/Go
7authors: honmaple
8---

Programming/Go 会生成层级分类,Programming 是父 Term,Go 是子 Term。

时间归档也可以通过 Taxonomy 实现:

1taxonomies:
2  "date:2006/01":
3    sort_by: "name desc"
4    path: "archives/"
5    template: "archives.html"
6    term:
7      path: "archives/{term}/"
8      template: "period_archives.html"

Pagination

Snow 支持 Section 和 Taxonomy Term 分页。

 1sections:
 2  posts:
 3    paginate: 10
 4    paginate_path: ""
 5    paginate_filter_by: ""
 6
 7taxonomies:
 8  tags:
 9    term:
10      paginate: 10
11      paginate_filter_by: ""

paginate_path 为空时,Snow 会根据输出路径自动选择:

  • pretty 路径,如 posts/,后续页默认类似 posts/page/2/

  • ugly 路径,如 posts.html,后续页默认类似 posts2.html

分页模板变量:

变量 说明
paginator.Path 当前分页相对链接
paginator.Permalink 当前分页绝对链接
paginator.PageNum 当前页码
paginator.Total 总页数
paginator.Pages 当前分页页面列表
paginator.HasPrev() 是否有上一页
paginator.Prev.Path 上一页链接
paginator.HasNext() 是否有下一页
paginator.Next.Path 下一页链接

输出格式

Snow 可以为 Section、Page、Taxonomy Term 生成 RSS、Atom、JSON 等格式。

全局模板默认值:

1formats:
2  rss:
3    template: "partials/rss.xml"
4  atom:
5    template: "partials/atom.xml"

Section 中配置:

 1---
 2formats:
 3  rss:
 4    path: "posts/index.xml"
 5  atom:
 6    path: "posts/atom.xml"
 7  json:
 8    path: "posts/index.json"
 9    template: "custom.json"
10---

Page 中配置:

1---
2formats:
3  json:
4    path: "api/articles/hello.json"
5    template: "article.json"
6---

path 为空时会禁用该格式。

多语言

全局默认语言:

1language: "zh"
2
3languages:
4  en:
5    translations: "i18n/en.yaml"
6  fr:
7    translations:
8      - id: "tags"
9        tr: "Tags"

页面语言可以通过文件名后缀识别:

1hello.zh.md
2hello.en.md

也可以在 FrontMatter 中指定:

1---
2lang: en
3---

模板中使用翻译函数:

1{{ i18n("tags") }}
2{{ T("articles %d", 12) }}
3{{ _("authors") }}

模板

Snow 使用 Pongo2 模板语法,接近 Django/Jinja2。

一个简单页面模板:

 1<!DOCTYPE html>
 2<html lang="{{ page.Lang }}">
 3  <head>
 4    <meta charset="utf-8">
 5    <title>{{ page.Title }} - {{ config.title }}</title>
 6  </head>
 7  <body>
 8    <article>
 9      <h1>{{ page.Title }}</h1>
10      <time>{{ page.Date | date:"2006-01-02" }}</time>
11      <div>{{ page.Content | safe }}</div>
12    </article>
13  </body>
14</html>

常用内容函数:

函数 说明
pages 当前语言全部普通页面
hidden_pages 当前语言隐藏页面
sections([lang]) 栏目列表
taxonomies([lang]) 分类列表
get_page(path, [lang]) 获取页面对象
get_section(path, [lang]) 获取栏目对象
get_taxonomy(name, [lang]) 获取分类对象
get_taxonomy_term(taxonomy, name, [lang]) 获取分类项
dict(k, v, ...) 构造 map
slice(v, ...) 构造列表
load_data(path, format) data/ 或 URL 加载数据
newScratch() 创建临时模板存储

页面列表支持排序、分组、过滤和截取:

 1{% for page in pages.OrderBy("date desc, title asc").Limit(10) %}
 2<li><a href="{{ page.Path }}">{{ page.Title }}</a></li>
 3{% endfor %}
 4
 5{% for group in pages.GroupBy("date:2006-01").OrderBy("name desc") %}
 6<h2>{{ group.Name }}</h2>
 7{% for page in group.Pages %}
 8<li>{{ page.Title }}</li>
 9{% endfor %}
10{% endfor %}

常用过滤器:

过滤器 说明
truncate:N 截取文本
date:"layout" Go 时间格式
lower / upper 大小写转换
split:sep 分割字符串
encrypt:"pw" 内容加密
markdown / org 字符串转 HTML
parser:"yaml" 解析 YAML/TOML/JSON 字符串
jsonify 转 JSON 字符串
absURL / relURL URL 转换

如果启用 snakecase hook,模板执行前会把 Go 结构体的导出字段和方法转换成 snake_case map,便于使用 page.titlepage.get_xxx() 这种命名。

主题

主题通过 theme 配置启用:

1theme: "mytheme"

对应目录:

 1themes/mytheme/
 2├── theme.yaml
 3├── templates/
 4│   ├── index.html
 5│   ├── page.html
 6│   ├── section.html
 7│   ├── taxonomy_list.html
 8│   ├── taxonomy_single.html
 9│   └── shortcodes/
10├── static/
11└── i18n/

模板查找优先级:

  1. 站点 templates/

  2. 主题 templates/

  3. 内置默认主题 templates/

静态资源优先级:

  1. 站点 static/

  2. 主题 static/

主题根目录下的 theme.yaml 会自动合并到站点配置,但不会覆盖站点已经设置的项。

Hooks 插件

Snow 的插件系统通过 Hook 在构建流程中插入逻辑。默认启用 assetsencryptlinksshortcode

Hook 配置示例:

 1hooks:
 2  assets:
 3    enabled: true
 4  encrypt:
 5    enabled: true
 6    weight: 50
 7    option:
 8      password: "123456"
 9  links:
10    enabled: true
11  shortcode:
12    enabled: true
13  minify:
14    enabled: false
15    option:
16      html: true
17      css: true
18      js: true

内置 Hook:

Hook 默认启用 说明
mount 把本地文件或目录挂载到虚拟文件系统
snakecase 模板上下文 snake_case 访问
assets 静态资源处理
pelican Pelican 迁移兼容处理
rewrite FrontMatter 重写
filter 页面筛选
encrypt 内容加密
links 内容链接处理
shortcode 短代码
minify 输出压缩
alias 页面别名输出

weight 越小越先执行;权重相同时按 Hook 名称排序。显式启用一个未注册的 Hook 会导致初始化失败。调试模式会输出实际启用顺序:

1Enabled hooks: assets(20), encrypt(50), links(55), shortcode(60)

shortcode

在内容中插入可复用组件:

1<shortcode youtube id="xxx" />
2
3<shortcode code lang="python">
4print("hello")
5</shortcode>

Shortcode 模板放在 templates/shortcodes/,可用变量包括 paramsbodynamecounter

encrypt

模板中可以通过过滤器加密内容:

1{{ page.Content | encrypt:"123456" }}

assets

模板中使用 assets 块处理 CSS、JS、SCSS 等资源:

1{% assets files="scss/style.scss" sass_compiler="dartsass" filters="cssmin" output="css/style.min.css" %}
2<link rel="stylesheet" href="{{ asset_url }}">
3{% endassets %}

也可以引用配置里的资源组:

1{% assets css %}
2<link rel="stylesheet" href="{{ config.base_url }}/{{ asset_url }}">
3{% endassets %}

dartsass 需要本机 sass 可执行文件在 PATH 中可用。

mount

mount hook 可以把站点外部的文件或目录挂载到 Snow 的虚拟文件系统中。适合把另一个目录里的内容、资源或模板接入当前站点。

1hooks.mount:
2  enabled: true
3  option:
4    - source: "../snow/docs/content"
5      target: "content/snow"
6      strategy: "base"
7    - source: "../golang/cloudfs/docs"
8      target: "content/cloudfs"
9      strategy: "base"

从旧版配置迁移

如果你使用的是旧版 Snow 文档里的配置,可以按下面规则迁移:

旧配置 新配置
site.url base_url
site.title title
site.subtitle description
site.author author
site.language language
mode.publish modes.publish
theme.name theme
sections.*.orderby sections.*.sort_by
sections.*.page_path pages.*.path
sections.*.page_template pages.*.template
taxonomies.*.term_path taxonomies.*.term.path
taxonomies.*.term_template taxonomies.*.term.template
build_filter / --filter 使用 drafthiddenfilter hook 或分页筛选配置
statics 使用固定 static/ 目录,或启用 assets / mount
hooks: ["assets"] hooks.assets.enabled: true

旧版 sofile 动态插件方式已经不再作为推荐用法。新增 parser 或 hook 时,推荐在源码中实现接口,在 init() 中注册,并在 CLI 入口通过空白导入挂载。

本地测试与正式发布

本地开发:

1snow server --autoload --debug

包含草稿预览:

1snow server --autoload --include-drafts

发布构建:

1snow build --mode publish --clean

发布配置:

 1site:
 2  url: "http://127.0.0.1:8000"
 3  output_dir: "output"
 4
 5mode.publish:
 6  site:
 7    url: "https://example.com"
 8    output_dir: "xxx"
 9
10mode.develop:
11  include: "develop.yaml"

完整文档可以在 Snow文档 查看