Czasami budujemy motyw typu agregator newsów, gdzie wpis nie ma własnej długiej treści, a jedynie linkuje do zewnętrznego artykułu. Albo chcemy, żeby pierwszy obrazek w treści automatycznie stawał się miniaturą, jeśli redaktor zapomni ustawić featured image.
W obu przypadkach trzeba przeskanować treść posta (the_content) i wyłowić z niej pierwszy tag <a> albo <img>. Ten wpis pokazuje stabilne podejście na DOMDocument, typowe pułapki UTF-8 oraz jak wpiąć wynik w pętlę WordPress bez XSS.
Kiedy to się przydaje w produkcji
Scenariusze, które wracają w projektach agencyjnych:
- Linkout posts: tytuł i zajawka lokalnie, „czytaj dalej” prowadzi na medium zewnętrzne.
- Import z RSS/Atom, gdzie treść przychodzi z linkami i obrazkami, a featured image jest puste.
- Karty na listingu, które mają pokazać „pierwszy zewnętrzny URL” jako CTA.
- Migracje ze starych motywów, gdzie redakcja wkładała obrazek tylko w treść, nigdy w meta box.
W jednym magazynie branżowym po migracji z klasycznego edytora do bloku około jednej trzeciej archiwalnych wpisów nie miała _thumbnail_id. Skrypt nocny brał pierwszy <img> z treści, pobierał plik na media library i ustawiał miniaturę. Regex padał na obrazkach z srcset i na figurach z nested markupiem; DOM przechodził.
Dlaczego nie regex na HTML
Wielu programistów sięga po wyrażenia regularne, bo „to tylko jeden tag”. Parsowanie HTML regexem to zła praktyka z uzasadnionych powodów: atrybuty w dowolnej kolejności, cudzysłowy mieszane, HTML5 void tags, komentarze, shortcode’y WordPressa w środku.
Regex może przejść testy na pięciu fixture’ach z edytora i wybuchnąć na pierwszym wpisie wklejonym z Google Docs. DOMDocument też nie jest idealny (libxml jest konserwatywny wobec HTML5), ale daje drzewo węzłów zamiast zgadywania pozycji nawiasów.
Metoda: klasa DOMDocument
Oto funkcja, którą możesz trzymać w małej wtyczce narzędziowej albo w inc/ motywu dziecka:
function get_first_link_url( $content ) {
if ( empty( $content ) ) {
return false;
}
$doc = new DOMDocument();
libxml_use_internal_errors( true );
$encoded = mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' );
$doc->loadHTML( $encoded );
$links = $doc->getElementsByTagName( 'a' );
if ( $links->length > 0 ) {
$href = $links->item( 0 )->getAttribute( 'href' );
libxml_clear_errors();
return $href ? $href : false;
}
libxml_clear_errors();
return false;
}libxml_use_internal_errors(true) wycisza ostrzeżenia przy <section>, <figure> i dziurawym markupie z pasty. libxml_clear_errors() sprząta kolejkę, żeby ostrzeżenia nie rosły w długich cronach.
Hack z HTML-ENTITIES nadal pojawia się w starszym PHP; na nowszych wersjach warto sprawdzić loadHTML z poprawnym meta charset w opakowaniu. Ważne, żeby polskie znaki w anchor text nie psuły drzewa - to częstszy bug niż „brak linku”.
Użycie w pętli WordPress
$raw = get_the_content();
$content = apply_filters( 'the_content', $raw );
$link = get_first_link_url( $content );
if ( $link ) {
printf(
'<a href="%s" class="read-more-external" rel="noopener noreferrer">Czytaj oryginał</a>',
esc_url( $link )
);
}Dwa detale bezpieczeństwa:
- Zawsze
esc_url()na wyjściu. Treść bywa z importu;javascript:w href to realny wektor. - Świadomie zdecyduj, czy parsujesz surowy
get_the_content(), czy po filtrach. Shortcode’y i bloki czasem wstawiają linki dopiero pothe_content.
Jeśli linkujesz rel=“nofollow” do partnerów, dopisz atrybut w printf, zamiast polegać na tym, że redaktor go wklei.
Pierwszy obrazek jako miniatura
Analogiczna funkcja na <img>:
function get_first_image_src( $content ) {
if ( empty( $content ) ) {
return false;
}
$doc = new DOMDocument();
libxml_use_internal_errors( true );
$doc->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ) );
$images = $doc->getElementsByTagName( 'img' );
if ( $images->length > 0 ) {
$src = $images->item( 0 )->getAttribute( 'src' );
libxml_clear_errors();
return $src ? $src : false;
}
libxml_clear_errors();
return false;
}Żeby ustawić featured image, potrzebujesz jeszcze sideload do media library (media_sideload_image albo download_url + media_handle_sideload) i set_post_thumbnail. Rób to w WP-CLI / cronie, nie synchronicznie przy każdym page view - inaczej pierwszy visit po imporcie zamieni się w timeout.
Pomijaj trackery i 1x1 pixele: filtruj po rozszerzeniu, minimalnych wymiarach z atrybutów width/height albo po domenie (odrzucaj znane hosty analytics).
Pułapki, które warto znać wcześniej
Linki wewnętrzne kotwic. Pierwszy <a href="#section"> to nie „linkout”. Jeśli budujesz agregator, filtruj href zaczynające się od # oraz puste href.
Linki relative. href="/oferta/" na obcym originie po imporcie może wskazywać złą domenę. Rozważ wp_make_link_relative albo składanie z home_url tylko gdy wiesz, że treść jest lokalna.
Blok Classic vs Gutenberg. W bloku treść w bazie to komentarze <!-- wp:... -->. get_the_content() zwraca markup z komentarzami; po the_content dostajesz HTML. Parsuj świadomie ten etap, którego potrzebujesz.
Wydajność. DOMDocument na liście 50 wpisów w archiwum jest kosztowny. Cache’uj wynik w post meta (_first_external_url) przy save_post, a w frontcie czytaj meta. Przeliczaj ponownie tylko gdy treść się zmienia.
Multisite i escaping. Na sieci z UGC upewnij się, że nie renderujesz surowego HTML z pierwszego noda - bierzesz atrybut, nie nodeValue z zagnieżdżonym markupiem.
Alternatywy: WP_HTML_Tag_Processor
Od nowszych wersji WordPressa masz też WP_HTML_Tag_Processor - lżejszy skaner tokenów niż pełny DOM. Do „znajdź pierwszy <a href>” bywa wystarczający i nie ciągnie libxml. Nadal: nie buduj na nim walidatora całego dokumentu; to narzędzie do celowanych odczytów atrybutów.
Dla prostego zadania produkcyjnego DOMDocument pozostaje czytelnym wyborem, zwłaszcza gdy zespół zna PHP dłużej niż HTML API core.
Testy, które warto mieć w repo
Zanim wkleisz snippet na produkcję, trzymaj fixture’y:
- treść z jednym linkiem na początku,
- treść z obrazkiem przed linkiem (gdy chcesz link, nie img),
- polskie znaki w anchor text,
hrefz query string i&,- pusty content i content bez
<a>, - link
javascript:alert(1)(musi zostać złapany przezesc_urljako pusty/odrzut).
To pięć minut pracy, które oszczędzają godzinę po imporcie 2000 wpisów.
Cache w post meta i invalidacja
Parsowanie DOM przy każdym renderze archiwum to zbędny koszt. Przy save_post wylicz pierwszy zewnętrzny URL i zapisz go pod kluczem w stylu _wppoland_first_external_url. Na froncie czytaj meta. Jeśli meta pusta, możesz policzyć raz i uzupełnić (lazy backfill), ale nie rób tego synchronicznie dla całej strony głównej naraz.
Invalidacja: czyść meta, gdy zmienia się treść, gdy import nadpisuje post, albo gdy redaktor ręcznie ustawia featured link w osobnym polu. Prefiks podkreślnika chowa klucz przed UI custom fields - świadomy wybór, jeśli nie chcesz, żeby ktoś edytował go ręcznie bez walidacji.
Przy WP-CLI migracjach batchem loguj posty, dla których parser nic nie znalazł. To zwykle drafty, cytaty bez linków albo treści zbudowane wyłącznie z bloków bez kotwic HTML.
Linki w blokach i shortcode’ach
Gutenberg potrafi trzymać URL w atrybutach bloku (JSON w komentarzu), a nie w klasycznym <a href>. Jeśli po get_the_content() bez filtrów „nie ma linku”, a w edytorze widać przycisk, prawdopodobnie patrzysz na surowy markup bloku. Odpal the_content albo renderuj blok przez do_blocks() przed parsowaniem.
Shortcode’y galerii i embedów YouTube też dokładają kotwice dopiero po rozwinięciu. Decyzja produktowa: czy „pierwszy link” ma być tym, co redaktor wpisał w tekście, czy tym, co ostatecznie trafia do HTML na froncie. Zapisz tę decyzję w komentarzu funkcji - za rok ktoś podziękuje.
Przy blokach przycisków (core/button) URL siedzi w JSON atrybutu url. Sam getElementsByTagName('a') po do_blocks() zwykle wystarcza, bo core renderuje prawdziwy anchor. Gorzej z blokami tredzych wtyczek, które trzymają link tylko w atrybutach i renderują go JS-em na froncie - wtedy parser PHP nic nie znajdzie, bo w HTML odpowiedzi nie ma jeszcze <a>.
Jeśli budujesz feed „linkout” dla newslettera, rozważ osobne pole meta na URL docelowy zamiast zgadywania z treści. Parser zostaje jako fallback dla archiwum, a nowe wpisy mają jawne źródło prawdy.
Podsumowanie
Wyciąganie pierwszego URL z treści to mały snippet, który zostaje w projekcie na lata. Lepiej oprzeć go na DOM (albo Tag Processorze) niż na regexie, cache’ować wynik w meta i zawsze escapować wyjście. Trzymaj fixture’y w repozytorium obok funkcji - migracje treści i importy RSS najszybciej pokazują, gdzie parser się myli.
Parsowanie treści wpisu to typowy przykład kodu, który nikt nie pamięta po pół roku. Gdy takich fragmentów zbiera się więcej, porządkujemy je przy przeglądzie motywu. Warto wtedy spisać, które meta klucze są źródłem prawdy, a które tylko cache’em wyliczonym z HTML. Przy code review pytaj wprost: czy wynik jest cache’owany przy zapisie, czy parser odpala się na każdym page view archiwum. Ta jedna decyzja zwykle różni motyw „działa na stagingu” od motywu, który dusi CPU na produkcji przy paginacji. Cache meta przy save_post jest tu domyślnym wyborem produkcyjnym.
Potrzebujesz pomocy przy imporcie archiwum albo automatycznym ustawianiu miniatur? Napisz przez formularz kontaktowy.






