How-tos  Scripts  Pricing  Testimonials  Support  Newsletter

This version of ssg has been retired. Check the current version.

If you use relative links on your pages (as I do), rss.xml won’t render them properly.

Tested on OpenBSD 6.3

Make a static site with lowdown(1) and rsync(1)

ssg1 is a static site generator written in shell and powered by lowdown(1), rsync(1), and entr(1).

It generates a site from HTML and Markdown articles.

  1. It copies the current directory to a temporary one, skipping .* and _*,
  2. renders all Markdown articles to HTML,
  3. generates RSS feed based on links from index.html,
  4. extracts the first <h1> tag from every article to generate a sitemap and use it as a page title,
  5. then wraps articles with a single HTML template,
  6. copies everything from the temporary directory to $DOCS/.

ssg1 240 LoC. Enlarge, enhance, zoom!

Why not Jekyll or “$X”?

ssg1 is one hundred times smaller than Jekyll.

ssg1 and its dependencies are about 800KB combined. Compare that to 78MB of ruby with Jekyll and all the gems. So ssg1 can be installed in just few seconds on almost any Unix-like operating system.

Obviously, ssg1 is tailored for my needs, it has all features I need and only those I use.

Keeping ssg1 helps you to master your Unix-shell skills: awk, grep, sed, sh, cut, tr. As a web developer you work with lots of text: code and data. So you better master these wonderful tools.


100 pps. On modern computers ssg1 generates a hundred pages per second. Half of a time for markdown rendering and another half for wrapping articles into the template. I heard good static site generators work—twice as fast—at 200 pps, so there’s lots of performance that can be gained. ;)


If you agree with the license, feel free to use this script, its HTML and CSS or/and re-write them for your needs.

Install dependencies and download ssg1. For example, on OpenBSD: as root install rsync(1), lowdown(1), and entr(1).

# pkg_add rsync-3.1.3-iconv lowdown entr
quirks-2.414 signed on 2018-03-28T14:24:37Z
rsync-3.1.3-iconv: ok
lowdown-0.3.1: ok
entr-4.0: ok
The following new rcscripts were installed: /etc/rc.d/rsyncd
See rcctl(8) for details.

Then as a regular user change into ~/.bin directory.

$ cd ~/.bin
$ ftp
100% |****************************************|  7257       00:00
7257 bytes received in 0.00 seconds (2.99 MB/s)
$ chmod +x ssg1

Let’s customize your ssg1 setup.


To configure ssg1 you need to set two variables:

There are three more variables, but these are optional:

You can set all those variables in enviornment with export or env, but I recommend to create _ssg.conf file. For example, here is mine:

: "${DOCS:=/var/www/htdocs/}"
WEBSITE_TITLE='Roman Zolotarev'
RSS_AUTHOR=' (Roman Zolotarev)'
RSS_DESCRIPTION='Personal website'

Note: in this example if $DOCS is set, then ssg1 uses the original value, not the value from _ssg.conf.

Required files

There is only one file required:

  1. index.html or - home page

Example of

# Jack

- [About](/about.html "01 Aug 2016")

ssg1 renders to index.html and then generates the RSS feed based on first 20 links, if they have the following syntax (it only uses page URL and date from <a> tag):

<li><a href="/about.html" title="01 Aug 2016">About</a></li>

Optional files

  1. _header.html - header of every page
  2. _footer.html - and its footer
  3. _styles.css - styles, take mine and customize

If you use my CSS, don’t forget to wrap the content of _header.html into <div class="header>...</div> and the content of _footer.html into <div class="footer>...</div>.

Reserved file names

There are also reserved filenames, these files are generated when you run ssg1 build. Don’t use these names.

  1. rss.xml - reserved for RSS feed
  2. sitemap.xml - for the sitemap

Your first page

Let’s create about.html with one header and some text about your site.

# About this site


ssg1 converts all .md article into .html and then uses content of the first <h1> tag as a page title.

Nota bene: Don’t use ===== in titles.


Now we are ready to build. If your current source directory looks like this:

|-- .git/
|-- _footer.html
|-- _header.html
|-- _styles.css

After you run ssg1 (don’t forget to set $DOCS):

$ ssg1 build
building /var/www/htdocs/www  2018-04-10T10:56:52+0000 4pp

You have your static website ready in /var/www/htdocs/www.

|-- about.html
|-- index.html
|-- rss.xml
`-- sitemap.xml


For OpenBSD I suggest to run httpd locally.

For macOS and Linux you can run:

$ cd /var/www/htdocs/www
$ python -m SimpleHTTPServer
Serving HTTP on port 8000...


To re-build pages on change run:

$ ssg1 watch
watching /home/jack/src/www
building /var/www/htdocs/www  2018-04-10T11:04:11+0000 4pp

entr(1) watches changes in *.html, *.md, *.css, *.txt files and runs ssg1 build on every file change.


If you’d like to delete all files in the destination directory during build, then run:

$ ssg1 build --clean
building /home/jack/src/www/docs --clean
2018-04-16T09:03:32+0000 4pp

The same option works for watching.

$ ssg1 watch --clean
watching /home/jack/src/www
building /home/jack/src/www/docs --clean
2018-04-16T09:04:25+0000 4pp

Deploy with rsync(1)

If you don’t have a public server yet, try Vultr. To deploy to remote server you can use rsync(1) like this:

$  rsync -avPc     /var/www/htdocs/www \

Or if you want to clean up the target directory on the remote server use:

$ rsync -avPc --delete-excluded \
                /var/www/htdocs/www \

Deploy with Git post-receive hook

Set up Git on your server.

As root install rsync(1) and lowdown(1) packages on that server.

# pkg_add rsync-3.1.3-iconv lowdown
quirks-2.414 signed on 2018-03-28T14:24:37Z
rsync-3.1.3-iconv: ok
lowdown-0.3.1: ok
The following new rcscripts were installed: /etc/rc.d/rsyncd
See rcctl(8) for details.

Then as git user download ssg1 on the server:

# cd /home/git
# su git
$ mkdir -p /home/git/bin
$ cd /home/git/bin
$ ftp
100% |****************************************|  7257       00:00
7257 bytes received in 0.00 seconds (2.99 MB/s)
$ chmod +x ssg1

Then add these lines to /home/git/REPOSITORY.git/hooks/post-receive:

TMPDIR="$(mktemp -d)"
git archive --format=tar HEAD | (cd "$TMPDIR" && tar xf -)
cd "$TMPDIR"
DOCS='/var/www/htdocs/' \
/home/git/ssg1 build --clean

As root make sure git user owns $DOCS directory:

# chown -R git:git /var/www/htdocs/

Thanks to Denis Borovikov for reading the draft of this, h3artbl33d, and Mischa Peters, and Tom Atkinson for testing ssg1, Kristaps Dzonsons for lowdown(1) and Eric Radman for entr(1).

© 2008–2019 Roman Zolotarev  User Agreement  Privacy Policy