For å automatisere denne arbeidsflyten må temaet kunne inspisere innleggsinnholdet (the_content) og hente ut måladressen (href) fra den første lenken (<a>) i teksten.
Mange utviklere tyr umiddelbart til regulære uttrykk (Regex). Denne veiledningen viser hvorfor det er risikabelt i produksjon, og hvordan du løser oppgaven med WordPress-klassen WP_HTML_Tag_Processor, kjernefunksjonen get_url_in_content(), WP_HTML_Processor når lenkens plassering betyr noe, den klassiske PHP-klassen DOMDocument eller den nye Dom\HTMLDocument i PHP 8.4.
Hvorfor regulære uttrykk skaper problemer med HTML
Det er en velkjent læresetning innen programvareutvikling at man ikke skal parse vilkårlig HTML med regulære uttrykk. Et typisk regex-mønster ser ofte slik ut:
// Problematisk tilnærming med regex:
preg_match('/<a\s[^>]*href=(\"??)([^\" >]*?)\\1[^>]*>(.*)<\/a>/siU', $content, $matches);Selv om dette ser ut til å fungere i en enkel test, oppstår det fort feil i virkelige publiseringsmiljøer:
- HTML-kommentarer: Hvis en redaktør har kommentert ut et avsnitt (
<!-- <a href="gammel-lenke.no"> -->), vil regex-mønsteret fange opp den inaktive lenken. Gutenberg legger dessuten kommentarer som<!-- wp:paragraph -->rundt hver blokk. - Innhold i
<script>og<style>: En egendefinert HTML-blokk der JavaScript bygger en streng som"<a href=...>", inneholder for regex en helt gyldig lenke. En HTML-parser vet at innholdet i<script>er tekst og ikke markup. - Attributtrekkefølge og linjeskift: Blokkredigeringen (Gutenberg) og innlimt innhold gir ofte attributter over flere linjer og
data-*-attributter foranhref. Regex feiler ofte på slike variasjoner. - Grensen for tilbakesporing: PCRE-motoren i PHP gir opp når et mønster overskrider
pcre.backtrack_limit, som har standardverdien 1000000 ifølge PHP-manualen. Da returnererpreg_match()falsei stedet for 0 eller 1, og barepreg_last_error()viserPREG_BACKTRACK_LIMIT_ERROR. I lange innlegg med grådige kvantorer som(.*)forsvinner lenken uten et eneste spor i loggen.
HTML er ikke et regulært språk og krever en parser. WordPress-kjernen gikk selv den veien: get_url_in_content() brukte et regulært uttrykk i over ti år før den gikk over til HTML API-et, som vist lenger ned.
Hvordan hente første lenke med WP_HTML_Tag_Processor
Fra og med WordPress 6.2 har utviklere tilgang til WP_HTML_Tag_Processor. Dokumentasjonen til klassen beskriver den slik: den parser ikke HTML fullstendig og går ikke rekursivt inn i strukturen, men leser dokumentet lineært og tolker bare åpningstaggene. Det er akkurat det som trengs for å lese en href, uten minnekostnaden ved et fullt DOM-tre.
Vi testet oppførselen mot kildekoden til WordPress 7.1.3: next_tag( 'a' ) hopper over HTML-kommentarer og innholdet i <script>, og get_attribute( 'href' ) returnerer verdien ferdig dekodet, slik at & i en spørrestreng kommer frem som &. Mangler attributtet, returneres null; et boolsk attributt uten verdi gir true. Derfor er is_string()-sjekken nødvendig.
Hjelpefunksjon for første lenke med WP_HTML_Tag_Processor
Legg denne funksjonen i functions.php eller i et Must-Use Plugin. Den hopper over mailto:, ankre og relative stier og stopper ved første http- eller https-adresse:
<?php
/**
* Trekker ut den første http(s)-lenken fra HTML-innhold ved hjelp av WP_HTML_Tag_Processor.
*
* @param string $content Innleggsinnholdet.
* @return string|false Den rensede URL-en eller false hvis ingen lenke finnes.
*/
function wppoland_finn_forste_lenke_processor( string $content ): string|false {
if ( '' === trim( $content ) ) {
return false;
}
// Initialiser WordPress sin innebygde HTML-prosessor
$prosessor = new WP_HTML_Tag_Processor( $content );
// Gå gjennom <a>-elementene i dokumentrekkefølge
while ( $prosessor->next_tag( 'a' ) ) {
$href = $prosessor->get_attribute( 'href' );
// Sjekk at attributtet er en ikke-tom streng
if ( ! is_string( $href ) || '' === trim( $href ) ) {
continue;
}
$href = trim( $href );
$skjema = strtolower( (string) wp_parse_url( $href, PHP_URL_SCHEME ) );
if ( in_array( $skjema, array( 'http', 'https' ), true ) ) {
return esc_url_raw( $href );
}
}
return false;
}Vise første lenke i archive.php og andre malfiler
I temasløyfen kan du nå enkelt sjekke om innlegget inneholder en ekstern referanse:
<?php
if ( have_posts() ) :
while ( have_posts() ) : the_post();
$lenke = wppoland_finn_forste_lenke_processor( get_the_content() );
?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<h2>
<?php if ( $lenke ) : ?>
<a href="<?php echo esc_url( $lenke ); ?>" rel="external noopener" target="_blank">
<?php the_title(); ?>
</a>
<?php else : ?>
<a href="<?php the_permalink(); ?>">
<?php the_title(); ?>
</a>
<?php endif; ?>
</h2>
<div class="entry-summary">
<?php the_excerpt(); ?>
</div>
</article>
<?php
endwhile;
endif;
?>Løsningen bruker lite minne og tåler feilformaterte tagger. I et arkiv med mange innlegg bør du likevel lese verdien fra postmeta, som vist i delen om save_post.
Hvordan bruke get_url_in_content i WordPress
Før du skriver din egen funksjon, er det verdt å vite at kjernen allerede har en: get_url_in_content( $content ), dokumentert som at den “trekker ut og returnerer den første URL-en fra innholdet den får” (egen oversettelse av den offisielle referansen). Den har eksistert siden WordPress 3.6.0 og brukes med innleggsformatet “link”: temaet Twenty Thirteen kaller den i twentythirteen_get_link_url() og faller tilbake til permalenken når ingen URL finnes.
Implementasjonen har endret seg på en viktig måte. Vi sammenlignet koden i grenene 6.8 og 6.9 i det offisielle repositoriet:
- Til og med WordPress 6.8 brukte funksjonen
preg_match( '/<a\s[^>]*?href=([\'"])(.+?)\1/is', ... ). Med samme test-HTML som i resten av artikkelen returnerte mønsteret lenken som lå inne i en HTML-kommentar, altså en lenke ingen leser så. - Fra WordPress 6.9 oppretter funksjonen en
WP_HTML_Tag_Processor, går gjennomA-taggene og returnerer den førstehref-verdien som ikke er tom, renset medsanitize_url(). Den utkommenterte lenken blir ikke lenger funnet.
$url = get_url_in_content( get_the_content() );
if ( $url ) {
printf(
'<a href="%s" rel="noopener">Les kilden</a>',
esc_url( $url )
);
}Funksjonen returnerer false når den ikke finner noe. To begrensninger gjelder. Den godtar alle skjemaer, så en mailto:-lenke eller et #kommentarer-anker tidlig i innlegget returneres før den redaksjonelle lenken. Og på et nettsted som fortsatt kjører WordPress 6.8 eller eldre, får du oppførselen til det regulære uttrykket. På 6.9 eller nyere, når den første href er det du trenger, bruker du kjernefunksjonen; trenger du skjemafiltrering eller støtte for eldre versjoner, passer funksjonen over bedre.
Hvordan finne første lenke i et avsnitt med WP_HTML_Processor
Tag Processor vet ikke hvor den er. Lenken rundt et bilde i en core/image-blokk og lenken midt i et avsnitt er bare to a-tagger for den. Når regelen er “den første lenken som er skrevet i teksten, ikke bildelenken”, trenger du WP_HTML_Processor, tilgjengelig siden WordPress 6.4. Den holder styr på stakken av åpne elementer og viser den gjennom get_breadcrumbs().
<?php
function wppoland_forste_lenke_i_avsnitt( string $content ): ?string {
$prosessor = WP_HTML_Processor::create_fragment( $content );
if ( null === $prosessor ) {
return null;
}
while ( $prosessor->next_tag( 'A' ) ) {
$sti = $prosessor->get_breadcrumbs();
// Bare lenker inne i <p> og utenfor <figure>
if ( ! in_array( 'P', $sti, true ) || in_array( 'FIGURE', $sti, true ) ) {
continue;
}
$href = $prosessor->get_attribute( 'href' );
if ( is_string( $href ) && '' !== trim( $href ) ) {
return esc_url_raw( trim( $href ) );
}
}
return null;
}Vi sjekker stien selv i stedet for å bruke spørringen next_tag( array( 'breadcrumbs' => array( 'P', 'A' ) ) ). Den spørringen krever at A er et direkte barn av P; i testen vår ble en lenke inne i <p><strong><a> hoppet over av spørringen, men funnet med in_array()-sjekken.
Dokumentasjonen til klassen er tydelig på én regel: når prosessoren møter markup den ennå ikke støtter, stopper den i stedet for å gjette. Da returnerer next_tag() false, og get_last_error() er ikke lenger null. Får du null for et innlegg med synlige lenker, sjekk get_last_error() og fall tilbake til Tag Processor.
Hvordan hente første lenke med DOMDocument
Hvis koden må kjøre utenfor WordPress, eller prosjektet kjører på versjoner før 6.2, er den innebygde DOMDocument-klassen i PHP et godt alternativ. Den bygger på HTML-parseren i libxml2, som følger regler fra før HTML5.
For å unngå kjente problemer med særnorske tegn (æ, ø, å) og advarsler om moderne HTML5-tagger, må klassen konfigureres med riktig feilhåndtering:
function wppoland_finn_forste_lenke_domdocument( string $content ): string|false {
if ( '' === trim( $content ) ) {
return false;
}
$dom = new DOMDocument();
// Undertrykk advarsler om ukjente HTML5-elementer fra libxml
$forrige_feil = libxml_use_internal_errors( true );
// Bevar UTF-8-tegnsettet korrekt under innlesing
$kodet_html = mb_encode_numericentity(
$content,
[ 0x80, 0x10ffff, 0, 0x1fffff ],
'UTF-8'
);
$suksess = $dom->loadHTML(
'<?xml encoding="UTF-8">' . $kodet_html,
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
libxml_use_internal_errors( $forrige_feil );
if ( ! $suksess ) {
return false;
}
$ankere = $dom->getElementsByTagName( 'a' );
if ( $ankere->length > 0 ) {
/** @var DOMElement $forste_anker */
$forste_anker = $ankere->item( 0 );
$href = $forste_anker->getAttribute( 'href' );
if ( ! empty( $href ) ) {
return esc_url_raw( trim( $href ) );
}
}
return false;
}mb_encode_numericentity() er riktig valg her. Eldre kodesnutter bruker mb_convert_encoding( $html, 'HTML-ENTITIES', 'UTF-8' ), men PHP 8.2 erklærte HTML-ENTITIES som foreldet i alle mbstring-funksjoner, og PHP 8.4 skriver ut en Deprecated-melding ved hvert kall.
Hvordan bruke Dom\HTMLDocument i PHP 8.4
PHP 8.4 innførte et nytt DOM-API i navnerommet Dom, med “standardkompatibel støtte for parsing av HTML5-dokumenter”, ifølge lanseringsnotatene (egen oversettelse). Dom\HTMLDocument parser HTML etter living standard, noe som fjerner advarslene om HTML5-tagger og trikset med XML-hodet. DOMDocument er fortsatt tilgjengelig av hensyn til kompatibilitet.
<?php
function wppoland_forste_lenke_dom84( string $html ): ?string {
if ( '' === trim( $html ) ) {
return null;
}
$dok = Dom\HTMLDocument::createFromString( $html, LIBXML_NOERROR, 'UTF-8' );
foreach ( $dok->querySelectorAll( 'a[href]' ) as $lenke ) {
$href = trim( $lenke->getAttribute( 'href' ) );
$skjema = strtolower( (string) parse_url( $href, PHP_URL_SCHEME ) );
if ( in_array( $skjema, array( 'http', 'https' ), true ) ) {
return $href;
}
}
return null;
}Tre detaljer fra manualsiden for createFromString(): det andre argumentet tar LIBXML_*-konstanter kombinert med OR; det tredje, overrideEncoding, låser tegnkodingen i stedet for å la PHP gjette; og en ukjent koding kaster ValueError. Med PHP 8.4.24 returnerte funksjonen https://exemplo.pt/ação?x=1&y=2 uendret, med & dekodet. CSS-selektorer gir også spørringer som p > a[href^="https://"], som krever XPath med DOMDocument.
Når er wp_extract_urls() god nok
WordPress har en egen funksjon for å trekke ut URL-er fra tekst: wp_extract_urls(). Den er nyttig for enkle formål, men har en vesentlig begrensning:
$alle_urler = wp_extract_urls( get_the_content() );
if ( ! empty( $alle_urler ) ) {
$forste_url = $alle_urler[0];
}Funksjonen søker etter alt som ligner en URL i ren tekst. Det betyr at den også returnerer bildelenker fra src-attributter og rene tekst-URL-er som ikke er klikkbare. Hvis du utelukkende er ute etter ekte hyperlenker (<a href="...">), bør du velge en HTML-parser som WP_HTML_Tag_Processor.
Hvilken metode passer for første lenke
Vi publiserer ikke kjøretider: de avhenger av serveren, PHP-versjonen og innleggets størrelse, og bare tall målt i ditt eget prosjekt sier noe. Forskjellene i oppbygning er derimot stabile:
| Kriterium | Regex (preg_match) | DOMDocument (libxml2) | Dom\HTMLDocument | WP_HTML_Tag_Processor |
|---|---|---|---|---|
| Parsemodell | Tekstmønster | DOM-tre, regler fra før HTML5 | DOM-tre, HTML5 | Lineær lesing av åpningstagger |
| Minne | Lavt | Helt tre | Helt tre | Ingen tre |
Kommentarer og <script> | Upålitelig | Riktig | Riktig | Riktig |
| Kontekst (lenkens forelder) | Nei | Ja | Ja, med CSS-selektorer | Nei (bruk WP_HTML_Processor) |
| Typisk feil | false ved tilbakesporingsgrensen | Advarsler og UTF-8 | ValueError ved ugyldig koding | Kjenner ikke lenkens kontekst |
| Minstekrav | Hvilken som helst PHP | DOM-utvidelsen | PHP 8.4 | WordPress 6.2+ |
På et oppdatert WordPress-nettsted er Tag Processor det enkleste pålitelige valget. Dom\HTMLDocument vinner når koden kjører utenfor WordPress eller trenger selektorer.
Hvilke URL-er avviser wp_http_validate_url og FILTER_VALIDATE_URL
Begge navnene antyder “er dette en gyldig URL”. Ingen av dem svarer på det spørsmålet for redaksjonelle lenker.
wp_http_validate_url() beskytter HTTP-forespørsler som serveren selv sender. Ut fra referansesiden og kildekoden:
- den godtar bare
httpoghttpsog avviser URL-er med brukernavn og passord; - den avviser relative URL-er, fordi den krever en vert;
- for alle andre verter enn ditt eget nettsted kaller den
gethostbyname(), altså et DNS-oppslag, og et navn som ikke løses opp akkurat da, blir avvist; - den avviser adresser i private nett med mindre filteret
http_request_host_is_externaltillater dem; - som standard godtar den bare portene 80, 443 og 8080 (filteret
http_allowed_safe_ports).
I en save_post-krok betyr det ett DNS-oppslag per lagring og stille tap av gyldige lenker til domener som er nede et øyeblikk, eller til demoservere på en annen port. Bruk den bare når koden faktisk skal hente URL-en.
PHP-filteret FILTER_VALIDATE_URL har et annet problem, beskrevet i manualen: “filteret fungerer bare med ASCII-URL-er”, så internasjonaliserte domenenavn avvises alltid (egen oversettelse). I vår test med PHP 8.4 ble https://exemplo.pt/ação avvist, og FILTER_SANITIZE_URL gjorde den samme adressen om til https://exemplo.pt/ao uten advarsel. For norske adresser med æ, ø og å er det et reelt problem. Sjekk skjemaet med wp_parse_url(), rens verdien med esc_url_raw() før lagring og esc_url() før utskrift.
Hvordan cache første lenke med save_post
Å parse HTML-koden til mange innlegg på hver eneste sidevisning i et arkiv skaper unødvendig CPU-belastning. En bedre arkitektur trekker ut lenken i det øyeblikket innlegget lagres, og lagrer deretter URL-en i wp_postmeta.
Lagre første lenke i post meta ved save_post
add_action( 'save_post', function ( int $post_id, WP_Post $post ): void {
// Unngå autosave og revisjoner
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) || 'post' !== $post->post_type ) {
return;
}
$lenke = wppoland_finn_forste_lenke_processor( $post->post_content );
if ( $lenke ) {
update_post_meta( $post_id, '_wppoland_kildelenke', $lenke );
} else {
delete_post_meta( $post_id, '_wppoland_kildelenke' );
}
}, 10, 2 );
/**
* Henter den ferdig indekserte URL-en direkte fra postmeta.
*/
function wppoland_hent_lagret_kildelenke( int $post_id ): string|false {
$lenke = get_post_meta( $post_id, '_wppoland_kildelenke', true );
return ! empty( $lenke ) ? (string) $lenke : false;
}Ved å hente verdien fra _wppoland_kildelenke slipper serveren all HTML-behandling under sidevisning. WP_Query laster som standard metadataene for innleggene i løkken inn i objekt-cachen, så verdien leses uten en ekstra spørring per innlegg.
Legger du til en current_user_can()-sjekk i kroken, husk at lagringer uten innlogget bruker, som wp post update i WP-CLI uten --user eller en import via cron, da ikke oppdaterer metafeltet.
Hvordan fylle postmeta for eldre innlegg med WP-CLI
save_post-kroken dekker bare innlegg som lagres etter at koden er satt i produksjon. For arkivet holder det med to dokumenterte WP-CLI-kommandoer. wp post list --field=ID skriver én ID per linje og returnerer ifølge kommandodokumentasjonen alle treff som standard (--posts_per_page er -1). wp eval-file kjører en PHP-fil med WordPress lastet og legger posisjonsargumentene i variabelen $args (dokumentasjon).
<?php
// fyll-kildelenke.php
foreach ( $args as $post_id ) {
$post = get_post( (int) $post_id );
if ( ! $post ) {
continue;
}
$lenke = wppoland_finn_forste_lenke_processor( $post->post_content );
if ( $lenke ) {
update_post_meta( $post->ID, '_wppoland_kildelenke', $lenke );
} else {
delete_post_meta( $post->ID, '_wppoland_kildelenke' );
}
}wp post list --post_type=post --post_status=publish --field=ID \
| xargs -n 100 wp eval-file fyll-kildelenke.phpxargs -n 100 sender ID-ene i bolker på 100, og hver bolk starter en ny PHP-prosess, så minnebruken vokser ikke med arkivet. Kjør det først på staging og kontroller noen innlegg manuelt.
Hvorfor lenker fra dynamiske blokker og kortkoder mangler
post_content i databasen er ikke den HTML-en besøkende får. get_the_content() henter den lagrede blokk-koden uten the_content-filtrene. Det gir to tilfeller der lenken finnes på siden, men ikke i råinnholdet:
- Dynamiske blokker. Blokken
core/navigation-linklagrerurlsom attributt i blokkommentaren, som JSON, og HTML-en lages på serveren via en render_callback.core/buttonlagrer derimot lenken i sin egen HTML: iblock.jsonhentes attributteturlfrahrefpåa-taggen (kildeattribute, selektora). Referansen for kjerneblokkene viser attributtene til hver blokk. - Kortkoder og embeds. De utvides først når innholdet går gjennom filteret
the_content.
Er regelen “den første lenken redaktøren skrev”, parser du $post->post_content. Er den “den første lenken besøkende ser”, kjører du innholdet gjennom do_blocks() og do_shortcode(), eller apply_filters( 'the_content', $content ), før du sender det til HTML-prosessoren. Merk at hele filteret også kjører det utvidelser legger til (delingsknapper, relaterte innlegg), som kan sette inn lenker foran din.
Hvordan håndtere ankre, mailto og relative lenker
Ikke alle href-attributter inneholder eksterne nettadresser. I innhold fra redigeringsverktøyet finner du ofte:
- Lokale ankre:
#kommentarer,#innledning. - Kontaktlenker:
mailto:[email protected],tel:+4722000000. - Relative stier:
/om-oss/,./nedlastinger/guide.pdf.
Funksjonene i denne artikkelen basert på WP_HTML_Tag_Processor og Dom\HTMLDocument forkaster allerede skjemaer som ikke er http eller https. Vil du godta relative stier, gjør dem først om til absolutte adresser. Kjernen har WP_Http::make_absolute_url( $href, home_url( '/' ) ), tilgjengelig siden WordPress 3.4.0. Ikke forveksle den med wp_make_link_relative(), som gjør det motsatte.
Oppsummering av hvordan hente første lenke
Uttrekking av data fra innleggsinnhold er en klassisk utfordring som krever en solid teknisk tilnærming. Ved å vrake upålitelige regulære uttrykk til fordel for WP_HTML_Tag_Processor, get_url_in_content() på WordPress 6.9 eller nyere, DOMDocument eller Dom\HTMLDocument, sikrer du at nettstedet ditt håndterer uforutsigbart redaksjonelt innhold.
Kombinerer du dette med lagring i postmeta via save_post og en engangsutfylling med WP-CLI, slipper arkivsidene å parse HTML ved hver visning.
Trenger du hjelp med skreddersydd temaarkitektur eller optimalisering av eksisterende WordPress-løsninger? Les mer om våre tjenester innen WordPress-utvikling.







