# ![Zas](https://i.imgur.com/e9abWRX.png) Most simple static site generator ever. ## Why another one? C'mon, you must be kidding I just wanted to set up a simple website, just some pages, using Jekyll, and it didn't feel right. I didn't want a blog. I checked other projects, but they were incomplete, cumbersome, or solved the wrong problem (blogs, blogs everywhere). I wanted a zen-like experience: a layout and some Markdown files as pages with unobtrusive structure and configuration. Yes, it is another NIH, but... I think Zas is a different kind of beast. I admit that I probably overlooked some projects at the moment. ### What is the difference? 1. Gophers. Yes, there is [Hugo](http://gohugo.io/) (kudos!) but... Who wants to learn another directory layout? There is also [Hastie](https://github.com/mkaz/hastie) and [lots of other static site generators](https://jamstack.org/generators/). 2. Pure Markdown. And HTML, if you want. 3. Just a loop. Zas loops over the current directory (and subdirectories), converting .md and .html files and copying everything else as-is - except dot-files and dot-directories, which are ignored entirely. 4. Your imagination is your limit. Zas has a simple extension mechanism based on subcommands, plus optional native sitemaps and RSS feeds for article sites. 5. Unobtrusive structure, no `_` files. More in the [Usage section](#usage). ## Documentation The [documentation source](docs/index.md) is published at [dario.cat/zas](https://dario.cat/zas/), with focused guides to configuration, templates, plugins and publishing. Hosting the guides there does not change the module path `github.com/darccio/zas`. Docs changes on `master` and published releases request a pinned source update in the site repository. A reviewed site pull request must be merged before Cloudflare Pages deploys the update. Configure `SITE_DISPATCH_TOKEN` as described in [the dispatch workflow](.github/workflows/docs-dispatch.yml); without it, the workflow reports a notice and skips dispatch. ## Usage Install: ```sh go install github.com/darccio/zas/cmd/zas@latest ``` Go to your site's directory and do: ```sh zas init ``` Zas will create a `.zas` directory with sane defaults, including a starter `.zas/layout.html` - replace it with your own whenever you like. ```sh zas ``` Yes. Enough. Your delightful site is on .zas/deploy. Enjoy. What is happening here? Well, Zas calls the `generate` subcommand by default. This subcommand accepts the following flags: * `-verbose`: print ALL the things! * `-full`: generate all the input files. By default, it has an incremental mode that keeps source and deploys directories in sync - it also picks up changes to `layout.html`, `config.yml`, `i18n.yml`, and any `.zas.yml` in a page's own directory tree, not just the page's own source, and it follows a page's (or `layout.html`'s own) `` targets too, recursively through further `Markdown`/`Html` embeds. Two narrower gaps remain: an `` whose `src` is itself a template action can't be resolved without running the page's own template, and an `mzs*` MIME type plugin's `src` file is tracked but whatever else the plugin reads isn't - use `-full` after editing either of those. ## Configuration and extension Zas is like water. It can flow, or it can cr... Nah, Zas doesn't crash (please file an issue if it does). Everything is configurable at .zas/config.yml. It is initialized with default values the first time you run `zas init`; running it again leaves an existing config.yml (and layout.html) alone unless you pass `-force`, which overwrites both with their defaults. You can override the `site` config section in two ways: 1. HTML comment in files (most precedence). 2. `.zas.yml` file at the directory level. Its scope is its directory and subdirectories (until another `.zas.yml` is found). By default, dot-files and dot-directories (anything whose name starts with `.`) are skipped entirely, at any depth - `.git`, editor swap files, and so on. A site that genuinely needs a specific top-level dot-directory published - `.well-known/`, for example, which browsers and ACME clients expect to find at a site's root - can opt it back in with `allowed_dotdirs` under the `zas` section: ```yaml zas: allowed_dotdirs: [".well-known"] ``` Only an exact, top-level match is honored: no prefix or glob matching, and a dot-directory nested anywhere - including inside an allowed one - still gets skipped. `.zas` and `.git` can never be allowlisted this way, no matter what's listed in config. Here's every key Zas itself understands in `config.yml`, together in one place (a real site's own file will typically be much shorter, since every one of these has a default and nothing here is required): ```yaml zas: layout: .zas/layout.html # default: .zas/layout.html deploy: .zas/deploy # default: .zas/deploy allowed_dotdirs: [".well-known"] # default: none - see above site: baseurl: https://example.com # default: http://example.com - see {{.Site.BaseURL}} below language: en # default: en - see {{.Language}} and I18N below image: https://example.com/og-image.png # default: unset - see {{.Site.Image}} below sitemap: true # default: false - see "Sitemap generation" below page_listings: true # default: false - expose static page lists to templates feed: true # default: false - see "RSS feeds" below feed_title: My articles # default: site.title; required when feeds are enabled feed_description: New writing # default: site.description, then feed title feed_sections: [posts] # default: all directories, with conservative article selection feed_limit: 50 # default: 50; positive integer, applied independently per feed feed_per_language: true # default: false; discover non-default article languages feed_strict: true # default: true; invalid selected articles fail generation mimetypes: text/markdown: markdown # default text/plain: plain # default text/html: html # default text/yaml+myplugin: myplugin # example custom MIME type plugin - see below ``` This is illustrative, not exhaustive of every key a *plugin* might read from its own section: plugins are free to define and read their own config (see "Beware" under Plugins below), and `config.yml` will happily carry whatever additional sections they need. To extend Zas functionality, you can use and create plugins. You can develop them in any language (not only in Golang) thanks to Unix magic. And more gophers. ### Plugins Any prefixed by `zs` or `mzs` is a potential Zas plugin. All plugins are Zas subcommands. For example, we invoke an imaginary plugin called `zshello` as a subcommand: ```sh $ zas hello Hello! $ zas hello World Hello World! ``` That's all. Zas passes any command-line argument after subcommand name to `zshello`. (The same `zshello` binary is also reachable from page content - see the script tag mechanism below.) **Beware:** Zas won't pass any configuration information. Plugins are responsible for reading configuration, even from directory and page levels. Helper libraries in different languages are welcome! Also, plugins are free to use `.zas` directory for their own needs. I recommend creating this directory's structure to avoid colliding issues: ```text .zas +-- plugins | +-- github.com | +-- darccio | +-- myplugin +-- ... ``` Any `zs` plugin can also be invoked from page content through a script tag with type `application/zas+myplugin`: ```html ``` The tag is deleted and replaced by whatever the plugin writes to stdout, as HTML. `data-args` supplies argv, split with shell-like quoting - `'single'` and `"double"` quotes group an argument containing spaces, and a backslash escapes the next character - but nothing here is ever handed to an actual shell, so none of `$`, `` ` ``, `*`, `~`, `#`, `|`, `;`, `&`, `<`, `>` are special; they reach the plugin literally. Unlike an ``, this tag's own inner content isn't parsed as HTML at all: ` ``` `json` marks only its freshly serialized output as a JSON expression for `html/template`, so the block contains an object rather than a quoted JSON document. Ordinary and pre-encoded strings remain JSON strings; construct objects with `dict` instead of passing raw JSON text. The serializer retains its default escaping of `<`, `>`, `&`, U+2028 and U+2029, including custom `json.Marshaler` and `json.RawMessage` values, so data cannot close the script element. Go types with custom JSON behavior remain responsible for their data's meaning. Use `json` for a complete JSON expression, outside any JavaScript quotes. HTML text and attribute contexts still receive `html/template`'s contextual escaping; the helper is not an HTML bypass. It is unavailable in a page's own `text/template` content and emits nothing unless a layout calls it. No JSON-LD schema or metadata is added automatically. ### But... I want to do pages beyond post-like format No problem! Just use our old friend ``. Imagine `` is a valid tag. ```html
``` What does it mean? It means you can have .html files with embedded markdown files. Or anything else supported by Zas. `navigation.md` here probably isn't meant to be its own page, only content embedded into others - so mark it with `publish: false` in its own config comment: ```markdown * [Home](/) * [About](/about.html) ``` Zas still reads and processes `navigation.md` normally wherever it's embedded, but it won't also show up on its own at `/navigation.html` in the deploy output. An ``'s `src` written inside a page's own body resolves relative to that page's own directory - the same way a relative ``/`` would once the page is deployed and viewed in a browser. So a page at `section/index.html` embedding `` looks for `section/navigation.md`, not a `navigation.md` at the site root, even if one happens to exist there too. A leading `../` is allowed as long as it still lands inside the site: `` from `section/index.html` reaches a `shared.md` at the site root, but nothing can be made to resolve outside the site root itself, however many `../` it uses. `` also works directly inside `layout.html` itself, not just inside a page's own body - useful for something every page shares, like a site-wide footer. This case is different: `layout.html` is a single, fixed file shared by every page rather than page content that lives in a particular directory, so an embed written there always resolves relative to the site root, regardless of which page is currently being rendered. Note that this is specific to Zas's own built-in embed handlers (`Markdown`, `Plain`, `Html` - the ones `mimetypes:` maps a `text/*` type to by default). An `mzs*` MIME type plugin (see "MIME type plugins" above) still receives its `src` argument exactly as written in ``, with no resolution applied at all - the plugin decides for itself how to interpret it. #### A note on output vs. source Every page goes through Zas's HTML5 parser twice: once on its own, to extract its body and settings, and once more after the layout wraps it, so any `` in the layout itself gets its turn too. Both passes re-serialize what they parse, and HTML5's parser is lenient by design - it repairs markup as it goes rather than rejecting it - so deployed output can differ mechanically from what you wrote: attributes get quoted, tag names get lowercased, void elements like ``/`
` get self-closed, stray `&` characters get entity-escaped. Nothing is lost, and this is also why the embed mechanism above can splice arbitrary snippets together reliably - but don't expect deployed HTML to be a byte-for-byte copy of your source. One consequence of that first, page-only parse: HTML5 places a `