Beim Umstieg von WordPress zu einer statischen Website mit Publii bleiben oft alte Gutenberg-Kommentare und veraltete Bildpfade in der SQLite-Datenbank zurück. Diese Schritt-für-Schritt-Anleitung zeigt, wie du die Datenbank von Altlasten befreist und gleichzeitig fehlende SEO-Metadaten automatisiert generierst.

1. Datenbank-Architektur von Publii

Publii nutzt eine schlanke SQLite-Datenbank (db.sqlite). Für Beiträge sind im Wesentlichen zwei Tabellen relevant:

  • posts: Enthält die Hauptdaten wie ID, Titel, Slug und den eigentlichen Artikeltext (text).
  • posts_additional_data: Verwaltet erweiterte Einstellungen und SEO-Daten. Diese werden als JSON-Objekt im Feld value unter dem Schlüssel key = '_core' gespeichert.

2. WordPress-Gutenberg-Blocks & Altlasten entfernen

WordPress speichert Block-Informationen als HTML-Kommentare (z. B. <!-- wp:paragraph -->). Zudem verweisen alte Bilder meist noch auf Verzeichnisse wie /wp-content/uploads/.

Gutenberg-Kommentare entfernen

Mit folgendem Regulären Ausdruck (RegEx) lassen sich die Block-Tags aus dem Beitragsfeld posts.text herausfiltern:

import sqlite3
import json
import re
import html
import shutil
import datetime

db_path = r'C:\Users\deinuser\Documents\Publii\sites\deine seite\input\db.sqlite'

# 1. Backup
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
shutil.copyfile(db_path, f'{db_path}.backup_{timestamp}')

conn = sqlite3.connect(db_path)
cursor = conn.cursor()

# 2. Beiträge verarbeiten
cursor.execute("SELECT id, title, text FROM posts")
posts = cursor.fetchall()

for post_id, title, text in posts:
    raw_text = text or ''

    # --- BEREINIGUNG LOGIK ---
    # A) Gutenberg-Kommentare entfernen ()
    clean_text = re.sub(r'', '', raw_text)
    
    # B) HTML-Tags entfernen
    clean_text = re.sub(r'<[^>]+>', ' ', clean_text)
    
    # C) HTML-Entities auflösen (& -> &, ä -> ä, $ -> $)
    clean_text = html.unescape(clean_text)
    
    # D) Kaputte Umlaute / Zeichen korrigieren
    replacements = {
        'ä': 'ä', 'ö': 'ö', 'ü': 'ü',
        'Ä': 'Ä', 'Ö': 'Ö', 'Ü': 'Ü',
        'ß': 'ß', '€': '€', '$': '$'
    }
    for bad_char, good_char in replacements.items():
        clean_text = clean_text.replace(bad_char, good_char)

    # E) Leerzeichen säubern
    clean_text = re.sub(r'\s+', ' ', clean_text).strip()

    # --- 1. POSTS-TABELLE UPDATEN (Falls du den gereinigten Text im Beitrag willst) ---
    cursor.execute("UPDATE posts SET text = ? WHERE id = ?", (clean_text, post_id))

    # --- 2. META-DESCRIPTION ERSTELLEN ---
    if len(clean_text) > 155:
        truncated = clean_text[:152]
        meta_desc = truncated.rsplit(' ', 1)[0] + '...' if ' ' in truncated else truncated
    else:
        meta_desc = clean_text or (title or '')

    # Core JSON verarbeiten
    cursor.execute("SELECT value FROM posts_additional_data WHERE post_id = ? AND key = '_core'", (post_id,))
    row = cursor.fetchone()
    core_data = json.loads(row[0]) if (row and row[0]) else {}

    core_data['metaTitle'] = title.strip() if title else ''
    core_data['metaDesc'] = meta_desc

    json_val = json.dumps(core_data, ensure_ascii=False)

    if row:
        cursor.execute("UPDATE posts_additional_data SET value = ? WHERE post_id = ? AND key = '_core'", (json_val, post_id))
    else:
        cursor.execute("INSERT INTO posts_additional_data (post_id, key, value) VALUES (?, '_core', ?)", (post_id, json_val))

