├── .gitignore ├── README ├── shlog └── shlog.conf.def /.gitignore: -------------------------------------------------------------------------------- 1 | shlog.conf 2 | README.old 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | SHLOG(1) SHLOG(1) 2 | NAME 3 | shlog - a WIP static blog generator in /bin/sh 4 | 5 | SYNOPSIS 6 | shlog [subcommand] 7 | 8 | DESCRIPTION 9 | Note: This projects has been abandoned in favour of shite: 10 | https://git.zakaria.org/shite/ 11 | 12 | Generate simple static sites. shlog is geared towards blogs. 13 | 14 | Warn: This project is no longer intended for any use other than my own. 15 | I will provide little to no support for anyone attempting to use this 16 | monstrosity - use at your own risk. 17 | 18 | The subcommands are as follows: 19 | 20 | i|index Generate the post index page. This page lists all posts and 21 | links to them. 22 | 23 | r|rss Generate the RSS feed. RSS feed location is configurable in the 24 | config file. 25 | 26 | u|update Find articles that haven't been converted to HTML and 27 | convert them. 28 | 29 | shlog defaults to u/update if no arguments are given. 30 | 31 | EXAMPLE 32 | To use shlog setup your site root as so: 33 | 34 | . # $SITE_ROOT 35 | |- posts/ # $POSTS_DIR 36 | | |- index.html # list of posts in chronological order 37 | | |- 2020-09-08-post.md # markdown post source 38 | | |- 2020-09-08-post.html # 'compiled' HTML post source 39 | |- html/ # dir containing html 40 | | |- footer.html # HTML added to the end of each post 41 | | |- head.html # HTML added to every post 42 | | |- nav.html # HTML added to the start of each post 43 | | |- posts.html # HTML added to $POSTS_DIR/index.html 44 | |- style.css # style (not required) 45 | |- index.html # index/homepage (not required) 46 | 47 | After running `shlog update` Markdown in /posts/ following the filename 48 | format DD-MM-YYYY-shortitle.md will be run through lowdown and converted 49 | into HTML. Additional HTML will be added from the html/head.html, 50 | html/nav.html and html/footer.html. 51 | 52 | To generate a post index file (located at /posts/index.html) which lists 53 | posts in order of the date in it's filename (most recent first) and links 54 | to them, run `shlog index`. 55 | 56 | DEPENDENCIES 57 | shlog depends on the following programs: 58 | 59 | /bin/sh Obviously. 60 | 61 | lowdown Markdown to HTML converter (https://kristaps.bsd.lv/lowdown/). 62 | 63 | GOALS 64 | - Be a shell script. 65 | - Minimal external dependencies. 66 | - Be fast. 67 | - Be cross-platform (POSIX?). 68 | 69 | NON-GOALS 70 | - JavaScript (though it should be easy to add it yourself). 71 | - Post tags/categories. 72 | 73 | CONFIGURATION 74 | The main configuration file is located at: 75 | ${XDG_CONFIG_HOME}/shlog/shlog.conf. 76 | This config file is just a regular shell script that the shlog script 77 | sources on startup. 78 | 79 | A default/reference config is included in shlog.conf.def. 80 | 81 | PLANS 82 | - Better configuration syntax. 83 | - Better docs. 84 | - Better usablity. 85 | -------------------------------------------------------------------------------- /shlog: -------------------------------------------------------------------------------- 1 | # 2 | 3 | usage() { 4 | cat</dev/null; then 67 | echo "$1" | tac 68 | else 69 | echo "$1" | tail -r 70 | fi 71 | } 72 | 73 | # log 74 | # $1 - message 75 | # $2 - (optional) log 'label' 76 | log() { 77 | message="$1" 78 | label="$2" 79 | 80 | if [ -z "$label" ]; then 81 | printf "%s: %s\\n" "$(bname "$0")" "$message" >&1 82 | return 83 | fi 84 | 85 | printf "%s: %s: %s\\n" "$(bname "$0")" "$label" "$message" >&1 86 | } 87 | 88 | # 89 | die() { 90 | message="$1" 91 | 92 | if [ -n "$message" ]; then 93 | log "$message" "error" >&2 94 | fi 95 | 96 | printf 'exiting.\n' >&2 97 | exit 1 98 | } 99 | 100 | # get markdown article title by extracting the first h1. 101 | # requires the first line of the markdown file to be the h1. 102 | # $1 - markdown file path 103 | get_md_title() { 104 | md_file="$1" 105 | head -n 1 "$md_file" | sed -e 's/^#\ \(.*\)$/\1/g' -e 's/\`//g' 106 | } 107 | 108 | # get date from post filename. 109 | # requires filename to be YYYY-MM-DD-blahblah-blah.md 110 | # $1 - markdown file path 111 | get_post_date() { 112 | md_filename="$(bname "$1")" 113 | echo "$md_filename" | sed -e 's/^\([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]\)-.*$/\1/g' 114 | } 115 | 116 | # get list of all posts 117 | get_post_list() { 118 | posts='' 119 | for post in ${site_root}/${posts_dir}/*.html; do 120 | if [ "$(bname "$post")" = "index.html" ]; then 121 | continue 122 | fi 123 | posts="${posts}\\n${post}" 124 | done 125 | reverse "$posts" 126 | } 127 | 128 | # get list of unposted posts (.md files without their .html counterparts) 129 | get_unpost_list() { 130 | posts='' 131 | for post in ${site_root}/${posts_dir}/*.md; do 132 | # if the .html exists, it has already been compiled 133 | if [ -f "${post%%.*}.html" ]; then 134 | continue 135 | fi 136 | if [ "$(bname "$post")" = "index.html" ]; then 137 | continue 138 | fi 139 | posts="${posts}\\n${post}" 140 | done 141 | reverse "$posts" 142 | } 143 | 144 | # generate navgation/header html 145 | gen_html_nav() { 146 | printf '
\n' 147 | printf '\n' 157 | printf '
\n' 158 | } 159 | 160 | # generate html head 161 | # $1 - html title (optional) 162 | # $2 - canonical url of page (optional, only needed for opengraph) 163 | gen_html_head() { 164 | html_title="$1" 165 | page_url="$2" 166 | 167 | printf '\n' 168 | if [ -n "$head_content" ]; then 169 | if [ -f "$head_content" ]; then 170 | cat "$head_content" 171 | else 172 | die "head_content file '${head_content}' does not exist." 173 | fi 174 | fi 175 | if [ -n "$html_title" ]; then 176 | printf '%s - %s\n' "$html_title" "$site_title" 177 | else 178 | printf '%s\n' "$site_title" 179 | fi 180 | if is_yes "$add_og_tags"; then 181 | printf '\n' "$html_title" 182 | printf '\n' "$base_url" "$page_url" 183 | printf '\n' 184 | fi 185 | printf '\n' 186 | } 187 | 188 | # generate footer 189 | # $1 - page plaintext link (optional) 190 | gen_html_footer() { 191 | page_pt="$1" 192 | 193 | printf '\n' 199 | } 200 | 201 | # generate post info 202 | # $1 - date posted 203 | # $2 - post last modified 204 | gen_html_postinfo() { 205 | post_date="$1" 206 | post_modified="$2" 207 | 208 | printf '

