about summary refs log tree commit diff stats


a tiny, tasty ssg

vienna is my (current) Platonic Ideal of a simple and extensible static site generator. I’ve written a couple of these over the years, and I think I’ve finally got something that I really like. Anyway, lemme tell you how it works.

vienna invocation

vienna has a similar command-line interface to make; that is, it runs commands on the files in a given folder and produces output. It has command-line switches as well as subcommands. By default, vienna simply builds the pages in the current directory into the output directory.

command-line arguments

  • -r URL: Use URL as the root url for the site. The default is the useless https://example.com/, since you really should set this either on the command line, in the config file, or in an environment variable.
  • -C DIRECTORY: Build the site from pages in DIRECTORY, instead of the current directory.
  • -o DIRECTORY: Build the site to DIRECTORY, instead of the default out.
  • -c FILE: Use FILE as the configuration file, instead of .vienna.sh.
  • -h: Show a usage note and exit.
  • -q: Disable any logging.


  • init: Initialize a site by creating a .vienna.sh in the site directory.
  • clean: Delete the output directory and temporary files before building.
  • preview: Preview the website on your local machine. By default, vienna doesn’t define a preview function, so you’ll want to write your own. I’ve included one in the .vienna.sh made with vienna init to get you started.
  • publish: Publish the website to your production server. By default, vienna doesn’t define a publish function, though I do include a sample in the default .vienna.sh made with vienna init.

file layout

Pretty much any SSG is a mapping from input files to output files, with various amounts of processing along the way. Usually, the input files are in some text-ish format like markdown, the output files are in html, and processing includes templating, static file copying, and other such mixins.

vienna is similar, except it uses a mostly-html input language and uses POSIX shell as its templating and extension language. I chose these because they require the smallest number of dependencies, and because shell scripting is fun! [citation required]

But more about the markup later. Let’s talk about the structure vienna expects your files to be in:


The content files of your site are all of the ones not beginning with a dot, while all of vienna’s are the ones that are. In UNIX-like environments, this makes them “hidden,” so you can focus on your content by default. However, your content is not something I can talk about in this README, because you still have to write it! Let’s talk about vienna’s files next.


vienna is a POSIX shell script. Before it builds any pages, it reads a configuration file. By default, this file is .vienna.sh in your site folder, but you can make it anything you want by passing -c <config> on the command line or by setting the $VIENNA_CONFIG environment variable (more on the command line and environment variables below).

