Wie ein kleines Python-Skript entstand, das für neue WordPress-Beiträge automatisch den Vorschau-Tag samt Miniaturbild erzeugt – per Vibe-Coding statt Handarbeit

16. September 2026

Wie ich mir per Vibe-Coding ein kleines Python-Skript zusammenklicken (lassen) habe, das für neue Blogbeiträge automatisch den WordPress-Vorschau-Tag erzeugt – inklusive Miniaturbild.

Für die Startseite meines Blogs brauche ich zu jedem neuen Beitrag einen kleinen HTML-Tabellen-Tag mit Titel, Datum, Teasertext und einem Vorschaubildchen. Bisher habe ich diesen Tag von Hand zusammengebastelt – Titel abtippen, Datum eintragen, ein Bild verkleinern, alles in die richtige Tabellenstruktur pressen. Das geht, ist aber auf Dauer lästig genug, um es sich zu automatisieren. Also habe ich mir die Sache von einer KI programmieren lassen, ausgehend von genau so einem fertigen Tag als Vorlage.

Bildschirmfoto zu 2026 09 16 13 56
Automatisch erstellte und eingebaute Vorschau-Tabellen-Tags. Die Zeitersparnis ist eine große Erleichterung.

Der erste Wurf: Ausgangspunkt war ein bestehender Vorschau-Tag aus meiner Vorlage sowie ein konkreter Beispiel-Beitrag von meinem Blog. Daraus sollte ein Python-Programm für Ubuntu entstehen, dem man einfach die URL eines neuen Beitrags übergibt, und das dann automatisch Titel, Veröffentlichungsdatum und den ersten längeren Absatz als Teasertext ausliest, das erste tatsächliche Bild im Beitrag erkennt (das Kopfzeilen-Logo wird dabei übersprungen) und daraus eine 150 Pixel breite Miniatur mit erhaltenem Seitenverhältnis erzeugt. Alles landet in einem eigenen Ordner, und der fertige Tag wird sowohl als Datei gespeichert als auch direkt in der Konsole ausgegeben, damit man ihn bequem kopieren kann.

Ein Knackpunkt dabei: Damit die Miniatur im fertigen Tag auch wirklich erreichbar ist, muss sie in der WordPress-Mediathek liegen. Die erste Version bot dafür zwei Wege an – entweder das Bildchen von Hand in die Mediathek laden und die Attachment-ID manuell in einen Platzhalter eintragen, oder einmalig Zugangsdaten samt Anwendungspasswort hinterlegen, damit das Skript den Upload und das Eintragen von ID und URL komplett selbst erledigt.

Vereinfachung statt Automatik: Mir war der komplette Upload-Mechanismus dann zu viel des Guten – ich wollte lieber selbst entscheiden, wann und wie das Bild hochgeladen wird. Also wurde der gesamte Upload-Teil wieder herausgenommen. Übrig blieb ein deutlich schlankeres Skript, das nur noch die Seite lädt, Titel, Datum, Teasertext und erstes Bild auswertet, daraus die Miniatur erzeugt und den Tag mit gut sichtbaren Platzhaltern für Bild-URL und Attachment-ID baut. Diese beiden Stellen trage ich seitdem nach dem Hochladen der Miniatur von Hand ein – ein bewusster Verzicht auf Automatisierung an der einen Stelle, an der ich lieber selbst die Kontrolle behalte.

Ein kleiner Bedienfehler und seine Behebung: Bei den ersten Testläufen bin ich über eine Kleinigkeit gestolpert: Eine Nachfrage, ob der Tag automatisch in die Seite eingefügt werden soll, erschien bei mir einfach nicht. Der Grund war simpel – dafür war ein zusätzlicher Aufrufparameter nötig, den ich beim gewohnten Starten des Programms schlicht nicht mitgegeben hatte. Statt mir zu merken, wann welcher Zusatz nötig ist, habe ich das Skript lieber so umbauen lassen, dass es sich meiner gewohnten Bedienung anpasst: Es fragt jetzt von sich aus am Ende nach, ob der Tag automatisch eingefügt werden soll – mit Vorschau der Einfügestelle davor – und das ganz ohne zusätzliches Flag. Der Parameter existiert zwar weiterhin, dient aber nur noch dazu, diese Nachfrage bei Bedarf zu überspringen.

