Skip to content

Alles für WordPress, Webentwicklung — und mehr

🗑 Programmatische Bereinigung der WordPress-Mediathek: PHP-Skripte gegen Datei-Müll

🗑 Programmatische Bereinigung der WordPress-Mediathek: PHP-Skripte gegen Datei-Müll

WordPress-Medienbibliothek funktioniert wie ein Dachboden: Sie löschen einen Beitrag, die Bilder bleiben. Sie wechseln das Theme, alte Bildgrößen liegen als Ballast herum. Sie migrieren die Website, die Hälfte der Thumbnails liefert 404.

Die manuelle Bereinigung über das Admin-Panel ist bei einer Website mit ein paar tausend Dateien eine Meditationsübung. Aber es gibt einen schnelleren Weg: fünf PHP-Funktionen, die in einem Durchlauf Datenmüll finden und entfernen. Keine Plugins, mit klarem Code und Kontrolle über jede gelöschte Datei.

Vor der Ausführung: vollständiges Backup. Diese Funktionen löschen Dateien endgültig: kein Papierkorb, kein Rückgängig. Führen Sie sie im Zweifel zuerst auf einer Staging-Kopie aus.

💡 Kurzer Überblick:

  • Löschen unverknüpfter Anhänge, also Dateien, die nach dem Löschen von Beiträgen zurückbleiben
  • Bereinigen von Mediendateien eines bestimmten Custom Post Type (CPT)
  • Bereinigen der Medienbibliothek von defekten Links, also 404-Anhängen ohne Datei auf dem Server
  • Finden und Löschen von Dateien in wp-content/uploads, die nicht als WordPress-Anhänge registriert sind
  • Szenario für Websites, bei denen Bilder in Custom Fields (ACF, Meta Box) gespeichert werden, nicht als Anhänge

Was Sie vor der Ausführung wissen müssen

Der folgende Code löscht Dateien physisch, von der Festplatte und aus der Datenbank. Drei Dinge, die Ihre Website retten werden.

Erstens: Bilder auf Tag-Archivseiten oder in SEO-Beschreibungen sind oft mit keinem Beitrag verknüpft. Sie hängen als „Waisen" herum, aber die Website braucht sie. Wenn Sie solche Bilder haben, schließen Sie sie vom Geltungsbereich dieser Funktionen aus oder passen Sie die Bedingungen an.

Zweitens: WordPress erstellt mehrere Größen jedes Bildes. Thumbnails erben den post_parent des Originals, daher berührt die Funktion delete_unattached_attachments() sie nicht, sie filtert strikt nach post_parent = 0. Das Problem entsteht nur, wenn das Original selbst seine Verknüpfung zum Beitrag verloren hat.

Drittens: Wenn ein Link zur gelöschten Datei im Beitragsinhalt existiert, wird er nach der Bereinigung defekt sein. Lassen Sie vor der Ausführung einen Crawler wie Screaming Frog oder Ähnliches über die Website laufen und kartieren Sie die Links.

1. Löschen unverknüpfter Anhänge

Häufigstes Szenario: Sie haben einen Beitrag gelöscht, Anhänge blieben in der Datenbank mit post_type = 'attachment' und post_parent = 0. Sie belegen Platz auf der Festplatte und in Backups.

Die folgende Funktion findet alle solchen Datensätze und löscht sie. Platzieren Sie sie in der functions.php eines Child-Themes oder über ein Snippet-Plugin wie WPCode. Sie läuft nicht von selbst, es ist eine Definition, die einen Aufruf benötigt.

1function delete_unattached_attachments() {
2 $attachments = get_posts( array(
3 'post_type' => 'attachment',
4 'numberposts' => -1,
5 'fields' => 'ids',
6 'post_parent' => 0,
7 ) );
8
9 if ( $attachments ) {
10 foreach ( $attachments as $attachment_id ) {
11 $attachment_path = get_attached_file( $attachment_id );
12 wp_delete_attachment( $attachment_id, true );
13 unlink( $attachment_path );
14 }
15 }
16}

get_posts() wählt alle Anhänge ohne übergeordneten Beitrag aus. wp_delete_attachment() mit dem Parameter true löscht sowohl den Datenbankeintrag als auch die Datei mit Thumbnails. Das zusätzliche unlink() ist eine Absicherung: Sollte die Datei irgendwie auf der Festplatte verblieben sein, wird sie endgültig gelöscht.

