Verified Commit 35610bab authored by Adam Matoušek's avatar Adam Matoušek
Browse files

One last monster-commit and then I'll behave

parent be1a95e4
Loading
Loading
Loading
Loading

README

0 → 100644
+125 −0
Original line number Diff line number Diff line
# Simple static(-ish) blog site generator

A collection of Perl scripts and templates integrated with a build system to
generate a simple static blog. The only dynamic component is a CGI script to
submit comments to.

Everything, including comments, is file-based (no DBMS necessary). Template
Toolkit (‹Template.pm›) is used as the templating engine. Metadata and comments
follow RFC822-ish format (mail-like header, empty line, content).

Note: there is a lot of Czech in the templates, as I first created this for my
Czech weblog.

## Dependencies

• Perl (reasonably recent version) and modules listed below
• ImageMagick tools for resizing images
• Make and a C99 compiler (used to build ‹gib›, so if you already have that…)
• sed
• An universal web minifier (‹minify›)

Perl modules (and which Debian packages they come from):

• Template toolkit (‹libtemplate-perl›)
• File::Slurper (‹libfile-slurper-perl›)
• Date::Parse (‹libtimedate-perl›)
• File::Find::Rule (‹libfile-find-rule-perl›)

## Articles

Articles go in the ‹articles› directory (which is not present in the repository;
you are expected to create it as a new content-only repo). Each article has its
own directory (arbitrarily deep under ‹articles›). containing a ‹meta› file,
a content file (currently ‹content.html›), pictures (‹.jpg›) and comments
(‹.comment›).

In order to save bandwidth, images are automatically rescaled to several sizes
for the user agents to choose; full-size images are then linked.

The user content may contain template directives; several are provided:

• ‹[% snip %]› – separates the perex from the rest of the article; only the
  perex part is shown in an archive listing. This should go after the first or
  second paragraph of the article.
• ‹[% pic( 'kittens.jpg', alt='…' caption='…' ) %]› – Inserts a figure with
  correctly set-up ‹srcset› and a caption.
• ‹[% autoimg src='…', alt='…' %]› – like ‹pic›, but only inserts the ‹<img>›
  element, no figure with caption is inserted.

### Metadata files

Article metadata (in the ‹meta› file) look like this:

    Date:     2020-09-09 15:45:38 CEST
    Category: log
    Title:    F1rst P0st!!
    Banner:   banner.jpg	The optional alt-text after a TAB
    Password: Are you sure, you are a human?	yes
    Password: What was the next Perl renamed to?	Raku