Installation und Aufruf: Für den Betrieb braucht es einmalig pip3 install –user requests beautifulsoup4 pillow, danach reicht der Aufruf python3 vorschau_tag_generator.py mit anschließender Eingabe der Beitrags-URL, oder direkt python3 vorschau_tag_generator.py https://elektronikbasteln.pl7.de/dein-beitrag mit der URL als Argument. Die erzeugte Miniatur landet unter ~/Vorschau-Tags/, der fertige Tag liegt dort zusätzlich als Datei und wird gleichzeitig in der Konsole ausgegeben, sodass man ihn direkt kopieren kann.

Das endgültige Skript von Claude.ai erstellt:

#!/usr/bin/env python3
"""
Vorschau-Tag-Generator für Volkers WordPress-Blog (elektronikbasteln.pl7.de)

Erzeugt automatisch den HTML-Vorschau-Tag (Tabelle mit Miniaturbild und
Teaser-Text), wie er auf der Startseite für neue Beiträge eingesetzt wird -
inklusive automatisch erzeugtem Miniaturbild aus dem ersten Bild des Beitrags.

Wenn Zugangsdaten hinterlegt sind (siehe --setup):
  - lädt das Programm das Miniaturbild automatisch in die WordPress-Mediathek
    hoch und setzt die echte Bild-URL und Attachment-ID direkt in den Tag ein.
  - fragt danach von sich aus nach, ob der fertige Tag zusätzlich automatisch
    auf der Startseite eingefügt werden soll, direkt nach "Die neuesten
    Beiträge:" und vor der bisher ersten Tabelle. Vor jeder Änderung wird der
    bisherige Seiteninhalt lokal als Sicherungskopie abgelegt, und es wird
    eine Vorschau sowie eine Bestätigung angezeigt, bevor live geschrieben
    wird. Mit --publish oder --yes lässt sich diese Nachfrage überspringen.

Ohne Zugangsdaten wird das Miniaturbild nur lokal erzeugt, mit Platzhaltern
im Tag zum manuellen Nachtragen.

Verwendung:
    python3 vorschau_tag_generator.py <URL-des-Beitrags>
    python3 vorschau_tag_generator.py <URL-des-Beitrags> --yes

Einmalige Einrichtung des automatischen Uploads/Einfügens:
    python3 vorschau_tag_generator.py --setup

Ohne Argument fragt das Programm interaktiv nach der URL.
"""

import argparse
import base64
import configparser
import re
import sys
import unicodedata
from datetime import datetime
from pathlib import Path
from urllib.parse import urljoin, urlparse

import requests
from bs4 import BeautifulSoup

try:
    from PIL import Image
except ImportError:
    print("Das Modul 'Pillow' fehlt. Installiere es mit:\n    sudo apt install python3-pil")
    sys.exit(1)


# ---------------------------------------------------------------------------
# Konfiguration
# ---------------------------------------------------------------------------

CONFIG_PATH = Path.home() / ".vorschau_tag_generator.ini"
OUTPUT_DIR = Path.home() / "Vorschau-Tags"
BACKUP_DIR = OUTPUT_DIR / "Backups"

MONATE = [
    "Januar", "Februar", "März", "April", "Mai", "Juni",
    "Juli", "August", "September", "Oktober", "November", "Dezember",
]

TABLE_STYLE = 'style="width: 97.381%;"'
LEFT_CELL_STYLE = 'style="width: 29.0412%;"'
RIGHT_CELL_STYLE = 'style="width: 68.2207%; text-align: left; vertical-align: top;"'
TABLE_OPEN_TAG = f"<table {TABLE_STYLE}>"
FRONTPAGE_MARKER = "Die neuesten Beiträge:"
DEFAULT_PAGE_ID = 93
THUMB_WIDTH = 150
USER_AGENT = "Mozilla/5.0 (VorschauTagGenerator/1.0)"

# Platzhalter für den Fall, dass kein automatischer Upload eingerichtet ist
PLATZHALTER_ID = "HIER-ID-EINTRAGEN"
PLATZHALTER_URL = "HIER-BILD-URL-NACH-UPLOAD-EINTRAGEN"


