← Zurück zum Blog

Isso-Kommentare einbinden – und was dabei schiefging

Isso-Kommentare einbinden – und was dabei schiefging

Kommentare auf einem statischen Blog sind ein echtes Problem. Kein Backend, keine Datenbank, kein Formular. Die Lösung: ein leichter Kommentarserver, den man selbst hostet. Isso ist genau das – Python, SQLite, ein paar Zeilen JavaScript. Aber der Weg dorthin war steiniger als erwartet.

Was ist Isso?

Isso ist ein Open-Source-Kommentarsystem, das als leichter Server läuft. Besucher schreiben Kommentare, die in einer SQLite-Datei gespeichert werden. Kein externer Dienst, keine Tracking-Skripte, keine Werbung. Alles bleibt auf dem eigenen Server.

Die Architektur ist simpel: Ein Python-Prozess lauscht auf einem Port, ein JavaScript-Client lädt die Kommentare und rendert ein Formular unter jedem Artikel. Fertig.

Das Setup-Script

Ich habe ein Shell-Script geschrieben, das die komplette Einrichtung automatisiert. Auf dem Incus-Host ausgeführt, legt es eine eigene Instanz an, installiert Isso, richtet einen systemd-Service ein und gibt die Konfiguration für den Reverse Proxy aus. Beliebig oft ausführbar, jedes Mal mit neuem Admin-Passwort.

Aber bis das Script funktionierte, musste ich ein paar Dinge herausfinden.

Problem 1: Instanz-Erkennung

Die erste Version hat geprüft, ob die Incus-Instanz existiert, mit:

if incus info "$INSTANZ" &>/dev/null; then

Das Problem: incus info gibt auch bei nicht-existierenden Instanzen Exit-Code 0 zurück. Das Script dachte, die Instanz existiert bereits, und übersprungen die Erstellung. Dann schlug alles weitere (die Paketinstallation, der User) fehl.

Lösung: Statt incus info die Ausgabe von incus list --format csv parsen und nach dem Instanznamen suchen. Zuverlässig, portabel.

if incus list "$INSTANZ" --format csv 2>/dev/null | grep -q "^$INSTANZ,"; then

Problem 2: Kein TTY für adduser

In Incus-Containern gibt es kein Terminal. Das Skript hat adduser --disabled-password --gecos "" isso aufgerufen – und der Aufruf ist einfach fehlgeschlagen, ohne Fehler. Der User wurde nie angelegt, sudo -u isso brach dann mit "unknown user" ab.

Lösung: useradd -m -s /bin/bash isso statt adduser. useradd ist nicht-interaktiv und braucht kein TTY.

Problem 3: Fehlender C-Compiler

Isso braucht misaka, ein Python-Paket mit C-Extension. Ohne gcc und die Python-Entwicklungspakete schlägt pip install fehl:

error: [Errno 2] No such file or directory: 'x86_64-linux-gnu-gcc'

Die Debian-Minimal-Image hat weder gcc noch python3-dev vorinstalliert.

Lösung: python3-dev gcc build-essential mitinstallieren. Das Skript installiert jetzt alle Abhängigkeiten, die pip für Kompilierung braucht.

Problem 4: dpkg-preconfigure meckert

dpkg-preconfigure: unable to re-open stdin: No such file or directory

apt-get install in Incus ohne TTY erzeugt diese Warnung. Kein echtes Problem, aber hässlich.

Lösung: DEBIAN_FRONTEND=noninteractive setzen und an apt-get übergeben. Die Warnung verschwindet.

Problem 5: incus file write gibt es nicht

In meiner Incus-Version gibt es den Befehl incus file write nicht. Das Skript ist mit "unknown command" abgebrochen.

Lösung: Dateien erst lokal in ein Temp-Verzeichnis schreiben, dann per incus file push in die Instanz übertragen. Funktioniert in allen Incus-Versionen:

cat > "$TMPDIR/isso.cfg" << EOF
...
EOF
incus file push "$TMPDIR/isso.cfg" "$INSTANZ"/home/isso/isso.cfg

