Documentation
Jopa [ˈʐopə]
is a very small, but powerful static website
generator written in bash. It uses a couple of conventions to make
writing stuff fun and easy.
The main idea is to [ab]use env variables to do both configuration of a
website and also its generation. Everything in Jopa world is a bash
script - including posts you write, layouts and, of course, ./jopa
itself.
To start using Jopa, you first need to make sure you have prerequisites
installed. Chances are you already have everything on your system. Jopa
uses bash >= 3
and envsubst
from the gettext-base
package. If these are not installed, go do, I'll wait.
Okay, now, when dependencies are ready, you need to grab the actual Jopa
code. You can grab the code directly from GitHub or just
copy-paste it from the code page. Put the code in a file
named jopa
and do not forget to make it executable:
chmod +x ./jopa
Now, you need to create layout.jsh
file, where the base layout for all
your pages will be defined. Something like below, I'll use the simplest
possible HTML5 page there and you can extend it later.
cat <<'EOF' > layout.jsh
website_title="My wonderful website"
multiline layout << 'JOPA'
<!doctype html>
<meta charset=utf-8>
<title>${title} | ${website_title}</title>
${content}
JOPA
EOF
Okay, let's jump to the fun stuff and create the first page of your.
future website The page will be located in pages/
directory, so we.
need to create one beforehand .
mkdir -p pages
cat <<'EOF' > pages/index.jsh
title="My first page"
multiline content << 'JOPA'
Hello world!
<br>
I am a happy Jopa user.
JOPA
EOF
Okay, we all set, let's just ask Jopa to do its work:
./jopa
Done! Jopa created your page in www
folder, you can now open it in
your browser:
open www/index.html
Congratulations! You are now an official Jopa blogger. You can now create more pages, edit example layout template, or jump into more advanced topics.
The idea of Jopa is extendability through simplicity. Everything in Jopa's world is a bash script, and almost everything is controlled through the use of environment variables.
This is done for you to be in control of what is going on and be able to
apply any logic at any step of site generation. You can do anything bash
scripts can, and modify your content as you wish - no need for
unnecessary plugins, libs, yadda-yadda-yadda. Just you and your shell -
grep
, cat
and cut
, anything.
But how does Jopa actually work?
Pages
First, it goes through the list of pages, specified in $pages
variable, and, to render them, does source
them as a bash script. This
leads the environment for further processing. The sourcing is done on a
per-page scope, to avoid interference.
By default, $pages
equals to pages/*.jsh
wildcard. The extension is
short for "Jopa shell", to differ your posts from regular shell scripts
(remember, everything is a script).
Pages can have any arbitrary logic, but most of the time they just
define metadata such as title, description and, obviously, the actual
page content. To make text easier to write, Jopa defines multiline
helper with an optional "filter" that can be used to transform content
from one format to another (we are generating a website, after all, so
we probably want the content to be in HTML). Here is an example of a
page:
title="My wonderful page"
multiline markdown content << 'JOPA'
# Hello World!
Just my blog post, how are you?
JOPA
Here, we define two env variables: $title
and $content
(names are
arbitrary), but $content
is defined with multiline
helper that
passes text through markdown
utility (which you should install
independently) to generate HTML content. Notice << 'JOPA'
on the line
with multiline
call and JOPA
at the very end. These are so-called
"heredocs" and in bash to define multiline strings. You can read about
them somewhere else, just note, that most of the time you
want the first delimiting identifier to be in single quotes, otherwise
your text is subject of command evaluation which is a dangerous thing...
Imagine the following page:
multiline content << JOPA
Don't do $(rm -rf ~/) in your scripts!
JOPA
You probably don't want this to be executed... But sometimes you do want the evaluation in these strings (for example, for in-line variable expansion), okay, it's up to you. Just be warned.
Another note is that you can use any thing as your delimiter, even Emojis. Isn't this fun?
Layouts
Next, after a page is sourced env variables are (re)defined, but if
$layout
is not defined yet while $layout_file
is, the latter is also
sourced. The script in this file must define $layout
(and can do
this conditionally). Again, you can have any logic, for example, have
defaults for $title
:
website_title="My Website"
# prepend website title to any page's title
if [[ "$title" ]]; then
head_title="$title | $website_title"
else
head_title="$website_title"
fi
# define mandatory layout variable
multiline layout << 'JOPA'
<!doctype html>
<meta charset=utf-8>
<title>${head_title}</title>
<h1>${title}</h1>
${content}
JOPA
Then, the string in $layout
is used as a template to render the
resulting file using envsubst
utility, which basically replaces all
variables in the template with their actual values. Logic-less templates
you say? Kind of.
Well, is that simple.
Indexes
But here is one more thing. What about the index page, table of
contents, Atom/RSS feeds and all alike? We need to display links to our
wonderful pages somehow, in most cases on the main page - the website's
index. For this purpose, there are indexes and the each
helper.
Indexes are regular pages, except that they are processed separately
from other pages and, obviously, are not "indexed", i. e. will not be
present in the resulting list of pages. You must specify indexes in
space-separated $indexes
env variable, for example:
# add Table Of Contents page to the list of indexes
indexes="$indexes $from/toc.jsh"
By default, only $from/index.jsh
is in this list.
Below is an example of such a page:
multiline indexer_main << 'JOPA'
<li>
<a href="/${target}">${title}</a>
</li>
JOPA
index_main="$(each "$pages" render "$indexer_main")"
multiline content << JOPA
My posts:
<ol>${index_main}</ol>
JOPA
Here, we have regular page content defined, but it uses $index_main
variable, which is defined as the result of each
function call on a
set of $pages
, and each page is then fed into render "$indexer_main"
function, i. e. rendered with the defined template. You also see that
$target
variable is used - this is a file name for a page being
processed and you can use it to refer to the page.
Of course, instead of calling render
you can call your own function
which can do filtering, skip some pages you don't want to be displayed,
and only then call render
from.
Hope this sheds some light on how this pretty simple thing is working for you to be able to move to the next (but optional!) step - extending Jopa.
Development of Jopa happens on GitHub, but you don't need to go there to grab the code. Here is the full Jopa code, just 62 lines:
#!/usr/bin/env bash
export from=${from-"$PWD/pages"}
export to=${to-"$PWD/www"}
export ext=${ext-".html"}
export indexes=${indexes-"$from/index.jsh"}
export layout_file=${layout_file-"layout.jsh"}
shopt -s nullglob
export pages=${pages-"$from/*.jsh"}
multiline() { # multiline [markdown] varname << 'JOPA'
if [[ "$#" -eq 2 ]]; then
read -r -d '' "${@:2}"
read -r -d '' $2 <<< "$($1 <<< "${!2}")"
else
read -r -d '' "$@"
fi
}
page_id() { echo "_page_env_${1//[^a-zA-Z0-9]/_}"; }
read_page() {
set -a
src="${1##*/}"
target="${src%.*}$ext"
source "$1" >&2
}
store_page() { printf -v $(page_id $1) '%s' "$(read_page $1; declare -px)"; }
load_page() { local id=$(page_id $1); echo "${!id}"; }
each() {
for p in $1; do
(set -a; eval "$(load_page $p)"; "${@:2}")
done
}
render() { envsubst <<< "$1"; }
to_layout() {
[[ ! "$layout" && -f "$layout_file" ]] && source "$layout_file" >&2
render "$layout" > "$to/$target"
}
process() { each "$pages $indexes" to_layout; }
jopa() {
mkdir -p "$to"
pages="$(for p in $pages; do
for i in $indexes; do [[ "$p" -ef "$i" ]] && continue 2; done
echo $p
done)"
for p in $pages; do store_page $p; done
for i in $indexes; do store_page $i; done
process
}
if [[ "$0" = "$BASH_SOURCE" ]]; then
if [[ "$#" -ne 0 ]]; then
printf "$0 does not accept any arguments; use env variables instead"
exit 1
fi
jopa
fi
Other site generators looking at Jopa's source code
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2004 Sam HocevarEveryone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO.