Snow静态博客生成器
一个简单却可配置的静态博客生成器。 很早之前(三年前)就想写一个静态博客生成器,但苦于一直没有时间,最近把之前写的重构了一下,让其可以支持更多的定制化配置。
至于为什么要重新写一个:
-
大概是因为想要把之前未完成的轮子补充完整
-
我目前使用的博客系统使用的是 pelican,一个基于 Python 的静态博客生成器,目前里面包括了很多我自己写的插件,比如
Emacs org mode的支持、文章加密、模版定制等,因为需要大量遍历所有文章,导致生成速度越来越慢,还有一点就是因为使用的是 Python,每次本地预览时都需要切换到虚拟环境 -
我习惯使用 Emacs + Org,除了前期的几篇文章,后面都是使用 org mode 书写,之前是因为 Python 没有一个好用的 org mode 解析库,所以专门写了一个
org-python用来解析 org mode;最近我也是完善了另一个我很早之前就写的org-golang解析库(轮子+1),准备趁次机会利用一下这个库 -
至于为什么不用最近几年流行的 Hugo, 因为我想要保持和我使用 Pelican 时一样的功能,比如文章加密,而 hugo 并不支持插件,想要自定义插件必须复制大段大段的启动函数, 甚至需要修改源代码。同样我想要定制一个相同的模版,而对于使用过其它模版系统如Django,jinja2, 再来使用 Go内置模版 的人来说, hugo 的内置模版除了难用就是难用,这也是我此次选用 pongo2 的原因
-
我的设想是提供插件的接口,并提供一个足够简单的启动函数, 在有用户需要自定义插件时只需要自己创建一个包, 使用三两行代码就能注册自定义插件并重新编译自己的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 常用命令包括 init、server、build 和 hooks。
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.url、site.title、theme.name、mode.publish、build_filter 这些写法。站点信息现在是扁平字段,例如 base_url、title、author;多环境配置统一放在 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 字段自动生成分类页面。常见字段是 tags、categories、authors。
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.title、page.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/
模板查找优先级:
-
站点
templates/ -
主题
templates/ -
内置默认主题
templates/
静态资源优先级:
-
站点
static/ -
主题
static/
主题根目录下的 theme.yaml 会自动合并到站点配置,但不会覆盖站点已经设置的项。
Hooks 插件
Snow 的插件系统通过 Hook 在构建流程中插入逻辑。默认启用 assets、encrypt、links、shortcode。
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/,可用变量包括 params、body、name、counter。
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 |
使用 draft、hidden、filter 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文档 查看