Hinweis: Beitragsbilder haben in einigen Konfigurationen ebenfalls post_parent = 0. Ersetzen Sie vor dem Produktiveinsatz wp_delete_attachment durch echo $attachment_id . '<br>', dann sehen Sie die Liste der IDs, die gelöscht würden. Wenn Sie bestätigt haben, dass alles korrekt ist, wechseln Sie zur Produktivversion.

Entfernen Sie die Funktion nach einem einzigen Durchlauf aus der functions.php. Sie muss nicht bei jedem init vorgehalten werden.

2. Löschen von Anhängen eines bestimmten CPT

Ein ehemaliger WooCommerce-Shop, eine alte Portfolio-Rubrik, ein gelöschter Custom Post Type, all deren Bilder liegen weiterhin auf dem Server. Die folgende Funktion bereinigt Anhänge, die mit Beiträgen eines angegebenen Typs verknüpft sind.

1function delete_cpt_attachments( $cpt = 'card' ) {
2 $attachments = get_posts( array(
3 'post_type' => 'attachment',
4 'numberposts' => -1,
5 ) );
6
7 if ( $attachments ) {
8 foreach ( $attachments as $attachment ) {
9 $parent_id = $attachment->post_parent;
10
11 if ( $cpt === get_post_type( $parent_id ) ) {
12 $attachment_path = get_attached_file( $attachment->ID );
13 wp_delete_attachment( $attachment->ID, true );
14 unlink( $attachment_path );
15 }
16 }
17 }
18}

Ersetzen Sie 'card' durch Ihren CPT-Slug. Für WooCommerce-Produkte 'product'. Wenn der CPT bereits gelöscht ist, gibt get_post_type() false zurück, Anhänge dieses Typs sind dann nicht betroffen. Für gelöschte CPTs muss die Logik angepasst werden: Prüfen Sie nicht den Typ des übergeordneten Elements, sondern die Zugehörigkeit zu einer Taxonomie oder einem Meta-Feld.

Seien Sie bei großen Datenbanken vorsichtig: 'numberposts' => -1 ohne 'fields' => 'ids' lädt vollständige WP_Post-Objekte. Bei über 10.000 Anhängen kann das an die memory_limit-Grenze stoßen. Fügen Sie für Produktivmengen 'fields' => 'ids' hinzu und holen Sie nur IDs, get_post_type() funktioniert auch mit übergeordneten IDs.

3. Bereinigen der Medienbibliothek von 404-Anhängen

Defekte Thumbnails in der Medienbibliothek sind ein Symptom dafür, dass die Datei auf der Festplatte gelöscht wurde (manuell, durch Hosting-Absturz oder ein fehlerhaftes Plugin), der Datenbankeintrag aber erhalten blieb. WordPress zeigt ein graues Rechteck, beim Klick erscheint jedoch ein 404-Fehler.

Die Funktion fragt jede Anhang-URL ab und löscht diejenigen, die 404 zurückgeben.

1function delete_404_attachments() {
2 $attachments = get_posts( array(
3 'post_type' => 'attachment',
4 'numberposts' => -1,
5 'fields' => 'ids',
6 ) );
7
8 if ( $attachments ) {
9 foreach ( $attachments as $attachment_id ) {
10 $file_url = wp_get_attachment_url( $attachment_id );
11 $file_headers = @get_headers( $file_url );
12
13 if ( $file_headers && strpos( $file_headers[0], '404' ) !== false ) {
14 wp_delete_attachment( $attachment_id, true );
15 }
16 }
17 }
18}

Wichtig: Diese Funktion ist ressourcenintensiv. Jeder get_headers()-Aufruf ist eine HTTP-Anfrage an Ihren eigenen Server. Bei tausend Anhängen stellen Sie tausend HTTP-Anfragen in einem Durchlauf. Ergebnis: langsam, Serverlast, einige Hosting-Anbieter beenden den Prozess durch Timeout.

Zerlegen Sie große Medienbibliotheken mit 'offset' und 'numberposts' in Blöcke oder führen Sie sie per WP-CLI mit einem Batch-Limit aus. Wenn die Website hinter einem CDN oder Proxy liegt, ersetzen Sie die Prüfung durch wp_remote_head() mit einem Timeout, get_headers() verarbeitet Weiterleitungen nicht immer korrekt und unterstützt keine Authentifizierung.

