Waline Comment System
Inhaltsverzeichnis
Warum überhaupt ein neues Kommentarsystem?
Auf meinem alten Blog hatte ich Giscus im Einsatz - ein System, das GitHub Discussions als Backend nutzt. Das funktionierte auch soweit ganz gut, aber ich wollte schon länger mal ein wirklich eigenständiges, selbst gehostetes Kommentarsystem ausprobieren. Etwas, das komplett unabhängig von externen Diensten läuft und mir volle Kontrolle über die Daten gibt.
Also hab ich mich auf die Suche gemacht und verschiedene Lösungen getestet. Remark42 hatte ich zunächst implementiert, aber das habe ich nicht sauber ans laufen gekommen. Nach einigem Hin und Her bin ich schließlich bei Waline gelandet - einem modernen, schlanken Kommentarsystem, das ursprünglich für den chinesischen Markt entwickelt wurde, aber international hervorragend funktioniert.
Die Integration in statische Sites funktioniert mit einem simplen Script-Tag, während man bei der Datenbank die freie Wahl zwischen SQLite, MySQL und PostgreSQL hat. Der Dark Mode Support funktioniert out-of-the-box ohne Gefrickel, und Features wie Markdown-Support, Emojis und Reactions sind bereits eingebaut.
Die Docker Compose Config
Erstmal das Grundgerüst. Ich nutze Traefik als Reverse Proxy, wie mein traefik stack kannst du direkt hier nachlesen.
---
services:
waline:
image: lizheming/waline:latest
container_name: waline
restart: unless-stopped
networks:
- frontend
volumes:
- ./data:/app/data
environment:
SQLITE_PATH: /app/data
# JWT Secret
# openssl rand -base64 32
JWT_TOKEN: "dein-super-geheimes-random-token-hier"
# Site Config
SITE_NAME: "deineseite.de"
SITE_URL: "https://waline.example.com"
AUTHOR_EMAIL: "deine@email.de"
# Sprache - ja, Deutsch wird (noch) nicht offiziell supported
# Aber dazu später mehr
LANG: de
# MEGA WICHTIG: Alle Domains, die auf Waline zugreifen!
SECURE_DOMAINS: "waline.example.de,2tap2.be,www.2tap2.be"
labels:
- "traefik.enable=true"
- "traefik.http.routers.waline.entrypoints=websecure"
- "traefik.http.routers.waline.rule=Host(`waline.steltner.cloud`)"
- "traefik.http.routers.waline.tls=true"
- "traefik.http.routers.waline.tls.certresolver=cloudflare"
- "traefik.http.routers.waline.service=waline"
- "traefik.http.services.waline.loadbalancer.server.port=8360"
- "traefik.docker.network=frontend"
- "traefik.http.routers.waline.middlewares=geoblock-de@file,crowdsec-bouncer@file"
networks:
- frontend
networks:
frontend:
external: true
Der SQLite-Stolperstein 🪨
Hier wird’s interessant. Anders als bei PostgreSQL oder MySQL erstellt Waline die Datenbanktabellen bei SQLite NICHT automatisch. Wenn du jetzt einfach docker compose up -d machst und denkst “läuft”, wirst du beim Login einen schönen SQLITE_ERROR: no such table: wl_Users bekommen.
Du musst die Template-Datenbank von GitHub holen:
# Container erstmal stoppen (falls schon gestartet)
docker compose down
# Template-Datenbank downloaden
curl -L https://github.com/walinejs/waline/releases/latest/download/waline.sqlite \
-o data/waline.sqlite
# Und jetzt läuft's
docker compose up -d
Das data Verzeichnis wird automatisch erstellt, wenn du den Container das erste Mal startest. Du kannst die Datenbank also erst danach runterladen.
Integration in Astro
Astro macht es einem echt easy, externe Scripts einzubinden. Ich hab eine Waline.astro Komponente gebaut:
---
const { slug } = Astro.props;
const WALINE_SERVER_URL = "https://waline.example.com";
---
<div id="waline"></div>
<script is:inline define:vars={{ WALINE_SERVER_URL }}>
const isDark = document.documentElement.classList.contains("dark");
import("https://unpkg.com/@waline/client@v3/dist/waline.js").then(
({ init }) => {
const walineInstance = init({
el: "#waline",
serverURL: WALINE_SERVER_URL,
lang: "de-DE",
locale: {
// Volle deutsche Lokalisierung - siehe unten
placeholder: "Schreibe einen Kommentar... (Markdown wird unterstützt)",
// ... mehr Übersetzungen
},
dark: isDark ? "html.dark" : false,
meta: ["nick", "mail", "link"],
requiredMeta: ["nick"],
pageSize: 10,
wordLimit: [0, 5000],
emoji: [
"//unpkg.com/@waline/emojis@1.2.0/weibo",
"//unpkg.com/@waline/emojis@1.2.0/tieba",
],
reaction: true,
});
// Theme-Wechsel automatisch synchronisieren
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "class") {
const isDark = document.documentElement.classList.contains("dark");
if (walineInstance?.update) {
walineInstance.update({
dark: isDark ? "html.dark" : false,
});
}
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
}
);
</script>
<style is:global>
@import "https://unpkg.com/@waline/client@v3/dist/waline.css";
#waline {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid rgb(229 231 235);
}
.dark #waline {
border-color: rgb(63 63 70);
}
:root {
--waline-theme-color: var(--accent);
--waline-active-color: var(--accent);
}
</style>
Deutsche Lokalisierung - Custom Style
Waline unterstützt Deutsch offiziell nicht out-of-the-box. Kein Problem, wir machen’s selbst! Die locale Option akzeptiert ein komplettes Übersetzungsobjekt:
locale: {
nick: "Name",
nickError: "Bitte geben Sie einen Namen ein",
mail: "E-Mail",
mailError: "Bitte geben Sie eine gültige E-Mail-Adresse ein",
link: "Website",
optional: "Optional",
placeholder: "Schreibe einen Kommentar... (Markdown wird unterstützt)",
sofa: "Keine Kommentare vorhanden",
submit: "Absenden",
like: "Gefällt mir",
cancelLike: "Gefällt mir nicht mehr",
reply: "Antworten",
cancelReply: "Abbrechen",
comment: "Kommentare",
refresh: "Aktualisieren",
more: "Mehr laden",
preview: "Vorschau",
emoji: "Emoji",
uploadImage: "Bild hochladen",
seconds: "Sekunden zuvor",
minutes: "Minuten zuvor",
hours: "Stunden zuvor",
days: "Tage zuvor",
now: "Gerade eben",
uploading: "Hochladen",
login: "Anmelden",
logout: "Abmelden",
admin: "Administrator",
sticky: "Anpinnen",
word: "Wörter",
wordHint: "Bitte geben Sie zwischen $0 und $1 Wörter ein!\n Aktuell: $2",
anonymous: "Anonym",
level0: "Kampfschlumpf",
level1: "Schreibschlumpf",
level2: "Chatterschlumpf",
level3: "Schmetterschlumpf",
level4: "Bloggerschlumpf",
level5: "Legendenschlumpf",
gif: "GIF",
gifSearchPlaceholder: "GIF suchen",
profile: "Profil",
approved: "Genehmigt",
waiting: "Wartet auf Genehmigung",
spam: "Spam",
unsticky: "Nicht mehr anpinnen",
oldest: "Älteste",
latest: "Neueste",
hottest: "Beliebteste",
reactionTitle: "Was denkst du?",
}
Ja, die Level-Namen sind… kreativ. Feel free, die anzupassen
Das habe ich einfach schnell von einem LLM Coden lassen...
Einbinden im Blog Layout
In deinem BlogLayout.astro:
---
import Waline from "../components/Waline.astro";
// ... andere imports
---
<BaseLayout>
<article>
<!-- Dein Content -->
<slot />
<!-- Waline am Ende -->
<Waline slug={frontmatter.slug} />
</article>
</BaseLayout>
Admin-Panel & erste Schritte
Nach dem ersten Start:
- Geh zu
https://waline.example.com/ui - Registriere dich mit einer E-Mail
- Wichtig: Der erste User wird automatisch Admin!
- Du kannst jetzt Kommentare moderieren, Nutzer verwalten, etc.
Das Admin Panel ist schlicht und selbsterklärend gebaut.
Backup-Strategie für SQLite
SQLite ist super einfach zu backuppen - es ist ja nur eine Datei. Ich hab mir ein kleines Script gebastelt:
#!/bin/bash
BACKUP_DIR=~/waline/backups
mkdir -p $BACKUP_DIR
# Backup mit Timestamp
cp ~/waline/data/waline.sqlite \
$BACKUP_DIR/waline.sqlite.$(date +%Y%m%d-%H%M%S)
# Alte Backups löschen (älter als 7 Tage)
find $BACKUP_DIR -name "waline.sqlite.*" -mtime +7 -delete
echo "Backup completed: $(date)"
Und ab in die Crontab damit (täglich um 3 Uhr nachts):
crontab -e
# Füge hinzu:
0 3 * * * /bin/bash ~/waline/backup.sh
Optional: E-Mail-Benachrichtigungen
Wenn du willst, dass Leute benachrichtigt werden, wenn jemand auf ihren Kommentar antwortet, brauchst du SMTP:
environment:
SMTP_HOST: "smtp.gmail.com"
SMTP_PORT: "465"
SMTP_USER: "deine-email@gmail.com"
SMTP_PASS: "dein-passwort"
SMTP_SECURE: "true"
SENDER_NAME: "2tap2.be Comments"
SENDER_EMAIL: "noreply@2tap2.be"