How-tos  Scripts  Pricing  Testimonials  Support  Newsletter

Tested on OpenBSD 6.3, 6.4, and macOS 10.14 with lowdown and

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

ssg is a static site generator written in shell. Optionally it converts Markdown files to HTML with lowdown(1) or

Unless a page has <HTML> tag ssg5 extracts its title from <H1> tag, wraps the page with _header.html, _footer.html.

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

ssg4 180 LoC. Enlarge, enhance, zoom!


On OpenBSD:

$ mkdir -p bin
$ ftp -Vo bin/ssg5
ssg5       100% |*********************|    4916      00:00
$ chmod +x bin/ssg5
$ doas pkg_add lowdown
quirks-2.414 signed on 2018-03-28T14:24:37Z
lowdown-0.3.1: ok

Or on macOS:

$ mkdir -p bin
$ curl -s > bin/ssg5
$ curl -s > bin/
$ chmod +x bin/ssg5 bin/

lowdown(1) and are optional. They are required only if there are any *.md files.


Make sure ssg5 and lowdown or are in your $PATH:

$ PATH="$HOME/bin:$PATH"
$ mkdir src dst
$ echo '# Hello, World!' > src/
$ echo '<html><title></title>' > src/
$ bin/ssg5 src dst 'Test' 'http://www'
[ssg] 1 files, 1 url
$ find dst
$ open dst/index.html

Markdown and HTML files

HTML files from src have greater priority than Markdown ones. ssg5 converts Markdown files from src to HTML in dst and then copies HTML files from src to dst. In the following example src/a.html wins:

src/   -> dst/a.html
src/a.html -> dst/a.html


Make sure you have /favicon.png in place.

Some browsers fetch /favicon.ico despite what you specified in the <LINK> tag, so you can use an empty one (180 bytes) as a placeholder.


ssg5 generates sitemap.xml with the list of all page. Don’t forget to add absolute URL of the sitemap to your robot.txt.
For example:

user-agent: *


To generate RSS feeds use rssg, then add their URLs to _header.html.
For example:

<link rel="alternate" type="application/atom+xml" href="/rss.xml">

Incremental updates

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

$ bin/ssg5 src dst 'Test' 'https://www'
[ssg] no files, 1 url

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

$ rm dst/.files
$ bin/ssg5 src dst 'Test' 'https://www'
[ssg] 1 file, 1 url


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

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

Install entr(1):

$ doas pkg_add entr
quirks-2.414 signed on 2018-03-28T14:24:37Z
entr-4.0: ok

Start the helper and keep it running:

$ ~/bin/s /var/www/htdocs/www
[ssg] 1 file, 1 url

Users — obviously ;)

Thanks to Devin Teske for helping with awk(1), Kristaps Dzonsons for lowdown(1), and Eric Radman for entr(1).

© 2008–2019 Roman Zolotarev  User Agreement  Privacy Policy