def load_config():
    cfg = configparser.ConfigParser()
    if CONFIG_PATH.exists():
        cfg.read(CONFIG_PATH)
    if "wordpress" not in cfg:
        cfg["wordpress"] = {}
    return cfg


def save_config(cfg):
    with open(CONFIG_PATH, "w") as f:
        cfg.write(f)
    CONFIG_PATH.chmod(0o600)


def setup_wizard():
    print("Einrichtung des automatischen Bild-Uploads und Seiten-Einfügens.\n")
    print("Dafür brauchst du ein 'Anwendungspasswort' (nicht dein normales Login-Passwort):")
    print("  WordPress-Backend -> Benutzer -> Profil -> Anwendungspasswörter")
    print("  Namen vergeben (z.B. 'vorschau-tag-generator'), erzeugen lassen,")
    print("  das angezeigte Passwort hier gleich einfügen (wird nur einmal angezeigt).\n")
    user = input("WordPress-Benutzername: ").strip()
    if not user:
        print("Abgebrochen. Es wurde nichts gespeichert.")
        return
    app_password = input("Anwendungspasswort: ").strip()
    cfg = load_config()
    cfg["wordpress"]["user"] = user
    cfg["wordpress"]["app_password"] = app_password
    save_config(cfg)
    print(f"\nGespeichert in {CONFIG_PATH} (nur für dich lesbar).")
    print("Ab jetzt lädt das Programm Miniaturbilder automatisch hoch.")
    print("Mit --publish kann es den Tag zusätzlich auf der Startseite einfügen.")


def auth_header(cfg):
    user = cfg["wordpress"].get("user")
    app_password = cfg["wordpress"].get("app_password")
    if not user or not app_password:
        return None
    token = base64.b64encode(f"{user}:{app_password}".encode()).decode()
    return {"Authorization": f"Basic {token}"}


# ---------------------------------------------------------------------------
# Beitragsseite laden und Daten extrahieren
# ---------------------------------------------------------------------------

def fetch_page(url):
    headers = {"User-Agent": USER_AGENT}
    r = requests.get(url, headers=headers, timeout=20)
    r.raise_for_status()
    if r.encoding is None or r.encoding.lower() == "iso-8859-1":
        r.encoding = r.apparent_encoding or "utf-8"
    return r.text


def get_meta(soup, prop=None, name=None):
    if prop:
        tag = soup.find("meta", attrs={"property": prop})
    else:
        tag = soup.find("meta", attrs={"name": name})
    return tag["content"].strip() if tag and tag.get("content") else None


def find_content_container(soup):
    for sel in ["div.entry-content", "div.post-content", "article", "div#content", "main"]:
        el = soup.select_one(sel)
        if el:
            return el
    return soup.body or soup


def extract_title(soup):
    h1 = soup.find("h1")
    if h1 and h1.get_text(strip=True):
        return h1.get_text(strip=True)
    title = get_meta(soup, prop="og:title")
    if title:
        return title
    if soup.title:
        return soup.title.get_text(strip=True)
    return "TITEL_NICHT_GEFUNDEN"


def extract_date(soup):
    raw = get_meta(soup, prop="article:published_time")
    if raw:
        try:
            dt = datetime.fromisoformat(raw)
            return f"{dt.day}. {MONATE[dt.month - 1]} {dt.year}"
        except ValueError:
            pass
    return datetime.now().strftime("%d.%m.%Y")


def extract_teaser(soup, content):
    for p in content.find_all("p"):
        text = p.get_text(" ", strip=True)
        if len(text) > 60:
            return text
    desc = get_meta(soup, prop="og:description") or get_meta(soup, name="description")
    return desc or "TEASERTEXT_BITTE_MANUELL_EINFUEGEN"


def extract_caption_for_image(img):
    # Klassisches WordPress-Bildunterschrift-Markup: <div class="wp-caption">...<p class="wp-caption-text">
    wrapper = img.find_parent(["div", "figure"])
    if wrapper:
        cap = wrapper.find("figcaption") or wrapper.find(
            lambda tag: tag.name == "p" and tag.get("class") and
            any("caption" in c for c in tag.get("class"))
        )
        if cap:
            return cap.get_text(" ", strip=True)
    return ""


