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ścia: WP_HTML_Tag_Processor z rdzenia WordPressa (od 6.2), wbudowaną funkcję get_url_in_content(), WP_HTML_Processor do zapytań z kontekstem, klasyczny DOMDocument oraz nowy Dom\HTMLDocument z PHP 8.4. Do tego typowe pułapki UTF-8 i to, jak wpiąć wynik w pętlę WordPress bez XSS.
Do czego służy wyciąganie pierwszego linku z wpisu
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. Typowy wzorzec z forów wygląda tak:
// PODEJŚCIE PROBLEMATYCZNE: naiwny regex
preg_match( '/<a\s[^>]*href=(\"??)([^\" >]*?)\\1[^>]*>(.*)<\/a>/siU', $content, $matches );
$first_url = $matches[2] ?? null;Przechodzi test na jednym zdaniu i po cichu zawodzi na prawdziwych wpisach:
- Komentarze bloków i szkice. Gutenberg otacza każdy blok komentarzami HTML w rodzaju
<!-- wp:paragraph -->. Jeśli redaktor zakomentował stary link, wzorzec złapie go przed czymkolwiek, co widzi czytelnik. - Zawartość
<script>i<style>. Blok „Własny HTML” z JavaScriptem, który składa string"<a href=...>", zawiera dla regexa całkiem poprawny tag kotwicy. Parser HTML wie, że wnętrze<script>to tekst, a nie markup. - Łamanie linii i kolejność atrybutów. Atrybuty rozbite na kilka linii, apostrofy zamiast cudzysłowów i
data-*przedhrefpsują naiwne wzorce. - Limit backtrackingu. Silnik PCRE w PHP poddaje się, gdy wzorzec przekroczy
pcre.backtrack_limit, domyślnie 1000000 według dokumentacji PHP. Wtedypreg_match()zwracafalsezamiast 0 albo 1, a oPREG_BACKTRACK_LIMIT_ERRORmówi tylkopreg_last_error(). Na długich wpisach z zachłannym(.*)jak powyżej link po prostu znika, bez śladu w logu.
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. Sam rdzeń WordPressa przeszedł tę drogę: get_url_in_content() przez ponad dekadę działała na regexie, zanim przeszła na HTML API, o czym niżej.
Jak wyciągnąć link przez WP_HTML_Tag_Processor
Od WordPressa 6.2 masz w rdzeniu WP_HTML_Tag_Processor, lżejszy skaner niż pełny DOM. Dokumentacja klasy opisuje go wprost: nie parsuje HTML w całości i nie schodzi rekurencyjnie w strukturę, tylko liniowo przechodzi przez dokument i parsuje same tagi otwierające. Do „znajdź pierwszy <a href>” to wystarczy i nie ciągnie libxml. Nadal: nie buduj na nim walidatora całego dokumentu; to narzędzie do celowanych odczytów atrybutów.
Sprawdziliśmy zachowanie na kodzie WordPressa 7.1.3: next_tag( 'a' ) pomija komentarze HTML i zawartość <script>, a get_attribute( 'href' ) zwraca wartość już zdekodowaną, więc & w query stringu dociera do kodu jako &. Brak atrybutu daje null, atrybut logiczny bez wartości daje true, dlatego sprawdzenie is_string() nie jest opcjonalne.
<?php
declare(strict_types=1);
/**
* Zwraca URL pierwszego linku http(s) we fragmencie HTML.
*/
function wppoland_pierwszy_link_tag_processor( string $content ): ?string {
if ( '' === trim( $content ) ) {
return null;
}
$processor = new WP_HTML_Tag_Processor( $content );
// Przechodzi po tagach <a> w kolejności dokumentu
while ( $processor->next_tag( 'a' ) ) {
$href = $processor->get_attribute( 'href' );
if ( ! is_string( $href ) || '' === trim( $href ) ) {
continue;
}
$href = trim( $href );
$schemat = strtolower( (string) wp_parse_url( $href, PHP_URL_SCHEME ) );
// Pomija mailto:, tel:, kotwice i ścieżki względne
if ( in_array( $schemat, array( 'http', 'https' ), true ) ) {
return esc_url_raw( $href );
}
}
return null;
}Zalety w skrócie: brak drzewa w pamięci, odporność na komentarze, <script>, <style> i atrybuty bez cudzysłowów (procesor trzyma się reguł tokenizacji HTML, a nie dopasowuje tekstu), zero dodatkowych rozszerzeń poza samym WordPressem. DOMDocument zostaje czytelnym wyborem tam, gdzie kod działa poza WordPressem albo zespół zna go lepiej niż HTML API rdzenia.
Jak użyć get_url_in_content w WordPressie
Zanim napiszesz własną funkcję, sprawdź tę z rdzenia: get_url_in_content( $content ), opisaną w oficjalnej dokumentacji jako funkcja, która „wyciąga i zwraca pierwszy URL z przekazanej treści” (tłumaczenie własne). Jest w WordPressie od wersji 3.6.0 i służy formatowi wpisu „link”: motyw Twenty Thirteen wywołuje ją w twentythirteen_get_link_url(), a gdy nic nie znajdzie, wraca do permalinku wpisu.
Implementacja zmieniła się w istotny sposób. Porównaliśmy kod gałęzi 6.8 i 6.9 w oficjalnym repozytorium:
- Do WordPressa 6.8 funkcja używała
preg_match( '/<a\s[^>]*?href=([\'"])(.+?)\1/is', ... ). Na tym samym testowym HTML co w całym artykule wzorzec zwrócił link z wnętrza komentarza HTML, czyli taki, którego żaden czytelnik nie widział. - Od WordPressa 6.9 funkcja tworzy
WP_HTML_Tag_Processor, przechodzi po tagachAi zwraca pierwszy niepustyhrefprzepuszczony przezsanitize_url(). Zakomentowany link przestaje być znajdowany.
$url = get_url_in_content( get_the_content() );
if ( $url ) {
printf(
'<a href="%s" rel="noopener">Czytaj u źródła</a>',
esc_url( $url )
);
}Gdy nic nie znajdzie, zwraca false. Dwa ograniczenia: akceptuje każdy schemat, więc mailto: albo #komentarze na początku tekstu wygra z linkiem redakcyjnym, a na stronie z WordPressem 6.8 lub starszym dostajesz zachowanie regexa. Na 6.9 i nowszych, gdy wystarcza pierwszy href, bierz funkcję z rdzenia; gdy trzeba filtrować schematy albo wspierać starsze wersje, lepsza jest funkcja z poprzedniej sekcji.
Jak znaleźć pierwszy link w akapicie przez WP_HTML_Processor
Tag Processor nie wie, gdzie jest. Link owijający obrazek w bloku core/image i link w środku akapitu to dla niego po prostu dwa tagi a. Gdy reguła brzmi „pierwszy link wpisany w tekst, nie link obrazka”, potrzebny jest WP_HTML_Processor, dostępny od WordPressa 6.4. Śledzi stos otwartych elementów i udostępnia go przez get_breadcrumbs().
<?php
function wppoland_pierwszy_link_w_akapicie( string $content ): ?string {
$processor = WP_HTML_Processor::create_fragment( $content );
if ( null === $processor ) {
return null;
}
while ( $processor->next_tag( 'A' ) ) {
$sciezka = $processor->get_breadcrumbs();
// Tylko linki wewnątrz <p> i poza <figure>
if ( ! in_array( 'P', $sciezka, true ) || in_array( 'FIGURE', $sciezka, true ) ) {
continue;
}
$href = $processor->get_attribute( 'href' );
if ( is_string( $href ) && '' !== trim( $href ) ) {
return esc_url_raw( trim( $href ) );
}
}
return null;
}Ścieżkę sprawdzamy sami zamiast zapytania next_tag( array( 'breadcrumbs' => array( 'P', 'A' ) ) ). To zapytanie wymaga, żeby A był bezpośrednim dzieckiem P; w naszym teście link w <p><strong><a> został przez nie pominięty, a sprawdzenie przez in_array() go znalazło.
Dokumentacja klasy jasno opisuje jedną zasadę: gdy procesor trafi na markup, którego jeszcze nie obsługuje, przerywa zamiast zgadywać. Wtedy next_tag() zwraca false, a get_last_error() przestaje być null. Jeśli dla wpisu z widocznymi linkami dostajesz null, sprawdź get_last_error() i wróć do Tag Processora.
Jak wyciągnąć pierwszy link przez 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();
$poprzednie = libxml_use_internal_errors( true );
// Nagłówek XML wymusza UTF-8 w parserze libxml2
$doc->loadHTML( '<?xml encoding="utf-8" ?>' . $content );
libxml_clear_errors();
libxml_use_internal_errors( $poprzednie );
$links = $doc->getElementsByTagName( 'a' );
if ( $links->length > 0 ) {
$href = $links->item( 0 )->getAttribute( 'href' );
return $href ? $href : false;
}
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.
Starsza wersja tego wpisu używała mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ). PHP 8.2 oznaczył kodowanie HTML-ENTITIES jako przestarzałe we wszystkich funkcjach mbstring (lista zmian PHP 8.2), a PHP 8.4 wypisuje przy tym wywołaniu komunikat Deprecated. Nagłówek <?xml encoding="utf-8" ?> albo mb_encode_numericentity() robią to samo bez ostrzeżeń. Ważne, żeby polskie znaki w anchor text nie psuły drzewa - to częstszy bug niż „brak linku”.
Jak użyć pierwszego linku 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.
Jak użyć Dom\HTMLDocument w PHP 8.4
PHP 8.4 dodał nowe API DOM w przestrzeni nazw Dom, ze „zgodną ze standardami obsługą parsowania dokumentów HTML5”, jak piszą informacje o wydaniu (tłumaczenie własne). Dom\HTMLDocument parsuje HTML zgodnie z living standard, więc znikają ostrzeżenia o tagach HTML5 i sztuczka z nagłówkiem XML. DOMDocument zostaje dla zgodności wstecznej.
<?php
function wppoland_pierwszy_link_dom84( string $html ): ?string {
if ( '' === trim( $html ) ) {
return null;
}
$doc = Dom\HTMLDocument::createFromString( $html, LIBXML_NOERROR, 'UTF-8' );
foreach ( $doc->querySelectorAll( 'a[href]' ) as $link ) {
$href = trim( $link->getAttribute( 'href' ) );
$schemat = strtolower( (string) parse_url( $href, PHP_URL_SCHEME ) );
if ( in_array( $schemat, array( 'http', 'https' ), true ) ) {
return $href;
}
}
return null;
}Trzy szczegóły ze strony podręcznika createFromString(): drugi argument przyjmuje stałe LIBXML_* łączone przez OR; trzeci, overrideEncoding, ustala kodowanie zamiast zostawiać zgadywanie PHP; nieznane kodowanie rzuca ValueError. Na PHP 8.4.24 funkcja zwróciła https://exemplo.pt/ação?x=1&y=2 bez zmian, z & zdekodowanym. Selektory CSS pozwalają też na zapytania typu p > a[href^="https://"], które w DOMDocument wymagają XPath.
Którą metodę wybrać do pierwszego linku
Nie publikujemy czasów wykonania: zależą od serwera, wersji PHP i długości wpisu, a sens mają tylko pomiary z własnego projektu. Różnice konstrukcyjne są za to stałe:
| Kryterium | Regex (preg_match) | DOMDocument (libxml2) | Dom\HTMLDocument | WP_HTML_Tag_Processor |
|---|---|---|---|---|
| Model parsowania | Wzorzec tekstowy | Drzewo DOM, reguły sprzed HTML5 | Drzewo DOM, HTML5 | Liniowy odczyt tagów otwierających |
| Pamięć | Niska | Całe drzewo | Całe drzewo | Bez drzewa |
Komentarze i <script> | Zawodne | Poprawnie | Poprawnie | Poprawnie |
| Kontekst (rodzic linku) | Nie | Tak | Tak, selektory CSS | Nie (użyj WP_HTML_Processor) |
| Typowa awaria | false po limicie backtrackingu | Ostrzeżenia i UTF-8 | ValueError przy złym kodowaniu | Nie zna kontekstu linku |
| Minimalne wymaganie | Dowolne PHP | Rozszerzenie DOM | PHP 8.4 | WordPress 6.2+ |
Na aktualnym WordPressie Tag Processor jest najprostszym pewnym wyborem. Dom\HTMLDocument wygrywa, gdy kod działa poza WordPressem albo potrzebuje selektorów.
Jak ustawić pierwszy obrazek jako miniaturę
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( '<?xml encoding="utf-8" ?>' . $content );
libxml_clear_errors();
$images = $doc->getElementsByTagName( 'img' );
if ( $images->length > 0 ) {
$src = $images->item( 0 )->getAttribute( 'src' );
return $src ? $src : false;
}
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).
Najczęstsze błędy przy parsowaniu pierwszego linku w WordPress
Linki wewnętrzne kotwic. Pierwszy <a href="#section"> to nie „linkout”. Jeśli budujesz agregator, filtruj href zaczynające się od # oraz puste href. To samo dotyczy mailto: i tel:; funkcje na Tag Processorze i Dom\HTMLDocument z tego wpisu przepuszczają tylko http i https.
Linki relative. href="/oferta/" na obcym originie po imporcie może wskazywać złą domenę. Jeśli wiesz, że treść jest lokalna, zamień ścieżkę na adres absolutny przez WP_Http::make_absolute_url( $href, home_url( '/' ) ) (w rdzeniu od WordPressa 3.4.0). Nie myl tego z wp_make_link_relative(), która robi odwrotnie: obcina domenę.
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 markupem.
Jakie URL-e odrzucają wp_http_validate_url i FILTER_VALIDATE_URL
Obie nazwy sugerują „czy to poprawny URL”. Żadna nie odpowiada na to pytanie dla linków redakcyjnych.
wp_http_validate_url() chroni zapytania HTTP wysyłane przez sam serwer. Z dokumentacji funkcji i jej kodu wynika, że:
- akceptuje tylko
httpihttpsi odrzuca adresy z loginem i hasłem; - odrzuca adresy względne, bo wymaga hosta;
- dla każdego hosta innego niż własna strona wywołuje
gethostbyname(), czyli robi zapytanie DNS, a nazwa, która w tej chwili się nie rozwiązuje, zostaje odrzucona; - odrzuca adresy z sieci prywatnych, chyba że pozwoli na nie filtr
http_request_host_is_external; - domyślnie dopuszcza tylko porty 80, 443 i 8080 (filtr
http_allowed_safe_ports).
W hooku save_post oznacza to jedno zapytanie DNS na każdy zapis i ciche gubienie poprawnych linków do domen chwilowo niedostępnych albo do serwerów demo na niestandardowym porcie. Używaj jej tylko wtedy, gdy kod faktycznie ma pobrać ten URL.
Filtr FILTER_VALIDATE_URL w PHP ma inny problem, opisany w podręczniku: filtr „działa tylko z adresami ASCII”, więc domeny z polskimi znakami są zawsze odrzucane (tłumaczenie własne). W naszym teście na PHP 8.4 adres https://exemplo.pt/ação został odrzucony, a FILTER_SANITIZE_URL zamienił go na https://exemplo.pt/ao, po cichu kasując litery z akcentami. Dla adresów w rodzaju /oferta-dla-szkół/ to realny błąd. Schemat sprawdzaj przez wp_parse_url(), wartość czyść esc_url_raw() przed zapisem i esc_url() przed wypisaniem.
Jakie testy warto mieć dla parsera pierwszego linku
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), - zakomentowany link
<!-- <a href="..."> -->i blok<script>ze stringiem<a href, oba do zignorowania.
To pięć minut pracy, które oszczędzają godzinę po imporcie 2000 wpisów.
Jak cache’ować pierwszy link w post meta
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.
add_action( 'save_post', function ( int $post_id, WP_Post $post ): void {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) || 'post' !== $post->post_type ) {
return;
}
$url = wppoland_pierwszy_link_tag_processor( $post->post_content );
if ( $url ) {
update_post_meta( $post_id, '_wppoland_first_external_url', $url );
} else {
delete_post_meta( $post_id, '_wppoland_first_external_url' );
}
}, 10, 2 );WP_Query domyślnie ładuje meta wpisów z pętli do cache obiektów, więc odczyt w archiwum nie dokłada zapytania na każdą kartę. Jeśli dopiszesz do hooka current_user_can(), pamiętaj, że zapisy bez zalogowanego użytkownika, jak wp post update w WP-CLI bez --user albo import z crona, przestaną aktualizować meta.
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.
Jak uzupełnić post meta starych wpisów przez WP-CLI
Hook save_post obejmuje tylko wpisy zapisane po wdrożeniu kodu. Dla archiwum wystarczą dwie udokumentowane komendy WP-CLI. wp post list --field=ID wypisuje jeden ID na linię i według dokumentacji komendy domyślnie zwraca wszystkie pasujące wpisy (--posts_per_page ma wartość -1). wp eval-file uruchamia plik PHP z załadowanym WordPressem i wkłada argumenty pozycyjne do zmiennej $args (dokumentacja).
<?php
// uzupelnij-pierwszy-link.php
foreach ( $args as $post_id ) {
$post = get_post( (int) $post_id );
if ( ! $post ) {
continue;
}
$url = wppoland_pierwszy_link_tag_processor( $post->post_content );
if ( $url ) {
update_post_meta( $post->ID, '_wppoland_first_external_url', $url );
} else {
delete_post_meta( $post->ID, '_wppoland_first_external_url' );
WP_CLI::log( "Brak linku: {$post->ID}" );
}
}wp post list --post_type=post --post_status=publish --field=ID \
| xargs -n 100 wp eval-file uzupelnij-pierwszy-link.phpxargs -n 100 przekazuje ID paczkami po 100, a każda paczka startuje nowy proces PHP, więc pamięć nie rośnie razem z archiwum. Puść to najpierw na stagingu. Przy WP-CLI migracjach batchem loguj posty, dla których parser nic nie znalazł (robi to WP_CLI::log() powyżej). To zwykle drafty, cytaty bez linków albo treści zbudowane wyłącznie z bloków bez kotwic HTML.
Dlaczego linki w blokach Gutenberg umykają parserowi
Gutenberg potrafi trzymać URL w atrybutach bloku (JSON w komentarzu), a nie w klasycznym <a href>. Tak działa na przykład blok dynamiczny core/navigation-link: jego url siedzi w komentarzu bloku, a HTML powstaje na serwerze. Jeśli po get_the_content() bez filtrów „nie ma linku”, a w edytorze go widać, 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. Pamiętaj, że pełny filtr the_content uruchamia też to, co dokładają wtyczki (przyciski udostępniania, powiązane wpisy), więc może wstawić link przed Twoim.
Blok przycisku (core/button) jest statyczny: według jego block.json atrybut url jest czytany z href tagu a w zapisanym HTML (źródło attribute, selektor a), więc parser znajdzie go już w surowym post_content. Atrybuty każdego bloku sprawdzisz w dokumentacji bloków rdzenia. Gorzej z blokami wtyczek trzecich, 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 Tag Processorze (albo get_url_in_content() od WordPressa 6.9, DOMDocument czy Dom\HTMLDocument poza WordPressem) niż na regexie, schemat sprawdzać przez wp_parse_url(), 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.