Problem 6: Isso horcht nur auf localhost

Die Isso-Konfiguration hatte listen = http://127.0.0.1:8080. Der Caddy-Reverse-Proxy im Blog-Container konnte Isso nicht erreichen, weil 127.0.0.1 nur den Loopback der Isso-Instanz meint – nicht den des Blog-Servers.

Lösung: listen = http://0.0.0.0:8080 in der Config UND gunicorn -b 0.0.0.0:8080 im systemd-Service. Beides muss zusammenpassen.

Die Integration ins Blog

Das Einbinden in die Artikel war der einfachste Teil. Im Build-Skript (build-static.py) kommt unter jedem Artikel der Isso-Client:

<script data-isso="https://kommentare.frischux.de/"
        data-isso-lang="de"
        data-isso-sorting="newest"
        data-isso-require-author="true"
        data-isso-require-email="true"
        data-isso-avatar="true"
        data-isso-vote="false"
        src="https://kommentare.frischux.de/js/embed.min.js"></script>

<section id="isso-thread"></section>

Das data-isso Attribut verweist auf die Isso-Instanz, das src lädt das JavaScript. Isso erkennt automatisch die URL des aktuellen Artikels und lädt die passenden Kommentare.

Problem 7: SPA-Zugriff zerstört Kommentare

Das Blog hat eine post.html, die Artikel per JavaScript lädt und rendert – ein kleiner SPA-Mechanismus. Beim Klick auf der Startseite wird post.html?slug=... aufgerufen, nicht die statische HTML-Seite. Das Problem: Isso läuft beim initialen Seitenaufruf durch, findet #isso-thread noch nicht (der Inhalt wird erst danach per JavaScript gerendert), und initialisiert sich nie.

Versuch 1: Isso-Script dynamisch einfügen, nachdem #isso-thread im DOM ist. Funktioniert nicht – Isso initialisiert sich nur einmal und scannt das DOM nicht erneut.

Versuch 2: MutationObserver, der auf DOM-Änderungen lauscht und Isso.init() aufruft. Funktioniert nicht – Isso.init() existiert nicht oder setzt den Thread nicht neu an.

Lösung: Die post.html einfach abschaffen. Statt post.html?slug=... verlinken die Artikel direkt auf artikel/{slug}.html. Die statischen Seiten haben Isso eingebaut, funktionieren zuverlässig. Der SPA-Komfort war es nicht wert – die Seiten laden eh schnell genug.

Was bleibt

Das Skript ist jetzt stabil und idempotent. Jeder Durchlauf:

  • Überspringt die Instanz-Erstellung, wenn sie schon existiert
  • Installiert Pakete nur, wenn sie fehlen
  • Erstellt den User nur, wenn er noch nicht da ist
  • Überschreibt Config und Service (mit neuem Admin-Passwort)
  • Startet Isso neu

HTTPS ist über einen externen Caddy gelöst – der terminiert TLS und leitet auf die Isso-Instanz weiter. Kein extra Zertifikat nötig, kein Certbot, kein Port 443 im Container.

Der erste Login

Die Admin-Oberfläche ist unter https://kommentare.frischux.de/admin erreichbar. Ein User muss nicht extra angelegt werden – das generierte Passwort aus dem Setup-Script reicht direkt zum Einloggen. Danach Domain frischux.de in den Domains hinzufügen, und los geht's.

Benachrichtigungen über ntfy

Isso kann bei notify = stdout Kommentar-Benachrichtigungen an die Standard-Ausgabe senden. Theoretisch. In der Praxis puffert gunicorn stdout – die Notifications kommen nie in der Pipe an.

Lösung: Ein systemd-Timer, der jede Minute die SQLite-Datenbank abfragt. Das Skript vergleicht die Anzahl freigeschalteter (mode=1) und ausstehender (mode=2) Kommentare mit dem letzten Stand. Bei neuen Einträgen geht eine Nachricht an ntfy.