4. Umgekehrte Prüfung: Dateien in Uploads ohne Datenbankeintrag

Die vorherigen drei Funktionen bereinigen die Datenbank, löschen Anhang-Datensätze. Aber wp-content/uploads kann Dateien enthalten, die überhaupt nicht als Anhänge registriert sind: per FTP hochgeladen, von Plugins hinterlassen, von Cache generiert.

Diese Funktion geht den umgekehrten Weg: nicht von der Datenbank zu den Dateien, sondern von den Dateien zur Datenbank. Sie durchsucht wp-content/uploads rekursiv und prüft für jede Datei per attachment_url_to_postid(), ob es sich um einen Anhang handelt. Wenn nicht, wird sie gelöscht.

1function clean_uploads_from_nonattachments() {
2 $uploads_dir = wp_upload_dir();
3 $search = $uploads_dir['basedir'];
4 $replace = $uploads_dir['baseurl'];
5 $root = $uploads_dir['basedir'];
6
7 $iter = new RecursiveIteratorIterator(
8 new RecursiveDirectoryIterator( $root, RecursiveDirectoryIterator::SKIP_DOTS ),
9 RecursiveIteratorIterator::SELF_FIRST,
10 RecursiveIteratorIterator::CATCH_GET_CHILD
11 );
12
13 foreach ( $iter as $fileinfo ) {
14 if ( $fileinfo->isFile() ) {
15 $image_path = $fileinfo->getPathname();
16 $image_url = str_replace( $search, $replace, $image_path );
17 $attachment_id = attachment_url_to_postid( $image_url );
18
19 if ( ! $attachment_id ) {
20 unlink( $image_path );
21 }
22 }
23 }
24}

Auf einem Testserver mit 1 GB Uploads lief die Funktion in 15 Sekunden und gab 700 MB frei, übrig blieben 300 MB tatsächlich genutzter Dateien. Bei Ordnern über 5 GB unterteilen Sie das Scannen nach Jahren: Ersetzen Sie $root durch $uploads_dir['basedir'] . '/2025/', dann '/2024/' und so weiter.

Führen Sie zuerst die Version ohne Löschung aus, ersetzen Sie unlink( $image_path ) durch echo $image_path . PHP_EOL. Sie sehen die vollständige Liste der Dateien, die die Funktion als Datenmüll betrachtet. Prüfen Sie sie visuell und wechseln Sie dann zu unlink().

5. Szenario mit Custom Fields: Wenn Bilder keine Anhänge sind

Der komplexeste Fall: eine Website, auf der Bilder nicht als WordPress-Anhänge, sondern als URLs in Custom Fields gespeichert werden (ACF, Meta Box, eigene Theme-Felder). Typisches Beispiel, ein Buchladen: Buchcover im Feld bookcover, Autorenfoto in bookauthor_picture, Listenbild in book_list_pictrue.

In dieser Architektur gibt attachment_url_to_postid() für alle Dateien in Uploads 0 zurück. Die vorherige Funktion würde alles löschen, einschließlich der tatsächlich verwendeten Bilder. Ein anderer Ansatz ist nötig.

5.1. Erstellen einer Whitelist

Sammeln Sie zuerst die URLs aller Bilder aus allen benötigten Custom Fields. Im folgenden Beispiel drei CPTs und drei Felder:

1$all_good_pictures = array();
2
3// Book covers (CPT 'post', field 'bookcover')
4$posts = get_posts( array(
5 'post_type' => 'post',
6 'posts_per_page' => -1,
7 'post_status' => 'any',
8 'fields' => 'ids',
9) );
10foreach ( $posts as $post_id ) {
11 $cover = get_field( 'bookcover', $post_id );
12 if ( $cover ) {
13 $all_good_pictures[] = $cover;
14 }
15}
16
17// List images (CPT 'book_list', field 'book_list_pictrue')
18$lists = get_posts( array(
19 'post_type' => 'book_list',
20 'posts_per_page' => -1,
21 'post_status' => 'any',
22 'fields' => 'ids',
23) );
24foreach ( $lists as $list_id ) {
25 $pic = get_field( 'book_list_pictrue', $list_id );
26 if ( $pic ) {
27 $all_good_pictures[] = $pic;
28 }
29}
30
31// Author photos (CPT 'bookauthor', field 'bookauthor_picture')
32$authors = get_posts( array(
33 'post_type' => 'bookauthor',
34 'posts_per_page' => -1,
35 'post_status' => 'any',
36 'fields' => 'ids',
37) );
38foreach ( $authors as $author_id ) {
39 $pic = get_field( 'bookauthor_picture', $author_id );
40 if ( $pic ) {
41 $all_good_pictures[] = $pic;
42 }
43}
44
45$all_good_pictures = array_filter( $all_good_pictures );

