Documentation ¶
Overview ¶
Package document supplies a static website generator. It has these goals:
- New content must be easy to edit. Old content must be hard to break.
- Be replaceable. Special syntax should look fine as plaintext if moved elsewhere.
Winter is part of the TadiWeb.
Easy to edit, hard to break ¶
To accomplish its first goal, Winter employs two mechanisms.
First, content managed by Winter is either warm or cold. Warm content is welcome to be synchronized into a Winter-managed directory by external tools, such as your mobile text editor or note-taking app of choice. These external tools and their synchronization steps are not provided by Winter, and they are not required to use it, but Winter's design goals assume they exist. Warm content is content you are actively working on. It is not ready to be listed anywhere, but it is published to a page on your website so you can view it in context and share it with friends for review.
Cold content is finished content. Cool URIs don't change. When your warm content is ready to be published, you should freeze it:
winter freeze src/cold/my_post.md
This converts it into cold content. The most important difference between the two is that your synchronization tools never, ever touch cold content. Sychronization tools, jobs, and scripts are run frequently and probably aren't your most robust pipelines. To limit the blast radius of any bugs they experience, content that you don't expect to edit often must be moved away from the directories they control. This is cold content.
Cold content is also guaranteed by Winter's quality checks. When a piece of content becomes cold, Winter commits its URL to memory and ensures that URL stays accessible in future runs. If ever a URL that once was accessible becomes inaccessible, Winter alerts you and prevents that build from succeeding. This protects cold content from you, your tools, and from Winter's current and future versions.
No lock-in ¶
To accomplish its second goal, Winter provides cherries on top of Markdown. However, using the same Markdown with any other static website generator results in no degradation. Just like Markdown itself, Winter Markdown looks great when not parsed.
Galleries ¶
A block containing only images is treated as a gallery, with the images placed in a responsive grid. Images can be clicked to zoom in or out.
![An image of a cat.](/img/cat.jpg) ![An image of a dog.](/img/dog.jpg)
Image captions ¶
A block of all-italic text immediately below an image or gallery is treated as a caption, and given a special visual treatment and accessibility structure.
![An image of a cat.](/img/cat.jpg) ![An image of a dog.](/img/dog.jpg) _My cat and dog like to play with each other._
Photos as first-class citizens ¶
For photographers, Winter supports photographs as first-class citizens. EXIF data is automatically extracted and can be displayed using template variables. Photos organized into galleries display next to each other neatly, with a built-in lightbox.
Photo EXIF safety ¶
Any photos Winter processes that have GPS data embedded in them are loudly rejected, failing the build.
Reduced load times ¶
Images and references to them are automatically converted into WebP format and several thumbnails are generated for each. Each image renders with <img srcset> to ensure only the smallest possible image that saturates the display density is loaded.
LaTeX ¶
Surround text in $dollar signs$ to render LaTeX, implemented via KaTeX.
$\LaTeX$ users rejoice!
Tables of contents ¶
A table of contents can be requested by setting the toc variable to true in frontmatter. Tables of contents are always rendered immediately above the first level-2 heading.
--- toc: true --- # My Article (table of contents will be rendered here) ## My Cat ... ## My Dog ...
Syntax highlighting ¶
Fenced code blocks that specify a language are syntax-highlighted.
```go package main func main() { fmt.Println("I'm syntax highlighted!") } ```
External links in new tabs ¶
Any links that navigate to external websites will automatically have a target=_blank set during generation.
Dark mode images ¶
If an image's extensionless filename ends in "-dark" or "-light", and another image exists at the same path but with the opposite suffix, the correct one will be rendered to the user based on their light-/dark-mode preference.
Index ¶
- Constants
- func ConfigPath() (string, error)
- func InteractiveConfig() error
- func NewIMG(logger *slog.Logger, src string, cfg *Config) (*img, error)
- type Config
- type Document
- type EXIF
- type ErrNotTracked
- type Gear
- type GeminiDocument
- type GeminiRenderer
- type HTMLDocument
- type LayoutDocument
- type MarkdownDocument
- type Metadata
- type OrgDocument
- type StaticDocument
- type Substructure
- type TemplateDocument
- type TemplateNode
Constants ¶
const (
AppName = "winter"
)
Variables ¶
This section is empty.
Functions ¶
func ConfigPath ¶
func InteractiveConfig ¶
func InteractiveConfig() error
Types ¶
type Config ¶
type Config struct { // Author is the information for the website author. // This is used in metadata such as that of the RSS feed. Author feeds.Author `yaml:"author,omitempty"` // Debug is a flag that enables debug mode. Debug bool `yaml:"debug,omitempty"` // Development contains options specific to development. // They have no impact when building for production. Development struct { // URL is the base URL you will connect to while developing your website or Winter. // If blank, it defaults to "http://localhost:8100". URL string `yaml:"url,omitempty"` } `yaml:"development,omitempty"` // Description is the Description of the website. // This is used as metadata for the RSS feed. Description string `yaml:"description,omitempty"` // Dist is the location the site will be built into, // relative to the working directory. // After a build, this directory is suitable for deployment to the web as a set of static files. // // In other words, the path of any file in dist, // relative to dist, // is equivalent to the path component of the URL for that file. // // If blank, defaults to ./dist. Dist string `yaml:"dist,omitempty"` // Known helps the generated site follow the "Cool URIs don't change" rule // by remembering certain facts about what the site looks like, // and checking newly-generated sites against those facts. Known struct { // URIs holds the path to the known URIs file, // which Winter will generate, update, and maintain. // // You should commit this file. // // If unset, defaults to src/uris.txt. URIs string `yaml:"urls,omitempty"` } `yaml:"known,omitempty"` // Name is the name of the website. // This is used in various places in and out of templates. Name string `yaml:"name,omitempty"` // Gear is an array of Gear objects, // each describing a camera, lens, or other piece of gear // whose information can be extracted from EXIF data. // // Gear is used by Winter when processing photos to display photograph information, // and provide links to purchase gear used in its creation. Gear []Gear `yaml:"gear,omitempty"` Production struct { // URL is the base URL you will connect to to view your deployed website // (e.g. twos.dev or one.twos.dev or twos.dev:6667). // This is used in various backlinks, like those in the RSS feed. // // Must not be blank. URL string `yaml:"url,omitempty"` } `yaml:"production,omitempty"` // Since is the year the website was established, // whether through Winter or otherwise. // This is used as metadata for the RSS feed, // and as a copyright notice when needed. Since int `yaml:"since,omitempty"` // Src is an additional list of directories to search for source files beyond ./src. Src []string `yaml:"srca,omitempty"` }
Config is a configuration for the Winter build.
func (*Config) GearByString ¶ added in v0.2.0
GearByString returns the Gear item for the given make and model EXIF tag values. If none could be found, ok is false.
type Document ¶
type Document interface { // DependsOn returns true if and only if the given source path, // when changed, // should cause this document to be rebuilt. DependsOn(src string) bool // Load reads or re-reads the source file from disk, // overwriting any previously stored or parsed contents. Load(r io.Reader) error // Metadata returns data about the document, // which may have been inferred automatically or set by frontmatter. Metadata() *Metadata // Render generates the final HTML for the document and writes it to w. Render(w io.Writer) error }
Document is something that can be built, usually from a source file on disk to a destination file on disk.
After a document has been built by calling [Build], it can be passed to a template during execution:
var buf bytes.Buffer t.Execute(&buf, d)
type ErrNotTracked ¶
type ErrNotTracked struct {
// contains filtered or unexported fields
}
func (ErrNotTracked) Error ¶
func (err ErrNotTracked) Error() string
type Gear ¶
type Gear struct { // Make is the user-readable brand that created this piece of gear. Make string `yaml:"make,omitempty"` // Model is the user-readable model for this piece of gear. Model string `yaml:"model,omitempty"` // Link is a URL // (beginning in https://) // at which a user can purchase or read more about this piece of gear. Link string `yaml:"link,omitempty"` // EXIF specifies what EXIF data a photograph must have in order to be identified as having been taken with this piece of gear. EXIF struct { // Make is the value a photograph's EXIF data must have in the "make" field, // whether camera or lens, // for the photograph to be considered as having been taken with this piece of gear. // // Before comparison, // Winter will strip all trailing and leading spaces from both the EXIF data field and from this field. // Then, a case-insensitive comparison is performed. Make string `yaml:"make,omitempty"` // Make is the value a photograph's EXIF data must have in the "model" field, // whether camera or lens, // for the photograph to be considered as having been taken with this piece of gear. // // Before comparison, // Winter will strip all trailing and leading spaces from both the EXIF data field and from this field. // Then, a case-insensitive comparison is performed. Model string `yaml:"model,omitempty"` } `yaml:"exif,omitempty"` }
type GeminiDocument ¶ added in v0.2.3
type GeminiDocument struct {
// contains filtered or unexported fields
}
GeminiDocument represents a document written in or converted to gemtext, the markup language for Geminispace (https://geminiprotocol.net/docs/gemtext.gmi).
GeminiDocument implements Document.
func NewGeminiDocument ¶ added in v0.2.3
func NewGeminiDocument(src string, meta *Metadata) *GeminiDocument
NewGeminiDocument creates a new gemtext document whose original source is at path src.
Nothing is read from disk; src is metadata. To read and parse gemtext, call [Load].
func (*GeminiDocument) DependsOn ¶ added in v0.2.3
func (doc *GeminiDocument) DependsOn(src string) bool
func (*GeminiDocument) Load ¶ added in v0.2.3
func (doc *GeminiDocument) Load(r io.Reader) error
Load reads Gemini from r and loads it into doc.
If called more than once, the last call wins.
func (*GeminiDocument) Metadata ¶ added in v0.2.3
func (doc *GeminiDocument) Metadata() *Metadata
type GeminiRenderer ¶ added in v0.2.3
type GeminiRenderer interface { // RenderGemini converts the document into gemtext, // then writes the result to w. RenderGemini(w io.Writer) error }
GeminiRenderer is a type that can render itself into gemtext, the markup language for Geminispace, an alternate web.
type HTMLDocument ¶
type HTMLDocument struct {
// contains filtered or unexported fields
}
HTMLDocument represents the HTML for a source file.
The document's source content may or may not be in HTML. It may be that the source file was written in another language, like Markdown, then converted to HTML via MarkdownDocument.
Therefore, any page generated from a file in src will at some point be an HTMLDocument.
HTMLDocument implements Document.
func NewHTMLDocument ¶
func NewHTMLDocument(src string, meta *Metadata, next Document) *HTMLDocument
NewHTMLDocument creates a new document whose original source is at path src.
Nothing is read from disk; src is metadata. It may or may not point to a file containing HTML. To read and parse HTML, call [Load].
func (*HTMLDocument) DependsOn ¶
func (doc *HTMLDocument) DependsOn(src string) bool
func (*HTMLDocument) Load ¶
func (doc *HTMLDocument) Load(r io.Reader) error
Load reads HTML from r and loads it into doc.
If called more than once, the last call wins.
func (*HTMLDocument) Massage ¶
func (doc *HTMLDocument) Massage() error
Massage messes with loaded content to improve the page when it is ultimately rendered.
Massage performs these tasks:
- Linkifies and stylizes the first <h1> as a page title
- Generates a table of contents, if requested by metadata
- Sets target=_blank for all <a> tags pointing to external sites
- Generates a preview for the document, if one wasn't manually specified
- Syntax-highlights code blocks
func (*HTMLDocument) Metadata ¶
func (doc *HTMLDocument) Metadata() *Metadata
func (*HTMLDocument) Post ¶
func (doc *HTMLDocument) Post() bool
type LayoutDocument ¶
type LayoutDocument struct {
// contains filtered or unexported fields
}
LayoutDocument represents a document to be assembled by being placed inside a layout-style template.
For example, a blog post document would not itself contain a site header or footer; instead, it would be fully rendered then placed inside a layout which includes it:
{{ template "body" }}
The LayoutDocument's job is to facilitate that embedding. It will usually come last in the load/render chain.
func NewLayoutDocument ¶
func NewLayoutDocument(src string, meta *Metadata, next Document) *LayoutDocument
func (*LayoutDocument) DependsOn ¶
func (doc *LayoutDocument) DependsOn(src string) bool
func (*LayoutDocument) Metadata ¶
func (doc *LayoutDocument) Metadata() *Metadata
type MarkdownDocument ¶
type MarkdownDocument struct {
// contains filtered or unexported fields
}
MarkdownDocument represents a source file written in Markdown, with optional Go template syntax embedded in it.
MarkdownDocument implements Document.
The MarkdownDocument is transitory; its only purpose is to create a TemplateDocument.
func NewMarkdownDocument ¶
func NewMarkdownDocument(src string, meta *Metadata, next map[Document]struct{}) *MarkdownDocument
NewMarkdownDocument creates a new document whose original source is at path src.
Nothing is read from disk; src is metadata. To read and parse Markdown, call [Load].
func (*MarkdownDocument) DependsOn ¶
func (doc *MarkdownDocument) DependsOn(src string) bool
func (*MarkdownDocument) Load ¶
func (doc *MarkdownDocument) Load(r io.Reader) error
Load reads Markdown from r and loads it into doc.
If called more than once, the last call wins.
func (*MarkdownDocument) Metadata ¶
func (doc *MarkdownDocument) Metadata() *Metadata
func (*MarkdownDocument) RenderGemini ¶ added in v0.2.3
func (doc *MarkdownDocument) RenderGemini(w io.Writer) error
type Metadata ¶
type Metadata struct { // Category is an optional category for the document. This is used // only for a small visual treatment on the index page (if this is // of kind post) and on the document page itself. // // Category MUST be a singular noun that can be pluralized by adding // a single "s" at its end, as this is exactly what the visual // treatment will do. If this doesn't work for you, go fix that // code. Category string `yaml:"category,omitempty"` // CreatedAt is the time the document was first published. CreatedAt time.Time `yaml:"date,omitempty"` // Kind specifies the type of document this is. // In every user-facing context, this is called "type". // In Go we cannot use the "type" keyword, so we use "kind" instead. Kind kind `yaml:"type,omitempty"` // Layout is the path to the source file for the layout this document should be rendered into. // // If unset, src/templates/text_document.html.tmpl is used. Layout string `yaml:"layout,omitempty"` // GeminiPath is the path component of the Geminispace URL that will point to this document, // once rendered. // GeminiPath MUST NOT contain any slashes; // everything is top-level. // // GeminiPath is equivalent to the path to the destination file relative to dist. GeminiPath string `yaml:"-,omitempty"` // ParentFilename is the filename component of another document that this one is a child of. // Parenthood is a purely semantic relationship for the benefit of the user. // Templates can access parents to influence rendering. ParentFilename string `yaml:"parent,omitempty"` // Preview is a sentence-long blurb of the document, // to be shown along with its title as a teaser of its contents. Preview string `yaml:"preview,omitempty"` // SourcePath is the location on disk of the original file that this document represents. // It is relative to the working directory. SourcePath string `yaml:"-"` // TemplateDir is the location on disk of a directory containing any templates that will be used in the document. // By default, it is src/templates. TemplateDir string `yaml:"-"` // Title is the human-readable title of the document. Title string `yaml:"title,omitempty"` // TOC is whether a table of contents should be rendered with the // document. If true, the table of contents is rendered immediately // above the first non-first-level heading. TOC bool `yaml:"toc,omitempty"` // UpdatedAt is the time the document was last meaningfully updated. UpdatedAt time.Time `yaml:"updated,omitempty"` // WebPath is the path component of the URL that will point to this document, // once rendered. // WebPath MUST NOT contain any slashes; // everything is top-level. // // WebPath is equivalent to the path to the destination file // relative to dist. WebPath string `yaml:"filename,omitempty"` }
Metadata holds information about a Document that isn't inside the document itself.
func NewMetadata ¶
NewMetadata returns a Metadata with some defaults filled in according path src.
NewMetadata is purely lexicographic; no files are opened or read.
Defaults that depend on parsing the content of the document, such as a Preview generated from its content, are not filled in.
type OrgDocument ¶
type OrgDocument struct { // SourcePath is the path on disk to the file this Org is read from or generated from. // The path is relative to the working directory. SourcePath string // contains filtered or unexported fields }
OrgDocument represents a source file written in Org, with optional Go template syntax embedded in it.
OrgDocument implements Document.
The OrgDocument is transitory; its only purpose is to create an HTMLDocument.
func NewOrgDocument ¶
func NewOrgDocument(src string, meta *Metadata, next Document) *OrgDocument
NewOrgDocument creates a new document whose original source is at path src.
Nothing is read from disk; src is metadata. To read and parse Org, call [Load].
func (*OrgDocument) DependsOn ¶
func (doc *OrgDocument) DependsOn(src string) bool
func (*OrgDocument) Load ¶
func (d *OrgDocument) Load(r io.Reader) error
Load reads Org from r and loads it into doc.
If called more than once, the last call wins.
func (*OrgDocument) Metadata ¶
func (doc *OrgDocument) Metadata() *Metadata
type StaticDocument ¶
type StaticDocument struct { SourcePath string // contains filtered or unexported fields }
StaticDocument represents a file on disk that will be copied as-is to the web root. The subdirectory of the file relative to the web root will match the relative directory of the source file relative to the ./public directory.
func NewStaticDocument ¶
func NewStaticDocument(src, webPath string) *StaticDocument
NewStaticDocument creates a new document whose original source is at path src, and whose desired web path is webPath.
Nothing is read from disk; src is metadata. To read the static file, call [Load].
func (*StaticDocument) DependsOn ¶
func (doc *StaticDocument) DependsOn(src string) bool
func (*StaticDocument) Metadata ¶
func (doc *StaticDocument) Metadata() *Metadata
type Substructure ¶
type Substructure struct {
// contains filtered or unexported fields
}
Substructure is a graph of documents on the website.
func NewSubstructure ¶
func NewSubstructure(logger *slog.Logger, cfg *Config) (*Substructure, error)
NewSubstructure returns a substructure with the given configuration. Upon initialization, a substructure is the result of a discovery phase of content on the filesystem. Further calls are needed to build the full graph of content and render it to HTML.
func (*Substructure) Build ¶
func (s *Substructure) Build(doc Document) error
Build builds doc.
To also build downstream dependencies, use [Rebuild] instead.
func (*Substructure) DocBySourcePath ¶
func (s *Substructure) DocBySourcePath(path string) (doc Document, ok bool)
DocBySourcePath returns the document that originated from the file at path.
If no such document exists, ok is false.
func (*Substructure) ExecuteAll ¶
func (s *Substructure) ExecuteAll(dist string) error
ExecuteAll builds all documents known to the substructure, as well as any site-scoped non-documents such as RSS feeds.
func (*Substructure) Rebuild ¶
func (s *Substructure) Rebuild(src string) error
Rebuild rebuilds the document or template at the given path. Then, it rebuilds any downstream dependencies.
If src isn't known to the substructure, Rebuild no-ops and returns no error.
func (*Substructure) SaveNewURIs ¶
func (s *Substructure) SaveNewURIs(dist string) error
SaveNewURIs indexes every HTML file in dist and saves their existence to disk. Later, validateURIsDidNotChange can read that file and ensure no file is missing.
The database file's location can be customized with winter.yml. It should be commited to the repository.
type TemplateDocument ¶
type TemplateDocument struct {
// contains filtered or unexported fields
}
TemplateDocument represents a source file containing Go template clauses. The surrounding syntax can be anything.
Note that TemplateDocument does not handle layout-style templates, where the document itself should be embedded in another template. For that behavior, use LayoutDocument further down the load/render chain.
TemplateDocument implements Document.
The TemplateDocument is transitory; its only purpose is to resolve templates then hand off the resolved source to another Document type.
func NewTemplateDocument ¶
func NewTemplateDocument(src string, meta *Metadata, docs *documents, photos map[string][]*img, next Document) *TemplateDocument
NewTemplateDocument returns a template document with the given pointers to existing document metadata, substructure docs, and substructure photos.
func (*TemplateDocument) DependsOn ¶
func (doc *TemplateDocument) DependsOn(src string) bool
func (*TemplateDocument) Load ¶
func (doc *TemplateDocument) Load(r io.Reader) error
Load reads a Go template from r and loads it into doc. Any templates referenced within are looked for looked for by name, relative to the working directory.
To use a template, treat its filepath as a name:
{{ template "_foo.html.tmpl" }}
Any referenced templates will be loaded as well, and attached to the main template. This operation is recursive.
If called more than once, the last call wins.
func (*TemplateDocument) Metadata ¶
func (doc *TemplateDocument) Metadata() *Metadata