def extract_first_image(content, base_url):
    for img in content.find_all("img"):
        src = img.get("src") or img.get("data-src")
        if not src:
            continue
        if any(skip in src.lower() for skip in ("avatar", "logo", "header")):
            continue
        full_url = urljoin(base_url, src)
        alt = img.get("alt", "").strip()
        caption = extract_caption_for_image(img)
        return full_url, alt, caption
    return None, None, None


# ---------------------------------------------------------------------------
# Bild herunterladen und verkleinern
# ---------------------------------------------------------------------------

def slugify_stem(url):
    stem = Path(urlparse(url).path).stem
    stem = unicodedata.normalize("NFKD", stem).encode("ascii", "ignore").decode()
    stem = re.sub(r"[^A-Za-z0-9_\-]", "", stem)
    return stem or "bild"


def download_and_resize(image_url, output_dir):
    headers = {"User-Agent": USER_AGENT}
    r = requests.get(image_url, headers=headers, timeout=20)
    r.raise_for_status()

    tmp_path = output_dir / "_original_tmp"
    with open(tmp_path, "wb") as f:
        f.write(r.content)

    img = Image.open(tmp_path)
    if img.mode not in ("RGB", "L"):
        img = img.convert("RGB")
    ratio = THUMB_WIDTH / img.width
    new_size = (THUMB_WIDTH, max(1, round(img.height * ratio)))
    thumb = img.resize(new_size, Image.LANCZOS)

    stem = slugify_stem(image_url)
    out_name = f"{stem}{THUMB_WIDTH}px.jpg"
    out_path = output_dir / out_name
    thumb.save(out_path, "JPEG", quality=88)
    tmp_path.unlink(missing_ok=True)
    return out_path, new_size


# ---------------------------------------------------------------------------
# Automatischer Upload in die WordPress-Mediathek
# ---------------------------------------------------------------------------

def upload_to_wordpress(cfg, thumb_path, alt_text, site_url):
    headers_auth = auth_header(cfg)
    if not headers_auth:
        return None

    api_url = site_url.rstrip("/") + "/wp-json/wp/v2/media"
    headers = {
        **headers_auth,
        "Content-Disposition": f'attachment; filename="{thumb_path.name}"',
        "Content-Type": "image/jpeg",
    }
    with open(thumb_path, "rb") as f:
        data = f.read()

    try:
        r = requests.post(api_url, headers=headers, data=data, timeout=30)
    except requests.RequestException as e:
        print(f"Hinweis: Upload zu WordPress fehlgeschlagen ({e}).")
        return None

    if r.status_code not in (200, 201):
        print(f"Hinweis: Upload zu WordPress fehlgeschlagen ({r.status_code}): {r.text[:200]}")
        return None

    media = r.json()
    media_id = media.get("id")
    source_url = media.get("source_url")

    if alt_text:
        try:
            requests.post(
                f"{api_url}/{media_id}",
                headers=headers_auth,
                data={"alt_text": alt_text},
                timeout=15,
            )
        except requests.RequestException:
            pass

    return media_id, source_url


# ---------------------------------------------------------------------------
# Automatisches Einfügen des Tags auf der Startseite
# ---------------------------------------------------------------------------

def get_page_content(cfg, site_url, page_id):
    headers_auth = auth_header(cfg)
    if not headers_auth:
        return None, "Keine Zugangsdaten eingerichtet (siehe --setup)."

    api_url = f"{site_url.rstrip('/')}/wp-json/wp/v2/pages/{page_id}?context=edit"
    try:
        r = requests.get(api_url, headers=headers_auth, timeout=20)
    except requests.RequestException as e:
        return None, f"Seite konnte nicht geladen werden ({e})."
    if r.status_code != 200:
        return None, f"Seite konnte nicht geladen werden ({r.status_code}): {r.text[:200]}"

    data = r.json()
    raw_content = data.get("content", {}).get("raw")
    if raw_content is None:
        return None, "Rohinhalt der Seite nicht gefunden (evtl. fehlende Berechtigung)."
    return raw_content, None


