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. gib/main +39 −12 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 Loading Loading @@ -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) Loading @@ -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 Loading @@ -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 Loading 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 resources/_comment.cgi +16 −6 Original line number Diff line number Diff line Loading @@ -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} ) Loading Loading @@ -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 { Loading resources/blogisek.js +13 −0 Original line number Diff line number Diff line Loading @@ -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'); Loading @@ -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
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.
gib/main +39 −12 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 Loading Loading @@ -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) Loading @@ -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 Loading @@ -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 Loading
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
resources/_comment.cgi +16 −6 Original line number Diff line number Diff line Loading @@ -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} ) Loading Loading @@ -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 { Loading
resources/blogisek.js +13 −0 Original line number Diff line number Diff line Loading @@ -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'); Loading @@ -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; }