Static Site Generator for Elitist

Migrated my blog from WordPress to "home grown" SSG, at long last.

Prior art

SSGs are nothing new.

  1. State of the Web: Static Site Generators [Hacker News] / 2022-01: discussion of purpose, features, cripping featuritis, various SSGs, alternatives… and everyone building their own SSG.
  2. Replacing my Octopress blog with 200 lines of Babashka [Hacker News] / 2021-10: discussion of ecosystem problems, tooling/scripting, Markdown, (YAML) Front Matter, first principles — composability, interoperability/portability, tool independence, lowest-common-denominator…
  3. Jamstack is all about static sites: directory lists hundreds of generators.
  4. Static Site Generators: one big table, lists 460 SSGs.


What I deem essential.

  1. Format: HTML. Markdown would just add the proverbial problem. I'm already comfortable (heavy use of snippets and code folding) editing HTML in Geany. And, my content is increasingly en"tangled" with my custom tooling (which, someday, should do proper WYSIWYG).
  2. Scaffolding
  3. Redirections to canonical URLs
    1. Drop dates from URLs. And trailing slashes.
  4. Automation
  5. Templates
    1. Navigation: just breadcrumbs — to front page, and tag index (if came from there ;o). <nav> at top of page, before <h1>.
    2. Metadata: dates, tags
    3. Motto, signature, credits
  6. Indexes
  7. Front page
  8. Versioning
  9. 404 page
    1. Bit rot.
  10. RSS?
  11. SEO stuff?


Features I gave up?

  1. Search (full text): WP relied on MySQL's, IIRC. Web search engines will do instead, I guess.
  2. Comments: spam was unbearable, like four orders of magnitude more than legit comments, unless effort put into filtering — Bayesian, rules, training… Wasn't worth it.
  3. Admin (UI): I manage (tasks, configuration) with my "in house" apps already; WP offers me nothing useful.

"Clean" URLs

  1. "Clean" meaning no ".html" extensions, no trailing slash.


Fight link rot, SEO…



If server supports it, respond to everything else with proper "404 Not found", and provide optional document body.

  1. Apache: Custom Error Responses. With SSI. Ha.


After Apache

Eventually, I'll replace Apache, with nGinx, Lighty, or some such, so worried about corresponding features.


I thought UTF-8 was the default?

  1. Ha, not default: UTF-8 is the only valid encoding for HTML5 documents.
  2. Firefox complains:
    The character encoding of the document was not declared, so the encoding was guessed from content. The character encoding needs to be declared in the Content-Type HTTP header, using a meta tag, or using a byte order mark.
  3. Didn't complain on localhost — I guess http-server sets that header? Indeed.
  4. Nu, but Apache doesn't, so I had to:
    # Set charset in HTTP header Content-Type.
    AddDefaultCharset utf-8
    AddCharset utf-8 .html .css .js .map


defer, async, ready… race condition

WP had <script>s loaded at end of <body> (and stylesheets in <head>), with its own dependency ordering thing. I now use "defer" as an optimization… but ran into a race condition!

Local(host) fallback (for CDN)

When developing on a portable computer with flaky Wi-Fi, but also generally, applying Leslie Lamport's definition to the web, and wanting to both eat the CDN cake and keep it… Expect computers to fail.

CSS Grid?

  1. Grid is neat! Look how concisely it renders <dl> of metadata in footer into a table:
    display grid
    grid-template-columns min-content auto // Two columns.
    column-gap 1ex // Gutters.
    margin 0 // Cancel indentation.
    content ":" // Delimiter stylistic, not content.
  2. But, for page layouts (multiple, responsive)…
    1. DOM's a straightforward sequence of title, timestamp, synopsis, table of contents, chapters… and footers.
    2. Ended up using oldest trick — float:
      @media (min-width 56em)
      aside // Sidebar.
      float right
      position sticky
  3. Sidebar: sticky and full height




Over existing WP site installed on AWS Lightsail using Bitnami…

