chai2

boring static site generator


chai2 can generate simple, paginated image galleries. It allows tagging images, and generates sub-sites for all tags. It's not really opinionated, and easily extensible. Without source code hacking, it allows for quite a lot of customization in the configuration file. For the lack of a better choice, the configuration language is the same as implementation language (it's not yaml thank you for asking though).

design choices

There's not much one can write about the design choices of 200 lines of code, yet I'll try. chai2 tries to be as much out of the way as possible. After initial configuration, adding images is as simple as executing the generator with source image and it's tags as arguments. It tries to mimic prahou's imgram in what it gives to the user by default. I think the design of imgram is good. As in, having the same script be the interface to adding images and the actual site generator is great. I've adapted the idea to my needs (sexp configuration file, more lambdas, more intellectual masturbation with combinators - you name it!). The only thing that left me uneasy looking at how imgram works was how data was stored. As far as i can tell, pre-generated HTML pieces are stored in a data directory. This is probably done for speed, and because the korn shell (or any login shell people really use) is not good at reading/writing structured data such as posts ↔ tags relations. I'm not willing to get proven wrong. I know it's possible and probably also very ugly.

The database solution I've decided to pick is a fasl-encoded association list storing key-value pairs of file-idmetadata. metadata is an association list with keys including, but not limited to:

I've decided to store the database as a fasl-encoded association list, and not a finite function so it would be easier to read it with other software. Because a virtual machine is not needed to unpack fasl encoded lists, it's much easier to interpret a database that uses alists instead of finite functions.

configuration

By default, configuration is stored in chai.cfg. This configuration file should contain a single (define-config ...) expression. The format is as follows:

(define-config
  key1 => value1
  key2 => value2
  ...)

