Kiedy admin WordPressa przestaje wystarczać
Klient przysyła maila: „Cześć, zmieniamy tekst pod formularzem kontaktowym na wszystkich stronach – poza szkoleniami, tam ma zostać jak było”. Otwierasz wp-admin, filtrujesz strony i zaczyna się liczenie: 40, 80, 200 stron. Ręcznie to pół dnia klikania i ktoś na pewno coś przeoczy.
Skrypt PHP zrobi to samo w minutę. Ale zanim napiszesz pierwszą linijkę, jest kilka rzeczy, które trzeba mieć ogarnięte – inaczej łatwo o katastrofę.
STOP – zanim zaczniesz cokolwiek pisać
1. Sprawdź gdzie właściwie jest twoja treść
To najważniejsza sprawa – i najczęstszy powód dlaczego skrypt „nic nie robi”. Każdy edytor trzyma treść w innym miejscu bazy:
- Klasyczny edytor + Gutenberg – treść jest w tabeli wp_posts, w kolumnie post_content. To o tym pisze ten wpis.
- ACF fields – treść jest w tabeli wp_postmeta, nie w post_content. Trzeba szukać po meta_key danego pola ACF. To osobny temat.
- Elementor – treść jest w wp_postmeta, w polu _elementor_data, przechowywana jako JSON z uciekniętymi cudzysłowami. Wymaga innego podejścia.
Jeśli twoja strona ma mieszankę (custom motyw + Gutenberg + parę ACF fields) – najpierw ustal gdzie konkretnie jest ta treść, którą chcesz zmienić. Otwórz stronę w edytorze i zobacz w którym polu ten tekst siedzi.
2. Zrób backup bazy
Skrypt PHP zapisuje bezpośrednio do bazy. Nie ma „Cofnij”. Jak coś pójdzie nie tak, jedyny ratunek to backup sprzed uruchomienia.
Ja najprościej robię tak: skoro i tak wchodzę do phpMyAdmin żeby sprawdzić ile jest wpisów pasujących do mojej frazy, to od razu klikam Eksport na tabeli wp_posts i zapisuję plik SQL na dysku. 30 sekund, śpię spokojnie.
Jeśli używasz wtyczek jak UpdraftPlus czy Duplicator – można oczywiście zrobić pełny backup, ale przy zmianie treści wystarczy sam eksport tabeli wp_posts.
3. Sprawdź encoding – zwłaszcza jeśli masz polskie znaki
Jeśli szukasz frazy z polskimi znakami, sprawdź trzy rzeczy:
- Plik PHP zapisany w UTF-8 – w VS Code w prawym dolnym rogu widać encoding pliku. Musi być UTF-8 bez BOM. Jeśli plik jest w Windows-1250, funkcja str_replace nie znajdzie polskiej frazy nawet jeśli w bazie jest identyczny tekst – bo bajty są różne.
- Baza WP w utf8mb4 – standard od WP 4.2. Sprawdź w phpMyAdmin że tabela wp_posts ma collation utf8mb4_unicode_ci.
- Encja HTML vs surowy znak – w treści Gutenberga apostrofy mogą być zapisane jako encja HTML. Jak szukasz jednego a jest drugie – nie znajdzie. Wyjście: skopiuj szukaną frazę bezpośrednio z edytora Gutenberga (widok „Kod”) – dostaniesz dokładny format taki jaki jest w bazie.
Trzy linijki na starcie skryptu wymuszają UTF-8 wszędzie i rozwiązują większość dziwnych znaków w outpucie:
header('Content-Type: text/html; charset=UTF-8');
mb_internal_encoding('UTF-8');
$wpdb->query("SET NAMES 'utf8mb4'");
Szkielet skryptu
Skrypt kładę jako plik PHP w motywie, w podkatalogu skryptów. Wgrywam przez FTP, uruchamiam wchodząc w przeglądarce na jego URL. Po użyciu usuwam plik – dopóki tam siedzi, ktokolwiek go znajdzie może odpalić.
Na górze konfiguracja – co szukamy, co wstawiamy, jakie wyjątki:
// Zabezpieczenie - tylko dla zalogowanego admina
if (!current_user_can('manage_options')) {
wp_die('Brak uprawnien');
}
// Konfiguracja
$SEARCH = 'Stary tekst do zmiany';
$REPLACE = 'Nowy tekst po aktualizacji';
// Wyjatki - te typy wpisow pomijamy
$EXCLUDED_POST_TYPES = ['event', 'acf-field-group', 'revision'];
$EXCLUDED_SLUGS = ['szkolenia', 'polityka-prywatnosci'];
// Tryb testowy - ustaw ID zeby skrypt zadzialal TYLKO na jednej stronie
$TEST_ONLY_ID = 123;
// Tryb - true = podglad, false = zapis do bazy
$DRY_RUN = isset($_GET['dry']) ? true : false;
Wyszukiwanie i podmiana
Sercem skryptu jest zapytanie o wpisy zawierające szukaną frazę, potem pętla z podmianą. Funkcja esc_like zabezpiecza znaki procenta i podkreślenia – żeby nie były traktowane jak wildcardy:
global $wpdb;
$posts = $wpdb->get_results($wpdb->prepare(
"SELECT ID, post_type, post_name, post_title
FROM {$wpdb->posts}
WHERE post_content LIKE %s
AND post_status = 'publish'",
'%' . $wpdb->esc_like($SEARCH) . '%'
));
foreach ($posts as $post) {
// Tryb testowy - pomin wszystko poza tym jednym ID
if ($TEST_ONLY_ID && (int) $post->ID !== $TEST_ONLY_ID) continue;
// Wyjatki
if (in_array($post->post_type, $EXCLUDED_POST_TYPES)) continue;
if (in_array($post->post_name, $EXCLUDED_SLUGS)) continue;
if (!$DRY_RUN) {
$content = get_post_field('post_content', $post->ID);
$new_content = str_replace($SEARCH, $REPLACE, $content);
// wp_slash() jest wazne - bez tego apostrofy sie rozjada
wp_update_post([
'ID' => $post->ID,
'post_content' => wp_slash($new_content),
]);
echo "Zmieniono: " . esc_html($post->post_title) . "<br>";
} else {
echo "[DRY RUN] Znaleziono: " . esc_html($post->post_title) . "<br>";
}
}
Dlaczego wp_slash()
Ten drobiazg potrafi zjeść pół dnia debugowania. Funkcja wp_update_post oczekuje danych „slashed” – tak jak wchodzą z formularza POST przez WordPressa. Jak dasz surowy string z apostrofem, WP zrobi swój wewnętrzny unslash i zjeździe apostrof.
Efekt bez wp_slash: fraza „it’s” po zapisie zostaje jako „its”. Fraza z linkiem HTML też może się rozsypać. Zawsze wp_slash przy wp_update_post.
Kolejność uruchamiania – moja rutyna
- Backup tabeli wp_posts przez phpMyAdmin. Zawsze.
- Wgraj plik skryptu przez FTP do katalogu skryptów w motywie. Zaloguj się w wp-admin (skrypt powinien sprawdzać uprawnienia przez current_user_can).
- Otwórz testową stronę w drugiej zakładce w edytorze Gutenberga. Nie odświeżaj.
- Dry-run – wchodzę na URL skryptu z parametrem dry. Widzę listę: ile znaleziono, co zostanie zmienione, co pominięte. Nic w bazie się nie dzieje.
- Test jednej strony – jeśli lista wygląda OK, uruchamiam z ustawionym ID testowej strony. Skrypt zmienia tylko jedną stronę. Sprawdzam w wp-admin czy wygląda dobrze.
- Właściwe wykonanie – jak test OK, uruchamiam na wszystkich pasujących.
- USUŃ plik z FTP po skończonej robocie. Krytyczne – inaczej publicznie dostępny plik zostaje w motywie na zawsze.
Backup treści na szybko – patent Ctrl+A / Ctrl+C
To nie zastępuje backupu bazy, ale ratuje w typowej sytuacji „coś podmieniłam i nie wygląda dobrze na tej jednej stronie”.
Zanim uruchomię skrypt, otwieram sobie testową stronę w drugiej zakładce – w edytorze Gutenberga. I NIE ODŚWIEŻAM tej zakładki.
Uruchamiam skrypt w pierwszej zakładce. Wpis w bazie się zmienia. Ale druga zakładka – ta z edytorem otwartym wcześniej – dalej pokazuje starą treść (bo Gutenberg wczytał ją do stanu React przed skryptem). Klikam w edytor, Ctrl+A, Ctrl+C. Mam całą starą treść w schowku.
Jak coś pójdzie źle – odświeżam zakładkę, teraz widzę nową (zepsutą) treść, zaznaczam Ctrl+A, wklejam Ctrl+V starą ze schowka, zapisuję. Wracam do stanu sprzed skryptu w 3 sekundy.
To działa dla pojedynczej strony testowej. Do właściwego backupu wszystkich stron – eksport bazy, jak wspominałam wyżej.
Idempotentność – dlaczego skrypt można uruchomić 10 razy
Skrypt jest z natury idempotentny – po pierwszej udanej podmianie, drugi run nic nie robi. Bo szukanej frazy już nie ma w bazie. Zapytanie SELECT ze starą frazą zwróci pustą listę.
Dzięki temu jak przypadkiem ktoś odpali skrypt drugi raz – nic złego się nie stanie. Zamiast pokazać „zmieniono 50” pokaże „znaleziono 0”.
Wyjątki – dwie, czasem trzy listy
Rozdzielam wyjątki na osobne listy, bo działają na różnych poziomach:
- Post types – wyklucza całe kategorie wpisów. „Nie ruszaj eventów”, „nie ruszaj revisions”
- Slugi – wyklucza pojedyncze konkretne strony. „Zostaw stronę kontaktu”
- Parent slugs – wyklucza całe drzewo podstron. „Wszystko pod /pomoc/ – dzieci, wnuki – zostaje”
Czego lepiej nie automatyzować
Nie każda zmiana treści nadaje się do skryptu. Ręcznie zrób jeśli:
- Zmian jest kilkanaście – szybciej otworzysz karty i klikniesz
- Tekst różni się na każdej stronie i nie ma jednego wspólnego wzorca
- Klient wymaga akceptacji każdej zmiany przed zapisem
- Treść jest w Elementorze lub ACF – wtedy skrypt jest inny, a błąd może zepsuć całą strukturę bloków
Skrypt uzasadnia się od kilkudziesięciu identycznych podmian. Poniżej tej granicy – to więcej roboty z pisaniem i testowaniem skryptu niż z ręcznym klikaniem.
Podsumowanie – żelazne zasady
- Sprawdź gdzie jest treść – Gutenberg vs ACF vs Elementor to trzy różne bajki
- Backup przed wszystkim – eksport tabeli przez phpMyAdmin, minuta roboty
- Dry-run – zawsze, bez wyjątków
- Test jednej strony – przed puszczeniem na 200
- Ctrl+A / Ctrl+C na testowej stronie otwartej w drugiej zakładce – szybki backup awaryjny
- Funkcja wp_slash() przy wp_update_post – inaczej apostrofy się rozjadą
- UTF-8 w pliku PHP – jak masz polskie znaki, sprawdź encoding pliku
- Usuń plik z FTP po skończonej robocie
Bez tego skrypt do masowej podmiany jest jak buldożer bez kierowcy – dojedzie, ale nie tam, gdzie miał.