In der Isso-Konfiguration muss sqlite3 installiert sein – das Setup-Script erledigt das mit. Die Spalte mode in der Tabelle comments unterscheidet zwischen freigeschaltet (1), ausstehend (2) und Spam (0).

Status

Die Kommentarfunktion ist testweise eingebaut. Ob ich sie behalten werde, weiß ich noch nicht. Mal schauen, ob sich jemand meldet.

Das Setup-Script

Das komplette Script zum Einrichten der Isso-Instanz. Auf dem Incus-Host ausfuehren, beliebig oft ausfuehren.

#!/bin/bash
set -e

INSTANZ="isso"
DOMAIN="kommentare.frischux.de"
BLOG_HOST="https://frischux.de"
ADMIN_PASSWD=$(openssl rand -base64 16)
TMPDIR=$(mktemp -d)

echo "=== Isso-Setup fuer Incus ==="
echo "Instanz:   $INSTANZ"
echo "Domain:    $DOMAIN"
echo "Blog:      $BLOG_HOST"
echo "Admin-PW:  $ADMIN_PASSWD"
echo ""

# --- Instanz anlegen ---
if incus list "$INSTANZ" --format csv 2>/dev/null | grep -q "^$INSTANZ,"; then
    echo "Instanz '$INSTANZ' existiert bereits."
else
    echo "Erstelle Incus-Instanz..."
    incus launch images:debian/13 "$INSTANZ"
    sleep 3
fi

# --- Pakete installieren ---
echo "Installiere Abhaengigkeiten..."
incus exec "$INSTANZ" -- bash -c "
    export DEBIAN_FRONTEND=noninteractive
    apt-get update -qq
    apt-get install -y -qq python3 python3-venv python3-dev gcc build-essential curl </dev/null
" >/dev/null

# --- Isso-User anlegen ---
if incus exec "$INSTANZ" -- bash -c "id isso" &>/dev/null; then
    echo "User 'isso' existiert bereits."
else
    echo "Erstelle User 'isso'..."
    incus exec "$INSTANZ" -- useradd -m -s /bin/bash isso
fi

# --- Virtualenv + Isso installieren ---
echo "Installiere Isso + Gunicorn..."
incus exec "$INSTANZ" -- sudo -u isso bash -c '
    if [ ! -d /home/isso/venv ]; then
        python3 -m venv /home/isso/venv
    fi
    /home/isso/venv/bin/pip install --quiet isso gunicorn
'

# --- Konfiguration ---
echo "Schreibe Konfiguration..."
SALT=$(openssl rand -hex 16)
cat > "$TMPDIR/isso.cfg" << EOF
[general]
dbpath = /home/isso/comments.db
host = ${BLOG_HOST}
notify = stdout
reply-notifications = false
gravatar = true

[moderation]
enabled = true
purge-after = 30d

[server]
listen = http://0.0.0.0:8080
public-endpoint = https://${DOMAIN}

[guard]
enabled = true
ratelimit = 2
direct-reply = 3
reply-to-self = false
require-author = true
require-email = true

[hash]
salt = ${SALT}
algorithm = pbkdf2

[admin]
enabled = true
password = ${ADMIN_PASSWD}

[markup]
options = strikethrough, superscript, autolink
allowed-elements = a, blockquote, br, code, em, p, pre, strong, ul, ol, li
allowed-attributes = a href
EOF
incus file push "$TMPDIR/isso.cfg" "$INSTANZ"/home/isso/isso.cfg
incus exec "$INSTANZ" -- chown isso:isso /home/isso/isso.cfg

# --- Notify-Script ---
echo "Erstelle Notify-Script..."
cat > "$TMPDIR/isso-check-comments.sh" << 'NOTIFY'
#!/bin/bash
DB="/home/isso/comments.db"
STATE="/home/isso/.last_comment_state"
NTFY_SERVER="https://ntfy.frischux.de"
NTFY_TOPIC="blog"

if [ ! -f "$DB" ]; then exit 0; fi