Every value after the `=>' token will be evaluated in the *toplevel* as an Owl Lisp expression. The values may be of any lisp type. Most config keys have sane defaults. Some config keys require Owl functions as values, they definitely have defaults. Values you can set in the configuration file are as follows:

key description
id-length How long a picture's randomly generated id will be. A number, preferably.
database Location of the database. Relative to PWD. A string, preferably.
www-root Location of the public WWW root. Relative to PWD. A string, preferably.
host Your webpage's hostname with the protocol. This will be used while generating rss. Examples include "https://example.com", "http://i-dislike-tls.net/here/are/images". A string, preferably.
items-per-page How many pictures should a single page hold. A number, preferably.
rss-description Description of the RSS feed. A string, preferably.
css Linked css file. This will be inserted to the generated page as a <link> element, so it should be relative to www-root. Note that tag directories will link this file as well, so it should be an absolute path, for example "/styles.css", or "/static/something.css". A string, preferably.
rss Linked rss file that will be generated every rebuild. Relative to www-root. Should follow same guidelines as css. A string, preferably.
name Webpage's name. A string, preferably.
default-tags List of default tags that will be set, when an image is not given any tags. Preferably a list of symbols.
display-time-function A function of posix timestamp -> robusta HTML expression. This should render the posix timestamp in a desired format.
html-heading A function of title -> robusta HTML expression. This should render the title in a desired format.
minimize-image-function Function of filename -> filename. This function may minimize a file filename and return the new file name. It should not remove the old file. The new (returned) filename will be served as the thumbnail.

REPL

chai2 includes a REPL used for editing the database. You can enter the REPL by using the -e or --edit flag. You will be greeted by a very non-user-friendly prompt:

? 

This is the toplevel, it accepts commands as follows:

The edit mode queues edits for applying with the save command. If you choose not to commit the changes, just use the quit command. The remove command is not queued, but it does ask for confirmation. All commands accepted by the edit mode:

Note that the REPL will not remove image files from the disk. It only operates on the database. Images removed from the database will still be accessible, just not listed. chai2 will not rebuild your website automatically after applying changes to the database. Use the -r, --rebuild flag or the rebuild command to rebuild it manually.

running chai2

Get the latest source code from github

; git clone https://github.com/krzysckh/chai2.git
; cd chai2

After running make, possibly after configuring OL to your location of Owl Lisp, it will spit out a chai binary in current working directory. Save it somewhere safe. Good places include

Note that chai2 loads the configuration file on demand; every run. It may be specified with the -c, or --config-file flag.

To add an image to your site, run chai image-filename.extension tag1 tag2 .... Note a single image can use many tags as well as none. If no tags are specified, chai2 will automatically set them to default-tags defined in the config. By default, this is (unknown) (- a list of single tag, called unknown).

To rebuild the entire page, without adding additional images, run chai with the -r, or --rebuild flag. This will ensure all pages are up to date with the database. This should not be needed unless you touch the database with external software.

bonus content

touching the database with external software

unrambling the session was left as an exercise to the reader

fuji# ol
You see a prompt.
> (define db (fasl-load "chai.db" #f))
;; Defined db
> (define img (assoc 'jylqqggm db))
;; Defined img
> img
'(jylqqggm (filename . "jylqqggm.jpg")
  (filename-minimized . "jylqqggm.jpg-small.jpg")
  (timestamp . 1785152592) (tags tatry))
> (define img* (led img 4 (C append '(panopticon))))
;; Defined img*
> img*
'(jylqqggm (filename . "jylqqggm.jpg")
  (filename-minimized . "jylqqggm.jpg-small.jpg")
  (timestamp . 1785152592) (tags tatry panopticon))
> (define db* (filter (B not (B (C eq? 'jylqqggm) car)) db))
;; Defined db*
> (define db** (cons img* db*))
;; Defined db**
> (fasl-save db** "chai.db")
#true
> ,quit
fuji#
         

touching the database with the REPL

fuji# ./chai -e
? tag raj
[yehzzdym] (slovensky raj) https://img.krzysckh.org/yehzzdym.jpg-small.jpg
[uifnirjm] (slovensky raj) https://img.krzysckh.org/uifnirjm.jpg-small.jpg
[nuvpndom] (slovensky raj) https://img.krzysckh.org/nuvpndom.jpg-small.jpg
[uglevdkm] (slovensky raj) https://img.krzysckh.org/uglevdkm.jpg-small.jpg
[bzzduqlm] (slovensky raj) https://img.krzysckh.org/bzzduqlm.jpg-small.jpg
[mksozrym] (slovensky raj) https://img.krzysckh.org/mksozrym.jpg-small.jpg
[utnexfjm] (slovensky raj) https://img.krzysckh.org/utnexfjm.jpg-small.jpg
? pick bzzduqlm
bzzduqlm? tags
(slovensky raj)
bzzduqlm? set-tags slovensky raj panopticon
ok
bzzduqlm? tags
(slovensky raj panopticon)
bzzduqlm? save
saved
? rebuild
? quit
bye
fuji#

configuration for img.krzysckh.org

(define-config
  host                     => "https://img.krzysckh.org"
  name                     => "img.krzysckh.org"
  rss-description          => "from img.krzysckh.org"
  www-root                 => "public/"
  id-length                => 8
  database                 => "chai.db"
  rss                      => "/images.rss"
  css                      => "/style.css"
  items-per-page           => 20
  default-tags             => '(panopticon)
  minimize-image-function  => (λ (image)
                                (lets ((new-name (str image "-small.jpg")))
                                  (system (list "exiftool" "-GPS:all=" "-overwrite_original" image))
                                  (system (list "convert" (str image) "-resize" "640" new-name))
                                  new-name))
  display-time-function    => (λ (timestamp)
                                `((div (class . "timestamp")) (h4 ,timestamp)))
  html-heading             => (λ (tag-name)
                                `(h1 ((a (href . "/")) "/") " " ,tag-name))
  )

a screenshot

screenshot of https://img.krzysckh.org

llms were not used. spelling mistakes are my own.

- kpm