conn.commit()
conn.close()
print("Bereinigung und Update erfolgreich abgeschlossen!")

Medienpfade anpassen

Alte absolute WordPress-Bildpfade (https://domain.de/wp-content/uploads/...) werden entweder durch relative Pfade oder die interne Medienstruktur von Publii (media/posts/ID/...) ersetzt.

3. SEO-Metadaten automatisch generieren

Damit Suchmaschinen die Inhalte optimal erfassen, liest Publii SEO-Titel und Meta-Beschreibungen aus dem JSON-Objekt der Tabelle posts_additional_data aus. Der entscheidende Schlüssel für die Meta-Beschreibung lautet hierbei metaDesc (nicht metaDescription).

Das folgende Python-Skript erstellt ein Backup, liest den Text jedes Beitrags aus, bereinigt ihn von HTML-Tags, kürzt ihn sauber am letzten Wort auf 155 Zeichen und schreibt das Ergebnis zurück in das JSON-Feld:

import sqlite3
import json
import re
import html
import shutil
import datetime

db_path = r'C:\Pfad\zu\deiner\Publii\input\db.sqlite'

# 1. Automatisches Sicherheits-Backup
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
shutil.copyfile(db_path, f'{db_path}.backup_{timestamp}')

conn = sqlite3.connect(db_path)
cursor = conn.cursor()

# 2. Beiträge verarbeiten
cursor.execute("SELECT id, title, text FROM posts")
posts = cursor.fetchall()

for post_id, title, text in posts:
    # HTML-Tags entfernen & Text säubern
    clean_text = re.sub(r'<[^>]+>', ' ', text or '')
    clean_text = re.sub(r'\s+', ' ', html.unescape(clean_text)).strip()
    
    # Auf max. 155 Zeichen kürzen
    if len(clean_text) > 155:
        truncated = clean_text[:152]
        meta_desc = truncated.rsplit(' ', 1)[0] + '...' if ' ' in truncated else truncated
    else:
        meta_desc = clean_text or title

    # JSON-Struktur abrufen / erstellen
    cursor.execute("SELECT value FROM posts_additional_data WHERE post_id = ? AND key = '_core'", (post_id,))
    row = cursor.fetchone()
    core_data = json.loads(row[0]) if (row and row[0]) else {}

    # SEO-Keys setzen (Wichtig: "metaDesc")
    core_data['metaTitle'] = title.strip() if title else ''
    core_data['metaDesc'] = meta_desc

    json_val = json.dumps(core_data, ensure_ascii=False)

    if row:
        cursor.execute("UPDATE posts_additional_data SET value = ? WHERE post_id = ? AND key = '_core'", (json_val, post_id))
    else:
        cursor.execute("INSERT INTO posts_additional_data (post_id, key, value) VALUES (?, '_core', ?)", (post_id, json_val))

conn.commit()
conn.close()

4. Wichtige Fallstricke & Best Practices

Achtung: Bevor Datenbanänderungen durchgeführt werden, müssen sowohl die Publii-App als auch etwaige SQLite-Viewer (wie der DB Browser) vollständig geschlossen sein.

  • Prozess-Sperre: SQLite-Datenbanken werden bei Schreibzugriffen für andere Programme gesperrt. Schließe Publii vor der Ausführung von Skripten.
  • Arbeitsspeicher-Cache: Publii lädt die Datenbank beim Programmstart in den RAM. Wenn Änderungen bei geöffnetem Publii durchgeführt werden, können sie beim Speichern innerhalb der App überschrieben werden.
  • Site Rendern: Nach der Datenbankbereinigung muss in Publii einmalig der Button „Render site“ geklickt werden. Erst dadurch schreibt Publii die neuen Werte aus der Datenbank in den <head>-Bereich der generierten HTML-Dateien.