快速导航
一个简单却可配置的静态博客生成器。 很早之前(三年前)就想写一个静态博客生成器,但苦于一直没有时间,最近把之前写的重构了一下,让其可以支持更多的定制化配置。
至于为什么要重新写一个:
大概是因为想要把之前未完成的轮子补充完整
我目前使用的博客系统使用的是 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 即可覆盖本地默认配置
知识共享署名-非商业性使用-相同方式共享4.0国际许可协议