Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
Adam Matoušek
BLO👏GÍ👏SEK
Commits
35610bab
Verified
Commit
35610bab
authored
Oct 15, 2022
by
Adam Matoušek
Browse files
One last monster-commit and then I'll behave
parent
be1a95e4
Changes
21
Hide whitespace changes
Inline
Side-by-side
README
0 → 100644
View file @
35610bab
# 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
View file @
35610bab
...
...
@@ -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
View file @
35610bab
#!/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
View file @
35610bab
...
...
@@ -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
{
...
...
resources/blogisek.js
View file @
35610bab
...
...
@@ -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
;
}
resources/favicon16.png
0 → 100644
View file @
35610bab
305 Bytes
resources/style.css
View file @
35610bab
/*** Colours ****/
html
,
#theme-selector
button
[
data-theme
=
light
]
{
--c-page-bg
:
#dfdcd7
;
--c-pane-bg
:
#fff
;
--c-text
:
#000
;
--c-paneless
:
#000
;
/* Text outside panes */
--c-inverse
:
#fff
;
--c-link
:
#137
;
/* Mostly links */
--c-emph
:
#751
;
/* Sidebar links, highlighting */
--c-navbutton-bg
:
#fff
;
--c-weak
:
#333
;
--c-separator
:
#888
;
--c-img-tint
:
#446ab6
;
--f-img-tint
:
contrast
(
80%
)
brightness
(
130%
)
grayscale
(
90%
)
opacity
(
80%
);
--c-was-hint
:
#ffedf0
;
--c-was-bg
:
pink
;
--c-footer-bg
:
rgba
(
0
,
0
,
0
,
0.08
);
--c-dimmer-bg
:
#03a
;
}
*[
data-theme
=
"dark"
]
{
--c-page-bg
:
#120800
;
--c-pane-bg
:
#3c3230
;
--c-text
:
#fff
;
--c-paneless
:
#dcc
;
--c-inverse
:
#000
;
--c-link
:
#fba1ae
;
--c-emph
:
#e87182
;
--c-navbutton-bg
:
#2b2020
;
--c-weak
:
#baa
;
--c-separator
:
#666
;
--c-img-tint
:
#97525c
;
--f-img-tint
:
contrast
(
90%
)
brightness
(
80%
)
grayscale
(
90%
)
opacity
(
80%
);
--c-was-hint
:
#4f2327
;
--c-was-bg
:
#885f65
;
--c-footer-bg
:
rgba
(
127
,
127
,
127
,
0.2
);
--c-dimmer-bg
:
#137
;
}
@media
(
prefers-color-scheme
:
dark
)
{
html
:
not
([
data-theme
]),
html
[
data-theme
=
"auto"
],
html
[
data-theme
=
""
]
{
--c-page-bg
:
#120800
;
--c-pane-bg
:
#3c3230
;
--c-text
:
#fff
;
--c-paneless
:
#dcc
;
--c-inverse
:
#000
;
--c-link
:
#fba1ae
;
--c-emph
:
#e87182
;
--c-navbutton-bg
:
#2b2020
;
--c-weak
:
#baa
;
--c-separator
:
#666
;
--c-img-tint
:
#97525c
;
--f-img-tint
:
contrast
(
90%
)
brightness
(
80%
)
grayscale
(
90%
)
opacity
(
80%
);
--c-was-hint
:
#4f2327
;
--c-was-bg
:
#885f65
;
--c-footer-bg
:
rgba
(
127
,
127
,
127
,
0.2
);
--c-dimmer-bg
:
#137
;
}
}
/* General - fonts and light theme colours */
html
,
body
{
margin
:
0
;
padding
:
0
;
font-family
:
serif
;
color
:
black
;
background-color
:
#dfdcd7
;
color
:
var
(
--c-paneless
)
;
background-color
:
var
(
--c-page-bg
)
;
font-size
:
12pt
;
-webkit-text-size-adjust
:
none
;
-moz-text-size-adjust
:
none
;
...
...
@@ -13,12 +76,17 @@ html, body {
h1
,
h2
,
h3
,
h4
,
h5
,
h6
{
font-family
:
sans-serif
;
line-height
:
initial
;
}
em
>
em
{
font-style
:
normal
;
}
#comments
.reply
input
:not
(
:checked
)
+
label
,
#reply-root
:not
(
:checked
)
+
label
::after
,
a
{
color
:
#137
;
color
:
var
(
--c-link
)
;
text-decoration
:
none
;
}
a
.nocolor
{
...
...
@@ -44,15 +112,15 @@ a:hover {
}
#comments
.quiz
em
,
#sidebar
a
{
color
:
#751
;
color
:
var
(
--c-emph
)
;
}
#navbutton
{
display
:
block
;
color
:
#137
;
color
:
var
(
--c-link
)
;
font-weight
:
bold
;
border
:
0.2em
solid
#137
;
background-color
:
white
;
border
:
0.2em
solid
var
(
--c-link
)
;
background-color
:
var
(
--c-navbutton-bg
)
;
aspect-ratio
:
1
;
padding
:
0.5em
;
box-sizing
:
content-box
;
...
...
@@ -77,17 +145,82 @@ a:hover {
#navigation
nav
>
header
{
font-size
:
110%
;
font-weight
:
bold
;
color
:
#333
;
color
:
var
(
--c-weak
)
;
}
#navigation
li
{
list-style
:
none
;
line-height
:
2
;
}
#navigation
li
small
{
color
:
#333
;
color
:
var
(
--c-weak
)
;
font-size
:
92%
;
}
#theme-selector
button
{
display
:
inline-block
;
cursor
:
pointer
;
font-size
:
0.9rem
;
width
:
1.2rem
;
height
:
1.2rem
;
background-color
:
var
(
--c-pane-bg
);
border
:
0.2rem
solid
var
(
--c-link
);
border-radius
:
50%
;
position
:
relative
;
margin-top
:
1em
;
opacity
:
30%
;
font-family
:
sans-serif
;
box-sizing
:
content-box
;
padding
:
0
;
}
html
:not
([
data-theme
])
#theme-selector
button
[
data-theme
=
"auto"
],
html
[
data-theme
=
""
]
#theme-selector
button
[
data-theme
=
"auto"
],
html
[
data-theme
=
"auto"
]
#theme-selector
button
[
data-theme
=
"auto"
],
html
[
data-theme
=
"light"
]
#theme-selector
button
[
data-theme
=
"light"
],
html
[
data-theme
=
"dark"
]
#theme-selector
button
[
data-theme
=
"dark"
],
#theme-selector
button
:hover
,
#theme-selector
button
:focus
{
opacity
:
100%
;
}
#theme-selector
button
+
button
{
margin-left
:
0.5em
;
}
#theme-selector
button
::before
,
#theme-selector
button
::after
{
content
:
""
;
display
:
block
;
position
:
absolute
;
}
#theme-selector
button
::before
{
top
:
0
;
left
:
0
;
width
:
50%
;
height
:
100%
;
background-color
:
var
(
--c-page-bg
);
border-radius
:
50%
0
0
50%
;
}
#theme-selector
button
::after
{
border-radius
:
50%
;
width
:
100%
;
height
:
100%
;
border
:
0.2rem
solid
var
(
--c-link
);
top
:
-0.2rem
;
left
:
-0.2rem
;
text-align
:
center
;
line-height
:
1.2rem
;
font-weight
:
bold
;
color
:
var
(
--c-emph
);
content
:
"•"
;
}
#theme-selector
button
[
data-theme
=
'auto'
],
#theme-selector
button
[
data-theme
=
'auto'
]
::after
{
border-color
:
var
(
--c-weak
);
color
:
var
(
--c-weak
);
background-color
:
transparent
;
content
:
"A"
;
}
#content
{
max-width
:
48em
;
margin
:
0
auto
;
...
...
@@ -95,9 +228,9 @@ a:hover {
}
#content
>
main
>
article
,
.pane
{
background-color
:
white
;
background-color
:
var
(
--c-pane-bg
);
color
:
var
(
--c-text
);
position
:
relative
;
}
...
...
@@ -106,23 +239,24 @@ a:hover {
display
:
table
;
}
#content
>
main
>
header
,
#comments
>
h2
{
#comments
>
h2
,
#localnav
>
h2
{
padding
:
0
1rem
;
}
#content
>
main
>
header
h1
{
line-height
:
initial
;
margin-bottom
:
1em
;
margin-top
:
0
;
/* only small */
}
#content
>
footer
{
padding
:
1em
;
background-color
:
rgba
(
0
,
0
,
0
,
0.08
);
color
:
#333
;
background-color
:
var
(
--c-footer-bg
);
color
:
var
(
--c-weak
)
;
}
article
.banner
{
display
:
block
;
margin
:
0
;
}
article
.banner
img
{
width
:
100%
;
...
...
@@ -131,96 +265,129 @@ article .banner img {
object-fit
:
cover
;
object-position
:
center
center
;
}
article
a
.banner
{
background-color
:
#446ab6
;
article
a
.banner
,
#localnav
a
.hasimg
{
background-color
:
var
(
--c-img-tint
);
}
article
a
.banner
img
{
filter
:
contrast
(
80%
)
brightness
(
130%
)
grayscale
(
90%
)
opacity
(
80%
);
article
a
.banner
img
,
#localnav
a
img
{
filter
:
var
(
--f-img-tint
);
transition
:
filter
250ms
;
}
article
a
.banner
:hover
img
,
article
a
.banner
:focus
img
,
article
a
.banner
:active
img
{
article
a
.banner
:active
img
,
#localnav
a
:hover
img
,
#localnav
a
:focus
img
,
#localnav
a
:active
img
{
filter
:
none
;
}
.article-
text
header
+
footer
{
.article-
meta
header
+
footer
{
margin-top
:
-1em
;
}
.article-
text
footer
ul
{
.article-
meta
footer
ul
{
padding
:
0
;
font-size
:
0
;
}
.article-
text
footer
li
{
.article-
meta
footer
li
{
font-size
:
1rem
;
font-family
:
sans-serif
;
display
:
inline
;
}
.article-
text
footer
li
+
li
{
.article-
meta
footer
li
+
li
{
margin-left
:
1em
;
padding-left
:
1em
;
border-left
:
0.1em
solid
#888
;
border-left
:
0.1em
solid
var
(
--c-separator
)
;