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.


Go Reference GitHub logo

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.

  1. Installation
  2. Documentation
    1. Directory Structure
    2. Commands
      1. winter init
      2. winter build
      3. winter freeze
    3. Documents
    4. Frontmatter
      1. filename
      2. date
      3. updated
      4. toc
      5. type
      6. category
    5. Templates
      1. Fields
        1. {{ .Category }}
        2. {{ .Dest }}
        3. {{ .IsDraft }}
        4. {{ .IsPost }}
        5. {{ .Title }}
        6. {{ .CreatedAt }}
        7. {{ .UpdatedAt }}
      2. Functions
        1. posts
        2. archives
        3. img
        4. video


1# CLI
2go install twos.dev/winter/[email protected]
3winter --help
5# Go library
6go get -u twos.dev/[email protected]


This section details the winter CLI documentation. For Go library documentation, see pkg.go.dev/twos.dev/winter.

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 followed by a Git add and push.

Cold content should not be touched by automated tools. This content must be preseved for years or decades, so less exposed surface area is better. When a piece of warm content is stable, it can be "frozen" into cold content using winter freeze.

Directory Structure


winter init

Usage: winter init

Initialize the current directory for use with Winter. The Winter directory structured 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]

Build all 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 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.


A document is an HTML file or a Markdown file with optional frontmatter. The first level 1 heading (<h1> in HTML or # in Markdown) will be used as the document title.

This is an example document called example.md:

2date: 2022-07-07
3filename: example.html
4type: post
7# My Example Document
9This is an example document.


Frontmatter is specified in YAML. All fields are optional.

 2filename: example.html
 3date: 2022-07-07
 4updated: 2022-11-10
 6category: arbitrary string
 7toc: true|false
 8type: post|page|draft
11# The Thing About Icebergs


Filename specifies the desired final location of the built file in dist. This must end in .html (even if the source document is Markdown) 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 minus extension is used in place. 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). The result can be accessed using the {{ .Shortname }} template var.


The publish date of the document as a Go time.Time. Coalesces to {{ .CreatedAt }} in templates.

Templates can format the time using Go's func (time.Time) Format function, which accepts a string of the reference time 01/02 03:04:05PM '06 -0700. For example, for a document dated 2022-07-08:

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 }}


The date the document was last meaningfully updated, if any, as a Go time.Time. Coalesces to {{ .UpdatedAt }} in templates.

Use {{ .UpdatedAt.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  {{ if not .UpdatedAt.IsZero }}
4    / last updated {{ .UpdatedAt.Format "2006 January" }}
5  {{ end }}
6{{ end }}


1published 2022 July / last updated 2022 August


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) and will list all level 2, 3, and 4 header in nested <ul>s. See the top of this page for an example.


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.


The category of the document. Accepts any string. This is exposed to templates via the {{ .Category }} field. It has no other effect.


Templates use the text/template Go library.


{{ .Category }}

Type: string

The value specified by the frontmatter category field. This can be any arbitrary string specified by the document and is not used internally by Winter.

{{ .Dest }}

Type: string

The path of the document, 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).

{{ .Title }}

Type: string

The value of the document's first level 1 heading (<h1> for HTML or # for Markdown).

{{ .CreatedAt }}

Type: time.Time

The parsed date specified by the frontmatter date field.

{{ .UpdatedAt }}

Type: time.Time

The parsed date specified by the frontmatter updated field.



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.


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.


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:


For example, to render one image with a caption and alt text:

1<!-- mypage.md -->
3{{ img
4   "A caption."
5   "test1"
6   "Descriptive alt text of what the image is of, for assistive tech"


Descriptive alt text of what the image is of, for assistive tech
A caption.

In this example, the image file must hold one or more of these forms:

If -light and/or a -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.

 2filename: example.html
 5{{ imgs
 6   "A pair of images."
 7   "test1"
 8   "Descriptive text of test1"
 9   "test1"
10   "Descriptive text of test2"


Descriptive text of imagename1 Descriptive text of imagename2
A pair of images.


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 light or dark mode variations for videos, so the wrong variant may be displayed.