def backup_page_content(content, page_id):
    BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    path = BACKUP_DIR / f"seite_{page_id}_{stamp}.html"
    path.write_text(content, encoding="utf-8")
    return path


def insert_tag_into_content(content, tag, post_url):
    if post_url in content:
        return None, "Dieser Beitrag ist schon auf der Seite verlinkt - nichts eingefügt (Duplikat vermieden)."

    marker_pos = content.find(FRONTPAGE_MARKER)
    if marker_pos == -1:
        return None, f"Text '{FRONTPAGE_MARKER}' wurde auf der Seite nicht gefunden."

    table_pos = content.find(TABLE_OPEN_TAG, marker_pos)
    if table_pos == -1:
        return None, f"Keine Tabelle ('{TABLE_OPEN_TAG}') nach dem Marker-Text gefunden."

    new_content = content[:table_pos] + tag.strip() + "\n" + content[table_pos:]
    return new_content, None


def update_page_content(cfg, site_url, page_id, new_content):
    headers_auth = auth_header(cfg)
    api_url = f"{site_url.rstrip('/')}/wp-json/wp/v2/pages/{page_id}"
    r = requests.post(api_url, headers=headers_auth, data={"content": new_content}, timeout=30)
    r.raise_for_status()
    return r.json()


# ---------------------------------------------------------------------------
# Tag zusammenbauen
# ---------------------------------------------------------------------------

def build_tag(url, title, date_str, teaser, thumb_url, thumb_size, alt_text, caption, attachment_id):
    width, height = thumb_size
    caption_text = caption or title
    return f'''<table {TABLE_STYLE}>
<tbody>
<tr>
<td {LEFT_CELL_STYLE}>

<a href="{url}"><img class="wp-image-{attachment_id} size-full" src="{thumb_url}" alt="{alt_text}" width="{width}" height="{height}" /></a> {caption_text}</td>
<td {RIGHT_CELL_STYLE}><a href="{url}"><strong>{title} - {date_str}:</strong></a> {teaser} - <a href="{url}">weiter</a> -.</td>
</tr>
</tbody>
</table>
'''