\n' 209 | printf 'posted: %s
\n' "$post_date" 210 | printf 'modified: %s
\n' "$post_modified" 211 | printf '

\n' 212 | } 213 | 214 | # generate post's html 215 | # $1 - post 216 | # $2 - post title 217 | # $3 - post html content 218 | # $4 - post footer 219 | # $5 - post info (optional) 220 | gen_html_post() { 221 | post="$(bname "$1")" 222 | post_title="$2" 223 | post_content="$(echo "$3" | sed -e '/^

/p')" 225 | post_foot="${4}" 226 | post_info="${5:-''}" 227 | 228 | post_url="${posts_dir}/${post%%.*}" 229 | 230 | if is_yes "$add_html_to_links"; then 231 | post_url="${post_url}.html" 232 | fi 233 | 234 | printf '\n' 235 | printf '\n' 236 | gen_html_head "${post_title}" "${post_url}" 237 | printf '\n' 238 | gen_html_nav 239 | printf '
\n' 240 | #printf '

%s

\n' "$(html_safe "$post_title")" "$post_title" 241 | printf '%s\n' "$extracted_h1" 242 | printf '%s\n' "$post_info" 243 | printf '%s\n' "$post_content" 244 | printf '
\n' 245 | printf '%s\n' "$post_foot" 246 | printf '\n' 247 | printf '\n' 248 | } 249 | 250 | # generate posts index page 251 | gen_index() { 252 | printf '\n' 253 | printf '\n' 254 | gen_html_head 255 | printf '\n' 256 | gen_html_nav 257 | printf '
\n' 258 | 259 | if [ -n "$postindex_content" ]; then 260 | if [ -f "$postindex_content" ]; then 261 | cat "$postindex_content" 262 | else 263 | die "postindex_content file '${postindex_content}' does not exist." 264 | fi 265 | fi 266 | 267 | printf '
    \n' 268 | 269 | # add a
  • for each post 270 | for post in $(get_post_list); do 271 | post_md="${post%%.*}.md" 272 | post_date="$(get_post_date "$post")" 273 | post_title="$(get_md_title "$post_md")" 274 | post_bname="$(bname "$post")" 275 | 276 | # use full paths, or relative paths ? 277 | if is_yes "$use_relative_paths"; then 278 | post_url="/${posts_dir}/${post_bname%%.*}" 279 | else 280 | post_url="${base_url}/${posts_dir}/${post_bname%%.*}" 281 | fi 282 | 283 | # add .html to the end of links ? 284 | if is_yes "$add_html_to_links"; then 285 | post_url="${post_url}.html" 286 | fi 287 | 288 | # construct 289 | # TODO: find a better way to do this 290 | fmt="$(echo "$post_list_format" | sed \ 291 | -e "s/%d/${post_date}/g" \ 292 | -e "s,%t,${post_title},g" \ 293 | -e "s,%l,${post_url},g")" 294 | 295 | printf '
  • %s
  • \n' "$fmt" 296 | done 297 | 298 | printf '
