Skip to content
jcostdPublic

About

A minimalist, strict static site generator written in Go. Zero dependencies. Text in, HTML out.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

131 Commits

Folders and files

Repository files navigation

SOFFIO(1)                     System Reference                     SOFFIO(1)

NAME
    soffio - a minimalist, strict static site generator

SYNOPSIS
    soffio [-a] [-n] [-baseurl url] [-o dir] [-s dir]... [-t dir]
           [-today day] dir
    soffio [-n] < text.soffio > text.html

DESCRIPTION
    Soffio turns the .soffio texts under dir into a static site in -o:
    one HTML page per text, keeping the directory tree, and nothing
    else. Before it writes anything it checks the whole site: every
    link points at a page, a section or a file that is there; every
    note is defined and referenced; no public page points at a private
    one. If a text is wrong it says where, file:line, writes nothing
    and exits 1; a page whose template fails is reported too, and
    soffio exits 1. It has no warnings: what is worth saying is worth
    fixing.

    With no dir, soffio is a filter: one text on standard input, an
    HTML fragment on standard output. Alone, a text can only check its
    notes and its links to its own sections; -n checks it and prints
    nothing, and the flags about a site are refused.

    Soffio knows its markup, links and files, and nothing of what a
    site is about: a site that needs more is a program of its own (see
    EXTENDING). It uses the Go standard library and nothing else.

OPTIONS
    -a          all texts: the private ones too, as for a preview with
                drafts. Without it, a text with 'visibility: private'
                is left out, and a link to it is an error.
    -n          check everything, templates and the copy of -s
                included, and write nothing, as sh -n.
    -baseurl url
                the site's address, http(s)://host[/path], without a
                final '/': the templates get it as .BaseURL, for what
                needs a full address. Without it .BaseURL is empty, and
                an address made of it starts at the root.
    -o dir      the output directory. Default public.
    -s dir      the static files, copied to <o>/static, but hidden
                ones: a link to one is an error. -s again adds a
                directory; a path in two of them is an error. Without
                -s a link or image under /static/ is an error. None
                can overlap <o>/static.
    -t dir      the templates; without it, the built-in ones. The two
                are never mixed. It can't be -o: the pages would be
                written over the templates.
    -today day  the day of the build, YYYY-MM-DD, as the templates'
                today: their only clock. Without it today is empty,
                and the same flags always make the same site.

MARKUP
    A text is UTF-8: a header, a blank line, and the body. The header
    is 'key: value' lines, keys of letters, digits and _, each once.
    Three keys mean something to soffio: 'title', which a page needs;
    'layout'; and 'visibility', public or private: each a word, never a
    link or a file. Two values mean something in any other key:
    '-> target' is a link to a page, written and checked as a link in
    the body is ('by: -> ../persone/arp'), and the templates get the ID
    of that page; a value under /static/ is a file, checked as an image
    is (see LINKS AND FILES). Every other value is a word, as written,
    for the templates: a date, a name, a kind are the site's, not
    soffio's. A byte order mark at the start is ignored.

    == id | Title                     section (== to ======: h2 to h6)
    *text*                            bold
    _text_                            italic
    (label -> target)                 link
    - text                            list item
    :: img: /static/path | caption    image
    :: note: id | text                note
    (*id)                             note reference

    A blank line ends a block; a section or :: line starts one, and a
    :: line that is not :: cmd: is refused, as ::img: is. A list
    item starts with "- ": "-5 gradi" is text. A * or _ opens only at
    the start of a word and closes only at its end: snake_case and
    2*3*4 stay as they are. (*x) is a note only if x is a valid ID, so
    (*bold*) is bold in parentheses. A link can't hold a link or a note
    ref. A backslash makes the next character plain. Section titles are
    plain text. A heading skips no level: the title is the h1, the
    first section ==, and each one at most one level below the one
    before; a section holds the ones of the levels below it, so the
    page's outline is in its HTML.

    IDs, of texts, their directories, sections and notes, are printable
    ASCII: no spaces, no accents, none of / \ : * ? " < > | # % ( ),
    no leading dot. Text IDs that differ only in case clash: they would
    be one file on Windows and macOS.

LINKS AND FILES
    dir/it/about.soffio is the page it/about, written as
    <o>/it/about.html: a page has one name, its file's.
    Hidden files and directories, as .git or the ._ files macOS leaves,
    are skipped; a link to a directory is refused.

    A target without a leading '/' is relative to the text's directory,
    so it/ and en/ link within themselves with the same words; with one,
    it starts from the site root. #section points at a section, of
    another page or, alone, of this one. A target under /static/ is a
    file in -s; anything else is a page. A target is cleaned, so .. can't
    climb out of the site, and must be a valid address. The addresses
    soffio writes are relative, so a site works under any base, even
    from the disk: a template writes them with href, as the texts do.

    An image is a file under /static/, or an address out of the site.
    One of the files of -s gets its width and height, read from the
    file (jpeg, png, gif, webp), so the browser keeps its place before
    it comes, whatever its shape.

