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

快速开始

开始(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, 所以如果要实现归档页可以有两种方式:

  1. 添加 taxonomies.{key}, {key} 可以是页面元数据里的任意字段, 比如 categories, tags, 如果需要按照时间归档, 格式为 date:2006/01, 其中 2006/01 为Go时间格式,表示按年月归档, 并生成链接 /archives/2022/10/index.html

    taxonomies:
      date:2006/01:
        path: "archives/index.html"
        template: "archives.html"
        term_path: "archives/{term}/index.html"
        term_template: "period_archives.html"
  2. {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)

使用者可以自定义草稿标志,但推荐使用两种形式:

  1. 添加元数据 draft: true, 构建时增加筛选条件

    • 草稿

      snow build --filter 'draft = true'
    • 非草稿

      snow build -F 'not draft'
  2. 创建一个单独的 drafts 目录存放草稿

    • 草稿

      snow build -F 'type = "drafts"'
    • 非草稿

      snow build -F 'type != "drafts"'

注: 默认筛选条件可以写入配置 build_filter

输出格式(Atom,Rss,JSON)

可以生成 rssatom 或者其它任意格式(需要自定义模版)

配置

# 设置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)

安装

开发

主题目录结构

其中 templatesstatic 名称不可修改

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 即可覆盖本地默认配置

作者: honmaple
链接: https://honmaple.me/articles/2022/10/Snow静态博客生成器.html
版权: CC BY-NC-SA 4.0 知识共享署名-非商业性使用-相同方式共享4.0国际许可协议
wechat
alipay

加载评论