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 ssg3 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.

ssg3 214 LoC. Enlarge, enhance, zoom!


Download and chmod it:

$ mkdir -p bin
$ ftp -Vo bin/ssg3
ssg3       100% |*********************|    5025      00:00
$ chmod +x bin/ssg3
$ 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/
$ echo '<p><a href="/">Home</a></p>' > src/_header.html
$ echo '<p>2018 Roman Zolotarev</p>' > src/_footer.html
$ ftp -Vo src/_styles.css
style.css    100% |**************************|  1020       00:00
$ bin/ssg3 src dst 'Test' 'https://www'
[ssg] 1 file, 1 url
$ find dst
$ open dst/index.html

Markdown and HTML files

ssg3 renders Markdown files first and then HTML files. In the following example src/a.html wins:

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


Every page wrapped by ssg3 has this HTML.

<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/favicon.png">

So 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.


To inject JS to all pages add it to _scripts.js. I recommend to avoid JS whenever possible, though.


To inject CSS to all pages add it to _styles.css.
Use my _styles.css as a starting point.


ssg3 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 _rss.xml.
For example:

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

Incremental updates

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

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

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

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


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

$ cat $HOME/bin/sssg
while :
    find . -type f ! -path '*/.*' |
    entr -d "$HOME/bin/ssg3" . "$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 <HTML> tag for pages your want to be excluded from parsing. If you don’t use *.md files you can uninstall lowdown(1).

ssg2 ssg3
Wraps pages with <H1> tag. Doesn’t wrap pages with <HTML> tag.
lowdown(1) is required. lowdown(1) is optional.


ssg3 depends on few programs from OpenBSD base:

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

└─┐└─┐│ ┬

Users and forks — obviously ;)

Thanks to Kristaps Dzonsons for lowdown(1) and Eric Radman for entr(1).

© 2008–2019 Roman Zolotarev  User Agreement  Privacy Policy