In einem realen Projekt, einem Buchladen, ermöglichte dieser Ansatz, die meisten Datenmüll-Dateien zu berechnen und einen erheblichen Teil des Speicherplatzes freizugeben.

5.2. Alles löschen, was nicht in der Whitelist steht

Gehen Sie nun wp-content/uploads durch und löschen Sie jede Datei, die nicht in $all_good_pictures enthalten ist:

1$uploads_dir = wp_upload_dir();
2$search = $uploads_dir['basedir'];
3$replace = $uploads_dir['baseurl'];
4$root = $uploads_dir['basedir'];
5
6$iter = new RecursiveIteratorIterator(
7 new RecursiveDirectoryIterator( $root, RecursiveDirectoryIterator::SKIP_DOTS ),
8 RecursiveIteratorIterator::SELF_FIRST,
9 RecursiveIteratorIterator::CATCH_GET_CHILD
10);
11
12foreach ( $iter as $fileinfo ) {
13 if ( $fileinfo->isFile() ) {
14 $image_path = $fileinfo->getPathname();
15 $image_url = str_replace( $search, $replace, $image_path );
16
17 if ( ! in_array( $image_url, $all_good_pictures, true ) ) {
18 unlink( $image_path );
19 }
20 }
21}

Die in_array()-Methode mit striktem Vergleich auf einem Array mit über 10.000 Elementen ist nicht die schnellste. Ersetzen Sie für Produktivmengen das reguläre Array durch ein assoziatives: $all_good_pictures = array_fill_keys( $all_good_pictures, true ) und prüfen Sie per isset(). Der Unterschied bei 40.000 Elementen: von Dutzenden Sekunden auf Sekundenbruchteile.

Wie Sie diese Funktionen ausführen

Alle obigen Snippets sind Funktions-Definitionen. Sie tun nichts, bis Sie sie aufrufen. Drei sichere Wege zur Ausführung:

Methode

Wann verwenden

Rollback

WP-CLI wp eval-file

Einmalige Bereinigung, Konsolenzugriff

Nein, nur Backup

Hook admin_init + URL-Parameter

Keine Konsole, Ausführung aus dem Admin-Bereich nötig

Nein, nur Backup

Snippet-Plugin (WPCode)

Bequeme Ablage und Aktivieren/Deaktivieren

Snippet deaktivieren, Funktion inaktiv

Beispiel für eine einmalige Ausführung per Admin:

1add_action( 'admin_init', 'run_cleanup_once' );
2function run_cleanup_once() {
3 if ( isset( $_GET['cleanup'] ) && 'confirmed' === $_GET['cleanup'] ) {
4 delete_unattached_attachments();
5 }
6}

Rufen Sie https://ihreseite.de/wp-admin/?cleanup=confirmed auf, die Funktion läuft einmal. Entfernen Sie das Snippet nach der Ausführung.

Für WP-CLI, die empfohlene Methode für die Produktion, speichern Sie den Funktionscode in einer temporären Datei und führen Sie aus:

1wp eval-file cleanup.php

Vor der Bereinigung ist es nützlich, den Prozess visuell zu sehen. Im folgenden Video eine schrittweise Aufschlüsselung der WordPress-Medienbibliotheksbereinigung mit manuellen und automatischen Methoden.

⁉️🤔 Häufig gestellte Fragen

Können Dateien nach dem Löschen wiederhergestellt werden?

Nein. Die Funktionen verwenden wp_delete_attachment() mit true und unlink(), Dateien werden physisch gelöscht, unter Umgehung des Papierkorbs. Die einzige Absicherung: vollständiges Backup von Dateien und Datenbank vor der Ausführung. Prüfen Sie, ob Ihr Hosting-Anbieter automatische tägliche Backups hat, bei Kinsta, WP Engine und SiteGround sind sie standardmäßig aktiviert. Das gibt einen zusätzlichen Wiederherstellungspunkt neben Ihrem manuellen Backup.

