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
快速开始
开始(Quickstart)
创建新的站点
1──╼ ./snow init 2Welcome to snow 0.1.0. 3> Where do you want to create your new web site? [.] mysnow 4> What will be the title of this web site? [snow] 5> Who will be the author of this web site? 6The input is required 7> Who will be the author of this web site? honmaple 8> What is your URL prefix? (no trailing slash) [http://127.0.0.1:8000] 9> Do you want to create first page? [Y/n]
编译和预览
1└──╼ cd mysnow 2└──╼ ../snow server -D 3DEBU Copying @theme/static/css/main.css to output/static/css/main.css 4INFO Done: Static Processed 1 static files in 588.705µs 5DEBU Writing output/categories/index.html 6DEBU Writing output/authors/index.html 7DEBU Writing output/tags/index.html 8DEBU Writing output/posts/index.html 9DEBU Writing output/authors/snow/index.html 10DEBU Writing output/tags/snow/index.html 11DEBU Writing output/categories/linux/index.html 12DEBU Writing output/tags/linux/index.html 13DEBU Writing output/tags/emacs/index.html 14DEBU Writing output/categories/linux/emacs/index.html 15INFO Done: Page Processed 1 normal pages, 0 hidden pages, 0 section pages in 10.087804ms 16INFO Done: Section Processed 1 posts in 10.1831ms 17INFO Done: Taxonomy Processed 1 authors, 3 tags, 1 categories in 10.18788ms
安装(Installation)
1└──╼ git install https://github.com/honmaple/snow
编译(Build)
1└──╼ git clone https://github.com/honmaple/snow --depth=1 2└──╼ cd snow 3└──╼ go mod tidy 4└──╼ go build .
命令行(Cli usage)
1└──╼ ./snow --help 2NAME: 3 snow - snow is a static site generator. 4 5USAGE: 6 snow [global options] command [command options] [arguments...] 7 8VERSION: 9 0.1.0 10 11COMMANDS: 12 init init a new site 13 build build and output 14 server server local files 15 help, h Shows a list of commands or help for one command 16 17GLOBAL OPTIONS: 18 --config FILE, -c FILE load configuration from FILE (default: "config.yaml") 19 --help, -h show help (default: false) 20 --version, -v print the version (default: false)
init
1└──╼ ./snow init 2└──╼ ./snow init myblog
如果不指定 myblog 目录,默认会在当前目录下生成一个 config.yaml 文件和一个 content 目录
build
该命令会构建站点内容内写入到 {output_dir} 目录, 如果该目录已经有文件存在,除非制定 -C 参数,否则不会自动清理
-
清理输出目录
1└──╼ ./snow build --clean 2└──╼ ./snow build -C
-
显示输出详情
1└──╼ ./snow build --debug 2└──╼ ./snow build -D
-
指定输出目录
1└──╼ ./snow build --output {output_dir} 2└──╼ ./snow build -o {output_dir}
-
指定mode
1└──╼ ./snow build --mode {mode} 2└──╼ ./snow build -m {mode}
-
筛选页面
1└──╼ ./snow build --filter {build_filter} 2└──╼ ./snow build -F {build_filter}
-
显示所有hooks
1└──╼ ./snow build --hooks
server
build 支持的命令 server也同样支持, 除此之外,还有
-
指定监听地址
1└──╼ ./snow server --listen 127.0.0.1:8088 2└──╼ ./snow server -l 127.0.0.1:8088
默认监听地址是
site.url -
监听文件修改并重新构建
1└──╼ ./snow server --autoload 2└──╼ ./snow server -r
目录结构(Driectory structure)
1. 2├── config.yaml 3├── content 4│ └── posts 5│ └── first-page.md 6├── static 7├── templates 8└── themes 9│ └── snow 10│ └── static 11│ └── template
-
config.yaml: 使用的配置文件
-
content: 包括所有的页面内容, 比如
.md,.org等,如果一个子目录包括index.{md,org}文件,那么这个目录将会成为一个页面,否则每一个子目录都是一个 section, 同样的,子目录下_index.{md,org}文件也是该 section 的配置文件 -
static:
statics指定的静态文件或目录,名称可修改 -
layouts: 主题模版覆盖目录
theme.override指定的主题覆盖文件,比如有一个主题模版{theme}/templates/post.html, 当指定了override目录后就可以在该目录创建一个同样名称为post.html的文件进行覆盖 -
themes: 主题目录, 该目录下包括的子目录就是主题名称,可以在
theme.name里指定
配置文件(Configuration)
1# 站点配置信息 2site: 3 url: "http://127.0.0.1:8000" 4 title: "snow" 5 subtitle: "Snow is a static generator." 6 language: "zh" 7 author: "honmaple" 8 9# 发布时使用的配置 10mode.publish: 11 site: 12 url: "https://honmaple.me" 13 14output_dir: "output" 15content_dir: "content" 16build_filter: "not draft" 17 18theme: 19 name: "snow" 20 21# 按照主题需要进行配置 22params.extra: 23 menus: 24 - name: "关于" 25 url: "/pages/about.html"
内容管理
Section
1content/ 2├── pages // no url, because sections.pages.path is "" 3│ └── about // <- http://127.0.0.1:8000/pages/about.html 4│ └── index.org // no url 5│ └── contact.org // <- http://127.0.0.1:8000/pages/contact.html 6└── posts // <- http://127.0.0.1:8000/posts/index.html 7 ├── post1.org // <- http://127.0.0.1:8000/posts/2022/02/post1.html 8 └── subposts // <- http://127.0.0.1:8000/posts/subposts/index.html 9 └── post2.org // <- http://127.0.0.1:8000/posts/2023/02/post2.html
配置
1sections: 2 _default: 3 # 页面默认排序, 多字段使用逗号分隔 4 orderby: "date desc" 5 # 自定义某个section下的页面筛选 6 filter: "" 7 # 页面默认分页, path必须使用{number}变量, 0表示不分页 8 paginate: 10 9 # 分页路径 10 paginate_path: "{name}{number}{extension}" 11 # 分页前筛选pages 12 paginate_filter: "" 13 # 生成路径, 为空表示禁止生成相关页面 14 path: "{section}/index.html" 15 # 使用的模版 16 template: "section.html" 17 # 当前section下所有页面生成路径 18 page_path: "{section}/{slug}/index.html" 19 # 页面使用的模版 20 page_template: "post.html" 21 formats.atom: 22 path: "{section:slug}/atom.xml" 23 posts: 24 page_path: "posts/{date:%Y}/{date:%m}/{slug}.html" 25 pages: 26 path: "" 27 pages/about: 28 # 自定义pages/about下的页面生成路径,同时继承pages.path不会生成所有页面 29 page_path: "{slug}/index.html"
filter 格式(下同):
1'emacs' in tags and not draft or weight > 1
其中 tags, draft 等都是page元数据
路径变量(sections.xxx.path)
| 变量 | 描述 |
|---|---|
| {section} | section名称 |
| {section:slug} | section slug, 中国 -> zhong-guo |
模版变量(sections.xxx.template)
| 变量 | 描述 |
|---|---|
| section | |
| section.Title | section标题 |
| section.Path | section相对链接 |
| section.Permalink | section绝对链接 |
| section.Content | section内容 |
| section.Pages | 当前section下的页面列表 |
| section.Children | 子section |
| section.Parent | 父section |
页面(Page)
元数据
-
markdown
1--- 2title: "title" 3categories: 4 - Snow/Templates 5tags: 6 - linux 7 - snow 8---
-
orgmode
1#+TITLE: title 2#+DATE: 2022-02-26 17:14:46 3#+CATEGORIES: Snow/Templates 4#+PROPERTY: TAGS linux,snow 5#+PROPERTY: MODIFIED 2023-02-26 14:35:37
-
html
1<head> 2 <title>Project</title> 3 <meta name="categories" content="Snow/Templates" /> 4 <meta name="tags" content="linux,snow" /> 5 <meta name="date" content="2015-12-22" /> 6</head>
配置
1# 页面目录所在, 其中该目录下应该包括一系列子目录,这些子目录的名称对应为 *页面的类型*, 比如 *content/drafts/* 目录下的 页面类型为 *drafts*, 当然也可以直接在 页面文件头添加 =type: drafts= 2content_dir: "content"
路径变量(sections.xxx.page_path)
| 变量 | 描述 |
|---|---|
| {date:%Y} | 创建页面的年份 |
| {date:%m} | 创建页面的月份 |
| {date:%d} | 创建页面的日期 |
| {date:%H} | 创建页面的小时 |
| {lang} | 页面语言 |
| {slug} | 页面标题或自定义slug |
| {filename} | 文件名称(不带后缀名) |
模版变量(sections.xxx.page_template)
| 变量 | 描述 |
|---|---|
| page | |
| page.Title | 页面标题 |
| page.Lang | 页面语言 |
| page.Date | 页面创建时间 |
| page.Modified | 页面修改时间 |
| page.Aliases | 页面其它链接 |
| page.Path | 页面相对链接 |
| page.Permalink | 页面绝对链接 |
| page.Summary | 页面简介 |
| page.Content | 页面内容 |
| page.Meta.xxx | 自定义的元数据 |
| page.Prev | 上一篇 |
| page.Next | 下一篇 |
| page.HasPrev() | 是否有上一篇 |
| page.HasNext() | 是否有下一篇 |
| page.PrevInType | 同一类型上一篇 |
| page.NextInType | 同一类型下一篇 |
| page.HasPrevInType() | 是否有同一类型上一篇 |
| page.HasNextInType() | 是否有同一类型下一篇 |
分类系统(Taxonomy)
配置
1taxonomies: 2 _default: 3 path: "{taxonomy}/index.html" 4 # terms排序, 可选name,count 5 orderby: "" 6 template: "{taxonomy}/list.html" 7 term_path: "{taxonomy}/{term:slug}/index.html" 8 term_template: "{taxonomy}/single.html" 9 # 页面列表筛选 10 term_filter: "" 11 # 页面列表排序 12 term_orderby: "date desc" 13 # 页面列表分页 14 term_paginate: 0 15 term_paginate_path: "" 16 term_paginate_filter: "" 17 categories: 18 authors: 19 tags:
路径变量
-
taxonomies.xxx.path
变量 描述 {taxonomy} 分类系统名称 -
taxonomies.xxx.term_path
变量 描述 {taxonomy} 分类系统名称 {term} 分类具体名称 {term:slug} 分类slug
模版变量
-
taxonomies.xxx.template
变量 描述 taxonomy taxonomy.Name 分类系统名称, 如:categories,tags,authors taxonomy.Terms -
taxonomies.xxx.term_template
变量 描述 term term.Name 分类名称 term.Path 相对链接 term.Permalink 绝对链接 term.List 页面列表 term.Children 子分类
归档页(Archive)
snow 中的分类系统是基于归档实现的,该功能类似 SQL 中的 group by, 所以如果要实现归档页可以有两种方式:
-
添加
taxonomies.{key},{key}可以是页面元数据里的任意字段, 比如categories,tags, 如果需要按照时间归档, 格式为date:2006/01, 其中2006/01为Go时间格式,表示按年月归档, 并生成链接 /archives/2022/10/index.html1taxonomies: 2 date:2006/01: 3 path: "archives/index.html" 4 template: "archives.html" 5 term_path: "archives/{term}/index.html" 6 term_template: "period_archives.html"
-
在
{content_dir}下添加一个archives.md的文件1path: archives.html 2template: archives.html 3section: true
然后在模板
{templates}/archives.html使用pages.GroupBy({key})1{%- for subterm in pages.GroupBy("date:2006-01").OrderBy("name desc") %} 2 {%- set date = subterm.Name | split:"-" %} 3 {%- set year = date[0] %} 4 {%- set month = date[1] %} 5 ... 6{%- endfor %}
分页(Pagination)
路径变量
| 变量 | 描述 |
|---|---|
| {name} | 路径名称 |
| {extension} | 路径扩展 |
| {number} | 页码, 第一页为空 |
| {number:one} | 页码, 第一页为"1" |
-
示例一:
1path: "section/index.html" 2paginate_path: "{name}{number}{extension}"
-
第一页:
section/index.html -
第二页:
section/index2.html -
第三页:
section/index3.html
-
-
示例二:
1path: "section/index.html" 2paginate_path: "page/{number:one}{extension}"
-
第一页:
section/page/1.html -
第二页:
section/page/2.html -
第三页:
section/page/3.html
-
模版变量
| 变量 | 描述 |
|---|---|
| paginator | |
| paginator.URL | 分页链接 |
| paginator.PageNum | 当前页 |
| paginator.Total | 总页数 |
| paginator.HasPrev() | 是否有上一页 |
| paginator.Prev | 上一页 |
| paginator.Prev.URL | 上一页链接 |
| paginator.HasNext() | 是否有下一页 |
| paginator.Next | 下一页 |
| paginator.Next.URL | 下一页链接 |
| paginator.All | 所有页 |
| paginator.List | 当前分页下的页面列表 |
草稿(Draft)
使用者可以自定义草稿标志,但推荐使用两种形式:
-
添加元数据
draft: true, 构建时增加筛选条件-
草稿
1snow build --filter 'draft = true' -
非草稿
1snow build -F 'not draft'
-
-
创建一个单独的
drafts目录存放草稿-
草稿
1snow build -F 'type = "drafts"' -
非草稿
1snow build -F 'type != "drafts"'
-
注: 默认筛选条件可以写入配置 build_filter
输出格式(Atom,Rss,JSON)
可以生成 rss ,atom 或者其它任意格式(需要自定义模版)
配置
1# 设置rss格式的默认值 2formats.rss: 3 template: "_internal/rss.xml" 4 5formats.atom: 6 template: "_internal/atom.xml" 7 8sections: 9 _default: 10 # rss生成路径, 模版将会使用默认模版 11 formats.rss.path: "{section:slug}/index.xml" 12 # 为空时禁止生成 13 formats.atom.path: "" 14 15taxonomies: 16 tags: 17 formats.atom: 18 path: "tags/{term:slug}/index.xml" 19 # 自定义模版 20 template: "custom.atom.xml"
模版变量
| 变量 | 描述 |
|---|---|
| section | 仅生成section 有效 |
| term | 仅生成taxonomy term 有效 |
| pages | 页面列表 |
静态文件(Static)
静态文件分 主题静态文件 和 配置指定的静态文件
主题静态文件
1├── themes 2│ └── snow 3│ └── static 4│ └── main.css
主题目录下的所有文件默认会复制到 output 目录, 除非设置 statics.@theme/static.path 为空
指定的静态文件
该文件需要在配置指定
1statics: 2 # 根目录下static目录下的文件将会拷贝到{output_dir}/static 3 static: 4 # 拷贝的路径, 为空时表示不写入, 如果以"/"结尾, 表示拷贝到该目录 5 # static -> {output_dir}/static 6 # static/ -> {output_dir}/static/static 7 path: "/" 8 # 指定扩展,不配置将会使用目录下的所有文件 9 exts: 10 - ".js" 11 - ".css" 12 # 如果指定的静态文件是一个目录,可以设置忽略文件, 比如忽略static目录下的images子目录 13 ignore_files: 14 - "^images/" 15 # 以@theme/开头表示主题目录, 以@theme/_internal/开头表示内置的主题目录 16 @theme/static: 17 path: "static" 18 @theme/_internal/static: 19 path: "static" 20 # 同样可以指定任意静态文件或目录 21 content/pages/css: 22 path: "static/css"
多语言(Multilingual)
需要配置 languages
1languages.en: 2 translations: "i18n/en.yaml" 3languages.fr: 4 translations: "i18n/fr.yaml"
页面格式:
-
{title}.en.md -
{title}.fr.md
或者可以在文件头指定 lang: en
模版(templates)
https://github.com/flosch/pongo2
主题(theme)
安装
开发
主题目录结构
其中 templates 和 static 名称不可修改
1simple/ 2├── templates 3│ ├── post.html 4│ ├── index.html 5│ ├── archives.html 6├── static 7│ ├── main.css
配置
1theme: 2 # 主题名称, 未设置将使用默认主题 3 name: "test-theme" 4 # 主题模版覆盖, 增加同名的文件到 *override* 配置的目录, snow将会优先使用该文件 5 override: "layouts"
TODO默认配置
插件(hooks)
1hooks: 2 - "i18n" 3 - "assets" 4 - "encrypt" 5 - "shortcode"
i18n
-
模版
1{% i18n "tags" %} 2{% T "tags %d" 12 %} 3{{ i18n("authors") }} 4{{ T("authors") }} 5{{ _("authors %f", 3.14) }}
甚至可以直接使用变量 {{ _(term.Name) }}
-
翻译文件 默认会加载主题下 i18n 目录下的文件
1i18n 2├── en.yaml 3└── zh.yaml
文件内容
1--- 2- id: "authors" 3 tr: "作者" 4- id: "tags" 5 tr: "标签"
也可以自定义文件位置或翻译内容覆盖主题原有的翻译
1languages.en: 2 translations: "i18n/en.yaml" 3languages.zh: 4 translations: 5 - id: "authors" 6 tr: "作者"
encrypt
内容加密, 需要一个密码
1{{ page.Content | encrypt:"123456" }}
shortcode
用于快速插入已有模版, 示例:
1<shortcode _name="encrypt" password="1234567"> 2hello *markdown* 3</shortcode> 4 5<shortcode _name="gist" author="spf13" id="7896402" />
可以自定义 shortcode 到主题的 templates/shortcodes 目录下, 目前内置 gist, encrypt
assets
静态文件处理
1params.assets: 2 css: 3 files: 4 - "@theme/static/scss/main.scss" 5 - "@theme/static/scss/entry.scss" 6 filters: 7 - libscss: 8 path: ["@theme/static/scss/"] 9 - cssmin: 10 output: "static/lib.min.css"
1{% assets files="css/style.scss" filters="libsass,cssmin" output="css/style.min.css" %} 2<link rel="stylesheet" href="{{ config.site.url }}/{{ asset_url }}"> 3{% endassets %} 4 5{% assets css %} 6<link rel="stylesheet" href="{{ config.site.url }}/{{ asset_url }}"> 7{% endassets %}
sofile
sofile 允许使用Go的 Plugin 系统支持自定义插件
-
创建一个
sofile.go的文件1package main 2 3import ( 4 "fmt" 5 6 "github.com/honmaple/snow/builder/hook" 7 "github.com/honmaple/snow/builder/page" 8 "github.com/honmaple/snow/builder/theme" 9 "github.com/honmaple/snow/config" 10) 11 12type testHook struct { 13 hook.BaseHook 14} 15 16func (testHook) Name() string { 17 return "test" 18} 19 20func (testHook) AfterPageParse(page *page.Page) *page.Page { 21 fmt.Println(page.Title) 22 return page 23} 24 25func NewHook(conf config.Config, theme theme.Theme) hook.Hook { 26 return &testHook{} 27}
-
编译为so文件
1go build -buildmode=plugin sofile.go -
注册插件
1hooks: 2 - "sofile" 3params.sofiles: 4 - "sofile.so"
本地测试和正式发布
snow 提供了 mode 配置用于区分本地测试和正式发布
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 build --mode publish 即可覆盖本地默认配置