Because the configuration file is written in the same language as vienna itself, you can redefine any variable or function that’s in vienna, fully customizing your experience. The main knobs you’ll want to turn include


  • $DOMAIN: The domain (and, well, protocol) of your published website. Used when building indexes and feeds. Default: https://www.example.com
  • $OUTDIR: The output directory where vienna will put the built site. Default: out
  • $PLUGINDIR: The directory where plugins can be found and sourced by vienna. Default: .plugins
  • $PAGE_TEMPLATE: The template to use for pages. Default: .page.tmpl.htm
  • $INDEX_TEMPLATE: The template to use for an index. Default: .index.tmpl.htm
  • $FEED_TEMPLATE: The template to use for an RSS feed. Default: .feed.tmpl.htm


  • publish: How to publish a finished site. By default, this function does nothing. vienna init defines a basic publish function in .vienna.sh.
  • preview: How to preview a site locally. By default, this function does nothing. vienna init defines a basic preview function in .vienna.sh.
  • sort_items: How to sort items when generating a list. By default, don’t sort files at all.
  • index_item: How to build each item in an index. The default outputs <li><a href=[LINK]>TITLE</a></li>, where TITLE is the page’s title and LINK is a link to the published page.
  • feed_item: How to build each item in a feed. The default outputs an RSS <item> tag suitable for inclusion in an RSS 2.0 feed.
  • filters: UNIX pipes that each page is run through as it’s being built. The default just uses phtml, but in my personal site I also pipe it through expand so I can use shell expansion as well.


  • .page.tmpl.htm: The template for individual pages.
  • .index.tmpl.htm: The template for the root index.html.
  • .feed.tmpl.htm: The template for the RSS feed.


  • .plugins/*.sh: Files matching this pattern will be sourced before building the site. They can define new functions, export variables, or do whatever, so make sure you know what’s in these files!


Now for the stuff you’re in charge of: your content. vienna takes a flat- or no-hierarchy approach to sites. Your site will just be one flat folder full of pages, though each page will be its own folder with an index.html for nicer urls. This might not be for you, and that’s okay! You can change it (complicated) or use another ssg (probably easier). It’s up to you.


By default, files matching the pattern *.htm will be processed by vienna and turned into finished pages. .htm was chosen because it’ll be picked up as html in most text editors (for the format they’re written in, see the phtml section below), but it’s also unfinished html (get it?!).

You can change this—you can change anything with vienna—but I’ll leave that as an exercise for you to figure out.

static files

Every other non-hidden file in the vienna folder will be copied as-is to the output folder.

phtml: pretty much html

While lightweight markup languages like Markdown are nice (I’m using it to write this README, for example), I realized that for my blogging needs it’s not really necessary. html is pretty much good enough for authoring, if I’m being honest—it’s just a little too verbose for fully-fluent drafting.

Thus, phtml was born. It’s a function in the vienna source that consists of one sed call:

sed -E '
    /./ {H;$!d}; x              # Hold lines til empty, then exchange to pattern
    s#^[ \n\t]+([^<].*)#\1#     # Replace non-HTML paragraph with itself
    t par; b end                # If successful, branch to :par; else :end
        s#([^\\])&#\1\&amp;#g   # Replace & with &amp;
        s#([^\\])<#\1\&lt;#g    # Replace < with &lt;
        s#([^\\])>#\1\&gt;#g    # Replace > with &gt;
        s#\\([&<>])#\1#g        # Replace \-escaped &,<,> with raw
        s#.*#<p>&</p>#          # Wrap the pattern space with <p> tags
    :end                        # [:par falls through to :end]
        s#^[ \n\t]+##           # Remove leading whitespace
        $!a                     # Add a final newline unless last line

To clarify the comments above: phtml allows a writer to leave out <p> tags, which I consider are the most annoying parts of html. Paragraphs that don’t begin with < are wrapped in <p> tags, and html reserved characters <, >, and & are turned into entities.

You can still write html, of course—either by backslash-escaping the reserved characters or by starting a paragraph with an html tag (really, the character <). Those paragraphs are passed through unprocessed.

This rule may seem as though it negates the benefits of leaving out <p> tags. After all, you might think, if I want to add a link or even emphasize text, I’ll have to escape the tags or wrap the whole thing in html! While you’d be right about that, I use plaintext paragraphs enough that it’s worth it.

expand: templating with here-docs

Here-docs are some of the most useful structures in any programming language, and in shell they can be especially powerful. I first came across using here-docs for templating on some Github repo I’ve since lost, but there are many other projects that do something similar. expand is pretty minimal, and uses here-docs and a little bit of escaping to provide lots of expressive power.

A template that looks like this:

Donuts now cost $$${CURRENT_DONUT_COST}, a $$(donut_perc_delta)% 
$$(if [ $$(donut_perc_delta) -ge 0 ]
then echo increase
else echo decrease
) from last week.

will turn into this, given that donuts currently cost $1.99 and $1.89 last week (donut_perc_delta truncates percentages to two digits):

Donuts now cost $1.99, a 5%
from last week.

Single $ and ` characters are escaped with backslashes so they won’t be expanded by the shell. Two $$ are converted to one $, and three $$$ in a row are converted to \$$$, since usually you’ll want the second pair of dollar signs to introduce a variable or function, not the first pair.

Other than that, expand simply evals the templates given on the command line as here-docs. Pages to be templated are passed into the function’s standard input, and everything just works. It’s pretty cool!

page metadata

vienna also supports colon-separated metadata in html comments in source files. I put something like the following at the tops of my pages:

    title: My really cool page
    date: 2023-01-01

vienna processes these as simple key-value pairs, accessible with the meta function. So in templates, you can access the title with $$(meta title), or the value of a key called foo with $$(meta foo).

vienna provides convenience functions title for the page title and pubdate for its date.

tweaking the behavior of phtml and expand (without re-writing the functions)

Of course, you can always rewrite phtml or expand to suit your needs, or rewrite filters to use other processers like markdown, asciidoc, or whatever you desire. However, for the “vanilla” experience, vienna includes a variable that tweaks the behavior of phtml: $PHTML_OPTIONS.

$PHTML_OPTIONS can be one or more of the following values, separated by spaces:

  • expand: Run expand on the content after it’s processed by phtml.
  • entities: Convert &, <, and > into html entities.

The default is expand entities.

using plugins

vienna also supports the use of plugins, which are shell scripts sourced before building the site. Plugins are placed in $VIENNA_PLUGINDIR, or .plugins by default, and must end in .sh to be sourced.

You can see the plugins directory of this repo for some example plugins that I found useful.

installing vienna

Clone this repository and copy or link vienna to somewhere in your $PATH. That’s what I do anyway.


Comments, bug reports, and merge requests are welcome! Send me an email or contact me on Mastodon.


vienna is licensed under the Good Choices License. See COPYING for details.