\n' 299 | printf '
\n' 300 | printf '\n' 301 | printf '\n' 302 | } 303 | 304 | # generate rss feed 305 | gen_rss() { 306 | printf '\n' "${base_url}" 307 | printf '\n' 308 | printf '%s\n' "${rss_title}" 309 | printf '\n' 310 | printf '%s\n' "${rss_link}" 311 | 312 | # add an for each post 313 | for post in $(get_post_list); do 314 | post_md="${post%%.*}.md" 315 | post_date="$(get_post_date "$post")" 316 | post_title="$(get_md_title "$post_md")" 317 | post_bname="$(bname "$post")" 318 | 319 | printf '\n' 320 | printf '%s/%s/%s\n' "${base_url}" "$posts_dir" "${post_bname%%.*}" 321 | printf '%s\n' "$post_title" 322 | 323 | # include post content ? 324 | if is_yes "$rss_include_html"; then 325 | printf '\n' 326 | run_lowdown "$post_md" 327 | printf '\n' 328 | fi 329 | 330 | printf '%s\n' "$(date_to_rfc2822 "$post_date" '%F')" 331 | printf '\n' 332 | done 333 | 334 | printf '\n' 335 | printf '\n' 336 | } 337 | 338 | # generate a blank page 339 | # $1 - page title 340 | # $2 - page file 341 | gen_page() { 342 | page_title="$1" 343 | page_file="$2" 344 | page_pt="$(bname "$page_file")" 345 | 346 | printf '\n' 347 | printf '\n' 348 | gen_html_head "${page_title}" 349 | printf '\n' 350 | gen_html_nav 351 | printf '
\n' 352 | run_lowdown "$page_file" 353 | printf '
\n' 354 | gen_html_footer "$page_pt" 355 | printf '\n' 356 | printf '\n' 357 | } 358 | 359 | # compile all .md files without .html couterparts 360 | compile_posts() { 361 | posts="$(get_unpost_list)" 362 | 363 | # return if there are no posts to update 364 | if [ -z "$posts" ]; then 365 | log "no posts to update" 366 | return 367 | fi 368 | 369 | # for each .md file without a .html counterpart... 370 | for post in $posts; do 371 | if [ -z "$post" ]; then 372 | continue 373 | fi 374 | 375 | log "adding $(bname "$post")..." 376 | 377 | # md 378 | post_md="${post%%.*}.md" 379 | 380 | # get post date 381 | post_date="$(get_post_date "$post")" 382 | 383 | # the current date is the last modified date 384 | post_modified="$(date '+%F')" 385 | 386 | # get post title 387 | post_title="$(get_md_title "$post_md")" 388 | 389 | # generate html from markdown 390 | post_html="$(run_lowdown "$post")" 391 | 392 | # generate footer 393 | post_foot="$(gen_html_footer "$post_md")" 394 | 395 | # generate info 396 | post_info="$(gen_html_postinfo "$post_date" "$post_modified")" 397 | 398 | # generate the post's html 399 | gen_html_post "$post" "$post_title" "$post_html" "$post_foot" "$post_info" > "${post%%.*}.html" 400 | done 401 | 402 | } 403 | 404 | main() { 405 | # load config 406 | DEF_CONFIG="${XDG_CONFIG_HOME:-${HOME}/.config}/shlog/shlog.conf" 407 | if [ -f "${DEF_CONFIG}" ]; then 408 | # shellcheck source=./shlog.conf.def 409 | . "${DEF_CONFIG}" 410 | else 411 | die "cannot load config." 412 | fi 413 | 414 | # 415 | case "$1" in 416 | h|-h|help) usage ;; 417 | ""|u|update) compile_posts ;; 418 | i|index) gen_index > "${site_root}/${posts_index}" ;; 419 | r|rss) gen_rss > "$rss_feed" ;; 420 | p|page) gen_page "$3" "$2" > "$4" ;; 421 | *) log "unsupported command $1" "error" ;; 422 | esac 423 | } 424 | 425 | main "$@" -------------------------------------------------------------------------------- /shlog.conf.def: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shlog.conf.def - example shlog config 3 | 4 | # FQDN 5 | fqdn=example.com #DOMAIN=example.com 6 | 7 | # base website url 8 | base_url="https://${DOMAIN}" 9 | 10 | # site title, used in 11 | site_title="example's website" 12 | 13 | # root site source 14 | site_root=${HOME}/src/blog 15 | 16 | # directory in the site_root containing posts 17 | posts_dir=posts 18 | 19 | # post list/index file location 20 | posts_index=${posts_dir}/index.html 21 | 22 | # pass additional options to lowdown 23 | lowdown_opts= 24 | 25 | 26 | # # 27 | # Y\N FLAGS # 28 | # # 29 | 30 | # use relative paths? 31 | use_relative_paths=y 32 | 33 | # add .html to the end of post links? 34 | add_html_to_links=y 35 | 36 | # add OpenGraph tags to posts 37 | add_og_tags=n 38 | 39 | 40 | # # 41 | # RSS # 42 | # # 43 | 44 | # location of rss feed xml 45 | rss_feed=${site_root}/rss.xml 46 | # rss feed <title> 47 | rss_title=${site_title} 48 | # rss feed <link> 49 | rss_link=${base_url} 50 | 51 | # include HTML of article in rss item? 52 | # when enabled the raw html of the article will be placed in the RSS item's 53 | # <description> tag. 54 | rss_include_html=y 55 | 56 | 57 | # # 58 | # CONTENT # 59 | # # 60 | 61 | html_dir=${site_root}/html 62 | 63 | # file to add to the end of the HTML <head> of all pages. 64 | # leave blank to add nothing. 65 | head_content=${html_dir}/head.html 66 | 67 | # content to add to the header of each page. 68 | # leave blank to add nothing. 69 | nav_content=${html_dir}/nav.html 70 | 71 | # content to add to the end of each post 72 | # after the metadata 73 | footer_content=${html_dir}/footer.html 74 | 75 | # content to add to the beginning of the post index. 76 | # leave blank to add nothing. 77 | postindex_content=${html_dir}/posts.html 78 | 79 | # format posts are listed in. (can include html) 80 | # '%d' = post date 81 | # '%t' = post title 82 | # '%l' = post url 83 | post_list_format="<span class=\"postdate\">%d</span> — <a href=\"%l\">%t</a>" 84 | --------------------------------------------------------------------------------