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