How-tos  Scripts  Pricing  Testimonials  Support  Newsletter

Tested on OpenBSD 6.3 and 6.4

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

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

Unless a page has <HTML> tag ssg4 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!


Download and chmod it:

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

lowdown(1) is optional. It’s required only if there are any *.md files.


$ mkdir src dst
$ echo '# Hello, World!' > src/
$ ftp -Vo src/_header.html
_header.html 100% |**************************|  3362       00:00
$ ftp -Vo src/_footer.html
_header.html 100% |**************************|   727       00:00
$ ftp -Vo src/favicon.png
favicon.png  100% |**************************|   408       00:00
$ bin/ssg4 src dst 'Test' 'http://www'
[ssg] 2 files, 1 url
$ find dst
$ open dst/index.html

Markdown and HTML files

HTML files from src have greater priority than Markdown ones. ssg4 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.


ssg4 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 ssg4 saves a list of files in dst/.files and updates only newer files. If no files were modified after that, ssg4 does nothing.

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

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

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


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

$ cat $HOME/bin/sssg
while :
    find . -type f ! -path '*/.*' |
    entr -d "$HOME/bin/ssg4" . "$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


Previous version of ssg has been retired.

Add <!DOCTYPE html>, <STYLE>...</STYLE> with your styles and an empty <TITLE></TITLE> tags to _header.html.

ssg4 captures page’s title from the first <H1> tag of the page and inject it into <TITLE>, if it’s present and empty.

Move _rss.html to _header.html, _styles.css to <STYLE> tag in _header.html, and _scripts.js to <SCRIPT> tag.

ssg3 ssg4
Builds 1,730 files in 8.54s in 5.43s
Contains basic HTML tags. Contains no HTML tags.
wc(1) is required. Doesn’t use wc(1).
List of feeds read from _rss.html, _rss.html,
styles from _styles.css, and _styles.css, and
scripts from _scripts.js. _scripts.js have been removed.


ssg4 depends on few programs from OpenBSD base:

$ for f in $(which cat cpio date sh awk find grep printf readlink sort tee)
do ldd "$f"
done | awk '/\//{print$7}' | grep '.' | sort -u

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