vienna
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 defaultout
.-c FILE
: Use FILE as the configuration file, instead of.vienna.sh
.-h
: Show a usage note and exit.-q
: Disable any logging.
subcommands
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 apreview
function, so you’ll want to write your own. I’ve included one in the.vienna.sh
made withvienna init
to get you started.publish
: Publish the website to your production server. By default,vienna
doesn’t define apublish
function, though I do include a sample in the default.vienna.sh
made withvienna 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:
/
/some-page.htm
/some-other-page.htm
[...]
/.vienna.sh
/.page.tmpl.htm
/.index.tmpl.htm
/.feed.tmpl.htm
/.plugins/[...]
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.sh
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
variables
$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 wherevienna
will put the built site. Default:out
$PLUGINDIR
: The directory where plugins can be found and sourced byvienna
. 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
functions
publish
: How to publish a finished site. By default, this function does nothing.vienna init
defines a basicpublish
function in.vienna.sh
.preview
: How to preview a site locally. By default, this function does nothing.vienna init
defines a basicpreview
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 usesphtml
, but in my personal site I also pipe it throughexpand
so I can use shell expansion as well.
templates
.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
.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!
content
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.
pages
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
:par
s#([^\\])&#\1\&#g # Replace & with &
s#([^\\])<#\1\<#g # Replace < with <
s#([^\\])>#\1\>#g # Replace > with >
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%
increase
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 eval
s 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
: Runexpand
on the content after it’s processed byphtml
.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.
contributing
Comments, bug reports, and merge requests are welcome! Send me an email or contact me on Mastodon.
license
vienna
is licensed under the Good Choices License. See COPYING for details.