Support for categories is currently a little underwhelming – each article has
exactly one (‹.› if none is specified, which equals to ‹/›, because I like to
think about categories as paths) and when you use nested categories
(‹supercat/subcat›), the index for the super-category won't list the article
(which is something I plan to do, but haven't had the need for so far).

Articles have implicit IDs, which is their path relative to the ‹articles›
directory. If the example file were ‹./articles/2020/first-post/meta›, its ID
would be ‹2020/first-post›. The correspondence of IDs and paths is present
everywhere in this little system.

The ‹Password› fields are used as a trivial anti-spam measure, see the comments
section. Instead of ‹Date›, ‹Stamp› with unix epoch time can be used (but as the
‹Date› field is parsed somewhat magically, perhaps epoch time can go there as
well? I don't know).

### Comment files

Comments are stored as files in the directories of their respective articles and
bear the ‹.comment› suffix. Each comment has its own file of the familiar
e-mail format:

    Article: 2020/first-post
    Author:  adamat
    Date:    2020-09-18 22:43:14 CEST
    Pass:    yes	the actual password after a TAB
    Parent:  20200918143753AjoC
    
    Oh, thank you for the lovely comment!

Every comment has its ID, which is normally taken from the file name (this one,
for example, would be named ‹20200919224314eAkN›), but an ‹Id› field may be
provided instead (something _might_ break if they contradict). IDs are useful to
create comment threads: the ‹Parent› field says which comment is this one
a reply to. If it's a top-level comment, the field is empty/missing/set to
‹root›.

The ‹Date›/‹Stamp› alternative from article metadata applies here, too.

The ‹Pass› field is generated by the comment submission script to indicate
whether the comment passed an anti-spam question and what was the answer the
author tried to use. See below.

## Comments

The only (so far) non-static part of this little project are the comments,
because my blogging experience would be dull without them. There is a CGI script
that just takes whatever it's given, does some preliminary checks and either
discards the request or saves a comment file into the comment queue.

The site author may then inspect the queued comments, remove spam and distribute
the rest into the article repo. That is boring, so putting them into
a ‹comments› directory and running an auto-sorter is enough.

To save me some work sorting the queue, each article may be equipped with
several “passwords”. I use something like “quiz“ questions about the article
(which I suspect will work, because it's all in Czech). Each time the comment
form is generated, a random question is selected as the anti-spam one. The
submission script knows, of course, which password is currently active for each
article, and fills the ‹Pass› field accordingly. A cronjob can then periodically
import passed comments and regenerate the pages.

Aside from per-article questions/passwords, a list of “master passwords” is kept
in ‹articles/passwords› – those are for me and my frequently commenting friends
to bypass the anti-spam system altogether.
+39 −12
Original line number Diff line number Diff line
@@ -35,11 +35,13 @@ cmd gib.nochange /bin/mkdir -p $(articles)

# assemble comments (this must be quite high, because a comment index is generated)
for a $(articles)
let commentfiles $(sources:articles/$(a)/*.comment)
out $(a)/comments.html
dep article dirs
dep $(sources:articles/$(a)/*.comment)
dep articles/$(a)/meta
dep $(commentfiles)
dep templates/comments.tt
cmd $(tool) mkcomments $(a)
cmd $(tool) mkcomments $(a) $(srcdir)/$(commentfiles)

# selected password for each article
for a $(articles)
@@ -74,25 +76,39 @@ dep $(sources:articles/*/meta)
dep $(articles)/comment_index
cmd $(tool) build_index

out index
out ./index
dep archives.gib
use transient

out index
dep ./index
use byproduct

# sets $(archives) with years and categories
# and $(archives.$path) with contained articles
sub archives.gib

for a $(archives)
# variables for local navigation (prev/next) of the article (created by build_index)
for a $(articles)
out $(a)/localnav.tt
dep archives.gib
use transient

for a $(archives-nonroot)
out $(a)/index
dep index
use transient

# assemble navigation
out nav.html
out generate navigation
dep templates/nav.tt
dep index
cmd $(tool) mknav

out nav.html
dep generate navigation
use transient

# TODO: md2html or sth

# copy ready-made (templated) .htmls
@@ -128,15 +144,14 @@ use byproduct
for i $(images)
sub $(i).gib

set images-all

for a $(articles)
out images for $(a)
let imgs $(images:$(a))
dep $(imgs)
dep resize $(imgs)
use byproduct
add images-all $(out)

add deployed-files $(resized-images)

# assemble content of the article
for a $(articles)
@@ -152,24 +167,23 @@ out $(a)/perex.html
dep $(a)/content.html
use transient

set articles-html

# assemble whole articles
for a $(articles)
out $(a)/index.html
dep $(a)/content.html
dep $(a)/comments.html
dep $(a)/localnav.tt
dep $(srcdir)/articles/$(a)/meta
dep nav.html
dep templates/page.tt
dep templates/article.tt
cmd $(tool) mkarticle $(a)
add articles-html $(out)
add deployed-files $(out)

# archive indices
for a $(archives)
out $(a)/index.html
out $(a)/archive.gib
dep $(a)/index
dep $(archives.$a)/perex.html
dep nav.html
@@ -178,8 +192,21 @@ dep templates/archive.tt
cmd $(tool) mkarchive $(a)
add deployed-files $(out)

set pages

for a $(archives)
sub $(a)/archive.gib

for a $(archives)
for p $(pages.$a)
out $(a)/$(p)
dep $(a)/archive.gib
use byproduct
add deployed-files $(out)


# other resources
set copied-resources robots.txt
set copied-resources robots.txt favicon16.png
set executables _comment.cgi
set minified-resources style.css blogisek.js
set extra-templated-pages comment-queued.html comment-accepted.html geoblocked.html

regen

0 → 100755
+39 −0
Original line number Diff line number Diff line
#!/bin/sh
set -e

if [ "$1" = "cron" ]; then
	cron=1
	exec > /dev/null
fi

repo=/home/adamat/src/blogisek
web=/var/www/blogisek

accepted=$repo/comments
queue=$web/_comments
articles=$repo/articles

mkdir -p $accepted
$repo/tools/autoaccept $queue $accepted
commentlist=$(mktemp)
$repo/tools/sort_queue $accepted $articles > $commentlist

cd $articles
if [ -z "$cron" ]; then
	git pull
fi
# TODO: autocommit?

echo
echo "Imported comments:"
cat $commentlist
echo "--- (end) ---"
echo

rm $commentlist

cd $repo
# git pull --autostash --rebase

make --silent 2>&1 > /dev/null || echo "Build failed" >&2
make --silent install 2>&1 > /dev/null || echo "Installation failed" >&2
+16 −6
Original line number Diff line number Diff line
@@ -46,13 +46,16 @@ unless ( -t STDIN || ( $ENV{REQUEST_METHOD} // '' ) eq 'POST' )
my $article_slug = $ENV{REQUEST_URI} =~ s,/comment(\?.*)?$,,rn;
$article_slug =~ s,^(https?://[^/]*)?/,,n;
my %c = (
    article  => scalar $q->param( 'article' ) // $article_slug,
    author   => scalar $q->param( 'author' ),
    body     => scalar $q->param( 'body' ),
    password => scalar $q->param( 'recog' ) // '',
    parent   => scalar $q->param( 'replyto' ) // '',
    article  => fixws( scalar $q->param( 'article' ) // $article_slug ),
    author   => fixws( scalar $q->param( 'author' ) ),
    body     => fixws( scalar $q->param( 'body' ) ),
    password => fixws( scalar $q->param( 'recog' ) // '' ),
    parent   => fixws( scalar $q->param( 'replyto' ) // '' ),
);

use Data::Dumper;
print STDERR Dumper(\%c);

unless ( $c{article} && $c{article} =~ m,^[-/a-z0-9]+$,
      && $c{author} && 0 < length $c{author} < 64 && no_weird_chars( $c{author} )
      && $c{body} && 0 < length $c{body} < 8000 && no_weird_chars( $c{body} )
@@ -140,9 +143,16 @@ sub print_errorpage {
    }
}

sub fixws {
    $_ = shift;
    s/\r\n/\n/gs;
    s/\t/ /g;
    return $_;
}

sub no_weird_chars {
    $_ = shift;
    return /^[\p{Print}\n]*$/s;
    return /^[\P{Control}\n]*$/s && !/\p{Surrogate}]/s;
}

sub sanitise {
+13 −0
Original line number Diff line number Diff line
@@ -9,6 +9,8 @@ ttc.onclick = (ev) => {
for ( e of document.getElementsByClassName('was') ) {
    e.dataset.tooltip = e.title;
    e.title = '';
    if ( e.tagName == 'A' )
        continue;
    e.onclick = (ev) => {
        let classes = ev.target.classList;
        let wasopen = classes.contains('open');
@@ -19,3 +21,14 @@ for ( e of document.getElementsByClassName('was') ) {
        dim.toggle('on', !wasopen);
    };
}
var tsel = document.getElementById('theme-selector');
if ( tsel ) {
    for ( e of tsel.getElementsByTagName('button') ) {
        e.onclick = (ev) => {
            let t = ev.target.dataset.theme;
            window.localStorage.theme = t;
            document.firstElementChild.dataset.theme = t;
        };
    }
    tsel.hidden = false;
}
Loading