(Plans for later: get rid of WordPress, Apache, Bitnami, Lightsail, Amazon… ;o)


  1. Where? Apparently, Apache stuff is in /opt/bitnami/apache2/{conf,htdocs}/. Except it serves files (document root) from /home/bitnami/apps/wordpress/htdocs/ instead. (I $#@! hate Apache.)
    1. /opt/bitnami/apache2/conf/httpd.conf says:
      ServerRoot "/opt/bitnami/apache2"
      DocumentRoot "/opt/bitnami/apache2/htdocs"

      Include "/opt/bitnami/apache2/conf/bitnami/bitnami.conf"
    2. … and that bitnami.conf:
      # Bitnami applications installed with a prefix URL (default)
      Include "/opt/bitnami/apache2/conf/bitnami/bitnami-apps-prefix.conf"
    3. … and bitnami-apps-prefix.conf:
      # Bitnami applications installed in a prefix URL
      Include "/opt/bitnami/apps/wordpress/conf/httpd-prefix.conf"
    4. … and that httpd-prefix.conf:
      # App url moved to root
      DocumentRoot "/opt/bitnami/apps/wordpress/htdocs"
      That explains.
    5. Except, same content as ~/apps/wordpress/htdocs/, but not seeing symlinks?
      $ ls --directory --inode ~/apps/wordpress/htdocs/
      401369 /home/bitnami/apps/wordpress/htdocs/
      $ ls --directory --inode /opt/bitnami/apps/wordpress/htdocs/
      401369 /opt/bitnami/apps/wordpress/htdocs/
      Nu, they're hardlinked. Way to go Bitnami.
  2. .htaccess — is disabled.
    <Directory />
    AllowOverride none
    1. Somehow WordPress works without it? Yeah, httpd-app.conf (that's included by httpd-prefix.conf):
      <Directory "/opt/bitnami/apps/wordpress/htdocs">
      RewriteEngine On
      RewriteRule ^index\.php$ - [S=1]
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteCond %{REQUEST_FILENAME} !-d
      RewriteRule . index.php [L]
      Which seems to redirect (internally, not HTTP) anything (".") — except index.php, and existing files and directories (REQUEST_FILENAME: full local filesystem path to the file or script matching the request) — to index.php, WP's main script.
    2. Q&D hacked it…
      RewriteRule ^(index\.php)?$ /elitist/ [last,redirect=temp]
      RewriteRule . /elitist/ [last,redirect=permanent]
  3. Configurations cached by Apache, so need to make it reload, by killing it:
    sudo /opt/bitnami/ restart apache
  4. After too much finagling, as usual with Apache, it works — except I might want a static 404.html instead of that catch-all and silent redirect.


Too many configuration changes, and, nu… Apache.

  1. Files hacked? Just the one:
    1. And the usual quarrelling with mod_rewrite — lots of iterations.
    2. SFTP in Thunar eases the pain. A little.
  2. HTTP→HTTPS: [301]
  3. no-www
    1. Cf discussion of www prefix on Wikipedia→WWW
  4. Root/default/front page
    1. /→/elitist/ [302]
    2. /elitist/index→/elitist/ [301]
  5. No ".html" extensions: /elitist/slithy.html→/elitist/slithy [301]
  6. Respond to all WP related URLs with "410 Gone".
  7. SEO redirections map
    1. /elitist/2014/07/coffeecup/→/elitist/coffeecup [301]
    2. /2014/07/coffeecup/→/elitist/coffeecup [301]


  1. SSH to server:
    $ ssh -i LightsailDefaultKey-region-1.pem
    Noisy, and in the way of, eg, using sftp:// in Thunar.
    # Elitist on AWS
    Host elitist
    User bitnami
    IdentityFile ~/.ssh/LightsailDefaultKey-region.pem
    IdentitiesOnly yes


  1. Naïvely (flags: batch mode (ie, never ask for credentials), compressed, preserve attributes, recursively):
    scp -BCpr dist/elitist elitist:/home/bitnami/apps/wordpress/htdocs/
    Works, fast xfer, but loop annoyingly slow, I guess synchronous filesystem stuff, so… be nice, Rsync.
  2. Wasn't installed!
    $ ssh elitist rsync --version
    bash: rsync: command not found
    Nu. SSH, and:
    # apt install rsync
  3. After --dry-run, to weed out several unsupported options (--mkpath, --crtimes, --open-noatime), added this to package.json (so can "npm run deploy"):
    "scripts": {
    "deploy":"rsync --recursive --links --safe-links --times --delete --compress --progress --exclude='.fuse_hidden*' dist/elitist elitist:/home/bitnami/apps/wordpress/htdocs/",


The real world is a special case