THE HTML
    A text becomes an HTML fragment, .Content, that says what it is
    and holds no word of soffio's own, so it reads well in any
    language:

    == id | Title       <section id="id"><h2>Title</h2> ... </section>,
                        holding the sections of the levels below it
    a paragraph         <p>
    *text*  _text_      <strong>  <em>
    (label -> target)   <a href="...">, relative; nothing more: where
                        a link opens is the reader's
    - text              <ul><li>
    :: img:             <figure><img alt width height loading=lazy>
                        <figcaption>: the caption is the alt too
    (*id)               <sup><a role="doc-noteref">1</a></sup>, numbered
                        in the order the notes are first referred to
    :: note:            at the end, <section role="doc-endnotes"><ol>,
                        each <li> with a way back, <a
                        role="doc-backlink">↩</a>, and no heading

    The page around it, <html lang>, <title>, the h1, <main> and the
    rest, is the template's.

PRIVATE TEXTS AND FILES
    A text with 'visibility: private' is a draft: without -a it is not
    written, and a public text that links to it is an error, as the
    link would lead nowhere. So the public site is closed: every link
    in it leads to a page or a file that is in it.

    Everything in -s is published, all of it: soffio does not guess
    which file is private. Keep the files only drafts use in a
    directory of their own, and give it to the preview alone:

        soffio -s static dir                         the public site
        soffio -a -s static -s drafts -o www dir     the preview

    A public text pointing at a file in drafts is then a missing file,
    as a link to a draft is an error: a file in the wrong place is
    missing, never published. When a draft goes public, move its
    files to static. Build the public site into an empty directory:
    soffio never deletes, and what a build with -a wrote would stay.