# ---------------------------------------------------------------------------
# Hauptprogramm
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(description="Erzeugt den WordPress-Vorschau-Tag für einen Blogbeitrag.")
    parser.add_argument("url", nargs="?", help="URL des Blogbeitrags")
    parser.add_argument("--setup", action="store_true", help="Zugangsdaten für Upload/Einfügen einrichten")
    parser.add_argument("--publish", action="store_true", help="Tag zusätzlich automatisch auf der Startseite einfügen")
    parser.add_argument("--page-id", type=int, default=DEFAULT_PAGE_ID, help=f"ID der Zielseite (Standard: {DEFAULT_PAGE_ID})")
    parser.add_argument("--yes", action="store_true", help="Bestätigung vor dem Veröffentlichen überspringen")
    args = parser.parse_args()

    if args.setup:
        setup_wizard()
        return

    url = args.url or input("URL des Beitrags eingeben: ").strip()
    if not url:
        print("Keine URL angegeben.")
        sys.exit(1)

    parsed = urlparse(url)
    site_url = f"{parsed.scheme}://{parsed.netloc}"

    OUTPUT_DIR.mkdir(exist_ok=True)
    cfg = load_config()

    print(f"Lade Beitrag: {url}")
    html = fetch_page(url)
    soup = BeautifulSoup(html, "html.parser")
    content = find_content_container(soup)

    title = extract_title(soup)
    date_str = extract_date(soup)
    teaser = extract_teaser(soup, content)
    image_url, alt_text, caption = extract_first_image(content, url)

    if not image_url:
        print("Kein Bild im Beitrag gefunden. Breche ab.")
        sys.exit(1)

    print(f"Titel:  {title}")
    print(f"Datum:  {date_str}")
    print(f"Bild:   {image_url}")

    print("Erzeuge Miniaturbild ...")
    thumb_path, thumb_size = download_and_resize(image_url, OUTPUT_DIR)
    print(f"Miniaturbild gespeichert unter: {thumb_path}  ({thumb_size[0]}x{thumb_size[1]} px)")

    upload_result = upload_to_wordpress(cfg, thumb_path, alt_text or title, site_url)
    have_credentials = auth_header(cfg) is not None
    if upload_result:
        attachment_id, thumb_url = upload_result
        print(f"Miniaturbild automatisch in die Mediathek hochgeladen (ID {attachment_id}).")
    else:
        attachment_id = PLATZHALTER_ID
        thumb_url = PLATZHALTER_URL
        print()
        print("Kein automatischer Upload eingerichtet oder er ist fehlgeschlagen (einmalig mit --setup möglich).")

    tag = build_tag(url, title, date_str, teaser, thumb_url, thumb_size, alt_text or title, caption, attachment_id)

    out_file = OUTPUT_DIR / f"tag_{slugify_stem(url)}.html"
    out_file.write_text(tag, encoding="utf-8")

    print()
    print("=" * 70)
    print(tag)
    print("=" * 70)
    print(f"Tag außerdem gespeichert unter: {out_file}")

    # -----------------------------------------------------------------
    # Automatisches Einfügen auf der Startseite
    # -----------------------------------------------------------------
    if not have_credentials:
        print("\nNoch zu erledigen:")
        print(f"  1. '{thumb_path.name}' von Hand in die Mediathek hochladen.")
        print(f"  2. Im Tag '{PLATZHALTER_URL}' durch die dortige Bild-URL ersetzen.")
        print(f"  3. Im Tag '{PLATZHALTER_ID}' (zweimal) durch die Attachment-ID ersetzen.")
        print(f"  4. Fertigen Tag von Hand in die Übersichtsseite einfügen.")
    elif attachment_id == PLATZHALTER_ID:
        print("\nBild-Upload ist fehlgeschlagen - automatisches Einfügen wird übersprungen.")
    else:
        print(f"\nLade aktuellen Inhalt von Seite {args.page_id} ...")
        page_content, err = get_page_content(cfg, site_url, args.page_id)
        if err:
            print(f"Automatisches Einfügen nicht möglich: {err}")
        else:
            backup_path = backup_page_content(page_content, args.page_id)
            print(f"Sicherungskopie der bisherigen Seite gespeichert unter: {backup_path}")

            new_content, err = insert_tag_into_content(page_content, tag, url)
            if err:
                print(f"Automatisches Einfügen nicht möglich: {err}")
            else:
                marker_pos = new_content.find(FRONTPAGE_MARKER)
                preview_start = max(0, marker_pos)
                preview = new_content[preview_start:preview_start + 400]
                print("\nVorschau der Stelle nach dem Einfügen:")
                print("-" * 70)
                print(preview)
                print("-" * 70)

                proceed = args.publish or args.yes
                if not proceed:
                    antwort = input(f"\nTag jetzt automatisch auf Seite {args.page_id} einfügen? [j/N]: ").strip().lower()
                    proceed = antwort in ("j", "ja", "y", "yes")

                if proceed:
                    try:
                        update_page_content(cfg, site_url, args.page_id, new_content)
                        print(f"Seite {args.page_id} wurde aktualisiert: {site_url}/")
                    except requests.RequestException as e:
                        print(f"Fehler beim Schreiben der Seite: {e}")
                        print(f"Die Sicherungskopie liegt weiterhin unter: {backup_path}")
                else:
                    print("Nicht eingefügt. Der Tag liegt weiterhin lokal zum manuellen Einfügen bereit.")


if __name__ == "__main__":
    main()

Fazit: Am Ende reicht bei mir wieder der denkbar einfachste Aufruf – Programm starten, URL eingeben, fertig. Titel, Datum, Teaser und Miniaturbild werden automatisch erzeugt, der fertige Tag liegt zum Kopieren bereit, und nur das Hochladen der Miniatur sowie das Einfügen von Bild-URL und Attachment-ID mache ich bewusst von Hand. Für mich ein schönes Beispiel dafür, wie sich mit ein bisschen Vibe-Coding eine kleine, aber lästige Handarbeit im Blog-Alltag sauber automatisieren lässt, ohne dass man dafür selbst tief in Python oder die WordPress-API einsteigen muss.