Warum hat die Funktion nicht funktioniert, Dateien sind geblieben?

Häufigster Grund: Sie haben die Funktionsdefinition zu functions.php hinzugefügt, sie aber nicht aufgerufen. Ein function ... { }-Block ist nur eine Anweisung. Damit der Code ausgeführt wird, muss die Funktion über add_action() an einen Hook gehängt oder manuell per WP-CLI ausgeführt werden. Im Abschnitt „Wie Sie diese Funktionen ausführen" finden Sie drei Methoden, wählen Sie basierend auf Ihrer Serverzugriffsebene.

Löscht die Funktion Thumbnails verwendeter Bilder, wenn es sich um unverknüpfte Anhänge handelt?

Nein. Thumbnails (thumbnail, medium, large) haben denselben post_parent wie der ursprüngliche Anhang. Die Funktion filtert strikt nach post_parent = 0, nur Datensätze ohne jeglichen übergeordneten Beitrag. Größen von Originalen erben post_parent und fallen nicht in die Auswahl. Das Problem entsteht nur, wenn das Original selbst seine Verknüpfung verloren hat, dann löscht die Funktion es mitsamt allen Größen.

Was, wenn einige Bilder in Custom Fields und andere als reguläre Anhänge vorliegen?

Kombinieren Sie die Ansätze aus Abschnitt 4 und 5. Sammeln Sie zuerst eine Whitelist aus Custom Fields (Abschnitt 5.1). Prüfen Sie dann beim Scannen der Uploads (Abschnitt 4) für jede Datei beide Bedingungen: Ist die Datei ein WordPress-Anhang per attachment_url_to_postid() UND ist sie in der Whitelist vorhanden. Eine Datei wird nur gelöscht, wenn keine der Bedingungen erfüllt ist: if ( ! $attachment_id && ! isset( $good_pictures[ $image_url ] ) ) { unlink( $image_path ); }.

Wie sicher ist das für eine WooCommerce-Website?

WooCommerce speichert Produktbilder als standardmäßige WordPress-Anhänge, sie sind mit dem Beitragstyp product verknüpft. Die Löschfunktion für unverknüpfte Anhänge (Abschnitt 1) berührt sie nicht. Die Funktion für einen bestimmten CPT (Abschnitt 2) hingegen schon, wenn Sie 'product' übergeben. Für WooCommerce ist die Methode aus Abschnitt 5 (Whitelist) am sichersten: Sie operiert auf dem, was tatsächlich verwendet wird, nicht auf dem, was verknüpft ist. Exportieren Sie vor der Ausführung die IDs aller Produktanhänge zur Gegenprüfung.

Was Sie für Ihr Projekt verwenden: abschließende Aufschlüsselung

Die Methodenwahl hängt von der Website-Architektur ab:

  • Standard-Blog oder Nachrichtenseite, Funktionen aus Abschnitt 1 (unverknüpfte Anhänge) und Abschnitt 3 (404-Anhänge) genügen. Einmal halbjährlich ausführen, die Medienbibliothek bleibt in Ordnung.
  • Website mit alten CPTs (Portfolio, Katalog, Kleinanzeigen), ergänzen Sie Abschnitt 2. Bereinigen Sie gezielt Datenmüll aus gelöschten oder aufgegebenen Beitragstypen.
  • Projekt auf ACF/Meta Box mit Custom Fields für Bilder, Ihre Option: Abschnitt 5. Erstellen Sie eine Whitelist, löschen Sie den Rest. Einmal einrichten, dann bei Bedarf einfach wiederholen.
  • Alles zusammen und unklar, beginnen Sie mit dem rekursiven Scannen der Uploads (Abschnitt 4). Sehen Sie, wie viel Datenmüll auf der Platte liegt. Wenden Sie dann Abschnitte 1-3 und 5 je nach Situation selektiv an.

Keines dieser Skripte ersetzt regelmäßige Website-Hygiene. Aber wenn Sie die benötigte Funktion einmal schreiben und in der Projektdokumentation ablegen, sparen Sie bei der nächsten Prüfung Stunden manueller Arbeit.

Und ja, Sie haben bereits ein Backup erstellt.

WordPress-Mediathek von überflüssigen Dateien bereinigen