APPROVED=$(sqlite3 "$DB" "SELECT COUNT(*) FROM comments WHERE mode=1;" 2>/dev/null || echo 0)
PENDING=$(sqlite3 "$DB" "SELECT COUNT(*) FROM comments WHERE mode=2;" 2>/dev/null || echo 0)

PREV_APPROVED=0
PREV_PENDING=0
[ -f "$STATE" ] && read -r PREV_APPROVED PREV_PENDING < "$STATE"

MSG=""
if [ "$APPROVED" -gt "$PREV_APPROVED" ]; then
    NEW=$((APPROVED - PREV_APPROVED))
    MSG="${MSG}${NEW} freigeschalteter Kommentar\n"
fi
if [ "$PENDING" -gt "$PREV_PENDING" ]; then
    NEW=$((PENDING - PREV_PENDING))
    MSG="${MSG}${NEW} Kommentar wartet auf Freischaltung\n"
fi

if [ -n "$MSG" ]; then
    curl -s -d "$(echo -e "$MSG")" "$NTFY_SERVER/$NTFY_TOPIC" >/dev/null
fi

echo "$APPROVED $PENDING" > "$STATE"
NOTIFY
incus file push "$TMPDIR/isso-check-comments.sh" "$INSTANZ"/home/isso/check-comments.sh
incus exec "$INSTANZ" -- chmod +x /home/isso/check-comments.sh
incus exec "$INSTANZ" -- chown isso:isso /home/isso/check-comments.sh
incus exec "$INSTANZ" -- apt-get install -y -qq sqlite3 >/dev/null

# --- Systemd-Service ( Kommentar-Check ) ---
echo "Erstelle Kommentar-Check-Service..."
cat > "$TMPDIR/isso-comments-check.service" << 'EOF'
[Unit]
Description=Check for new Isso comments

[Service]
Type=oneshot
User=isso
ExecStart=/home/isso/check-comments.sh
EOF
incus file push "$TMPDIR/isso-comments-check.service" "$INSTANZ"/etc/systemd/system/isso-comments-check.service

cat > "$TMPDIR/isso-comments-check.timer" << 'EOF'
[Unit]
Description=Check for new Isso comments every minute

[Timer]
OnBootSec=60
OnUnitActiveSec=60

[Install]
WantedBy=timers.target
EOF
incus file push "$TMPDIR/isso-comments-check.timer" "$INSTANZ"/etc/systemd/system/isso-comments-check.timer

# --- Systemd-Service (Isso) ---
echo "Erstelle Systemd-Service..."
cat > "$TMPDIR/isso.service" << 'EOF'
[Unit]
Description=Isso commenting system
After=network.target

[Service]
Type=simple
User=isso
Environment=ISSO_SETTINGS=/home/isso/isso.cfg
ExecStart=/home/isso/venv/bin/gunicorn -b 0.0.0.0:8080 -w 4 --preload isso.run
Restart=on-failure
LimitNOFILE=16384

[Install]
WantedBy=multi-user.target
EOF
incus file push "$TMPDIR/isso.service" "$INSTANZ"/etc/systemd/system/isso.service

incus exec "$INSTANZ" -- systemctl daemon-reload
incus exec "$INSTANZ" -- systemctl enable --now isso
incus exec "$INSTANZ" -- systemctl enable --now isso-comments-check.timer

# --- Verifikation ---
sleep 2
if incus exec "$INSTANZ" -- curl -s http://0.0.0.0:8080/info | grep -q "version"; then
    echo "Isso laeuft."
else
    echo "FEHLER: Isso antwortet nicht!"
    incus exec "$INSTANZ" -- systemctl status isso --no-pager || true
    exit 1
fi

rm -rf "$TMPDIR"

echo ""
echo "=== Fertig ==="
echo ""
echo "Isso laeuft auf http://0.0.0.0:8080 in der Instanz '${INSTANZ}'"
echo ""
echo "Admin-Interface: https://${DOMAIN}/admin"
echo "Admin-Passwort:  ${ADMIN_PASSWD}"