Warning: Winter is in early alpha and so is this documentation. You may find inconsistencies or pieces of code present in twos.dev that have not yet been migrated to Winter.
Winter
Winter is the bespoke static website generator that powers twos.dev. It can power your static website as well, either as a CLI tool or Go library. Winter is strongly opinionated to serve my pecularities around building twos.dev.
On the surface, Winter is similar to other static website generators. It can take a directory of files of varying content types—Markdown, Org, or HTML—apply some templating, and render them to HTML files ready for deployment to a static web server.
Winter is different because of its unique design goals.
Winter's design goals are to allow content to be both easy to edit and hard to break. These goals work against each other by default, so Winter splits content into two modes: warm and cold.
Warm content should be synchronized into the project directory from an
outside tool of your choice. Generally this is a writing program such as iA
Writer hooked up to a shared or cloud directory;
simplistically, it can be a cron job that runs rsync
.
Cold content should not be touched by automated tools. This content must be
preseved for years or decades, so fewer moving parts is better. When a
piece of warm content is stable, it can be "frozen" into cold content using
winter freeze
.
Installation
1# CLI
2go install twos.dev/winter@latest
3winter --help
4
5# Go library
6go get -u twos.dev/winter@latest
Documentation
This section details the winter
CLI documentation. For Go library
documentation, see
pkg.go.dev/twos.dev/winter.
Directory Structure
./src
—Content to be built./src/cold
—Stable content, optionally templated*.md
—Markdown files*.html
—HTML files*.org
—Org mode files
./src/warm
—Unstable content, optionally templated*.md
—Markdown files*.html
—HTML files*.org
—Org mode files
./src/templates
—Reusable contenttext_document.html.tmpl
—Default page containerimgcontainer.html.tmpl
—Gallery container*.html
—HTML templates
./src/img
—Gallery images...
—Any directory structure
./public
—Static files copied directly to the build directory./dist
—Build directory
Commands
Pass --help
to any command to see detailed usage and behavior.
winter init
Usage: winter init
Initialize the current directory for use with Winter. The Winter directory
structure detailed above will be created, and default starting templates will
be populated so that you have a working index.html
listing posts.
winter build
Usage: winter build [--serve] [--source directory]
Build all source content into dist
. When --serve
is passed, a fileserver is
stood up afterwards pointing to dist
, content is continually rebuilt as it
changes, and the browser automatically refreshes.
Winter always builds text content from src/cold
and src/warm
, gallery
content from src/img
, and templates from src/template
. If --source
is
specified, Winter will also build text content from that file or directory.
--source
can be specified any number of times.
winter freeze
Usage: winter freeze shortname...
Freeze all arguments, specified by shortname. This moves the files from
src/warm
to src/cold
and reflects the change in Git.
Documents
A document is an HTML, Markdown, or Org file with optional
frontmatter. The first level 1 heading (<h1>
in HTML, #
in
Markdown, *
in Org) will be used as the document title.
This is an example document called example.md
:
1---
2date: 2022-07-07
3filename: example.html
4type: post
5---
6
7# My Example Document
8
9This is an example document.
Frontmatter
Frontmatter for HTML and Markdown documents is specified in YAML. Frontmatter for Org files is specified using Org keywords of equivalent names (in whatever case you choose). All fields are optional.
The available frontmatter fields for HTML and Markdown are:
1filename: example.html
2date: 2022-07-07
3updated: 2022-11-10
4category: arbitrary string
5toc: true|false
6type: post|page|draft
The same fields are available in Org files:
1#+FILENAME: example.html
2#+DATE: 2022-07-07
3#+UPDATED: 2022-11-10
4#+CATEGORY: arbitrary string
5#+TOC: true|false
6#+TYPE: post|page|draft
See below for details of each.
filename
Filename specifies the desired final location of the built file in
dist
. This must end in .html
no matter the source document type
and must not be in a subdirectory. Winter enforces this because if you
later move off Winter, web paths that end in .html
and do not use
subdirectories will be the easiest to migrate.
When not set the filename of the source document is used, with any
extensions replaced with .html
. For example, envy.html.tmpl
and
envy.md
would both become envy.html
(though if two source files
would produce the same destination file, Winter will error).
A document's shortname is defined as its extensionless filename
.
The shortname is accessible to templates using the {{
.Shortname }}
template variable. The filename is not
accessible to templates, but can be calculated by appending .html
to
the shortname.
date
Date is the publish date of the document written as YYYY-MM-DD
. It
is available to templates as a Go
time.Time
using {{
.CreatedAt }}
.
When not set the document will not have a publish date attached to it.
updated
Updated is the date the document was last meaningfully updated, if
any, written as YYYY-MM-DD
. It is available to templates as a Go
time.Time
using {{
.UpdatedAt }}
.
When not set the document will not have an update date attached to it.
toc
Whether to render a table of contents (default false
). If true
,
the table of contents will be rendered just before the first level 2
header (<h2>
in HTML, ##
in Markdown, **
in Org) and will list
all level 2, 3, and 4 headers. See the top of this page for an
example.
type
The kind of document. Possible values are post
, page
, draft
.
post
documents are programmatically included in template functions.
page
and draft
documents have no programmatic action taken on
them and will not be discoverable unless linked to.
The default template set provided by winter init
gives each a
slightly different visual treatment:
- Posts have a publish date and update date at the top and bottom of the page
- Pages have a publish date and update date at the bottom of the page
- Drafts behave like posts but have a large "DRAFT" banner at the top of the page
Beyond this, the types are only different in semantics.
category
The category of the document. Accepts any string. This is exposed to templates
via the {{ .Category }}
field.
The default template set provided by winter init
gives a minor
visual treatment to the listing and display of documents with
categories.
Otherwise, the category is semantic. There is no programmatic access to documents by category.
Templates
Any file in ./src
can be templated using the format expressed in the text/template
Go
library.
Document Fields
The following fields are available to templates rendering documents.
{{ .Category }}
Type: string
The value specified by the frontmatter category
field.
This is an arbitrary string.
{{ .Dest }}
Type: string
The filesystem path of the document after having been rendered, relative to the web root.
{{ .IsDraft }}
Type: bool
Whether the document is a draft (i.e. has frontmatter specifying type: draft
).
{{ .IsPost }}
Type: bool
Whether the document is a post (i.e. has frontmatter specifying type: post
).
{{ .Shortname }}
Type: string
The shortname of the document. This is the extensionless filename frontmatter attribute if set, or the source document's extensionless filename otherwise.
{{ .Title }}
Type: string
The value of the document's first level 1 heading (<h1>
in HTML, #
in Markdown, *
in Org).
{{ .CreatedAt }}
Type: time.Time
The publish date of the document as specified by the frontmatter
date
attribute.
This value can be formatted using Go's func (time.Time)
Format
function:
1{{ .CreatedAt.Format "2006 January" }} <!-- Renders 2022 July -->
2{{ .CreatedAt.Format "2006-01-02" }} <!-- Renders 2022-07-08 -->
Use {{ .CreatedAt.IsZero }}
to see if the date was not set.
You can use this to hide unset dates:
1{{ if not .CreatedAt.IsZero }}
2 published {{ .CreatedAt.Format "2006 January" }}
3{{ end }}
{{ .UpdatedAt }}
Type: time.Time
The date the document was last meaningfully updated, as specified by
the frontmatter updated
attribute.
This value behaves identically to {{ .CreatedAt
}}
. For details on dealing with it, see that
documentation.
Functions
posts
Usage: {{ range posts }} ... {{ end }}
Returns a list of all documents with type post
, from most to least recent.
See Document Fields for a list of fields available to documents.
archives
Usage: {{ range archives }}{{ .Year }}: {{ range .Documents }} ... {{ end }}{{ end }}
Returns a list of archive
types ordered from most to least recent. An
archive
has two fields, .Year
(integer) and .Documents
(array of
documents). This allows you to display posts sectioned by year.
See Document Fields for a list of fields available to documents.
img
Alias: imgs
Usage: {{ img[s] <caption> <imageshortname alttext>... }}
Render an image or images with a single caption. Image files must be present in this format:
1public/img/<pageshortname>-<imageshortname>[-<light|dark>].<png|jpg|jpeg>
For example, to render one image with a caption and alt text:
1<!-- mypage.md -->
2
3{{ img
4 "A caption."
5 "test1"
6 "Descriptive alt text of what the image is of, for assistive tech"
7}}
Result:
In this example, the image file must hold one or more of these forms:
public/img/mypage-test1.jpg
public/img/mypage-test1-light.jpg
public/img/mypage-test1-dark.jpg
public/img/mypage-test1.jpeg
public/img/mypage-test1-light.jpeg
public/img/mypage-test1-dark.jpeg
public/img/mypage-test1.png
public/img/mypage-test1-light.png
public/img/mypage-test1-dark.png
If -light
and/or -dark
variants exist, they will be used when the user is
in the respective dark mode setting.
Any number of images can be rendered together with one caption beneath the group by passing multiple images and alt texts. They will appear next to each other when the page width allows it, or stacked vertically otherwise.
1{{ imgs
2 "A pair of images."
3 "test1"
4 "Descriptive text of test1"
5 "test1"
6 "Descriptive text of test2"
7}}
Result:
video
Alias: videos
Usage: {{ video[s] <caption> <videoshortname alttext>... }}
Behaves exactly as img
but searches for mp4
/mov
files instead
and renders them in <video>
tags.
Note that most browsers do not currently support varying a video source by light/dark mode setting, so the wrong variant may be displayed.