TEMPLATES
    Go html/template files. A page goes through its 'layout' (post means
    post.html), or layout.html; a layout that is not there is an error.
    A page gets .ID, .Meta (its header, title too: .Meta.title; a key it
    doesn't have is "", as one it has empty), .Content, .BaseURL and
    .Docs, every page, by ID: range .Docs goes through them in that
    order; and .Pages, the same pages in the same order, as a list, for
    a site's functions. A page among others has .ID and .Meta.

    The templates are the .html files of the directory: layouts and
    partials. Every page is a text: a 404 page too, 404.soffio, which
    a server gives at any address, so its layout's links start from
    the root, as .BaseURL or a '/'.

    The functions are what only soffio knows:

    doc id            the page of an ID, or nothing: a header link's
                      page is doc .Meta.by
    href id target    the address of target, written as in a text,
                      seen from the page id: relative, as in the texts
    size path         an image's .Width and .Height, as for the
                      images of the texts, or nothing
    today             -today, the day of the build, or nothing

        {{ with doc .Meta.by }}
          <a href="{{ href $.ID (print "/" .ID) }}">{{ .Meta.title }}</a>
        {{ end }}
        <link rel="stylesheet" href="{{ href .ID "/static/style.css" }}">
        <img src="{{ href .ID .Meta.image }}"{{ with size .Meta.image }}
          width="{{ .Width }}" height="{{ .Height }}"{{ end }} alt="">

    Without -t, soffio uses the template built into it, layout.html,
    and says nothing about it, as it says nothing when all is well. It
    needs only a title, and has no word of its own: the header's lang
    is the page's language if there is one, the text is an <article>
    in <main>, a page leads to the home, index, by its title, and the
    home lists every page in a <nav>. Pages come by
    ID, so a name says where one goes: 2026-10-19-soffio. The same file
    is in templates/ of the release archives and of the source: copy it
    into a directory of your own to start one for -t.

EXTENDING
    Soffio is to a site what Lua's core is to its host: it gives the
    mechanisms, the site says the rest, in four ways and no other.

    1. Functions for its templates, and 2. a check of its texts, which
    reads them and changes none: what it finds is said after what
    soffio finds, all of it at once. Both go to Main of package soffio,
    which is the command:

        func main() { soffio.Main(nil, nil) }

    and a site's program, with soffio's flags, checks and output, and
    its own:

        func main() { soffio.Main(funcs, check) }

    A function of soffio's can't be taken.

    3. Its templates: every page is a text through a layout of the
    site's.

    4. Programs of its own on package soffio, which reads the texts as
    soffio does: Load gives every Document, whose Lines say where each
    header key is, HeaderLinks which hold a link, and Links the
    pages it links to; the pages that link to one are Links read the
    other way; Links, the function, says them of any Document. Parse,
    Check and Render are there too: go doc github.com/jcostd/soffio/v5
    says the rest, with examples.

    The markup and what a header means are not extended.

    A site's program, whole: a check that every date is a date, and a
    function that lists the pages, the newest first: newest .Pages.

        package main

        import (
            "errors"
            "fmt"
            "html/template"
            "maps"
            "slices"
            "strings"
            "time"

            "github.com/jcostd/soffio/v5"
        )

        func check(docs map[string]*soffio.Document) error {
            var errs []error
            for _, id := range slices.Sorted(maps.Keys(docs)) {
                d := docs[id]
                date := d.Meta["date"]
                _, err := time.Parse(time.DateOnly, date)
                if date != "" && err != nil {
                    errs = append(errs, fmt.Errorf("%s:%d: %q is no date",
                        d.File, d.Lines["date"], date))
                }
            }
            return errors.Join(errs...)
        }

        func newest(pages []*soffio.Document) []*soffio.Document {
            return slices.SortedStableFunc(slices.Values(pages),
                func(a, b *soffio.Document) int {
                    return strings.Compare(b.Meta["date"], a.Meta["date"])
                })
        }

        func main() {
            soffio.Main(template.FuncMap{"newest": newest}, check)
        }

EXAMPLES
    A site, checked, then built into an empty directory, for any
    static server:

        soffio -n -s static texts && rm -rf public &&
            soffio -baseurl https://example.org -s static texts

    One text, as HTML, or only checked:

        soffio < about.soffio > about.html
        soffio -n < about.soffio

    A sitemap and a robots.txt are made of the pages, by what publishes
    them: the sitemap protocol takes a list of addresses, one a line.

        cd public && find . -name '*.html' ! -name 404.html | sort |
            sed 's,^\.,https://example.org,' > sitemap.txt
        printf 'User-agent: *\nAllow: /\n\nSitemap: %s\n' \
            https://example.org/sitemap.txt > robots.txt

LIMITS
    No code blocks, tables, ordered or nested lists, raw HTML or h1 in
    the body: the title is the h1. No inline markup in section titles.
    A line of a block is reported as the line where the block starts.
    A * or _ closes at the end of a word even inside a link: in
    _see (here -> a_) now_ the italic ends at a_ and the link is lost;
    escape it, a\_.

    Soffio makes pages and nothing else: no sitemap, robots.txt, feed
    or 404 of its own, and no server. It knows no dates, languages or
    orders but its IDs': a site that wants them is a program of its own
    (see EXTENDING). The links of the texts are checked; those a
    template writes are the template's. An svg or avif image gets no
    size.

    Soffio never deletes: a page that is gone or became private stays
    in -o from an earlier build, so build into an empty directory. It
    has no version flag: go version -m soffio says which it is, and
    the name of its archive.

MACOS
    macOS will not run a program downloaded from the internet that
    Apple has not notarized. The darwin archives hold unquarantine.sh:
    after unpacking, in the Terminal, in the archive's folder:

        sh unquarantine.sh

    It lifts the quarantine from that folder alone, gives back the
    exec bits, and checks that soffio runs.

BUILD
    Requires Go 1.26 or higher.

    make                       soffio: static, for the baseline CPU,
                               stripped
    make GOOS=os GOARCH=arch OUT=dir   the same for another system
    make test                  go vet, then the tests and their
                               coverage, 100% of the statements
    ./release.sh               the release archives, one per system,
                               from a clean tag

    ChangeLog.txt says what changed, newest first.

SEE ALSO
    go doc github.com/jcostd/soffio/v5, the package and its examples;
    https://pkg.go.dev/html/template, the templates;
    https://www.sitemaps.org/protocol.html, sitemaps as text;
    https://www.w3.org/TR/dpub-aria-1.1/, the roles of the notes;
    https://developers.google.com/speed/webp/docs/riff_container,
    where a WebP says its size;
    https://developer.mozilla.org/docs/Web/HTML, the elements soffio
    writes; ChangeLog.txt, what changed and why.

AUTHOR
    Written and designed by Jacopo Costantini.

COPYRIGHT AND LICENSE
    Copyright (C) 2026 Jacopo Costantini.

    Soffio is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published
    by the Free Software Foundation, either version 3 of the License,
    or (at your option) any later version.

    See the LICENSE file in the source distribution for full details.

About

A minimalist, strict static site generator written in Go. Zero dependencies. Text in, HTML out.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages