support and follow me on Mastodon and Twitter

“I just found ssg! You are so damn cool. I love your approach to things.”

Derek Sivers
Entrepreneur and Book Publisher

"SSG by @romanzolotarev is an impressively small static site generator with a tiny installed footprint. Really good for when you just need the core features."
Simon Dann (@carbontwelve)

Tested on OpenBSD 6.3

Make a static site with find(1), grep(1), and lowdown(1)

ssg2 is a static site generator written in shell and powered by lowdown(1).

It converts *.md files to HTML.

If a page has <H1> tag ssg2 extracts its title, wraps it with _header.html, _footer.html, and injects _styles.css, _scripts.js, _rss.html into <HEAD>.

Then copies everything (excluding .*, CVS, and _*) from src to dst directory.

ssg2 212 LoC. Enlarge, enhance, zoom!


Download and chmod it:

$ ftp -Vo bin/ssg2
ssg2       100% |*********************|    4137      00:00
$ chmod +x bin/ssg2
$ doas pkg_add lowdown entr
quirks-2.414 signed on 2018-03-28T14:24:37Z
lowdown-0.3.1: ok
entr-4.0: ok


$ mkdir src dst
$ echo '# Hello, World!' > src/
$ echo '<p><a href="/">Home</a></p>' > src/_header.html
$ echo '<p>2018 Roman Zolotarev</p>' > src/_footer.html
$ ftp -Vo src/_styles.css
_styles.css  100% |**************************|  1020       00:00
$ ssg2 src dst 'Test' ''
[ssg] 4 files
$ firefox dst/index.html

Incremental updates

On every run ssg2 saves a list of files in dst/.files and updates only newer files. If no files were modified after that, ssg2 does nothing.

$ ssg2 src dst 'Test'
[ssg] ok

To force the update delete dst/.files and re-run ssg2.

$ rm dst/.files
$ ssg2 src dst 'Test' ''
[ssg] 4 files


Save this helper to ~/bin/s. It re-runs ssg2 with entr(1) on every file change.

$ cat ~/bin/s
while :
do find . -type f ! -path '*/.*' |
entr -d "$HOME/bin/ssg" . "$1" "$(date)" '//www'

Start it and keep it running:

$ ~/bin/s /var/www/htdocs/www
[ssg] ok


Previous version of ssg has been retired.

Add a wrapper for entr(1).
Delete _ssg.conf.
Add _rss.html(optionally).
Update run script and post-* git hooks.
Uninstall rsync(1), if you don't use it.

ssg1 ssg2
102 pp (31,306 words) 2.08s 1.61s
second run (+1 page) 1.92s 0.13s
rss.xml extracted to rssg
sitemap.xml same
convert MD to HTML same
wrap HTML pages same
get title from <h1> same
_header.html same
_footer.html same
_styles.css same
_scripts.js same
command line and env
env vars and _ssg.conf removed
  DOCS moved to 2rd argument
ssg build ssg2 src dst ...
ssg build --clean cd dst && rm -rf * .* && ssg2 ...
ssg watch cd src && find . | entr ssg2 ...
lowdown same
entr removed
rsync removed

└─┐└─┐│ ┬

Thanks to Mischa Peters for testing and using this version in production, Kristaps Dzonsons for lowdown(1), and Eric Radman for entr(1).