1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
|
# 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 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.
### 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 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:
```
/
/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 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`*
#### functions
- `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.
### 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 <code>\`</code> 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`: 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.
## contributing
Comments, bug reports, and merge requests are welcome!
[Send me an email](mailto:vienna@code.acdw.net) or
[contact me on Mastodon](https://tilde.zone/@acdw).
## license
`vienna` is licensed under the Good Choices License. See COPYING for details.
|